Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

13 — State Management (Auth Module)

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.


1. Session lifecycle (global, claims ownership of the token pair)

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.

2. Per-screen Cubits

ScreenCubitEvents → State
LoginLoginCubitLogin(email,pw) → {initial, loading, success(model), error(code)}
2FA (planned)TotpCubitSubmit(code) → {idle, verifying, success, error}
RegisterRegisterCubitRegister(form) → {idle, submitting, success(tokens→AuthCubit), duplicate, error}
VerifyVerifyEmailCubitVerify(token) → {verifying, success, resendable(error/cooldown)}
ForgotForgotCubitSubmit(email) → {idle, sent, rateLimited(countdown)}
ResetResetCubitSubmit(token,pw) → {idle, submitting, success, expired)}
SessionsSessionsCubitLoad, Refresh, Revoke(id) → {initial, loading, loaded([session]), empty, error, revoking}
ApiKeysApiKeysCubitLoad, LoadMore(n/a), Create(form), Revoke(id) → parallel {list, createFlow{sheetState, revealing(secret), done}, error}
2faDetailTfaDetailCubitLoadStatus, Enable, Verify(code), Disable(code) → {off, setupReady,on, verifying, disabled}
LockLockCubitAuthorize(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).

3. State objects (concise)

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; }

4. Events & actions map (UI → Cubit → API)

UI eventCubit methodRepository call
LoginScreen submitlogin()authRepo.login(email,pw)
VerifyEmailScreenverify(token)authRepo.verifyEmail
Resend pressresend()authRepo.resendVerification
Forgot Screensend()authRepo.forgot
Reset Screensubmit()authRepo.reset
Sessions loadload()authRepo.sessions
Sessions row menurevoke(id)authRepo.revokeSession
LogoutAll dialog confirmlogoutAll()authRepo.logoutAll
ApiKeys listload()authRepo.apiKeys
FAB createcreate(name,scopes)authRepo.createApiKey() → secret snapshots to sheet revealing
key menurevoke(id)authRepo.revokeApiKey
2fa cardenable()authRepo.enable2fa
2fa codeverify(code)authRepo.verify2fa
Turn offrequestDisable(code)authRepo.disable2fa

5. Caching & refresh

  • 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).

6. Realtime

  • 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.

7. Error states per action

ActionErrorState →
login401error(reason AuthError.invalidCredentials) → inline form
login429rateLimited(remaining) → countdown, disable CTA
refresh401SessionExpired(reason) → router /login
revoke404treat-as-removed; error snackbar
create-key429/4xxcreateError(code) → sheet stays, message

8. Testing hooks (00-shared/06 §6)

  • 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.

9. Cross-cutting interplay

  • ConnectivityCubit gates auth submission (offline) → banner + disable CTA.
  • FeatureFlagsCubit not used (auth module runs pre-feature-gate).
  • On logoutAuthCubit clears secure storage then navigates /login (sessionExpired iff server originally revoked).