Per-screen Cubits (Flutter/bloc; proposal, 00-shared/06 ) + the module-wide session
lifecycle that every other module depends on. Backed by AuthRepository (dio) which
calls the endpoints in 12_API_Mapping.md .
stateDiagram-v2
[*] --> unauthenticated : no stored tokens
unauthenticated --> authenticating : SubmitLogin | SubmitRegister
authenticating --> authenticated : tokens stored
authenticating --> unauthenticated : 401 (invalid) reason
unauthenticated --> authenticating : restore (stale tokens) -> refresh
authenticated --> refreshing : access 401 / ttl gone
refreshing --> authenticated : refresh 200 (new tokens swapped)
refreshing --> sessionExpired : refresh failed
sessionExpired --> unauthenticated : clear tokens + reason
authenticated --> sessionExpired : logout-all / revoked / password reset on server
unauthenticated --> [*] : app closed
Owner: AuthCubit (single instance, injected). Persists tokens in
flutter_secure_storage (00-shared/11 §11); on boot reads → refresh → if refresh
succeeds ⇒ authenticated; else unauthenticated.
sessionExpired reason: tokenReuse, sessionRevoked, passwordChanged, expired
(mapped from error path).
Single-flight refresh in dio interceptor (00-shared/11 §5): concurrent 401s coalesce
into one POST /auth/refresh; swap pair; replay queue.
Screen Cubit Events → State
Login LoginCubitLogin(email,pw) → {initial, loading, success(model), error(code)}
2FA (planned) TotpCubitSubmit(code) → {idle, verifying, success, error}
Register RegisterCubitRegister(form) → {idle, submitting, success(tokens→AuthCubit), duplicate, error}
Verify VerifyEmailCubitVerify(token) → {verifying, success, resendable(error/cooldown)}
Forgot ForgotCubitSubmit(email) → {idle, sent, rateLimited(countdown)}
Reset ResetCubitSubmit(token,pw) → {idle, submitting, success, expired)}
Sessions SessionsCubitLoad, Refresh, Revoke(id) → {initial, loading, loaded([session]), empty, error, revoking}
ApiKeys ApiKeysCubitLoad, LoadMore(n/a), Create(form), Revoke(id) → parallel {list, createFlow{sheetState, revealing(secret), done}, error}
2faDetail TfaDetailCubitLoadStatus, Enable, Verify(code), Disable(code) → {off, setupReady,on, verifying, disabled}
Lock LockCubitAuthorize(biometric) — device-level, (forward-looking)
All prefixed AuthHub* not needed — AuthCubit + per-screen cubits.
Loading & caching: LoadState from 00-shared/06 §3.1; lists refresh via
RefreshIndicator bypassing cache; no staleness for auth (server truth).
class AuthState { AuthStatus status; AuthenticatedUser? user; String? reason; }
class Session { id, deviceName?, browser?, os?, platform?, ip?, location?, expiresAt, lastActivityAt, isCurrent; }
class ApiKeyRef { id, name, prefix, scopes, createdAt, lastUsedAt; }
class ReadyApiKey { ApiKeyRef meta; String secret; } // only in create-sheet scope
class TfaStatus { enabled; }
UI event Cubit method Repository call
LoginScreen submit login()authRepo.login(email,pw)
VerifyEmailScreen verify(token)authRepo.verifyEmail
Resend press resend()authRepo.resendVerification
Forgot Screen send()authRepo.forgot
Reset Screen submit()authRepo.reset
Sessions load load()authRepo.sessions
Sessions row menu revoke(id)authRepo.revokeSession
LogoutAll dialog confirm logoutAll()authRepo.logoutAll
ApiKeys list load()authRepo.apiKeys
FAB create create(name,scopes)authRepo.createApiKey() → secret snapshots to sheet revealing
key menu revoke(id)authRepo.revokeApiKey
2fa card enable()authRepo.enable2fa
2fa code verify(code)authRepo.verify2fa
Turn off requestDisable(code)authRepo.disable2fa
AuthCubit cache = token pair + user meta (needed everywhere; local_cache).
Sessions/api-keys: no persistence cache → always fetch on screen open; RefreshIndicator re-fetch.
Verify/reset: no cache (one-shot).
No WS for auth — session changes broadcast to user room via WsModule
(00-shared/07 §8) is (planned); on any Session-related push press navigate or
re-fetch with snackbar 「 signed in elsewhere 」 on next app use.
Action Error State →
login 401 error(reason AuthError.invalidCredentials) → inline form
login 429 rateLimited(remaining) → countdown, disable CTA
refresh 401 SessionExpired(reason) → router /login
revoke 404 treat-as-removed; error snackbar
create-key 429/4xx createError(code) → sheet stays, message
Pure-Dart cubits; unit-test session transition matrix (state diagram above).
Widget tests: login loading/error/success; sessions empty/error/list; key reveal secret-only-once.
ConnectivityCubit gates auth submission (offline) → banner + disable CTA.
FeatureFlagsCubit not used (auth module runs pre-feature-gate).
On logout → AuthCubit clears secure storage then navigates /login (sessionExpired
iff server originally revoked).