feat(web): login, sesión, interceptor, guards y layout con menú por permisos

- Login contra POST /api/auth/login; SessionStore en localStorage con el
  timeout deslizante que informa el servidor (sessionTimeoutMinutes).
- Interceptor: Bearer en /api, renueva actividad y, ante 401, cierra la
  sesión y vuelve al login recordando la ruta (solo rutas internas).
- Guards: sesión, invitado y permiso por pantalla (data.path). Corrige que
  inject() no puede usarse tras un await en permissionGuard.
- Layout con barra superior (empresa, usuario, tema, salir) y menú lateral
  filtrado por GET /api/user/menu; plegable y con botón en pantallas angostas.
- Menú único en libs/sap-types (MENU, filterMenu); el backend verifica por
  test que las rutas de permiso de sus recursos existan en él.
- Una ruta por pantalla del menú; las no construidas muestran "no disponible".
- Verificado en Chrome headless contra el backend y SAP reales: redirección
  sin sesión, menú de 20 pantallas, navegación, plegado, tema oscuro y
  persistencia, vista móvil, salir, token inválido y credenciales malas.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-13 19:18:05 -05:00
1 parent f1156929c7
commit 4c8c499a72
36 files changed
+1291 -52

No files matched your search

+1
View File
@@ -24,6 +24,7 @@ describe('Auth (e2e, SAP simulado)', () => {
it('flujo completo: login -> me -> ruta protegida -> logout -> 401', async () => {
const res = await login('ana', 'secreto').expect(200);
expect(res.body.user).toMatchObject({ webUser: 'ana', empId: 7, firstName: 'ana' });
expect(res.body.sessionTimeoutMinutes).toBe(30);
expect(JSON.stringify(res.body)).not.toContain('secreto');
const auth = { Authorization: `Bearer ${res.body.token}` };