fix: [TESIS-82] clear the user state on logout and keep tabs in sync - #51
Conversation
The draft of the manual order lives in sessionStorage and survived the logout. Whoever logged in next in the same tab found the previous user's customer, document and lines waiting in /orders/new. The logout now empties everything that belongs to the user of the session: the React Query cache, as it already did, and the order draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tabs share localStorage but each one has its own copy of the store in memory, and persist does not update it when another tab writes. Logging out in one tab left the other showing data with a revoked token. When that other tab got its 401, it wrote the empty session to localStorage and wiped the saved session of whoever had logged in meanwhile. Three changes close it: - followSessionAcrossTabs() listens to the storage event, which only reaches the other tabs, and rehydrates the store. When the token changed it also empties the data of the previous user, like the logout does. main.tsx registers it once. - Rehydrating from a storage with no token now ends the session of the tab, instead of keeping whatever it had in memory. - The interceptor only ends the session on a 401 of a request that left with the current token. A late 401 of an older token says nothing about the session that is open now. ADR-002 and the architecture guide describe the sync; the authStore entry of the guide was also still describing the pre-TESIS-117 store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API now stops the login after 10 attempts in 3 minutes per IP and answers 429. The form showed its generic message, "Probá de nuevo en unos segundos", which is wrong while the limit lasts minutes. A 429 now gets its own message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LauAubert
left a comment
There was a problem hiding this comment.
Aprobado. Revisé la sincronización entre pestañas: la rehidratación no reescribe localStorage, un 401 de un token viejo ya no cierra la sesión nueva y /me se vuelve a pedir cuando cambia el token. No depende del #89 de la API, así que puede entrar antes.
Nit que no bloquea: con el #89, la API responde 401 con el motivo real (empresa inactiva, cuenta pendiente de aprobación), pero el interceptor muestra siempre «Tu sesión expiró». Se puede ver en otra card.
TomasMartin2004
left a comment
There was a problem hiding this comment.
Revisión — TESIS-82 (PR #51) · El lado del front de la QA de Auth
Revisado sobre TESIS-82-auth-multitenancy-qa-fixes (web) contra origin/master, junto con el PR de backend (api#89), que aprobé aparte.
| Check | Resultado |
|---|---|
npm run test (local) |
550 tests, 0 fallas |
npm run lint · npm run build |
Limpios |
✅ Los dos hallazgos están bien resueltos, y los tests los protegen
Rompí los dos arreglos, de a uno, y corrí src/shared:
| Rotura | Resultado |
|---|---|
Que el interceptor vuelva a mirar sólo getAuthToken() |
1 failure: leaves alone a session that was opened while the request was on its way |
Sacar clearDraft() de clearUserData() |
2 failures: drops the order draft of the user that is leaving y drops the data of the previous user when the session changes hands |
Ninguno pasa por construcción.
El hallazgo 9 es el más fino de los dos, y la explicación del PR es la parte que más me gustó: el 401 no habla de la sesión de ahora, habla de la credencial con la que salió el request. sentWithCurrentSession compara el header que efectivamente viajó contra el token actual, que es la única forma de distinguir «mi sesión venció» de «llegó tarde un 401 de la sesión anterior». Y la consecuencia que evita —que esa pestaña escriba la sesión vacía y le borre la sesión recién abierta a la otra— es de las que en producción se reportan como «se me cierra solo cada tanto» y nadie reproduce.
El hallazgo 8 es más simple pero igual de real: el borrador vive en sessionStorage, así que sobrevivía al logout dentro de la misma pestaña. Que clearUserData() junte las dos cosas que pertenecen al usuario que se va —cache de React Query y borrador— deja un solo lugar donde agregar la tercera el día que exista.
✅ Detalles que están bien elegidos
- El
mergeque cierra la sesión cuando el storage no tiene token. Es lo que hace que el logout de una pestaña se propague de verdad y no sólo «no pise» a la otra. event.key === nulltratado como cambio. Es ellocalStorage.clear()de otra pestaña, y es exactamente el caso que se escapa cuando uno filtra por clave.followSessionAcrossTabs()devuelve su propia baja. Registrarlo enmain.tsxy no dentro de un componente evita que el listener dependa del ciclo de vida de una pantalla.- El mensaje propio para el 429 llega con su card: sin el backend simplemente no aparece.
🟡 Un caso de borde, para que quede decidido y no descubierto
clearUserData() corre cuando cambia el token, no sólo cuando desaparece. Así que si alguien tiene el alta manual a medio cargar en la pestaña A y en la pestaña B vuelve a loguearse con la misma cuenta, el token nuevo dispara el borrado y la pestaña A pierde el borrador.
Es correcto para el caso que la card persigue —cambió de dueño la sesión, lo del anterior se va— y el caso que describo es raro. Pero si querés distinguirlos, alcanza con comparar el company_id (o el sub) del JWT viejo contra el nuevo y vaciar sólo cuando cambia de persona. Lo digo por si conviene anotarlo, no para que lo cambies acá.
Aparte: coordinación con TESIS-107
Sin impacto en este PR, pero por si lo ves al mergear: api#86 cambia el cuerpo de error del registro a { "error": "..." }. Este front no consume ese endpoint, así que no lo afecta.
Veredicto
APPROVE.
Los dos hallazgos del front están cerrados, con tests que fallan si se deshacen, y el del 401 tardío es un arreglo que no se le ocurre a cualquiera. El caso de borde del relogin en otra pestaña no bloquea.
🤖 Generated with Claude Code
Ticket de Jira
https://proyectofinalfrlp.atlassian.net/browse/TESIS-82
Descripción
Corrige los dos hallazgos del front de la validación del módulo de Auth y Multi-tenancy (TESIS-82), y agrega un mensaje para el nuevo límite de intentos del login.
persistno la actualiza cuando otra escribe. Al cerrar sesión en una pestaña, la otra seguía mostrando datos con un token revocado. Cuando esa otra recibía el 401, escribía la sesión vacía en localStorage y borraba la sesión guardada de quien había entrado mientras tanto.No depende del PR de backend: sin el límite de intentos, el 429 simplemente no llega.
Cambios:
authStorela funciónclearUserData(), que el logout usa para vaciar todo lo del usuario que se va: la cache de React Query (como antes) y el borrador deorderDraftStore(hallazgo 8)followSessionAcrossTabs()enauthStore, que se registra enmain.tsx: escucha el eventostoragey rehidrata el store cuando otra pestaña cambia la sesión. Si cambió el token, vacía lo del usuario anterior (hallazgo 9)mergede la rehidratación: si el storage no tiene token, la pestaña cierra su sesión en vez de conservar la que tenía en memoriaclient.tspara que solo cierre la sesión ante un 401 de un request que salió con el token actual. Un 401 tardío de un token viejo ya no cierra la sesión nueva (hallazgo 9)loginErrorMessageun mensaje propio para el 429, en lugar del genérico «Probá de nuevo en unos segundos»authStoretodavía describía el store de antes de TESIS-117Evidencia visual
El único cambio visible es el mensaje del login ante un 429, que solo aparece con el PR de backend mergeado:
Cómo probar
Precondición: API local con los seeds (contraseña
password123) ynpm run dev. El caso 4 necesita el PR de TESIS-82 en proyecto-api.Caso 1: el logout vacía el borrador de la orden
admin@norte.com, ir a/orders/newy completar el paso 1 (cliente, documento y un producto)/orders/new→ El paso 1 aparece vacío
Caso 2: cerrar sesión en otra pestaña
→ La pestaña B vuelve sola al login, sin recargar ni tocar nada
Caso 3: entrar con otra cuenta en otra pestaña
operador@norte.com→ La B toma esa sesión y el menú de usuario muestra
operador@norte.com→ La sesión sigue abierta
Caso 4: límite de intentos
→ Aparece «Hubo demasiados intentos. Esperá unos minutos y volvé a probar.»
npm run testpasa 550 tests en 59 archivos, ynpm run lintynpm run buildterminan sin errores.Impacto y consideraciones
¿Introduce breaking changes?
No.
¿Requiere nuevas variables de entorno?
No.
¿Afecta la arquitectura o genera un nuevo patrón?
Sí. La sesión se sincroniza entre pestañas con
followSessionAcrossTabs(), que se registra una sola vez enmain.tsx. Está documentado en la sección «La sesión entre pestañas» dedocs/adr/ADR-002-state-management.md. Es el único store que se sincroniza:uiStoreytenantStoreguardan preferencias y datos del portal, que no cambian de dueño.🤖 Generated with Claude Code