Autorización
Quién puede hacer qué, dónde se comprueba y por qué un recurso ajeno responde 404.
Regla fundamental
Ocultar un botón no es autorización. Toda comprobación que importe ocurre en el servidor, dentro de la acción o la consulta, no en el componente que la dibuja.
Un empleado que manipule un identificador en la URL no debe obtener nada. Y no debe poder distinguir entre «este servicio no existe» y «este servicio existe pero no es tuyo»: ambos casos devuelven 404 con el mismo mensaje.
Roles
Sólo existen dos:
| Rol | Quién es | Alcance |
|---|---|---|
ADMIN | Los dos propietarios | Todo el sistema |
EMPLOYEE | Personal de campo | Únicamente sus propias asignaciones |
No hay jerarquía, ni permisos granulares, ni roles personalizados. Añadirlos sería complejidad sin demanda.
Guardas centrales
Todas viven en src/modules/auth/guards.ts.
Ninguna ruta consulta user.role a mano.
Para páginas — redirigen
| Guarda | Comportamiento |
|---|---|
requireUser() | Sin sesión → /login. Con contraseña temporal → /cambiar-contrasena |
requireUserAllowingPasswordChange() | Igual, pero sin el segundo redirect (la usa esa misma página) |
requireAdmin() | No es ADMIN → /app |
requireEmployee() | No es EMPLOYEE → /admin |
Para acciones y route handlers — lanzan
| Guarda | Error si falla |
|---|---|
requireUserOrThrow() | AuthorizationError 401 |
requireAdminOrThrow() | AuthorizationError 403 |
requireEmployeeOrThrow() | AuthorizationError 403 |
requireServiceAccess(serviceId) | NotFoundError 404 si el servicio no es suyo |
requireAssignedEmployee(serviceId) | NotFoundError 404, o 403 si el actor es un admin |
requirePropertyAccess(propertyId) | NotFoundError 404 |
Por qué 404 y no 403
if (user.role !== "ADMIN" && !isAssignedEmployee) {
// Deliberadamente 404: no confirmamos la existencia del servicio.
throw new NotFoundError("El servicio no existe.");
}Un 403 confirmaría que el recurso existe. Con identificadores secuenciales eso permitiría enumerar el negocio entero; con cuid sigue siendo información que no hace falta dar.
Matriz de permisos
| Recurso / acción | ADMIN | EMPLOYEE |
|---|---|---|
| Ver el panel administrativo | Sí | No |
| Crear y editar clientes | Sí | No |
| Crear y editar propiedades | Sí | No |
| Ver códigos de acceso de una propiedad | Sí | Sólo si está asignado y el servicio está activo |
| Crear empleados | Sí | No |
| Ver tarifas y salarios | Sí | No, en ningún caso |
| Crear servicios | Sí | No |
| Asignar empleados | Sí | No |
| Cancelar un servicio | Sí | No |
| Ver un servicio | Todos | Sólo los suyos |
| Iniciar y finalizar un servicio | No | Sí, si está asignado |
| Completar tareas | No | Sí, si está asignado |
| Subir evidencia | Sí | Sí, si está asignado y en progreso |
| Eliminar evidencia | Cualquiera | Sólo la suya y con el servicio en progreso |
| Reportar incidentes | No | Sí, si está asignado |
| Resolver incidentes | Sí | No |
| Corregir horarios | Sí | No |
| Registrar pagos | Sí | No |
| Ver el precio de un servicio | Sí | No |
| Ver su propio historial y horas | Sí | Sí, sin importes |
| Generar el reporte PDF | Sí | No |
| Ver la auditoría | Sí | No |
| Cambiar la configuración | Sí | No |
Aislamiento del personal de campo
Todas las consultas de
src/modules/services/employee-queries.ts
filtran en el WHERE de SQL:
where: {
assignments: { some: { employeeId, releasedAt: null } },
...
}El filtro está en la consulta, no en la interfaz. Un empleado no puede enumerar servicios ajenos ni aunque construya la petición a mano.
Información sensible de propiedades
Los códigos de puerta y de caja de llaves son la parte más delicada del sistema: filtrarlos equivale a entregar la llave de la casa de un cliente.
La decisión vive en un módulo puro con tests
(src/modules/properties/sensitive.ts):
| Estado del servicio | Empleado asignado |
|---|---|
DRAFT | No — el servicio aún no está confirmado |
ASSIGNED | Sí |
IN_PROGRESS | Sí |
COMPLETED | Sólo durante 2 horas tras completarlo |
CANCELLED | No |
La ventana de gracia cubre el caso real de cerrar la casa después de haber pulsado «Finalizar servicio».
Cuando el acceso está denegado, el servidor elimina los campos antes de serializar. No viajan al navegador ocultos con CSS:
const sensitive = applySensitiveAccess(service.property, { ... });
// sensitive.property ya no contiene doorCodeAdemás, aun teniendo derecho, los códigos aparecen tapados hasta que el empleado pulsa «Mostrar códigos», y nunca se incluyen en el texto de una notificación, que aparecería en la pantalla bloqueada del teléfono.
Dinero y personal de campo
El personal nunca ve importes. Ni su tarifa, ni lo que gana, ni el precio
del servicio, ni el margen. Esto se garantiza en la capa de consultas: los
select de employee-queries.ts y employee-detail.ts sencillamente no piden
esos campos.
Su historial muestra fecha, propiedad, duración y estado de las tareas.
Sesiones y revocación
Las sesiones son opacas y viven en PostgreSQL. La cookie contiene el token en claro; la base guarda su SHA-256, así que un volcado de la tabla no permite suplantar a nadie.
Se revocan todas las sesiones de un usuario cuando:
- Se desactiva su cuenta.
- Un administrador restablece su contraseña.
- El propio usuario cambia su contraseña (menos la sesión desde la que actúa).
Un usuario archivado o inactivo se trata como no autenticado aunque su cookie siga siendo criptográficamente válida.
Al añadir una función nueva
- ¿Quién puede hacer esto? Elige la guarda correcta.
- Llámala al principio de la acción, antes de tocar nada.
- Si es específica de un servicio, usa
requireServiceAccessorequireAssignedEmployee: nunca compares identificadores a mano. - Si devuelve datos, comprueba que el
selectno arrastra campos que ese rol no debe ver. - Si toca dinero, tiempo o permisos, escribe la auditoría dentro de la misma transacción.
- Añade un test de integración que intente el acceso indebido y espere un 404.