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:
1 parent
f1156929c7
commit
4c8c499a72
36 files changed
+1291
-52
No files matched your search
@@ -24,7 +24,12 @@ export class AuthController {
|
||||
@HttpCode(200)
|
||||
async login(@Body(new ZodValidationPipe(loginSchema)) body: LoginDto, @Req() req: AuthenticatedRequest) {
|
||||
const { token, user } = await this.auth.login(body.username, body.password, req.ip ?? 'desconocida');
|
||||
return { token, user, company: this.config.get('SAPB1_SL_COMPANYDB') };
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
company: this.config.get('SAPB1_SL_COMPANYDB'),
|
||||
sessionTimeoutMinutes: this.config.get('SESSION_TIMEOUT_MINUTES'),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { menuPaths } from '@basic-erp/sap-types';
|
||||
import { SL_RESOURCES, SL_RESOURCE_MAP } from './sl-resources.js';
|
||||
import { entityPath, getQuerySchema, listQuerySchema, pickSlHeaders } from './sl-request.js';
|
||||
|
||||
@@ -14,6 +15,15 @@ describe('registro de recursos', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('todas las rutas de permiso existen en el menú compartido', () => {
|
||||
const known = new Set(menuPaths());
|
||||
for (const r of SL_RESOURCES) {
|
||||
for (const path of [r.read, r.write].flat()) {
|
||||
if (path !== null) expect(known.has(path), `${r.name}: ${path}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('incluye los recursos pedidos', () => {
|
||||
for (const n of ['Quotations', 'Orders', 'PurchaseOrders', 'StockTransfers', 'InventoryGenEntries', 'InventoryGenExits', 'InventoryTransferRequests']) {
|
||||
expect(SL_RESOURCE_MAP.has(n), n).toBe(true);
|
||||
|
||||
Reference in new issue
Block a user