Seguridad y Autenticacion
Tabla de Contenidos
- Autenticacion con Clerk
- Autenticacion Movil (nvito-client)
- Cadena de Guards
- Sistema RBAC
- Decoradores de Seguridad
- Seguridad HTTP
- Aislamiento de Datos (Multi-Tenancy)
- Manejo de Secretos
- Seguridad en nvito-admin (Frontend)
1. Autenticacion con Clerk
Nvito delega la autenticacion de usuarios a Clerk, un servicio externo de identidad. El frontend obtiene un JWT a traves del Clerk SDK y lo envia en cada request al API como Bearer token en el header Authorization.
Flujo de Verificacion del JWT
El ClerkAuthGuard es el guard global principal que intercepta todas las peticiones (excepto las marcadas con @Public()). Su proceso es:
- Extraccion del token: Se obtiene el JWT del header
Authorizationeliminando el prefijoBearer. - Verificacion con Clerk:
authService.verifyJWT(token)valida firma y expiracion contra las claves publicas de Clerk. - Busqueda del usuario en BD: Con el
sub(clerkUserId) del token decodificado, se busca el usuario en la tablausers. - Validacion de seguridad: Se verifica que el usuario no este eliminado (
deletedAt === null) y que este activo (isActive === true). - Resolucion de Super Admin: Se consulta
user_rolesbuscandorole=SUPER_ADMIN,scope=GLOBAL,isActive=true(sin expirar). - Resolucion de Platform Admin: Si no es Super Admin, se busca
role=PLATFORM_ADMIN,scope=GLOBAL,isActive=true. Si existe, se obtienen las organizaciones asignadas (roles conscope=ORGANIZATION) y se lee el headerX-Organization-Idpara la org activa. - Resolucion de contexto organizacional: Si no es Super Admin ni Platform Admin, se busca el rol mas reciente en
user_rolesconscope=ORGANIZATION. - Construccion del AuthenticatedUser: Se arma el objeto con
userId,clerkUserId,organizationId,role,email,isSuperAdmin,isPlatformAdmin,assignedOrganizationIds,activeOrganizationIdyclaims.
Throttling de lastLoginAt
Para evitar escrituras excesivas a la BD, la actualizacion de lastLoginAt usa un throttle en memoria:
- Un
Map<string, number>registra el timestamp de la ultima actualizacion por usuario. - Solo se actualiza si han pasado mas de 5 minutos (
LOGIN_UPDATE_INTERVAL_MS = 300000ms). - La actualizacion es fire-and-forget: los errores se loguean pero no bloquean la autenticacion.
Diagrama de Secuencia: Flujo Completo de Autenticacion
Interface AuthenticatedUser
| Campo | Tipo | Descripcion |
|---|---|---|
userId | string | UUID interno del usuario en la BD |
clerkUserId | string | ID del usuario en Clerk |
organizationId | string | null | UUID de la organizacion (null para SA/PA) |
role | 'owner' | 'admin' | 'member' | 'super_admin' | 'platform_admin' | Rol simplificado del usuario |
email | string | Email del usuario |
isSuperAdmin | boolean | Indicador rapido de Super Admin |
isPlatformAdmin | boolean | Indicador rapido de Platform Admin |
superAdminId | string? | ID del registro en user_roles (si aplica) |
assignedOrganizationIds | string[]? | Orgs donde PA puede escribir |
activeOrganizationId | string | null? | Org activa desde header X-Organization-Id |
claims | Record<string, any>? | Claims adicionales del JWT |
2. Autenticacion Movil (nvito-client)
La app movil utiliza un sistema de autenticacion independiente de Clerk, diseƱado para permitir que tanto anfitriones como invitados accedan sin necesidad de crear una cuenta. El sistema usa JWT firmado con un secreto dedicado (MOBILE_JWT_SECRET).
Diferencias con Autenticacion Web (Clerk)
| Aspecto | Web (Clerk) | Movil (Mobile Auth) |
|---|---|---|
| Proveedor | Clerk (servicio externo) | Sistema propio (MobileAuthModule) |
| JWT Secret | Claves publicas de Clerk | MOBILE_JWT_SECRET (simetrico) |
| Guard | ClerkAuthGuard | MobileAuthGuard |
| Identidad | userId (tabla users) | userId (HOST) o guestId (GUEST) |
| Registro | Requiere cuenta Clerk | No requiere cuenta |
| Acceso | Email + password | Codigo evento + PIN / Access token |
| Roles | owner, admin, member, super_admin, platform_admin | HOST, GUEST |
| Prefijo | /v1/* | /v1/mobile/* |
Flujo de Login del Anfitrion
- Desde la pagina "Acceso App Movil" en nvito-admin, el organizador genera un
EventAccessCodecon un codigo alfanumerico de 8 caracteres y un PIN de 6 digitos hasheado con bcrypt. El evento debe estar en estadoACTIVE. Maximo 3 codigos activos por evento. - El organizador comparte el codigo y PIN con el anfitrion. El PIN solo es visible al momento de crear o regenerar (one-time reveal).
- El anfitrion abre la app movil e ingresa el codigo + PIN.
POST /v1/mobile/auth/loginvalida el codigo y el PIN contra bcrypt.- Si es valido, se crea una
MobileSessionconrole=HOSTy se genera:- JWT (15 min de vida) con payload
{ sub: eventId, role: "HOST", userId, sessionId } - Refresh token (30 dias) almacenado en la tabla
MobileSession
- JWT (15 min de vida) con payload
Flujo de Login del Invitado
- Se crea un
GuestAccessTokenautomaticamente por cada invitado en dos momentos: al generar pases QR (individual o masivo viaensureGuestAccessToken()) y al enviar comunicaciones (email/WhatsApp viainvitation-dispatcher.service.ts). Cada token es un string aleatorio de 64 caracteres (crypto.randomBytes(32).toString('hex')). - El invitado recibe un deep link (
nvito://join/{token}) o escanea un QR con el token. POST /v1/mobile/auth/guest-accessvalida el token y lo vincula al invitado y evento.- Si es valido, se crea una
MobileSessionconrole=GUESTy se genera:- JWT (15 min de vida) con payload
{ sub: eventId, role: "GUEST", guestId, sessionId } - Refresh token (30 dias)
- JWT (15 min de vida) con payload
MobileAuthGuard
El MobileAuthGuard protege todos los endpoints bajo /v1/mobile/ (excepto auth):
- Extrae el JWT del header
Authorization: Bearer <token> - Verifica firma y expiracion usando
MOBILE_JWT_SECRET - Extrae
eventId,role(HOST/GUEST),userId/guestIdysessionId - Verifica que la
MobileSessioneste activa y no expirada - Construye
MobileAuthenticatedUsery lo adjunta al request
Interface MobileAuthenticatedUser
| Campo | Tipo | Descripcion |
|---|---|---|
sessionId | string | UUID de la sesion movil activa |
eventId | string | UUID del evento asociado |
role | 'HOST' | 'GUEST' | Rol movil del usuario |
userId | string | null | UUID del usuario (solo HOST) |
guestId | string | null | UUID del invitado (solo GUEST) |
deviceId | string | Identificador del dispositivo |
Refresh Token (Rotacion)
La renovacion del JWT sigue el patron de rotacion de refresh tokens:
POST /v1/mobile/auth/refreshcon el refresh token actual- El API valida el refresh token contra la tabla
MobileSession - Se genera un nuevo JWT (15 min) y un nuevo refresh token
- El refresh token anterior se invalida (cada token solo se usa una vez)
- Si el refresh token ya fue usado, se invalida toda la sesion (posible robo)
Almacenamiento Seguro
Los tokens en la app movil se almacenan usando expo-secure-store, que utiliza:
- iOS: Keychain Services (almacenamiento encriptado por hardware)
- Android: Android Keystore System (almacenamiento encriptado)
Diagrama de Secuencia: Login Movil
Modelos de Datos para Auth Movil
| Modelo | Proposito |
|---|---|
EventAccessCode | Codigo + PIN (bcrypt) para login de anfitrion |
GuestAccessToken | Token aleatorio de 64 chars para acceso de invitado |
MobileSession | Sesion activa con refreshToken, pushToken, deviceId |
Push Token Registration
Tras un login exitoso, la app registra el push token de Expo:
- La app solicita permisos de notificacion al usuario
- Si se otorgan, obtiene el Expo Push Token via
getExpoPushTokenAsync() POST /v1/mobile/auth/register-pushalmacena el token enMobileSession.pushToken- El backend usa este token para enviar push notifications via Expo Push API
3. Cadena de Guards
El orden de ejecucion de los guards es critico. NestJS ejecuta los guards globales en el orden en que se registran como APP_GUARD, seguidos por los guards declarados a nivel de controlador o metodo.
Guards Globales (app.module.ts)
- ClerkAuthGuard - Autenticacion y resolucion de identidad
- UserThrottlerGuard - Rate limiting por usuario/IP
- RoleGuard - Verificacion de roles basicos (owner/admin/member)
Guards Opcionales por Endpoint
- RolesGuard - Roles RBAC avanzados (
RoleTypeenum) viaPermissionsService.hasRole() - PermissionsGuard - Permisos granulares (
resource:action:scope) - EventAccessGuard - Acceso a un evento especifico
- AdminPanelGuard - Acceso al panel admin (Super Admins + Platform Admins)
- SuperAdminGuard - Restriccion exclusiva para Super Admins (endpoints destructivos)
- InvitationStateGuard - Bloqueo de edicion segun estado de invitacion
- MobileRoleGuard - Verificacion de rol movil (HOST/GUEST) en endpoints
/v1/mobile/*
Diagrama de la Cadena de Guards
Detalle de Cada Guard
| Guard | Proposito | Bypass Super Admin |
|---|---|---|
ClerkAuthGuard | Verifica JWT, busca usuario en BD, resuelve roles desde user_roles | N/A |
UserThrottlerGuard | Rate limiting: trackea por user:{userId} o ip:{ip} | No |
RoleGuard | Valida roles basicos (owner, admin, member) del decorator @Roles() | Si |
RolesGuard | Valida roles RBAC avanzados (RoleType enum) via PermissionsService | Implicito |
PermissionsGuard | Valida permisos granulares (resource:action:scope) | Implicito |
EventAccessGuard | Verifica acceso al evento especifico via user_roles | Si |
AdminPanelGuard | Permite acceso si isSuperAdmin || isPlatformAdmin. Usado a nivel de clase en controllers del panel admin | N/A |
SuperAdminGuard | Restringe acceso exclusivamente a Super Admins. Usado a nivel de metodo para endpoints destructivos (DELETE) | N/A |
InvitationStateGuard | Bloquea escrituras si la invitacion esta PUBLISHED o CLOSED | No |
MobileAuthGuard | Verifica JWT movil (MOBILE_JWT_SECRET), resuelve sesion y rol (HOST/GUEST). Protege endpoints /v1/mobile/* | N/A (sistema independiente) |
MobileRoleGuard | Verifica que el usuario movil tenga el rol requerido (HOST o GUEST) via @RequireMobileRole(). Se usa despues de MobileAuthGuard | N/A (sistema independiente) |
4. Sistema RBAC
El sistema RBAC utiliza la tabla user_roles como fuente unica de verdad. Cada registro vincula un usuario con un rol, un scope y opcionalmente una organizacion o evento.
RoleType Enum
| Rol | Descripcion |
|---|---|
SUPER_ADMIN | Acceso completo al sistema. Gestiona todas las organizaciones. |
PLATFORM_ADMIN | Administrador de plataforma. Lectura cross-org, escritura en orgs asignadas. No puede eliminar usuarios/orgs ni gestionar SA/PA. |
ORGANIZATION_OWNER | Dueno de la organizacion. Control total sobre su organizacion y usuarios. |
ORGANIZATION_ADMIN | Administrador. Gestion operativa completa de eventos y recursos. |
EVENT_MANAGER | Gestor de eventos. Control completo sobre eventos asignados. |
EVENT_EDITOR | Editor. Puede editar eventos e invitados, pero no eliminar. |
EVENT_COLLABORATOR | Colaborador. Acceso limitado: agregar/editar invitados y ver info del evento. |
EVENT_VIEWER | Visualizador. Solo lectura de los eventos asignados. |
RoleScope Enum
| Scope | Significado | Ejemplo de uso |
|---|---|---|
GLOBAL | Aplica a todo el sistema | Super Admin, Platform Admin |
ORGANIZATION | Aplica a toda una organizacion | Owner, Admin (y PA con asignacion de org) |
EVENT | Aplica a un evento especifico | Manager, Editor, Collaborator, Viewer |
Formato de Permisos
Los permisos siguen el formato {recurso}:{accion}:{alcance}. Ejemplo: events:create:organization.
18 recursos: organizations, events, event_services, guests, invitations, templates, media, qr_passes, tables, music, itinerary, locations, users, roles, settings, analytics, ai_generation, audit_logs.
8 acciones: create, read, update, delete, manage (todas), view_all, export, import.
5 alcances: global, organization, event, assigned, own.
Tabla de Permisos por Rol
| Recurso | SUPER_ADMIN | PLATFORM_ADMIN | ORG_OWNER | ORG_ADMIN | EVT_MANAGER | EVT_EDITOR | EVT_COLLAB | EVT_VIEWER |
|---|---|---|---|---|---|---|---|---|
| Organizations | * | R | R/U/D | R | R | - | - | - |
| Events | * | CRUD/M/E (1) | CRUD/M/E | CRUD/M/E | R/U/M/E | R/U | R | R |
| Guests | * | CRUD/I/E (1) | CRUD/I/E | CRUD/I/E | CRUD/I/E | CRUD/I/E | C/R/U | R |
| Invitations | * | CRUD/P/S (1) | CRUD/P/S | CRUD/P/S | CRUD/P/S | C/R/U/P | R | R |
| Templates | * | CRUD | CRUD | CRUD | R | R | - | - |
| Media | * | CRUD/M (1) | CRUD/M | CRUD/M | CRUD | CRUD | R | R |
| QR Passes | * | CRUD/V (1) | CRUD/V | CRUD/V | CRUD/V | R/V | R | R |
| Tables | * | CRUD/A (1) | CRUD/A | CRUD/A | CRUD/A | R/U/A | R | R |
| Music | * | CRUD (1) | CRUD | CRUD | CRUD | C/R/U | R | R |
| Itinerary | * | CRUD (1) | CRUD | CRUD | CRUD | C/R/U | R | R |
| Locations | * | CRUD (1) | CRUD | CRUD | CRUD | C/R/U | R | R |
| Users | * | R/U (2) | CRUD | R | - | - | - | - |
| Roles | * | R/A (3) | R/A | R/A(ev) | A(ev) | - | - | - |
| Settings | * | R | R/U | R | - | - | - | - |
| Analytics | * | R/E | R/E | R/E | R | R | R | R |
| AI Generation | * | C/R (1) | C/R | C/R | C/R | C/R | - | - |
| Event Services | * | CRUD/M (1) | CRUD/M | CRUD/M | CRUD/M | R/U | R | R |
| Audit Logs | * | R | R | - | - | - | - | - |
Leyenda: C=Create, R=Read, U=Update, D=Delete, M=Manage, E=Export, I=Import, P=Publish, S=Send, V=Validate, A=Assign, *=Wildcard (todos los permisos). Los roles de evento operan solo sobre recursos asignados.
Notas PLATFORM_ADMIN:
- (1) Escritura solo en organizaciones asignadas explicitamente. Lectura cross-org (todas las organizaciones).
- (2) No puede editar Super Admins ni otros Platform Admins. No puede cambiar
isSuperAdmin. No puede eliminar usuarios. - (3) Puede asignar roles org/event en sus orgs asignadas. No puede asignar
SUPER_ADMINniPLATFORM_ADMIN.
Restricciones de negocio
- SA y PA son mutuamente excluyentes: Un usuario no puede tener ambos roles simultaneamente. El backend valida esta restriccion y el frontend desactiva un toggle al activar el otro.
- Usuarios inactivos son de solo lectura: No se puede editar ni asignar roles a un usuario inactivo. La unica accion permitida es reactivarlo (
isActive: true).
Wildcards
*(wildcard completo): Coincide con cualquier permiso. Asignado alSUPER_ADMIN.PLATFORM_ADMINno tiene wildcard -- sus permisos son explicitos y acotados.- La funcion
matchesPermission()descompone permisos en{resource}:{action}:{scope}y evalua coincidencia componente por componente, soportando wildcards en cada segmento.
5. Decoradores de Seguridad
Ubicados en src/common/decorators/. Se combinan con los guards correspondientes.
| Decorador | Guard asociado | Proposito |
|---|---|---|
@Public() | ClerkAuthGuard (skip) | Marca endpoint como publico, omite autenticacion |
@CurrentUser() | Ninguno (param) | Inyecta AuthenticatedUser o una propiedad especifica en el controller |
@Roles('owner','admin') | RoleGuard | Requiere uno de los roles basicos listados |
@RequireRole(RoleType) | RolesGuard | Requiere roles RBAC avanzados (enum RoleType de Prisma) |
@RequirePermission(p) | PermissionsGuard | Requiere permiso granular resource:action:scope |
@RequirePermissions(p) | PermissionsGuard | Requiere TODOS los permisos listados |
| (AdminPanelGuard) | AdminPanelGuard | Permite acceso a SA + PA. Se aplica a nivel de clase en controllers admin |
@RequireSuperAdmin() | SuperAdminGuard | Restringe a Super Admins exclusivamente. Se aplica a nivel de metodo para DELETE y endpoints destructivos |
@RequireEventAccess() | EventAccessGuard | Valida acceso al evento del parametro eventId |
@CheckInvitationState() | InvitationStateGuard | Bloquea escrituras segun estado de la invitacion |
@RequireMobileRole() | MobileRoleGuard | Requiere rol movil especifico (HOST o GUEST) en endpoints mobile |
Ejemplo de Uso Combinado
@UseGuards(ClerkAuthGuard, PermissionsGuard, EventAccessGuard, InvitationStateGuard)
@RequirePermission('guests:create:assigned')
@RequireEventAccess()
@CheckInvitationState('block_if_published_or_closed', 'invitados')
@Post(':eventId/guests')
async createGuest(
@CurrentUser() user: AuthenticatedUser,
@Param('eventId') eventId: string,
@Body() dto: CreateGuestDto,
) { /* ... */ }
Modos de @CheckInvitationState
block_if_published- Bloquea solo si la invitacion esta PUBLISHED.block_if_closed- Bloquea solo si esta CLOSED.block_if_published_or_closed- Bloquea en ambos estados.
Soporta resolucion indirecta de eventId desde recursos: location, itineraryItem, media, giftRegistryItem, giftBankAccount, musicTrack, accommodationHotel.
6. Seguridad HTTP
Configuracion aplicada en main.ts antes de inicializar la aplicacion.
Helmet (Headers de Seguridad)
| Header / Directiva | Configuracion |
|---|---|
| Content-Security-Policy | default-src 'self', object-src 'none', frame-src 'none' |
| Strict-Transport-Security (HSTS) | max-age=31536000; includeSubDomains; preload |
| X-Frame-Options | DENY |
| X-Content-Type-Options | nosniff |
| X-Permitted-Cross-Domain-Policies | none |
| Referrer-Policy | strict-origin-when-cross-origin |
| Permissions-Policy | geolocation=(), microphone=(), camera=() |
CORS
- Origenes: Configurables via
CORS_ORIGINS(array de dominios). Wildcard*prohibido en produccion. - Credenciales: Habilitadas (
credentials: true). - Cache preflight: 24 horas (
maxAge: 86400). - Metodos:
GET,POST,PUT,PATCH,DELETE. - Headers:
Content-Type,Authorization.
Rate Limiting
| Parametro | Valor |
|---|---|
| Limite | 100 requests por minuto |
| Ventana | 60,000 ms (1 minuto) |
| Tracking | user:{userId} si autenticado, ip:{ip} si publico |
| Storage | RedisThrottlerStorage (Redis) |
| Fail-open | Si Redis no esta disponible, permite el request |
Body y Compresion
- Limite de body: 20 MB para JSON y URL-encoded (permite subida de imagenes grandes).
- Compresion gzip: Nivel 6, umbral 1 KB. Reduccion estimada de 70-90% de bandwidth. Desactivable con header
X-No-Compression.
7. Aislamiento de Datos (Multi-Tenancy)
Nvito implementa un modelo multi-tenant donde cada organizacion es un tenant. El aislamiento se logra mediante Row-Level Security (RLS) de PostgreSQL.
TenantMiddleware
Se ejecuta despues de la autenticacion y antes de los controladores:
- Obtiene el
AuthenticatedUserdel request (resuelto porClerkAuthGuard). - Si el usuario tiene
organizationId, ejecuta:SELECT set_config('app.current_tenant_id', '{orgId}', true). - El parametro
trueindica configuracion local a la transaccion, evitando contaminacion entre requests.
Prevencion de SQL injection: Se usa $executeRaw con tagged template literals de Prisma para parametrizacion automatica.
Politicas RLS
CREATE POLICY tenant_isolation ON events
USING (organization_id = current_setting('app.current_tenant_id')::uuid);
Garantias:
- Un usuario de la Organizacion A nunca puede ver datos de la Organizacion B.
- No se requieren filtros manuales por
organizationIden el codigo. - La proteccion opera a nivel de base de datos, debajo de la capa de aplicacion.
Bypass para Super Admins y Platform Admins
Los Super Admins tienen organizationId = null, por lo que el TenantMiddleware no establece app.current_tenant_id. Las politicas RLS no se aplican y pueden ver datos de todas las organizaciones.
Los Platform Admins tambien tienen lectura cross-org. El TenantMiddleware usa user.activeOrganizationId || user.organizationId para el contexto RLS. Cuando el PA selecciona una organizacion via el header X-Organization-Id, el middleware establece ese contexto y el PA opera dentro de esa org. Sin organizacion activa, el comportamiento es similar al de un Super Admin (sin filtro RLS).
8. Manejo de Secretos
Nvito utiliza Bitwarden Secrets Manager como boveda centralizada, evitando almacenar credenciales en el codigo fuente.
Estructura de Proyectos en Bitwarden
| Proyecto | Entorno | Uso |
|---|---|---|
local | Desarrollo | Secretos para desarrollo local |
dev | Development | Secretos del servidor de dev |
test | Testing | Secretos del entorno de pruebas |
Inyeccion por Entorno
- Local: Se usa el CLI
bwspara inyectar secretos como variables de entorno (bws secret get <id>). El archivo.envlocal nunca se sube al repositorio. - GitLab CI/CD: El service account de Bitwarden se configura como variable protegida. Un paso del pipeline ejecuta
bwsy exporta los secretos sin registrarlos en logs. - Coolify: Los secretos se copian manualmente como variables de entorno del servicio en la interfaz de Coolify. Requiere actualizacion manual al rotar secretos.
Variables de Entorno Criticas
| Variable | Descripcion |
|---|---|
CLERK_SECRET_KEY | Clave secreta de Clerk para verificar JWT |
DATABASE_URL | Connection string de PostgreSQL |
REDIS_URL | Connection string de Redis |
CORS_ORIGINS | Origenes permitidos para CORS |
ENCRYPTION_KEY | Clave de encriptacion de campos sensibles |
MOBILE_JWT_SECRET | Clave secreta para firmar JWT de la app movil |
9. Seguridad en nvito-admin (Frontend)
El panel de administracion implementa multiples capas de seguridad complementarias al backend:
Autenticacion y Proteccion de Rutas
| Mecanismo | Implementacion |
|---|---|
| Clerk Provider | Contexto de autenticacion a nivel de layout raiz |
auth() en Server Actions | Cada server action obtiene el token JWT de Clerk y valida autenticacion antes de ejecutar |
| Layout guard | El layout del dashboard verifica que el usuario este autenticado |
| RBAC frontend | usePermissionsQuery + condicionales para mostrar/ocultar funcionalidad segun permisos |
Validacion en la Frontera
Todas las 15 server actions validan inputs con Zod antes de llamar a la API:
// Ejemplo de validacion en server action
const parsed = uploadUrlSchema.safeParse(input);
if (!parsed.success) {
return { success: false, error: 'Datos invalidos' };
}
Esto proporciona validacion tanto en el cliente (formularios con React Hook Form + Zod) como en el servidor (server actions con Zod safeParse).
Content Security Policy (CSP)
Headers CSP configurados en next.config.ts restringen los origenes de scripts, estilos, imagenes y conexiones, protegiendo contra XSS y data exfiltration.
Response Validation en Runtime
Los servicios criticos (eventos, invitados, organizaciones) usan apiClient.getValidated() con schemas Zod para validar las respuestas del API en runtime, proporcionando defense-in-depth contra responses malformadas.
Error Handling Seguro
El patron ActionResult<T> garantiza que los errores se manejan de forma explicita sin exponer stack traces ni detalles internos al usuario:
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string };
Ultima actualizacion: Febrero 2026