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

@@ -0,0 +1,36 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { SessionStore } from '../../core/auth/session.store';
import { MenuService } from '../../core/menu/menu.service';
@Component({
selector: 'sbo-home-page',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterLink],
template: `
<h1>Bienvenido, {{ session.user()?.firstName }}</h1>
<p class="sub">{{ session.company() }}</p>
@for (m of menu.modules(); track m.id) {
<section>
<h2>{{ m.label }}</h2>
<div class="links">
@for (i of m.items; track i.path) {
<a [routerLink]="i.path">{{ i.label }}</a>
}
</div>
</section>
}
`,
styles: `
h1 { margin: 0; font-size: 18px; font-weight: 600; }
.sub { margin: 2px 0 16px; color: var(--quiet); }
h2 { margin: 12px 0 6px; font-size: 13px; color: var(--quiet); text-transform: uppercase; letter-spacing: .4px; }
.links { display: flex; flex-wrap: wrap; gap: 6px; }
a { padding: 4px 12px; background: var(--panel); border: 1px solid var(--line); border-radius: 3px; color: var(--ink); text-decoration: none; }
a:hover { background: var(--row-sel); border-color: var(--focus); }
`,
})
export class HomePage {
protected readonly session = inject(SessionStore);
protected readonly menu = inject(MenuService);
}