Skip to content

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étodoRutaBody mobileRespuesta
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, clave session). 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ático

Reglas offline-first (auth)

  1. Login requiere red: primera autenticación obliga conexión con el backend.
  2. Sesión persiste offline: refresh token en Drift sobrevive al cierre de la app.
  3. Silent refresh automático: interceptor Dio en 401 + al recuperar conectividad.
  4. Sin red + sesión válida: app funciona offline con datos locales.
  5. Sin red + sesión expirada: "Tu sesión expiró, conéctate para renovar".
  6. Hydration post-login: hook SyncService.initialHydration() sigue no-op — cada feature sincroniza su propia pantalla al abrirla (ver ADR-0021).
  7. Replay del outbox: al arrancar autenticado con red y al recuperar conectividad, SyncService.replayOutbox() reenvía las escrituras de campo encoladas offline — ver plataforma/sync-offline-flow.md y ADR-0055.
  8. 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

ArchivoContenido
lib/features/auth/auth_notifier.dartNotifier: boot, signin, signout, silentRefresh, onAuthExpired
lib/features/auth/auth_repository.dartAcceso a API + Drift para tokens
lib/features/auth/auth_state.dartSealed class: AuthLoading, Authenticated, Unauthenticated
lib/features/auth/login_screen.dartUI de login
lib/core/network/api_client.dartDio factory + AuthInterceptor (JWT, 401 refresh, cola)
lib/core/network/connectivity_listener.dartListener que trigger silent refresh al recuperar red
lib/core/sync/sync_service.dartinitialHydration() no-op; orquesta replayOutbox()
lib/shared/database/app_database.dartTabla AuthTokens (Drift)
lib/app/providers.dartWiring de todos los providers
lib/app/router.dartGoRouter + auth guard