Apariencia
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/meSÍ incluyenpermissions(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
clientsincluyendoweb. - Mobile: requiere ≥1 permiso en módulo con
clientsincluyendomobile(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(trasAccessJwtGuard).- El guard llama
PermissionsService.getPermissionsForRole(roleId)(cache-aside):GET perms:role:{roleId}en Redis → HIT: ~1 ms- MISS (o Redis caído) → consulta PostgreSQL (fuente de verdad) →
SETcon TTL 15 min - Si falta el permiso requerido → 403.
- Invalidación:
PUT /roles/:id/permissionsyDELETE /roles/:idhacenDELde 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 — verplataforma/superadmin-flow.md) → sin sesión, devuelveonboardingTokenpara que el invitado nombre su empresa enPOST /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.
Flujo de invitación (magic link)
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).
invitedque 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
Userse creainvitedsinOrganizationUser.accept-invitationlo detecta (sin membresía) y en vez de "estado active, logueado" deja al usuario enpending_onboardingcon unonboardingToken— completa recién al nombrar su empresa enPOST /auth/onboarding. Verplataforma/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
| Ruta | Tipo | Guard |
|---|---|---|
/accept-invitation?token=... | pública | sin 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 desdecamaroneras_platform— verplataforma/superadmin-flow.md(/organizations/:id/members,/organizations/:id/roles, gateadas porusuarios-tenant/roles-tenant).
- Tras login, si
clientsno incluyeweb→ 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).