Persiste proposal, specs delta (auth, api-client, access-requests, platform-shell, testing), design con 8 ADRs, tasks (27 items en 10 fases), verify-report (28 tests, 0 críticos, 0 warnings tras el cierre) y archive-report final. Cierra el ciclo SDD del cleanup de deuda técnica del console.
4.6 KiB
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
loginApiresolves andlogin()is called with the response - THEN
state.accessTokenholds the received JWT - AND
state.tokendoes not exist on the state object
Scenario: Refresh success updates accessToken only
- GIVEN a valid refresh cookie exists and
SessionCoordinator.refresh()succeeds - WHEN
setSessionis called with the new token - THEN
state.accessTokenis updated to the refreshed JWT - AND
state.tokendoes not exist on the state object
Scenario: Logout clears accessToken and user
- GIVEN a user is authenticated (
state.accessTokenis non-null) - WHEN
logout()is called - THEN
state.accessTokenisnull - AND
state.userisnull - AND the persisted key
omnioil-console-authis removed fromlocalStorage
Scenario: ProtectedRoute reads accessToken
- GIVEN the store has
accessToken = "eyJ..."andtokenfield is absent - WHEN
ProtectedRouteevaluates the session - THEN the component reads
state.accessTokenwithout referencingstate.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
loginApiresolves andonSubmitprocesses the response - THEN
login()is called withaccessToken: response.access_token(no?? response.tokenfallback)
Scenario: LoginResponse type has no refresh_token field
- GIVEN the
LoginResponseinterface is the only type representing a login API response - WHEN a developer reads or extends
LoginResponse - THEN the interface contains
access_token,token,role, andemail— norefresh_token
Scenario: Refresh endpoint response aligned in RefreshResponse
- GIVEN
SessionCoordinator.performRefreshreceives the refresh response - WHEN the backend returns
{ access_token: "eyJ...", token: "eyJ...", role: "...", email: "..." } - THEN
performRefreshreadsdata.access_tokenas the canonical field; the?? data.tokenfallback MAY remain as a defensive guard but MUST NOT silently mask a missingaccess_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 (
useris null,accessTokenis null) - WHEN the user navigates to a protected route
- THEN the component renders
<Navigate to="/login" replace />immediately - AND "Restaurando sesion..." is never rendered
Scenario: Warm visit — persisted user, expired token, refresh succeeds
- GIVEN
useris non-null (persisted) butaccessTokenis null (in-memory only) - WHEN
ProtectedRoutemounts - THEN it renders the "Restaurando sesion..." indicator while
SessionCoordinator.refresh()is pending - AND when refresh resolves with a new token,
setSessionis called and the outlet renders
Scenario: Warm visit — persisted user, refresh fails
- GIVEN
useris non-null (persisted) butaccessTokenis null - WHEN
SessionCoordinator.refresh()rejects or returns null - THEN
ProtectedRoutenavigates to/loginafter the refresh attempt completes - AND "Restaurando sesion..." disappears before the redirect