Saltar al contenido principal

Verificación de edad e identidad — Referencia técnica

← Volver a Verificación de edad e identidad

Cubre dos mecanismos de cumplimiento distintos, ambos deliberadamente separados de la insignia de verificación de notoriedad (esa insignia trata sobre autenticidad/notoriedad y no tiene nada que ver con edad ni KYC):

  1. Edad — se aplica una sola vez, al registrarse, a partir de la fecha de nacimiento de la cuenta.
  2. Identidad (KYC) — un flujo manual, revisado por admin, de documento oficial + selfie, que condiciona los retiros de dinero real.

Dónde vive esto

Backend — edad

  • apps/backend/validators/user.validator.jsvalidateDateOfBirth exige una edad mínima de 18 (subida desde 13 en esta pasada), y validateRegistrationInput ahora requiere dateOfBirth (antes opcional). Esto era un hueco real: la columna date_of_birth y validateDateOfBirth ya existían, pero el registro trataba el campo como opcional y el formulario de registro nunca lo pedía, así que en la práctica ninguna verificación de edad corría de punta a punta.
  • apps/backend/database/models/user.js — el atributo preexistente dateOfBirth (date_of_birth, DATEONLY); sin cambios.

Backend — identidad

Frontend (frontend-nextjs)

  • apps/frontend-nextjs/src/components/Login.tsx — el formulario de registro ahora tiene un campo obligatorio de fecha de nacimiento con un control 18+ del lado del cliente (el backend también lo exige) y pasa dateOfBirth en el input de la mutation Register.
  • apps/frontend-nextjs/src/page-components/settings/IdentityVerificationPage.tsx (ruta /settings/identity-verification) — sube la foto del documento + la selfie al endpoint REST /upload existente (no hay mutation de GraphQL para subir medios), llama a submitIdentityVerification, y muestra el estado propio de quien llama (none/pending/approved/rejected + motivo de rechazo, con reenvío tras un rechazo) desde myIdentityVerificationStatus. Enlazada desde el menú de configuración en SettingsPage.tsx.
  • apps/frontend-nextjs/src/page-components/PaymentsPage.tsx — la pestaña de Retiros muestra un aviso de bloqueo por verificación de identidad (con enlace a la página de configuración, o un estado "en revisión") cuando la identidad de quien llama no está aprobada, para que el bloqueo del backend no sea una sorpresa.
  • apps/frontend-nextjs/src/page-components/settings/PayoutsPage.tsx (ruta /settings/payouts, enlazada desde el menú de configuración) — una página de configuración de retiros más reciente y dedicada, que muestra el mismo aviso de bloqueo por verificación de identidad.

Frontend (frontend-admin)

  • apps/frontend-admin/src/app/moderation/identity-verifications/page.tsx — la cola de revisión: lista adminPendingIdentityVerifications, abre un modal que muestra el documento + la selfie enviados, y aprueba (adminApproveIdentityVerification) o rechaza con un motivo obligatorio (adminRejectIdentityVerification). Protegida con MODERATE_CONTENT (super_admin la omite), reflejando la verificación del lado del servidor. Enlazada desde AdminLayout.tsx.

Checklist de implementación técnica

  • Edad mínima de 18 aplicada + dateOfBirth obligatorio al registrarse — user.validator.js, y el formulario de registro en Login.tsx ahora la recoge
  • submitIdentityVerificationidentity-verification.resolver.js + IdentityVerificationPage.tsx; documento + selfie subidos vía /upload, reenviable tras un rechazo
  • myIdentityVerificationStatus — pantalla de estado en IdentityVerificationPage.tsx y el aviso de bloqueo de la pestaña de retiros en PaymentsPage.tsx
  • adminPendingIdentityVerifications / adminApproveIdentityVerification / adminRejectIdentityVerification — cola de admin en frontend-admin, protegida con MODERATE_CONTENT
  • Bloqueo de retiros — coin-cashout.manager.js requestCashout / requestManualCashout requieren identidad aprobada vía requireApprovedForCashout
  • Verificación de identidad automatizada / de terceros (Stripe Identity, Persona, etc.) — no construida; hoy es totalmente manual/revisada por admin
  • Reverificación de edad más allá del control de fecha de nacimiento al registrarse — no construida

API de GraphQL

Definido en graphql/types/identity-verification.type.js, resuelto en graphql/resolvers/identity-verification.resolver.js.

El documento y la selfie deben subirse primero al endpoint REST /upload (igual que cualquier otra subida de medios en este código); las URLs resultantes se pasan a submitIdentityVerification. Enviar pone el estado de quien llama en pending; luego un admin aprueba o rechaza. Un usuario rechazado puede reenviar, lo que lo regresa a pending y borra el motivo de rechazo anterior.

# El estado propio de quien llama
query MyIdentityVerificationStatus {
myIdentityVerificationStatus {
identityVerificationStatus # none | pending | approved | rejected
identityVerificationRequestedAt
identityVerifiedAt
identityRejectionReason
}
}

# Enviar / reenviar (documentUrl + selfieUrl son URLs de S3 de /upload)
mutation SubmitIdentityVerification($input: IdentityVerificationSubmitInput!) {
submitIdentityVerification(input: $input) {
identityVerificationStatus
identityVerificationRequestedAt
}
}

# Cola de revisión de admin (requiere MODERATE_CONTENT)
query PendingIdentityVerifications($limit: Int, $offset: Int) {
adminPendingIdentityVerifications(limit: $limit, offset: $offset) {
id username email firstName lastName profilePicture
identityVerificationStatus
identityDocumentUrl
identitySelfieUrl
identityVerificationRequestedAt
}
}

# Decisiones de admin (requieren MODERATE_CONTENT)
mutation ApproveIdentityVerification($userId: ID!) {
adminApproveIdentityVerification(userId: $userId) { identityVerificationStatus }
}
mutation RejectIdentityVerification($userId: ID!, $reason: String!) {
adminRejectIdentityVerification(userId: $userId, reason: $reason) { identityVerificationStatus }
}

Modelo de datos (columnas en users)

ColumnaDescripción
identity_verification_statusnone | pending | approved | rejected
identity_document_urlURL de S3 de la foto del documento de identidad enviada
identity_selfie_urlURL de S3 de la selfie enviada
identity_verification_requested_atCuándo se envió la solicitud (más reciente)
identity_verified_atCuándo la aprobó un admin
identity_verified_by_admin_idEl admin que aprobó/rechazó (FK → admin_user)
identity_rejection_reasonMotivo mostrado al usuario en el rechazo

Bloqueo de retiros

coin-cashout.manager.js#requireApprovedForCashout(userId) lanza errors.identity_verification.not_approved a menos que el identity_verification_status del usuario sea approved. Se llama al inicio tanto de requestCashout (Stripe Connect) como de requestManualCashout (CLABE/RFC de México), así que ningún flujo de retiro puede iniciarse sin una identidad aprobada. Comprar monedas, recibir propinas/suscripciones y vender productos de la tienda no están bloqueados — solo convertir monedas de vuelta a dinero real.