1. Autenticación con Clerk
Nvito delega la autenticación 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:
- Extracción del token: Se obtiene el JWT del header
Authorizationeliminando el prefijoBearer. - Verificacion con Clerk:
authService.verifyJWT(token)valida firma y expiración contra las claves públicas de Clerk. - Búsqueda del usuario en BD: Con el
sub(clerkUserId) del token decodificado, se busca el usuario en la tablausers. - Validación de seguridad: Se verifica que el usuario no este eliminado (
deletedAt === null) y que este activo (isActive === true). - Resolución de Super Admin: Se consulta
user_rolesbuscandorole=SUPER_ADMIN,scope=GLOBAL,isActive=true(sin expirar). - Resolución 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. - Resolución de contexto organizacional: Si no es Super Admin ni Platform Admin, se busca el rol más reciente en
user_rolesconscope=ORGANIZATION. - Construcción 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 actualización de lastLoginAt usa un throttle en memoria:
- Un
Map<string, number>registra el timestamp de la última actualización por usuario. - Solo se actualiza si han pasado más de 5 minutos (
LOGIN_UPDATE_INTERVAL_MS = 300000ms). - La actualización es fire-and-forget: los errores se loguean pero no bloquean la autenticación.
Diagrama de Secuencia: Flujo Completo de Autenticación
Flujo de Autenticacion con Clerk
Interface AuthenticatedUser
| Campo | Tipo | Descripción |
|---|---|---|
userId | string | UUID interno del usuario en la BD |
clerkUserId | string | ID del usuario en Clerk |
organizationId | string | null | UUID de la organización (null para SA/PA) |
role | 'owner' | 'admin' | 'member' | 'super_admin' | 'platform_admin' | Rol simplificado del usuario |
email | string | Email del usuario |
isSuperAdmin | boolean | Indicador rápido de Super Admin |
isPlatformAdmin | boolean | Indicador rápido 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. Autenticación Movil (nvito-client)
La app movil utiliza un sistema de autenticación 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 Autenticación Web (Clerk)
| Aspecto | Web (Clerk) | Movil (Mobile Auth) |
|---|---|---|
| Proveedor | Clerk (servicio externo) | Sistema propio (MobileAuthModule) |
| JWT Secret | Claves públicas 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 | Código 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 página "Acceso App Movil" en nvito-admin, el organizador genera un
EventAccessCodecon un código alfanumerico de 8 caracteres y un PIN de 6 digitos hasheado con bcrypt. El evento debe estar en estadoACTIVE. Máximo 3 códigos activos por evento. - El organizador comparte el código 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 código + PIN.
POST /v1/mobile/auth/loginvalida el código 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
GuestAccessTokenautomáticamente 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 expiración 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 | Descripción |
|---|---|---|
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 (Rotación)
La renovacion del JWT sigue el patrón de rotación 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
Login Movil — Anfitrion e Invitado
Modelos de Datos para Auth Movil
| Modelo | Proposito |
|---|---|
EventAccessCode | Código + 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 notificación 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 ejecución 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 - Autenticación y resolución de identidad
- UserThrottlerGuard - Rate limiting por usuario/IP
- RoleGuard - Verificacion de roles básicos (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 específico
- AdminPanelGuard - Acceso al panel admin (Super Admins + Platform Admins)
- SuperAdminGuard - Restriccion exclusiva para Super Admins (endpoints destructivos)
- InvitationStateGuard - Bloqueo de edicion según estado de invitación
- MobileRoleGuard - Verificacion de rol movil (HOST/GUEST) en endpoints
/v1/mobile/* - CloudflareTokenGuard - Valida tokens de Cloudflare para endpoints de CDN/revalidación
- TwilioSignatureGuard - Valida firma de webhooks de Twilio (WhatsApp/SMS)
Diagrama de la Cadena de Guards
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 básicos (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 específico 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 método para endpoints destructivos (DELETE) | N/A |
InvitationStateGuard | Bloquea escrituras si la invitación está 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 después de MobileAuthGuard | N/A (sistema independiente) |
CloudflareTokenGuard | Valida tokens de Cloudflare para endpoints de CDN y revalidación de cache | No |
TwilioSignatureGuard | Valida firma de webhooks de Twilio (WhatsApp/SMS) usando HMAC | No |
4. Sistema RBAC
El sistema RBAC utiliza la tabla user_roles como fuente única de verdad. Cada registro vincula un usuario con un rol, un scope y opcionalmente una organización o evento.
RoleType Enum
| Rol | Descripción |
|---|---|
SUPER_ADMIN | Acceso completo al sistema. Gestióna 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 organización. Control total sobre su organización y usuarios. |
ORGANIZATION_ADMIN | Administrador. Gestión 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 organización | Owner, Admin (y PA con asignación de org) |
EVENT | Aplica a un evento específico | Manager, Editor, Collaborator, Viewer |
Formato de Permisos
Los permisos siguen el formato {recurso}:{acción}:{alcance}. Ejemplo: events:create:organization.
19 recursos: organizations, events, event_services, guests, invitations, templates, media, qr_passes, tables, music, itinerary, locations, users, roles, settings, analytics, ai_generation, audit_logs, collaborators.
8 acciones: create, read, update, delete, manage (todas), view_all, export, import.
5 alcances: global, organization, event, assigned, own.
Endpoint de Permisos Dinamicos
El endpoint GET /events/:id/access retorna el nivel de acceso del usuario autenticado a un evento específico, incluyendo la lista completa de permisos de su rol:
// Respuestá de GET /events/:id/access
(
hasAccess: boolean,
role: RoleType | null,
isSuperAdmin: boolean,
permissions: string[] // Lista de permisos del rol (ej: "guests:create:assigned")
)
Los permisos se obtienen dinámicamente con getRolePermissions(role) desde el backend. Esto permite que el frontend nunca hardcodee que puede hacer un rol, consultando siempre al backend como fuente de verdad. El componente ShowIfCan en nvito-admin consume estos permisos para proteger 12 vistas de evento.
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 |
| Collaborators | * | M (1) | M | M | M | - | - | - |
| 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 está 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 única acción 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 función
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 público, omite autenticación |
@CurrentUser() | Ninguno (param) | Inyecta AuthenticatedUser o una propiedad específica en el controller |
@Roles('owner','admin') | RoleGuard | Requiere uno de los roles básicos 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 método para DELETE y endpoints destructivos |
@RequireEventAccess() | EventAccessGuard | Valida acceso al evento del parametro eventId |
@CheckInvitationState() | InvitationStateGuard | Bloquea escrituras según estado de la invitación |
@RequireMobileRole() | MobileRoleGuard | Requiere rol movil específico (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 invitación está PUBLISHED.block_if_closed- Bloquea solo si está CLOSED.block_if_published_or_closed- Bloquea en ambos estados.
Soporta resolución indirecta de eventId desde recursos: location, itineraryItem, media, giftRegistryItem, giftBankAccount, musicTrack, accommodationHotel.
6. Seguridad HTTP
Configuración aplicada en main.ts antes de inicializar la aplicación.
Helmet (Headers de Seguridad)
| Header / Directiva | Configuración |
|---|---|
| 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 producción. - 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 público |
| Storage | RedisThrottlerStorage (Redis) |
| Fail-open | Si Redis no está 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 organización es un tenant. El aislamiento se logra mediante Row-Level Security (RLS) de PostgreSQL.
TenantMiddleware
Se ejecuta después de la autenticación 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 configuración local a la transacción, evitando contaminacion entre requests.
Prevencion de SQL injection: Se usa $executeRaw con tagged template literals de Prisma para parametrizacion automática.
Politicas RLS
CREATE POLICY tenant_isolation ON events
USING (organization_id = current_setting('app.current_tenant_id')::uuid);
Garantias:
- Un usuario de la Organización A nunca puede ver datos de la Organización B.
- No se requieren filtros manuales por
organizationIden el código. - La protección opera a nivel de base de datos, debajo de la capa de aplicación.
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 también tienen lectura cross-org. El TenantMiddleware usa user.activeOrganizationId || user.organizationId para el contexto RLS. Cuando el PA selecciona una organización via el header X-Organization-Id, el middleware establece ese contexto y el PA opera dentro de esa org. Sin organización 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 código 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 |
Inyección 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 actualización manual al rotar secretos.
Variables de Entorno Criticas
| Variable | Descripción |
|---|---|
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 administración implementa múltiples capas de seguridad complementarias al backend:
Autenticación y Protección de Rutas
| Mecanismo | Implementación |
|---|---|
| Clerk Provider | Contexto de autenticación a nivel de layout raiz |
auth() en Server Actions | Cada server action obtiene el token JWT de Clerk y valida autenticación antes de ejecutar |
| Layout guard | El layout del dashboard verifica que el usuario este autenticado |
| RBAC frontend | usePermissionsQuery obtiene permisos dinámicos del backend via GET /events/:id/access. El componente ShowIfCan protege 12 vistas de evento verificando permisos contra la lista retornada por el backend. El frontend NUNCA hardcodea permisos por rol |
Validación en la Frontera
Todas las 15 server actions validan inputs con Zod antes de llamar a la API:
// Ejemplo de validación en server action
const parsed = uploadUrlSchema.safeParse(input);
if (!parsed.success) {
return { success: false, error: 'Datos invalidos' };
}
Esto proporciona validación 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 conexiónes, 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 patrón 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 };
10. Seguridad en Endpoints Publicos de Invitaciones
ShortCode para Personalización por Invitado
El endpoint GET /v1/invitations/public/guest/:shortCode es público (decorado con @Public()) y expone datos limitados del invitado para personalización client-side de la invitación.
Medidas de seguridad:
| Aspecto | Implementación |
|---|---|
| Entropia | ShortCode de 12 caracteres generado con crypto.randomBytes (71 bits de entropia), URL-safe |
| Datos expuestos | Solo firstName, lastName, title, groupSize — nunca email, teléfono ni IDs internos |
| Validación de estado | Solo retorna datos si isActive === true y expiresAt no ha pasado |
| Prevencion XSS | Los textos inyectados en el HTML usan textContent (no innerHTML), evitando ejecución de código |
| No enumeracion | El shortCode tiene espacio de búsqueda de 2^71 combinaciones, haciendo inviable la enumeracion por fuerza bruta |
| Rate limiting | Protegido por el UserThrottlerGuard global de la API |
| Respuestá en error | Retorna 404 generico sin revelar si el shortCode existe pero está inactivo vs. no existe |