Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions backend/middlewares/roleCheck.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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()
}
}

Expand Down
13 changes: 8 additions & 5 deletions backend/services/auth/module-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppModule, (role: string) => 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
}
Expand Down
161 changes: 161 additions & 0 deletions backend/tests/auth/role-access.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, Record<AppModule, boolean>> = {
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/)
})
})
7 changes: 0 additions & 7 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading