Apariencia
Auth Mobile — Flujo y reglas offline-first
Alcance mobile
La app mobile solo hace signin + logout + silent refresh. El alta de cuentas (por invitación, sin auto-registro público — ver plataforma/auth-flow.md) es exclusiva del admin web. Si un usuario con registro incompleto (pending_onboarding) intenta login desde mobile, se le muestra: "Completa tu registro desde la web".
Endpoints (backend ya existente)
| Método | Ruta | Body mobile | Respuesta |
|---|---|---|---|
| POST | /auth/signin | {email, password} | {status, accessToken, refreshToken, user, org} |
| POST | /auth/refresh | {refreshToken} | {accessToken, refreshToken, permissions} |
| POST | /auth/signout | {refreshToken} | {signedOut: true} |
Mobile envía el refresh token en el body (no cookies httpOnly — eso es para admin web).
Almacenamiento de tokens
- Refresh token: persistido en Drift (tabla
AuthTokens, clavesession). Sobrevive al cierre de la app. - Access token: solo en memoria (campo en
AuthInterceptor). Muere con la app.
Flujo de autenticación
App abre → AuthNotifier.boot()
→ ¿Refresh token en Drift?
NO → Unauthenticated → /login
SÍ → ¿Hay red?
SÍ → POST /auth/refresh → Authenticated → Home
NO → Authenticated(offline) → Home (funciona con datos locales)
/login → email + contraseña → POST /auth/signin
→ status: active → guardar tokens → hydration (no-op) → Home
→ status: pending_* → "Completa tu registro desde la web"
→ 401 → "Email o contraseña incorrectos"
→ sin red → "Sin conexión. Necesitas internet para iniciar sesión"
Interceptor Dio (cualquier request protegido)
→ 401 → POST /auth/refresh con token del body
→ OK → reintentar request original
→ Fail → logout → /login
→ Cola: requests simultáneos esperan a que termine el refresh
Logout
→ POST /auth/signout (si hay red)
→ borrar refresh de Drift + access de memoria
→ Unauthenticated → /login
Conectividad recuperada
→ si estaba offline con sesión → silent refresh automáticoReglas offline-first (auth)
- Login requiere red: primera autenticación obliga conexión con el backend.
- Sesión persiste offline: refresh token en Drift sobrevive al cierre de la app.
- Silent refresh automático: interceptor Dio en 401 + al recuperar conectividad.
- Sin red + sesión válida: app funciona offline con datos locales.
- Sin red + sesión expirada: "Tu sesión expiró, conéctate para renovar".
- Hydration post-login: hook
SyncService.initialHydration()sigue no-op — cada feature sincroniza su propia pantalla al abrirla (ver ADR-0021). - Replay del outbox: al arrancar autenticado con red y al recuperar conectividad,
SyncService.replayOutbox()reenvía las escrituras de campo encoladas offline — verplataforma/sync-offline-flow.mdy ADR-0055. - Logout sin red: borra tokens locales; el backend no recibe la revocación (aceptable).
Guard de navegación (go_router)
- Rutas protegidas:
authState is Authenticated→ permite. - Sin sesión → redirect a
/login. - Con sesión en
/login→ redirect a/. - Estado
AuthLoading→ no redirige (esperando boot).
Archivos clave
| Archivo | Contenido |
|---|---|
lib/features/auth/auth_notifier.dart | Notifier: boot, signin, signout, silentRefresh, onAuthExpired |
lib/features/auth/auth_repository.dart | Acceso a API + Drift para tokens |
lib/features/auth/auth_state.dart | Sealed class: AuthLoading, Authenticated, Unauthenticated |
lib/features/auth/login_screen.dart | UI de login |
lib/core/network/api_client.dart | Dio factory + AuthInterceptor (JWT, 401 refresh, cola) |
lib/core/network/connectivity_listener.dart | Listener que trigger silent refresh al recuperar red |
lib/core/sync/sync_service.dart | initialHydration() no-op; orquesta replayOutbox() |
lib/shared/database/app_database.dart | Tabla AuthTokens (Drift) |
lib/app/providers.dart | Wiring de todos los providers |
lib/app/router.dart | GoRouter + auth guard |