Royal Clean CRM · Documentación
Arquitectura

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:

RolQuién esAlcance
ADMINLos dos propietariosTodo el sistema
EMPLOYEEPersonal 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

GuardaComportamiento
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

GuardaError 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ónADMINEMPLOYEE
Ver el panel administrativoNo
Crear y editar clientesNo
Crear y editar propiedadesNo
Ver códigos de acceso de una propiedadSólo si está asignado y el servicio está activo
Crear empleadosNo
Ver tarifas y salariosNo, en ningún caso
Crear serviciosNo
Asignar empleadosNo
Cancelar un servicioNo
Ver un servicioTodosSólo los suyos
Iniciar y finalizar un servicioNoSí, si está asignado
Completar tareasNoSí, si está asignado
Subir evidenciaSí, si está asignado y en progreso
Eliminar evidenciaCualquieraSólo la suya y con el servicio en progreso
Reportar incidentesNoSí, si está asignado
Resolver incidentesNo
Corregir horariosNo
Registrar pagosNo
Ver el precio de un servicioNo
Ver su propio historial y horasSí, sin importes
Generar el reporte PDFNo
Ver la auditoríaNo
Cambiar la configuraciónNo

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 servicioEmpleado asignado
DRAFTNo — el servicio aún no está confirmado
ASSIGNED
IN_PROGRESS
COMPLETEDSólo durante 2 horas tras completarlo
CANCELLEDNo

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 doorCode

Ademá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

  1. ¿Quién puede hacer esto? Elige la guarda correcta.
  2. Llámala al principio de la acción, antes de tocar nada.
  3. Si es específica de un servicio, usa requireServiceAccess o requireAssignedEmployee: nunca compares identificadores a mano.
  4. Si devuelve datos, comprueba que el select no arrastra campos que ese rol no debe ver.
  5. Si toca dinero, tiempo o permisos, escribe la auditoría dentro de la misma transacción.
  6. Añade un test de integración que intente el acceso indebido y espere un 404.

On this page