Saltar al contenido principal

Verificación de Usuario — Referencia Técnica

← Volver a Verificación de Usuario

Dónde vive esto

Backend — sistema de insignias (no administrador)

  • apps/backend/graphql/resolvers/verification.resolver.js — Query isVerified, getVerificationStatus, verificationPriceCoins; Mutation requestVerification, grantVerification, removeVerification. Nota: getPendingVerificationRequests y rejectVerificationRequest solían vivir aquí también, pero fueron eliminados — los propios comentarios del archivo de resolver explican que "solían ser accesibles por cualquier usuario regular sin ninguna verificación de rol" y se trasladaron a las operaciones con espacio de nombres de administrador que aparecen más abajo (este documento antes indicaba que seguían presentes aquí — corregido).
  • apps/backend/graphql/resolvers/user.resolver.jseste archivo ya no existe (este documento antes lo citaba como una fuente de resolvers superpuestos/duplicados — desactualizado; el archivo monolítico se dividió en muchos archivos user-*.resolver.js). El sobreviviente relacionado con verificación es apps/backend/graphql/resolvers/user-verification.resolver.js, que ahora solo tiene verificationRequest (singular, consulta por ID) y verificationBadgeInfo — el comentario de su encabezado explica que los antiguos resolvers duplicados isVerified/getPendingVerificationRequests/requestVerification/grantVerification/removeVerification "tenían errores reales de orden de argumentos" y fueron eliminados por completo en lugar de mantenerse como código muerto.
  • apps/backend/graphql/types/verification.type.js — su bloque extend type Mutation ahora solo declara grantVerification; getPendingVerificationRequests/rejectVerificationRequest se declaran únicamente en el archivo de tipos de administrador que aparece más abajo.
  • apps/backend/managers/user-managers/verification-badges.manager.js — lógica de negocio de solicitud/otorgamiento/rechazo. También contiene un método reviewVerificationRequest (líneas ~443-523) que llama internamente a grantVerification pero opera sobre un objeto de solicitud simulado (mock) codificado de forma fija — no lo llama ningún resolver encontrado en el repositorio; es código muerto, no forma parte del flujo real.

Backend — revisión de administrador (frontend-admin, aplicación separada de frontend-nextjs)

No existe un verification-admin.resolver.js dedicado — las operaciones de administrador están agrupadas en los archivos generales de moderación de usuarios de administrador:

  • apps/backend/graphql/types/admin/user-moderation.type.jsadminGetPendingVerificationRequests, adminVerifyUser(userId, verificationType), adminRemoveVerification(userId, reason), adminRejectVerificationRequest(userId, reason). Reutiliza los tipos compartidos VerificationRequestsResponse/VerificationRequest de verification.type.js en lugar de volver a declararlos.
  • apps/backend/graphql/resolvers/admin/user-moderation.resolver.js — las cuatro están protegidas por el permiso de administrador VERIFY_USERS. Listar y rechazar son envoltorios delgados alrededor de exactamente los mismos métodos del manager que el resolver no administrador de arriba (adminGetPendingVerificationRequestsuserManager.getPendingVerificationRequestsverificationBadgesManager.getPendingVerificationRequests; adminRejectVerificationRequestuserManager.rejectVerificationRequestverificationBadgesManager.rejectVerificationRequest — misma lógica, solo que protegida por un JWT real de AdminUser + verificación de permiso en lugar de las antiguas consultas sin protección). Aprobar es una implementación genuinamente separada: adminVerifyUser llama a userModerationManager.verifyUser en apps/backend/managers/admin-managers/user-moderation.manager.js (líneas ~777-820) — no a verificationBadgesManager.grantVerification. Establece isVerified: true, verificationStatus: 'approved', verifiedAt, y deliberadamente deja verifiedBy: null (un comentario lo explica: en esta ruta de administrador admin.adminId es un admin_user.id, una tabla distinta de users.id, por lo que escribirlo en la columna FK verifiedBy violaría la restricción user_verified_by_fkey — el rastro de auditoría de quién aprobó pasa en su lugar por logModerationAction()). A diferencia de grantVerification, esta ruta no persiste verificationCategory/verificationBadgeType (el frontend sí envía verificationType, pero solo se usa en una cadena de mensaje de notificación, nunca se escribe en la fila del usuario) y no tiene protección contra volver a aprobar a un usuario que ya está verificado.

Frontend

  • apps/frontend-nextjs/src/page-components/ProfilePage.tsx / PublicProfilePage.tsx — leen user?.isVerified y renderizan un <VerifiedBadge> junto al nombre de usuario
  • apps/frontend-nextjs/src/page-components/settings/VerificationRequestPage.tsx — consulta getVerificationStatus y llama a requestVerification. No existe interfaz de revisión de administrador ni formulario de apelación en apps/frontend-nextjs — eso vive por completo en la aplicación de administrador separada que se describe más abajo.
  • apps/frontend-admin/src/app/verification/page.tsx — la cola real de revisión de administrador (este documento antes decía que no existía ninguna interfaz de administrador en ningún lugar — corregido). Una tabla paginada (20 por página) de solicitudes pendientes que muestra avatar, nombre de usuario, insignia de verificationCategory, verificationNotes truncadas y fecha de solicitud con formato, con botones de Aprobar/Rechazar por fila; Rechazar abre un modal que recoge un motivo de texto libre. Protegida del lado del cliente por admin.role === 'super_admin' || admin.permissions.VERIFY_USERS. Nota: la consulta obtiene verificationDocuments, pero el componente nunca los renderiza — no hay un modal de revisión de imágenes de documentos en esta página (a diferencia de la página de administrador separada de verificación de identidad, que sí tiene uno — ver más abajo).
  • Esta página maneja únicamente el sistema de notoriedad/insignias. El sistema no relacionado de edad/identificación (estilo KYC) tiene su propia página de administrador separada, apps/frontend-admin/src/app/moderation/identity-verifications/page.tsx — operaciones GraphQL diferentes (adminPendingIdentityVerifications/adminApproveIdentityVerification/adminRejectIdentityVerification), un permiso diferente (MODERATE_CONTENT, no VERIFY_USERS), y sí cuenta con un modal de revisión de documento + selfie. Consulta Verificación de Edad e Identidad para ese sistema.

Backend — apelaciones de restricción (construido en este pase, reemplaza la nota anterior de "no verificado" que aparecía más abajo)

Frontend — apelaciones de restricción

  • apps/frontend-nextjs/src/page-components/settings/AccountStatusPage.tsx (/settings/account-status) — consulta accountStandingStatus + myLatestAppeal juntos; mientras está restringida, renderiza una AppealSection que muestra un formulario de envío (llama a appealRestriction), un estado "pendiente" o un estado "denegado" con las notas del administrador, según myLatestAppeal.status
  • apps/frontend-admin/src/app/moderation/appeals/page.tsx (/moderation/appeals) — cola de revisión de administrador, paginada, protegida del lado del cliente por MODERATE_CONTENT; un modal de revisión muestra el estado actual del usuario y el motivo de la apelación con acciones de Aprobar/Denegar (Denegar requiere notas). Consulta Revisión de Apelaciones.

Checklist de implementación técnica

  • isVerifiedverification.resolver.js; el frontend ProfilePage.tsx/PublicProfilePage.tsx lo lee y renderiza la insignia de verificación
  • getVerificationStatus — resolver conectado; VerificationRequestPage.tsx lo consulta y lee verificationStatus/verificationCategory/verifiedAt/verificationRequestedAt para impulsar su interfaz
  • verificationBadgeInfo — resolver conectado en user-verification.resolver.js; no se encontró ningún consumidor en el frontend
  • requestVerificationVerificationRequestPage.tsx lo llama directamente
  • adminGetPendingVerificationRequests — la página /verification de frontend-admin lo llama (este documento antes decía que no existía ninguna interfaz de administrador en ningún lugar — corregido)
  • adminVerifyUser — botón Aprobar de la página /verification; implementación separada de grantVerification, ver arriba
  • adminRejectVerificationRequest — botón Rechazar de la página /verification (el modal recoge el motivo)
  • adminRemoveVerification — conectado a la acción "Quitar verificación" de la página /users/[id] de frontend-admin, mostrada una vez que un usuario está verificado — este documento antes decía que ninguna página de administrador lo llamaba; corregido
  • grantVerification (la mutación no administrativa) — todavía está declarada y conectada, pero ningún frontend la llama directamente; el flujo real de aprobación pasa por adminVerifyUser en su lugar
  • appealRestriction / getAppealStatus / myLatestAppeal — construidos en este pase, implementación real sobre user.accountStatus. appealRestriction/myLatestAppeal son consumidos por AccountStatusPage.tsx; getAppealStatus (una sola apelación por id) no tiene ningún consumidor directo en el frontend — el frontend usa myLatestAppeal en su lugar
  • adminGetAppeals / adminReviewAppeal — construidos en este pase, conectados a la cola de revisión /moderation/appeals de frontend-admin

Verificación del estado de verificación

isVerified es un booleano ligero — úsalo para decidir si renderizar un ícono de insignia sin cargar el objeto de verificación completo.

getVerificationStatus devuelve el registro de verificación completo, incluyendo verificationStatus (si la solicitud está pendiente, aprobada o rechazada) y verificationCategory. Úsalo en la pantalla de configuración de un usuario para mostrar el estado de su solicitud.

verificationBadgeInfo devuelve todos los datos de renderizado necesarios para mostrar la insignia: badgeColor, badgeIcon, y displayName se devuelven para que los clientes puedan renderizar el estilo de insignia correcto según la categoría sin codificarlo de forma fija.

query IsVerified($userId: ID!) {
isVerified(userId: $userId)
}

query VerificationStatus($userId: ID!) {
getVerificationStatus(userId: $userId) {
isVerified
verificationStatus # pending | approved | rejected
verificationCategory # creator | business | public_figure | etc.
verifiedAt
verificationRequestedAt
}
}

query BadgeInfo($userId: ID!) {
verificationBadgeInfo(userId: $userId) {
isVerified
badgeType
verificationCategory
verifiedAt
badgeColor
badgeIcon
displayName
}
}

Solicitud de verificación

requestVerification envía una nueva solicitud. El usuario debe subir primero los documentos de respaldo a S3 y pasar las URL resultantes en verificationDocuments. La solicitud entra en la cola de revisión de administrador con estado pending. Un usuario solo puede tener una solicitud activa a la vez.

mutation RequestVerification($input: RequestVerificationInput!) {
requestVerification(input: $input) {
success message
user { id username isVerified }
}
}

Campos de RequestVerificationInput:

CampoDescripción
verificationCategoryLa categoría que se solicita (por ejemplo, creator, business, public_figure)
verificationDocumentsArreglo de URL de S3 que apuntan a los documentos de respaldo subidos
verificationNotesExplicación opcional de texto libre

Revisión de administrador

Se accede desde la página /verification de frontend-admin, protegida por el permiso de administrador VERIFY_USERS — estas son las operaciones reales y vigentes (este documento antes mostraba aquí los nombres no administrativos getPendingVerificationRequests/grantVerification/rejectVerificationRequest/removeVerification, que o bien se trasladaron al espacio de nombres de administrador o en realidad no los llama ningún frontend — corregido):

adminGetPendingVerificationRequests lista todas las solicitudes de verificación sin resolver — un envoltorio delgado alrededor de la misma lógica de verificationBadgesManager.getPendingVerificationRequests que antes, solo que protegida por una verificación de permiso de administrador real en lugar de la antigua consulta sin protección.

adminVerifyUser aprueba la solicitud. A diferencia de la antigua mutación grantVerification, esta es una implementación separada (userModerationManager.verifyUser) que establece isVerified/verificationStatus/verifiedAt pero no persiste verificationCategory, verificationBadgeType, ni verifiedBy (se deja en null — ver "Dónde vive esto" arriba para saber por qué), y no tiene protección contra aprobar a un usuario que ya está verificado.

adminRejectVerificationRequest deniega la solicitud con un reason — otro envoltorio delgado alrededor de la misma lógica del manager que antes.

adminRemoveVerification existe y está conectado de extremo a extremo con la página /users/[id] de frontend-admin (acción "Quitar verificación", mostrada una vez que un usuario está verificado) — este documento antes decía que ninguna página de administrador lo llamaba; corregido.

# List pending requests
query AdminPendingVerificationRequests($limit: Int, $offset: Int) {
adminGetPendingVerificationRequests(limit: $limit, offset: $offset) {
total
requests {
id userId verificationCategory verificationStatus
verificationRequestedAt verificationDocuments verificationNotes
user { id username profilePicture }
}
}
}

# Approve a verification request
mutation AdminVerifyUser($userId: ID!, $verificationType: String) {
adminVerifyUser(userId: $userId, verificationType: $verificationType) {
success message
}
}

# Deny a request with a reason communicated to the user
mutation AdminRejectVerificationRequest($userId: ID!, $reason: String) {
adminRejectVerificationRequest(userId: $userId, reason: $reason) { success message }
}

# Revoke an already-granted badge — wired to /users/[id]'s "Quitar verificación" action
mutation AdminRemoveVerification($userId: ID!, $reason: String!) {
adminRemoveVerification(userId: $userId, reason: $reason) { success message }
}

La mutación no administrativa grantVerification (mostrada en versiones anteriores de este documento) todavía existe en el esquema y todavía funciona, pero ningún frontend la llama — el flujo real de aprobación pasa por adminVerifyUser en su lugar.

Apelaciones de restricción

Construido en este pase — reemplaza los antiguos stubs rotos (appealRestriction/getAppealStatus solían vivir en contacts-validation.type.js/user-restrictions.resolver.js con discrepancias de argumentos, formas de retorno incorrectas y sin persistencia real; ambos fueron eliminados por completo). La implementación real es un nuevo modelo/tabla Appeal, construido sobre el sistema de restricción real (user.accountStatussuspended/banned, establecido por suspendUser/banUser), no el sistema muerto separado isRestricted/restrictedUntil.

Un usuario cuyo accountStatus está actualmente en suspended o banned puede enviar una apelación explicando por qué debería levantarse la restricción (appealRestriction) — el manager rechaza un segundo envío mientras ya hay uno pending. myLatestAppeal devuelve la apelación más reciente de quien llama (cualquier estado) para que el frontend pueda renderizar el estado correcto (formulario / pendiente / previamente denegada) sin necesitar de antemano un id de apelación; getAppealStatus obtiene una apelación específica por id, con una verificación de propiedad, pero hoy no tiene ningún consumidor en el frontend ya que myLatestAppeal cubre la necesidad real de la interfaz.

Del lado del administrador, adminGetAppeals lista las apelaciones (por defecto pending, de más antigua a más reciente), protegida por el permiso MODERATE_CONTENT. adminReviewAppeal aprueba o deniega una: al aprobar, llama a los unsuspendUser/unbanUser reales en user-moderation.manager.js (eligiendo el que coincida con el accountStatus actual del usuario) para que la restricción se levante efectivamente, no solo se marque como revisada; al denegar, simplemente registra la decisión y la restricción permanece vigente. Ambas operaciones comparten el tipo GraphQL AppealStatus con el esquema orientado al usuario (el mismo precedente que CoinCashout, compartido entre los archivos de tipos de pago).

mutation AppealRestriction($reason: String!) {
appealRestriction(reason: $reason) {
id status reason submittedAt
}
}

query MyLatestAppeal {
myLatestAppeal {
id status reason submittedAt reviewedAt decisionNotes
}
}

query AppealStatus($appealId: ID!) {
getAppealStatus(appealId: $appealId) {
id status reason submittedAt reviewedAt decisionNotes
reviewer { username }
}
}

Revisión de administrador

query AdminGetAppeals($status: String, $limit: Int, $offset: Int) {
adminGetAppeals(status: $status, limit: $limit, offset: $offset) {
total limit offset
appeals {
id status appealType reason submittedAt reviewedAt decisionNotes
user { id username email accountStatus suspensionReason suspendedUntil }
}
}
}

mutation AdminReviewAppeal($appealId: ID!, $decision: String!, $notes: String) {
adminReviewAppeal(appealId: $appealId, decision: $decision, notes: $notes) {
success message
appeal { id status }
}
}

Consulta Revisión de Apelaciones para el lado del panel de administrador.