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

+32 -2
View File
@@ -1,3 +1,33 @@
import { Routes } from '@angular/router';
import type { Routes } from '@angular/router';
import { MENU } from '@basic-erp/sap-types';
import { authGuard, guestGuard, menuLoadedGuard, permissionGuard } from './core/auth/guards';
export const routes: Routes = [];
/** Una ruta por pantalla del menú; las que aún no existen muestran "no disponible". */
const screens: Routes = MENU.flatMap((m) => m.items).map((item) => ({
path: item.path.slice(1),
canActivate: [permissionGuard],
data: { path: item.path, title: item.label },
loadComponent: () => import('./features/shared/coming-soon.page').then((m) => m.ComingSoonPage),
}));
export const routes: Routes = [
{
path: 'login',
canActivate: [guestGuard],
loadComponent: () => import('./features/login/login.page').then((m) => m.LoginPage),
},
{
path: '',
canActivate: [authGuard, menuLoadedGuard],
loadComponent: () => import('./layout/main-layout').then((m) => m.MainLayout),
children: [
{ path: '', pathMatch: 'full', loadComponent: () => import('./features/home/home.page').then((m) => m.HomePage) },
{
path: 'sin-acceso',
loadComponent: () => import('./features/shared/no-access.page').then((m) => m.NoAccessPage),
},
...screens,
],
},
{ path: '**', redirectTo: '' },
];