Skip to content

fix: [TESIS-82] clear the user state on logout and keep tabs in sync - #51

Merged
TomasMartin2004 merged 3 commits into
masterfrom
TESIS-82-auth-multitenancy-qa-fixes
Sep 25, 2026
Merged

TomasMartin2004 merged 3 commits into
masterfrom
TESIS-82-auth-multitenancy-qa-fixes

Conversation

@LoLoo03

@LoLoo03 LoLoo03 commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

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.

  • El logout no vaciaba el borrador del alta de órdenes (hallazgo 8). El borrador vive en sessionStorage, así que quien entraba después en la misma pestaña veía el cliente, el documento y las líneas del usuario anterior.
  • Las pestañas no se enteraban de los cambios de sesión de las otras (hallazgo 9). Cada pestaña tiene su propia copia del store en memoria, y persist no 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:

  • Agrega a authStore la función clearUserData(), que el logout usa para vaciar todo lo del usuario que se va: la cache de React Query (como antes) y el borrador de orderDraftStore (hallazgo 8)
  • Agrega followSessionAcrossTabs() en authStore, que se registra en main.tsx: escucha el evento storage y rehidrata el store cuando otra pestaña cambia la sesión. Si cambió el token, vacía lo del usuario anterior (hallazgo 9)
  • Corrige el merge de 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 memoria
  • Corrige el interceptor de client.ts para 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)
  • Agrega en loginErrorMessage un mensaje propio para el 429, en lugar del genérico «Probá de nuevo en unos segundos»
  • Actualiza ADR-002 con la sección «La sesión entre pestañas», y la guía de arquitectura, cuya entrada de authStore todavía describía el store de antes de TESIS-117

Evidencia visual

El único cambio visible es el mensaje del login ante un 429, que solo aparece con el PR de backend mergeado:

Antes Después
No pudimos iniciar sesión. Probá de nuevo en unos segundos. Hubo demasiados intentos. Esperá unos minutos y volvé a probar.

Cómo probar

Precondición: API local con los seeds (contraseña password123) y npm 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

  1. Loguearse como admin@norte.com, ir a /orders/new y completar el paso 1 (cliente, documento y un producto)
  2. Cerrar sesión desde el menú de usuario
  3. Volver a loguearse y entrar a /orders/new
    → El paso 1 aparece vacío

Caso 2: cerrar sesión en otra pestaña

  1. Loguearse y abrir la app en dos pestañas
  2. Cerrar sesión en la pestaña A
    → La pestaña B vuelve sola al login, sin recargar ni tocar nada

Caso 3: entrar con otra cuenta en otra pestaña

  1. Con la pestaña B en el login (como quedó en el caso 2), loguearse en la A como operador@norte.com
    → La B toma esa sesión y el menú de usuario muestra operador@norte.com
  2. Recargar la pestaña A
    → La sesión sigue abierta

Caso 4: límite de intentos

  1. En el login, ingresar una contraseña incorrecta 10 veces seguidas
  2. Ingresar la contraseña correcta
    → Aparece «Hubo demasiados intentos. Esperá unos minutos y volvé a probar.»

npm run test pasa 550 tests en 59 archivos, y npm run lint y npm run build terminan 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 en main.tsx. Está documentado en la sección «La sesión entre pestañas» de docs/adr/ADR-002-state-management.md. Es el único store que se sincroniza: uiStore y tenantStore guardan preferencias y datos del portal, que no cambian de dueño.


🤖 Generated with Claude Code

LorenzoBellomo and others added 3 commits September 24, 2026 19:34
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>
@LoLoo03
LoLoo03 requested a review from Sanntinat September 24, 2026 23:44
@LoLoo03
LoLoo03 requested a review from a team as a code owner September 24, 2026 23:44

@LauAubert LauAubert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TomasMartin2004 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 merge que 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 === null tratado como cambio. Es el localStorage.clear() de otra pestaña, y es exactamente el caso que se escapa cuando uno filtra por clave.
  • followSessionAcrossTabs() devuelve su propia baja. Registrarlo en main.tsx y 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

https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp

@TomasMartin2004
TomasMartin2004 merged commit 722298c into master Sep 25, 2026
4 checks passed
@TomasMartin2004
TomasMartin2004 deleted the TESIS-82-auth-multitenancy-qa-fixes branch September 25, 2026 02:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants