Skip to content

Flujo RBAC — Roles, Permisos y Usuarios

Base URL: /api/v1. Implementado en camaroneras_backend (módulos permissions, roles, users).

Requests y responses 2xx: bodies (tipos, opcionales, enums) y el shape de las respuestas exitosas están en la API Reference, generada desde el spec OpenAPI. Acá quedan solo las respuestas de error (4xx/5xx) y las reglas de negocio/flujo.

Modelo

Role ──< Permission >── Module (clients: web|mobile)
                    └── Action (ver|crear|editar|eliminar|exportar)
OrganizationUser: User ↔ Organization ↔ Role
  • Module.clients: a qué app(s) pertenece el módulo. Define acceso por cliente.
  • Permisos vía cache Redis (NO en el JWT): el access token solo lleva identidad (roleId, role, etc.). El guard resuelve los permisos por rol en cada request: perms:role:{roleId} en Redis (TTL seguridad 15 min) con fallback a PostgreSQL. Un cambio en la matriz de un rol aplica al instante (invalidación por DEL).
  • Las respuestas de signin/onboarding/accept-invitation y GET /auth/me SÍ incluyen permissions (los clientes las usan para pintar su UI; la autorización real es el guard).
  • Catálogo (Module/Action) se siembra al arrancar (idempotente). Crece con cada módulo nuevo.

Roles por defecto (al crear organización)

  • Se siembran Administrador, Técnico, Bodeguero.
  • Administrador: todos los permisos de los módulos existentes.
  • Técnico/Bodeguero: sin permisos administrativos por ahora (se agregan cuando existan sus módulos).

Acceso por cliente (web vs mobile)

Derivado de permisos, NO del nombre del rol:

  • Admin web: requiere ≥1 permiso en módulo con clients incluyendo web.
  • Mobile: requiere ≥1 permiso en módulo con clients incluyendo mobile (gate al construir mobile).

La respuesta de signin/onboarding/accept-invitation y GET /auth/me incluyen clients y role. El admin web usa clients para bloquear cuentas solo-de-campo.

Enforcement

  • @RequirePermission('modulo','accion') + PermissionGuard (tras AccessJwtGuard).
  • El guard llama PermissionsService.getPermissionsForRole(roleId) (cache-aside):
    1. GET perms:role:{roleId} en Redis → HIT: ~1 ms
    2. MISS (o Redis caído) → consulta PostgreSQL (fuente de verdad) → SET con TTL 15 min
    3. Si falta el permiso requerido → 403.
  • Invalidación: PUT /roles/:id/permissions y DELETE /roles/:id hacen DEL de la clave tras confirmar la transacción → el cambio aplica en el siguiente request.
  • Redis caído: la app sigue funcionando (fallback a PostgreSQL + warning en logs).
  • La UI de los clientes puede quedar desactualizada hasta renovar sesión (cosmético); la autorización del backend siempre está fresca.

Endpoints

GET /catalog/modules — (roles:ver)

Catálogo de módulos y acciones para construir la matriz de permisos.

GET /roles — (roles:ver)

Lista de roles de la organización.

POST /roles — (roles:crear)

Crear rol personalizado.

  • name único por organización → 409.

PATCH /roles/:id — (roles:editar)

Renombrar rol (solo si no es por defecto).

json
// response 400 (rol por defecto)
{ "error": "...", "message": "No se puede editar un rol por defecto", "statusCode": 400 }

DELETE /roles/:id — (roles:eliminar)

Eliminar rol personalizado.

json
// response 400 (rol por defecto o usuarios asignados)
{ "error": "...", "message": "No se puede eliminar: rol por defecto o tiene usuarios", "statusCode": 400 }

GET /roles/:id/permissions — (roles:ver)

Matriz de permisos de un rol (módulo × acción).

PUT /roles/:id/permissions — (roles:editar)

Reemplazar matriz de permisos de un rol (invalidación Redis automática).

  • Reemplaza toda la matriz de ese rol → DEL de Redis automático.

GET /users — (usuarios:ver)

Lista de usuarios de la organización.

POST /users/invite — (usuarios:crear)

Invitar usuario (envía magic link por email).

json
// response 409 (email ya existe en la org)
{ "error": "...", "message": "Email ya existe en la organización", "statusCode": 409 }
  • Email con botón "Activar mi cuenta" → link con token/accept-invitation?token=....

POST /users/:id/resend-invitation — (usuarios:crear)

Reenviar invitación (invalida el token anterior).

POST /users/:id/cancel-invitation — (usuarios:eliminar)

Cancelar invitación (borra el usuario invitado, libera el email).


PATCH /users/:id/role — (usuarios:editar)

Cambiar rol de un usuario (no permite dejar la org sin admin).

json
// response 403 (último admin)
{ "error": "...", "message": "No puedes cambiar el rol del último administrador", "statusCode": 403 }

PATCH /users/:id/deactivate — (usuarios:eliminar)

Desactivar usuario (soft delete; revoca todas sus sesiones activas).

json
// response 403 (último admin)
{ "error": "...", "message": "No puedes desactivar el último administrador", "statusCode": 403 }
  • Usuario desactivado NO puede iniciar sesión (signin → 401 "Tu cuenta está desactivada").

PATCH /users/:id/reactivate — (usuarios:editar)

Reactivar usuario desactivado.


POST /auth/accept-invitation — público

Aceptar invitación (define contraseña). Respuesta polimórfica según needsOnboarding:

  • false (caso de esta sección: invitación a una org ya existente) → sesión completa, activa la cuenta.
  • true (invitación delegada desde plataforma, sin organización todavía — ver plataforma/superadmin-flow.md) → sin sesión, devuelve onboardingToken para que el invitado nombre su empresa en POST /auth/onboarding (plataforma/auth-flow.md).
json
// response 400 (token expirado o inválido)
{ "error": "...", "message": "Invitación inválida o expirada", "statusCode": 400 }
  • Password 8-16 caracteres.
  • Token válido 7 días; se invalida tras usarse.
Admin invita (email + rol)
  → se crea User estado "invited" + OrganizationUser
  → token aleatorio 32 bytes hex; se guarda su sha256 en VerificationToken
    (type "invitation", TTL 7 días, un solo uso)
  → email con botón "Activar mi cuenta" → {WEB_URL}/accept-invitation?token=...
Invitado abre el link → define solo su contraseña
  → POST /auth/accept-invitation { token, password }
  → estado "active", queda logueado (tokens + clients)
  • Email ya existente en el sistema → 409 (multi-org diferido al backlog).
  • invited que intenta signin antes de activar → 401 ("revisa el correo de invitación").
  • Token expirado → reenviar invitación (nuevo token, el anterior queda inválido).
  • Cancelar invitación elimina al usuario invitado por completo y libera el email.
  • Variante delegada (alta de tenants desde plataforma, sin organización todavía): el User se crea invited sin OrganizationUser. accept-invitation lo detecta (sin membresía) y en vez de "estado active, logueado" deja al usuario en pending_onboarding con un onboardingToken — completa recién al nombrar su empresa en POST /auth/onboarding. Ver plataforma/superadmin-flow.md.

Estados de usuario

  • invited + sin contraseña → pendiente de aceptar (acciones: reenviar / cancelar invitación).
  • active + isActive: true → normal (acción: desactivar).
  • active + isActive: false → desactivado; signin → 401 "Tu cuenta está desactivada" (acción: reactivar).
  • Último administrador: no se puede desactivar ni cambiar de rol al único usuario activo cuyo rol tenga roles:ver + roles:editar (definición de administrador).

Flujo cliente — Admin

RutaTipoGuard
/accept-invitation?token=...públicasin token en la URL → redirige a /login

Usuarios y roles ya no tienen UI en camaroneras_admin: la gestión (cross-tenant, de cualquier organización) se hace desde camaroneras_platform — ver plataforma/superadmin-flow.md (/organizations/:id/members, /organizations/:id/roles, gateadas por usuarios-tenant/roles-tenant).

  • Tras login, si clients no incluye web → bloquear ("usa la app móvil").
  • Layout con navegación que muestra solo las secciones permitidas (hook usePermission).
  • Acciones (crear/editar/eliminar) ocultas/deshabilitadas según permisos.

Flujo cliente — Mobile

Pendiente: gate de acceso mobile + módulos de campo (cuando exista la app).