diff --git a/backend/middlewares/roleCheck.ts b/backend/middlewares/roleCheck.ts index 96230a2..cb402bb 100644 --- a/backend/middlewares/roleCheck.ts +++ b/backend/middlewares/roleCheck.ts @@ -1,6 +1,7 @@ // middlewares/roleCheck.ts import { Request, Response, NextFunction } from 'express' +import { FRONT_DESK_ROLES } from '../services/auth/module-access.js' type RoleCheckMiddleware = (req: Request, res: Response, next: NextFunction) => void @@ -159,24 +160,22 @@ export const canAccessMaintenance: RoleCheckMiddleware = (req, res, next) => { } /** - * Excluye al rol mantenimiento de acceder a módulos generales + * Verifica que el usuario tenga uno de los roles de mostrador * Uso: Para rutas de logbook, parking, blacklist, etc. - * El rol mantenimiento SOLO puede acceder a /api/maintenance + * Solo admin, recepcionista y group-admin pueden acceder; el resto (mantenimiento, etc.) no. */ export const excludeMantenimiento: RoleCheckMiddleware = (req, res, next) => { - const blockedRoles = ['mantenimiento'] - if (!req.user?.role) { res.status(403).json({ error: 'No se pudo verificar el rol del usuario' }) return } - if (blockedRoles.includes(req.user.role.toLowerCase())) { + if (FRONT_DESK_ROLES.includes(req.user.role.toLowerCase())) { + next() + } else { res.status(403).json({ - error: 'Acceso denegado. Tu rol solo tiene acceso al módulo de mantenimiento', + error: 'Acceso denegado. Tu rol no tiene acceso a este módulo', }) - } else { - next() } } diff --git a/backend/services/auth/module-access.ts b/backend/services/auth/module-access.ts index 13fff2f..d688349 100644 --- a/backend/services/auth/module-access.ts +++ b/backend/services/auth/module-access.ts @@ -7,14 +7,17 @@ export type AppModule = 'parking' | 'maintenance' | 'groups' | 'blacklist' | 'cashier' | 'logbook' -const allButMaintenanceRole = (role: string) => role !== 'mantenimiento' +// Front-desk modules: an allowlist, so a new role gets no access until someone decides it +export const FRONT_DESK_ROLES: readonly string[] = ['admin', 'recepcionista', 'group-admin'] + +const frontDeskRoles = (role: string) => FRONT_DESK_ROLES.includes(role) const staffRoles = ['admin', 'recepcionista', 'group-admin', 'mantenimiento'] const MODULE_ACCESS: Record boolean> = { - parking: allButMaintenanceRole, // excludeMantenimiento - blacklist: allButMaintenanceRole, // excludeMantenimiento - cashier: allButMaintenanceRole, // excludeMantenimiento - logbook: allButMaintenanceRole, // excludeMantenimiento + parking: frontDeskRoles, // excludeMantenimiento + blacklist: frontDeskRoles, // excludeMantenimiento + cashier: frontDeskRoles, // excludeMantenimiento + logbook: frontDeskRoles, // excludeMantenimiento maintenance: (role) => staffRoles.includes(role), // canAccessMaintenance groups: (role) => staffRoles.includes(role), // canViewGroups } diff --git a/backend/tests/auth/role-access.test.ts b/backend/tests/auth/role-access.test.ts new file mode 100644 index 0000000..b9fbd9c --- /dev/null +++ b/backend/tests/auth/role-access.test.ts @@ -0,0 +1,161 @@ +// tests/auth/role-access.test.ts +// Pin the current role access behaviour for excludeMantenimiento and +// canOpenModule. This test documents what the code does today; B4 will +// change the implementation but keep these tests green. + +import { describe, it, expect, vi } from 'vitest' +import type { Request, Response } from 'express' +import { excludeMantenimiento } from '../../middlewares/roleCheck.js' +import { canOpenModule, type AppModule } from '../../services/auth/module-access.js' +import { readFileSync } from 'fs' +import { resolve } from 'path' + +// Helper to create mock req/res/next +function createMockReqRes(role?: string) { + const req = { + user: role ? { role } : undefined, + } as unknown as Request + const res = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + } as unknown as Response + const next = vi.fn() + return { req, res, next } +} + +describe('excludeMantenimiento', () => { + it('allows admin', () => { + const { req, res, next } = createMockReqRes('admin') + excludeMantenimiento(req, res, next) + expect(next).toHaveBeenCalled() + expect(res.status).not.toHaveBeenCalled() + }) + + it('allows recepcionista', () => { + const { req, res, next } = createMockReqRes('recepcionista') + excludeMantenimiento(req, res, next) + expect(next).toHaveBeenCalled() + expect(res.status).not.toHaveBeenCalled() + }) + + it('allows group-admin', () => { + const { req, res, next } = createMockReqRes('group-admin') + excludeMantenimiento(req, res, next) + expect(next).toHaveBeenCalled() + expect(res.status).not.toHaveBeenCalled() + }) + + it('blocks mantenimiento with 403', () => { + const { req, res, next } = createMockReqRes('mantenimiento') + excludeMantenimiento(req, res, next) + expect(next).not.toHaveBeenCalled() + expect(res.status).toHaveBeenCalledWith(403) + }) + + it('blocks missing req.user with 403', () => { + const { req, res, next } = createMockReqRes(undefined) + excludeMantenimiento(req, res, next) + expect(next).not.toHaveBeenCalled() + expect(res.status).toHaveBeenCalledWith(403) + }) + + it('allows ADMIN in uppercase (toLowerCase)', () => { + const { req, res, next } = createMockReqRes('ADMIN') + excludeMantenimiento(req, res, next) + expect(next).toHaveBeenCalled() + expect(res.status).not.toHaveBeenCalled() + }) + + it('blocks unknown role "auditor" with 403', () => { + const { req, res, next } = createMockReqRes('auditor') + excludeMantenimiento(req, res, next) + expect(next).not.toHaveBeenCalled() + expect(res.status).toHaveBeenCalledWith(403) + }) +}) + +describe('canOpenModule', () => { + const roles = ['admin', 'recepcionista', 'group-admin', 'mantenimiento', 'auditor'] as const + const modules: AppModule[] = [ + 'parking', + 'maintenance', + 'groups', + 'blacklist', + 'cashier', + 'logbook', + ] + + // Expected results based on implementation: + // - parking, blacklist, cashier, logbook: frontDeskRoles (admin, recepcionista, group-admin) + // - maintenance: staffRoles (admin, recepcionista, group-admin, mantenimiento) + // - groups: staffRoles (admin, recepcionista, group-admin, mantenimiento) + const expected: Record> = { + admin: { + parking: true, + maintenance: true, + groups: true, + blacklist: true, + cashier: true, + logbook: true, + }, + recepcionista: { + parking: true, + maintenance: true, + groups: true, + blacklist: true, + cashier: true, + logbook: true, + }, + 'group-admin': { + parking: true, + maintenance: true, + groups: true, + blacklist: true, + cashier: true, + logbook: true, + }, + mantenimiento: { + parking: false, + maintenance: true, + groups: true, + blacklist: false, + cashier: false, + logbook: false, + }, + auditor: { + parking: false, + maintenance: false, + groups: false, + blacklist: false, + cashier: false, + logbook: false, + }, + } + + it.each(roles)('role %s', (role) => { + for (const mod of modules) { + expect(canOpenModule(role, mod)).toBe(expected[role][mod]) + } + }) +}) + +describe('router wiring: excludeMantenimiento', () => { + const routersWithExcludeMantenimiento = [ + 'routes/blacklist/blacklist-routes.ts', + 'routes/cashier/cashier-routes.ts', + 'routes/checklist/checklist-routes.ts', + 'routes/conciliation/conciliation.routes.ts', + 'routes/departments/departments-routes.ts', + 'routes/logbook/logbook-routes.ts', + 'routes/parking/bookings.routes.ts', + 'routes/parking/parking.routes.ts', + 'routes/parking/stats.routes.ts', + ] + + it.each(routersWithExcludeMantenimiento)('%s uses excludeMantenimiento', (routerPath) => { + const fullPath = resolve(process.cwd(), routerPath) + const content = readFileSync(fullPath, 'utf-8') + expect(content).toContain('excludeMantenimiento') + expect(content).toMatch(/router\.use\(.*excludeMantenimiento/) + }) +}) diff --git a/docs/TODO.md b/docs/TODO.md index 473ba85..539dd3f 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -286,13 +286,6 @@ preview. de Python. Enlazar la demo y el repo. Es el gancho para promocionar el proyecto. Se escribe en el repo `portfolio-bp`, no aquí. _Decidido el 2026-10-05._ -- [ ] **`excludeMantenimiento` es una lista de exclusión** — parking, lista - negra, caja, partes, conciliación y departamentos dejan pasar cualquier - rol salvo `mantenimiento`, y `services/auth/module-access.ts` lo replica. - Hoy solo hay roles conocidos (el rol va en el token firmado y sale de - `roles`), pero un rol nuevo tendría acceso sin que nadie lo decida. Pasar - a lista de permitidos. _Comprobado el 2026-10-05 al retirar - `demo-admin`._ - [ ] **`20260512_disable_demo_user.sql` desactivaría la cuenta demo** si se volviera a ejecutar (`UPDATE users SET is_active = 0 WHERE username = 'demo'`). `setup:local` no la ejecuta (es anterior al baseline); no