# Auth Specification # Delta for: Auth — console-tech-debt-cleanup ## ADDED Requirements ### Requirement: Single Access Token Field in Auth Store The auth store MUST expose exactly one access-token field: `accessToken`. The `token` field MUST NOT exist in `AuthState`. `setSession` MUST write only `accessToken`. `logout` MUST clear only `accessToken` (plus `refreshToken` and `user`). `isAuthenticated` MUST derive its result from `accessToken`. All consumers (`ProtectedRoute`, `LoginPage`, `apiClient`) MUST read `state.accessToken`; they MUST NOT reference `state.token`. #### Scenario: Login success stores token in accessToken only - GIVEN a user submits valid credentials - WHEN `loginApi` resolves and `login()` is called with the response - THEN `state.accessToken` holds the received JWT - AND `state.token` does not exist on the state object #### Scenario: Refresh success updates accessToken only - GIVEN a valid refresh cookie exists and `SessionCoordinator.refresh()` succeeds - WHEN `setSession` is called with the new token - THEN `state.accessToken` is updated to the refreshed JWT - AND `state.token` does not exist on the state object #### Scenario: Logout clears accessToken and user - GIVEN a user is authenticated (`state.accessToken` is non-null) - WHEN `logout()` is called - THEN `state.accessToken` is `null` - AND `state.user` is `null` - AND the persisted key `omnioil-console-auth` is removed from `localStorage` #### Scenario: ProtectedRoute reads accessToken - GIVEN the store has `accessToken = "eyJ..."` and `token` field is absent - WHEN `ProtectedRoute` evaluates the session - THEN the component reads `state.accessToken` without referencing `state.token` --- ### Requirement: LoginResponse Matches Backend Runtime Contract The `LoginResponse` type MUST declare both `access_token: string` (primary) and `token: string` (legacy compatibility field), because the backend always emits both. The `??` fallback across non-equivalent fields MUST be removed; `LoginPage` MUST read `response.access_token` directly. The `refresh_token` field is NOT present in the login JSON body (it is delivered as an HttpOnly cookie only) and MUST NOT be declared on `LoginResponse`. #### Scenario: Login page reads access_token directly - GIVEN the backend returns `{ access_token: "eyJ...", token: "eyJ...", role: "platform_admin", email: "op@x.com", projects: [] }` - WHEN `loginApi` resolves and `onSubmit` processes the response - THEN `login()` is called with `accessToken: response.access_token` (no `?? response.token` fallback) #### Scenario: LoginResponse type has no refresh_token field - GIVEN the `LoginResponse` interface is the only type representing a login API response - WHEN a developer reads or extends `LoginResponse` - THEN the interface contains `access_token`, `token`, `role`, and `email` — no `refresh_token` #### Scenario: Refresh endpoint response aligned in RefreshResponse - GIVEN `SessionCoordinator.performRefresh` receives the refresh response - WHEN the backend returns `{ access_token: "eyJ...", token: "eyJ...", role: "...", email: "..." }` - THEN `performRefresh` reads `data.access_token` as the canonical field; the `?? data.token` fallback MAY remain as a defensive guard but MUST NOT silently mask a missing `access_token` --- ### Requirement: ProtectedRoute Uses Persisted User for Refresh Heuristic `ProtectedRoute` MUST decide whether to attempt a refresh based on the persisted `user` field (which survives hydration), NOT on `accessToken` (which is intentionally not persisted). When `user` is null, the component MUST navigate to `/login` immediately without showing "Restaurando sesion...". #### Scenario: Cold visit — no persisted user, no flash - GIVEN no prior session exists (`user` is null, `accessToken` is null) - WHEN the user navigates to a protected route - THEN the component renders `` immediately - AND "Restaurando sesion..." is never rendered #### Scenario: Warm visit — persisted user, expired token, refresh succeeds - GIVEN `user` is non-null (persisted) but `accessToken` is null (in-memory only) - WHEN `ProtectedRoute` mounts - THEN it renders the "Restaurando sesion..." indicator while `SessionCoordinator.refresh()` is pending - AND when refresh resolves with a new token, `setSession` is called and the outlet renders #### Scenario: Warm visit — persisted user, refresh fails - GIVEN `user` is non-null (persisted) but `accessToken` is null - WHEN `SessionCoordinator.refresh()` rejects or returns null - THEN `ProtectedRoute` navigates to `/login` after the refresh attempt completes - AND "Restaurando sesion..." disappears before the redirect