Docs

Seguridad y Autenticación

Sistema de autenticación con Clerk, cadena de guards, RBAC con permisos granulares, seguridad HTTP, multi-tenancy y manejo de secretos en Nvito.

PublicadoMarzo 2026Equipo de desarrollo, arquitectos, stakeholders

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:

  1. Extracción del token: Se obtiene el JWT del header Authorization eliminando el prefijo Bearer .
  2. Verificacion con Clerk: authService.verifyJWT(token) valida firma y expiración contra las claves públicas de Clerk.
  3. Búsqueda del usuario en BD: Con el sub (clerkUserId) del token decodificado, se busca el usuario en la tabla users.
  4. Validación de seguridad: Se verifica que el usuario no este eliminado (deletedAt === null) y que este activo (isActive === true).
  5. Resolución de Super Admin: Se consulta user_roles buscando role=SUPER_ADMIN, scope=GLOBAL, isActive=true (sin expirar).
  6. 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 con scope=ORGANIZATION) y se lee el header X-Organization-Id para la org activa.
  7. Resolución de contexto organizacional: Si no es Super Admin ni Platform Admin, se busca el rol más reciente en user_roles con scope=ORGANIZATION.
  8. Construcción del AuthenticatedUser: Se arma el objeto con userId, clerkUserId, organizationId, role, email, isSuperAdmin, isPlatformAdmin, assignedOrganizationIds, activeOrganizationId y claims.

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

Interface AuthenticatedUser

CampoTipoDescripción
userIdstringUUID interno del usuario en la BD
clerkUserIdstringID del usuario en Clerk
organizationIdstring | nullUUID de la organización (null para SA/PA)
role'owner' | 'admin' | 'member' | 'super_admin' | 'platform_admin'Rol simplificado del usuario
emailstringEmail del usuario
isSuperAdminbooleanIndicador rápido de Super Admin
isPlatformAdminbooleanIndicador rápido de Platform Admin
superAdminIdstring?ID del registro en user_roles (si aplica)
assignedOrganizationIdsstring[]?Orgs donde PA puede escribir
activeOrganizationIdstring | null?Org activa desde header X-Organization-Id
claimsRecord<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)

AspectoWeb (Clerk)Movil (Mobile Auth)
ProveedorClerk (servicio externo)Sistema propio (MobileAuthModule)
JWT SecretClaves públicas de ClerkMOBILE_JWT_SECRET (simetrico)
GuardClerkAuthGuardMobileAuthGuard
IdentidaduserId (tabla users)userId (HOST) o guestId (GUEST)
RegistroRequiere cuenta ClerkNo requiere cuenta
AccesoEmail + passwordCódigo evento + PIN / Access token
Rolesowner, admin, member, super_admin, platform_adminHOST, GUEST
Prefijo/v1/*/v1/mobile/*

Flujo de Login del Anfitrion

  1. Desde la página "Acceso App Movil" en nvito-admin, el organizador genera un EventAccessCode con un código alfanumerico de 8 caracteres y un PIN de 6 digitos hasheado con bcrypt. El evento debe estar en estado ACTIVE. Máximo 3 códigos activos por evento.
  2. 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).
  3. El anfitrion abre la app movil e ingresa el código + PIN.
  4. POST /v1/mobile/auth/login valida el código y el PIN contra bcrypt.
  5. Si es valido, se crea una MobileSession con role=HOST y se genera:
    • JWT (15 min de vida) con payload { sub: eventId, role: "HOST", userId, sessionId }
    • Refresh token (30 dias) almacenado en la tabla MobileSession

Flujo de Login del Invitado

  1. Se crea un GuestAccessToken automáticamente por cada invitado en dos momentos: al generar pases QR (individual o masivo via ensureGuestAccessToken()) y al enviar comunicaciones (email/WhatsApp via invitation-dispatcher.service.ts). Cada token es un string aleatorio de 64 caracteres (crypto.randomBytes(32).toString('hex')).
  2. El invitado recibe un deep link (nvito://join/{token}) o escanea un QR con el token.
  3. POST /v1/mobile/auth/guest-access valida el token y lo vincula al invitado y evento.
  4. Si es valido, se crea una MobileSession con role=GUEST y se genera:
    • JWT (15 min de vida) con payload { sub: eventId, role: "GUEST", guestId, sessionId }
    • Refresh token (30 dias)

MobileAuthGuard

El MobileAuthGuard protege todos los endpoints bajo /v1/mobile/ (excepto auth):

  1. Extrae el JWT del header Authorization: Bearer <token>
  2. Verifica firma y expiración usando MOBILE_JWT_SECRET
  3. Extrae eventId, role (HOST/GUEST), userId/guestId y sessionId
  4. Verifica que la MobileSession este activa y no expirada
  5. Construye MobileAuthenticatedUser y lo adjunta al request

Interface MobileAuthenticatedUser

CampoTipoDescripción
sessionIdstringUUID de la sesion movil activa
eventIdstringUUID del evento asociado
role'HOST' | 'GUEST'Rol movil del usuario
userIdstring | nullUUID del usuario (solo HOST)
guestIdstring | nullUUID del invitado (solo GUEST)
deviceIdstringIdentificador del dispositivo

Refresh Token (Rotación)

La renovacion del JWT sigue el patrón de rotación de refresh tokens:

  1. POST /v1/mobile/auth/refresh con el refresh token actual
  2. El API valida el refresh token contra la tabla MobileSession
  3. Se genera un nuevo JWT (15 min) y un nuevo refresh token
  4. El refresh token anterior se invalida (cada token solo se usa una vez)
  5. 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

ModeloProposito
EventAccessCodeCódigo + PIN (bcrypt) para login de anfitrion
GuestAccessTokenToken aleatorio de 64 chars para acceso de invitado
MobileSessionSesion activa con refreshToken, pushToken, deviceId

Push Token Registration

Tras un login exitoso, la app registra el push token de Expo:

  1. La app solicita permisos de notificación al usuario
  2. Si se otorgan, obtiene el Expo Push Token via getExpoPushTokenAsync()
  3. POST /v1/mobile/auth/register-push almacena el token en MobileSession.pushToken
  4. 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)

  1. ClerkAuthGuard - Autenticación y resolución de identidad
  2. UserThrottlerGuard - Rate limiting por usuario/IP
  3. RoleGuard - Verificacion de roles básicos (owner/admin/member)

Guards Opcionales por Endpoint

  1. RolesGuard - Roles RBAC avanzados (RoleType enum) via PermissionsService.hasRole()
  2. PermissionsGuard - Permisos granulares (resource:action:scope)
  3. EventAccessGuard - Acceso a un evento específico
  4. AdminPanelGuard - Acceso al panel admin (Super Admins + Platform Admins)
  5. SuperAdminGuard - Restriccion exclusiva para Super Admins (endpoints destructivos)
  6. InvitationStateGuard - Bloqueo de edicion según estado de invitación
  7. MobileRoleGuard - Verificacion de rol movil (HOST/GUEST) en endpoints /v1/mobile/*
  8. CloudflareTokenGuard - Valida tokens de Cloudflare para endpoints de CDN/revalidación
  9. TwilioSignatureGuard - Valida firma de webhooks de Twilio (WhatsApp/SMS)

Diagrama de la Cadena de Guards

Detalle de Cada Guard

GuardPropositoBypass Super Admin
ClerkAuthGuardVerifica JWT, busca usuario en BD, resuelve roles desde user_rolesN/A
UserThrottlerGuardRate limiting: trackea por user:{userId} o ip:{ip}No
RoleGuardValida roles básicos (owner, admin, member) del decorator @Roles()Si
RolesGuardValida roles RBAC avanzados (RoleType enum) via PermissionsServiceImplicito
PermissionsGuardValida permisos granulares (resource:action:scope)Implicito
EventAccessGuardVerifica acceso al evento específico via user_rolesSi
AdminPanelGuardPermite acceso si isSuperAdmin || isPlatformAdmin. Usado a nivel de clase en controllers del panel adminN/A
SuperAdminGuardRestringe acceso exclusivamente a Super Admins. Usado a nivel de método para endpoints destructivos (DELETE)N/A
InvitationStateGuardBloquea escrituras si la invitación está PUBLISHED o CLOSEDNo
MobileAuthGuardVerifica JWT movil (MOBILE_JWT_SECRET), resuelve sesion y rol (HOST/GUEST). Protege endpoints /v1/mobile/*N/A (sistema independiente)
MobileRoleGuardVerifica que el usuario movil tenga el rol requerido (HOST o GUEST) via @RequireMobileRole(). Se usa después de MobileAuthGuardN/A (sistema independiente)
CloudflareTokenGuardValida tokens de Cloudflare para endpoints de CDN y revalidación de cacheNo
TwilioSignatureGuardValida firma de webhooks de Twilio (WhatsApp/SMS) usando HMACNo

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

RolDescripción
SUPER_ADMINAcceso completo al sistema. Gestióna todas las organizaciones.
PLATFORM_ADMINAdministrador de plataforma. Lectura cross-org, escritura en orgs asignadas. No puede eliminar usuarios/orgs ni gestionar SA/PA.
ORGANIZATION_OWNERDueno de la organización. Control total sobre su organización y usuarios.
ORGANIZATION_ADMINAdministrador. Gestión operativa completa de eventos y recursos.
EVENT_MANAGERGestor de eventos. Control completo sobre eventos asignados.
EVENT_EDITOREditor. Puede editar eventos e invitados, pero no eliminar.
EVENT_COLLABORATORColaborador. Acceso limitado: agregar/editar invitados y ver info del evento.
EVENT_VIEWERVisualizador. Solo lectura de los eventos asignados.

RoleScope Enum

ScopeSignificadoEjemplo de uso
GLOBALAplica a todo el sistemaSuper Admin, Platform Admin
ORGANIZATIONAplica a toda una organizaciónOwner, Admin (y PA con asignación de org)
EVENTAplica a un evento específicoManager, 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

RecursoSUPER_ADMINPLATFORM_ADMINORG_OWNERORG_ADMINEVT_MANAGEREVT_EDITOREVT_COLLABEVT_VIEWER
Organizations*RR/U/DRR---
Events*CRUD/M/E (1)CRUD/M/ECRUD/M/ER/U/M/ER/URR
Guests*CRUD/I/E (1)CRUD/I/ECRUD/I/ECRUD/I/ECRUD/I/EC/R/UR
Invitations*CRUD/P/S (1)CRUD/P/SCRUD/P/SCRUD/P/SC/R/U/PRR
Templates*CRUDCRUDCRUDRR--
Media*CRUD/M (1)CRUD/MCRUD/MCRUDCRUDRR
QR Passes*CRUD/V (1)CRUD/VCRUD/VCRUD/VR/VRR
Tables*CRUD/A (1)CRUD/ACRUD/ACRUD/AR/U/ARR
Music*CRUD (1)CRUDCRUDCRUDC/R/URR
Itinerary*CRUD (1)CRUDCRUDCRUDC/R/URR
Locations*CRUD (1)CRUDCRUDCRUDC/R/URR
Users*R/U (2)CRUDR----
Roles*R/A (3)R/AR/A(ev)A(ev)---
Settings*RR/UR----
Analytics*R/ER/ER/ERRRR
AI Generation*C/R (1)C/RC/RC/RC/R--
Event Services*CRUD/M (1)CRUD/MCRUD/MCRUD/MR/URR
Collaborators*M (1)MMM---
Audit Logs*RR-----

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. (1) Escritura solo en organizaciones asignadas explicitamente. Lectura cross-org (todas las organizaciones).
  2. (2) No puede editar Super Admins ni otros Platform Admins. No puede cambiar isSuperAdmin. No puede eliminar usuarios.
  3. (3) Puede asignar roles org/event en sus orgs asignadas. No puede asignar SUPER_ADMIN ni PLATFORM_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 al SUPER_ADMIN.
  • PLATFORM_ADMIN no 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.

DecoradorGuard asociadoProposito
@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')RoleGuardRequiere uno de los roles básicos listados
@RequireRole(RoleType)RolesGuardRequiere roles RBAC avanzados (enum RoleType de Prisma)
@RequirePermission(p)PermissionsGuardRequiere permiso granular resource:action:scope
@RequirePermissions(p)PermissionsGuardRequiere TODOS los permisos listados
(AdminPanelGuard)AdminPanelGuardPermite acceso a SA + PA. Se aplica a nivel de clase en controllers admin
@RequireSuperAdmin()SuperAdminGuardRestringe a Super Admins exclusivamente. Se aplica a nivel de método para DELETE y endpoints destructivos
@RequireEventAccess()EventAccessGuardValida acceso al evento del parametro eventId
@CheckInvitationState()InvitationStateGuardBloquea escrituras según estado de la invitación
@RequireMobileRole()MobileRoleGuardRequiere 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 / DirectivaConfiguración
Content-Security-Policydefault-src 'self', object-src 'none', frame-src 'none'
Strict-Transport-Security (HSTS)max-age=31536000; includeSubDomains; preload
X-Frame-OptionsDENY
X-Content-Type-Optionsnosniff
X-Permitted-Cross-Domain-Policiesnone
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policygeolocation=(), 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

ParametroValor
Limite100 requests por minuto
Ventana60,000 ms (1 minuto)
Trackinguser:{userId} si autenticado, ip:{ip} si público
StorageRedisThrottlerStorage (Redis)
Fail-openSi 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:

  1. Obtiene el AuthenticatedUser del request (resuelto por ClerkAuthGuard).
  2. Si el usuario tiene organizationId, ejecuta: SELECT set_config('app.current_tenant_id', '{orgId}', true).
  3. El parametro true indica 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 organizationId en 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

ProyectoEntornoUso
localDesarrolloSecretos para desarrollo local
devDevelopmentSecretos del servidor de dev
testTestingSecretos del entorno de pruebas

Inyección por Entorno

  • Local: Se usa el CLI bws para inyectar secretos como variables de entorno (bws secret get <id>). El archivo .env local nunca se sube al repositorio.
  • GitLab CI/CD: El service account de Bitwarden se configura como variable protegida. Un paso del pipeline ejecuta bws y 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

VariableDescripción
CLERK_SECRET_KEYClave secreta de Clerk para verificar JWT
DATABASE_URLConnection string de PostgreSQL
REDIS_URLConnection string de Redis
CORS_ORIGINSOrigenes permitidos para CORS
ENCRYPTION_KEYClave de encriptacion de campos sensibles
MOBILE_JWT_SECRETClave 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

MecanismoImplementación
Clerk ProviderContexto de autenticación a nivel de layout raiz
auth() en Server ActionsCada server action obtiene el token JWT de Clerk y valida autenticación antes de ejecutar
Layout guardEl layout del dashboard verifica que el usuario este autenticado
RBAC frontendusePermissionsQuery 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:

AspectoImplementación
EntropiaShortCode de 12 caracteres generado con crypto.randomBytes (71 bits de entropia), URL-safe
Datos expuestosSolo firstName, lastName, title, groupSize — nunca email, teléfono ni IDs internos
Validación de estadoSolo retorna datos si isActive === true y expiresAt no ha pasado
Prevencion XSSLos textos inyectados en el HTML usan textContent (no innerHTML), evitando ejecución de código
No enumeracionEl shortCode tiene espacio de búsqueda de 2^71 combinaciones, haciendo inviable la enumeracion por fuerza bruta
Rate limitingProtegido por el UserThrottlerGuard global de la API
Respuestá en errorRetorna 404 generico sin revelar si el shortCode existe pero está inactivo vs. no existe
Esta pagina fue util?