Autenticación
JWT + 3 guards globales (roles legacy y CASL dinámico) + el flujo real de login/refresh.
JWT
El payload es { sub: userId, tenantId, role } (sub, no userId — ojo si buscas el
claim en el token). Se firma y valida en src/modules/auth/.
POST /api/v1/auth/login— público, throttle 5/min.POST /api/v1/auth/refresh— público, throttle 10/min.POST /api/v1/auth/accept-invite— público, crea contraseña desde invitación + login automático.POST /api/v1/auth/logout— requiere JWT; invalida el refresh token en BD y agregablacklist:{userId}en Redis (TTL 900s).GET /api/v1/auth/me— requiere JWT.
JwtStrategy (src/modules/auth/strategies/jwt.strategy.ts) valida el Bearer token,
chequea la blacklist de Redis, carga el User completo de Prisma (con role, roleId,
tenantId, permissions, nodeAccess) y actualiza lastSeenAt (throttled a 1
escritura/2min) para presencia en tiempo real.
Los 3 guards globales
Registrados en src/app.module.ts vía APP_GUARD, en este orden de ejecución:
ThrottlerGuard → JwtAuthGuard → RolesGuard → PermissionsGuard → DemoReadOnlyGuard| Guard | Path | Comportamiento |
|---|---|---|
JwtAuthGuard | src/common/guards/jwt-auth.guard.ts | Si hay @Public(), deja pasar; si no, valida el JWT vía Passport. |
RolesGuard | src/common/guards/roles.guard.ts | Legacy — lee @Roles(...UserRole[]). SUPER_ADMIN siempre pasa. Si no hay roles requeridos, pasa. |
PermissionsGuard | src/common/authorization/permissions.guard.ts | Dinámico (CASL) — lee @CheckPermission(action, subject), construye la Ability del usuario y verifica ability.can(...). Si no hay decorador, pasa (deja el control a RolesGuard). |
@Roles y @CheckPermission pueden coexistir en la misma ruta durante la migración de
legacy → dinámico — ambos deben pasar si ambos están presentes.
CaslAbilityFactory
src/common/authorization/casl-ability.factory.ts construye una PureAbility por
usuario:
- Si tiene
customPermissionCodesexplícitos → los usa directo. - Si tiene
roleId→ cargaRole+RolePermissionde Prisma, cacheado en Redis (ability:{roleId}, TTL 300s).SUPER_ADMIN→can('manage', 'all'). - Si no hay
roleId(legacy) → cae aDEFAULT_ROLE_PERMISSIONSpor el string del enumrole.
invalidateRole(roleId) limpia la caché al editar un rol — ver
Implementadores → Roles y permisos para el
catálogo completo y el impacto de la caché de 5 minutos.
Decoradores
@Roles(...roles), @CheckPermission(action, subject), @Public(), @CurrentUser(),
@TenantId() — todos en src/common/decorators/.