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— QueryisVerified,getVerificationStatus,verificationPriceCoins; MutationrequestVerification,grantVerification,removeVerification. Nota:getPendingVerificationRequestsyrejectVerificationRequestsolí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).— este 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 archivosapps/backend/graphql/resolvers/user.resolver.jsuser-*.resolver.js). El sobreviviente relacionado con verificación esapps/backend/graphql/resolvers/user-verification.resolver.js, que ahora solo tieneverificationRequest(singular, consulta por ID) yverificationBadgeInfo— el comentario de su encabezado explica que los antiguos resolvers duplicadosisVerified/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 bloqueextend type Mutationahora solo declaragrantVerification;getPendingVerificationRequests/rejectVerificationRequestse 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étodoreviewVerificationRequest(líneas ~443-523) que llama internamente agrantVerificationpero 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.js—adminGetPendingVerificationRequests,adminVerifyUser(userId, verificationType),adminRemoveVerification(userId, reason),adminRejectVerificationRequest(userId, reason). Reutiliza los tipos compartidosVerificationRequestsResponse/VerificationRequestdeverification.type.jsen lugar de volver a declararlos.apps/backend/graphql/resolvers/admin/user-moderation.resolver.js— las cuatro están protegidas por el permiso de administradorVERIFY_USERS. Listar y rechazar son envoltorios delgados alrededor de exactamente los mismos métodos del manager que el resolver no administrador de arriba (adminGetPendingVerificationRequests→userManager.getPendingVerificationRequests→verificationBadgesManager.getPendingVerificationRequests;adminRejectVerificationRequest→userManager.rejectVerificationRequest→verificationBadgesManager.rejectVerificationRequest— misma lógica, solo que protegida por un JWT real deAdminUser+ verificación de permiso en lugar de las antiguas consultas sin protección). Aprobar es una implementación genuinamente separada:adminVerifyUserllama auserModerationManager.verifyUserenapps/backend/managers/admin-managers/user-moderation.manager.js(líneas ~777-820) — no averificationBadgesManager.grantVerification. EstableceisVerified: true,verificationStatus: 'approved',verifiedAt, y deliberadamente dejaverifiedBy: null(un comentario lo explica: en esta ruta de administradoradmin.adminIdes unadmin_user.id, una tabla distinta deusers.id, por lo que escribirlo en la columna FKverifiedByviolaría la restricciónuser_verified_by_fkey— el rastro de auditoría de quién aprobó pasa en su lugar porlogModerationAction()). A diferencia degrantVerification, esta ruta no persisteverificationCategory/verificationBadgeType(el frontend sí envíaverificationType, 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— leenuser?.isVerifiedy renderizan un<VerifiedBadge>junto al nombre de usuarioapps/frontend-nextjs/src/page-components/settings/VerificationRequestPage.tsx— consultagetVerificationStatusy llama arequestVerification. No existe interfaz de revisión de administrador ni formulario de apelación enapps/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 deverificationCategory,verificationNotestruncadas 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 poradmin.role === 'super_admin' || admin.permissions.VERIFY_USERS. Nota: la consulta obtieneverificationDocuments, 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, noVERIFY_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)
apps/backend/database/migrations/20260718000000-create-appeal.js+apps/backend/database/models/Appeal.js— nueva tablaappealdedicada (migración idempotente, protección conshowAllTables()), indexada en(user_id, status)apps/backend/data-access-services/user/appeal.access-service.js+apps/backend/managers/user-managers/appeal.manager.js— lógica de negocio real, construida sobreuser.accountStatus/suspensionReason/suspendedUntil(el sistema real de restricción usado poruser-moderation.manager.js), deliberadamente no el sistema muerto separadoisRestricted/restrictedUntilderestrictions-limits.manager.jsapps/backend/graphql/types/appeal.type.js+apps/backend/graphql/resolvers/appeal.resolver.js—appealRestriction/getAppealStatus/myLatestAppealorientados al usuario. Reemplaza los antiguos stubs rotos con los mismos nombres que solían vivir encontacts-validation.type.js/user-restrictions.resolver.js(esos fueron eliminados — discrepancias de argumentos, formas de retorno incorrectas, sin persistencia real)apps/backend/graphql/types/admin/appeal-admin.type.js+apps/backend/graphql/resolvers/admin/appeal-admin.resolver.js— cola de revisión de administrador,adminGetAppeals/adminReviewAppeal, protegida por el permisoMODERATE_CONTENT(reutilizado en lugar de agregar una nueva clave de permiso, el mismo precedente que la cola de verificación de identidad). Aprobar una apelación llama a losunsuspendUser/unbanUserreales enuser-moderation.manager.jspara levantar efectivamente la restricción; denegar solo registra la decisión.apps/backend/graphql/types/account-standing.type.js—accountStandingStatusobtuvo dos campos nuevos en este pase,reasonysuspendedUntil, que se completan solo mientras quien llama está realmente suspendido/baneado, de modo queAccountStatusPage.tsxpueda mostrar por qué y hasta cuándo sin una segunda consulta. Consulta Configuración → Estado de la cuenta para los demás campos de esa consulta.
Frontend — apelaciones de restricción
apps/frontend-nextjs/src/page-components/settings/AccountStatusPage.tsx(/settings/account-status) — consultaaccountStandingStatus+myLatestAppealjuntos; mientras está restringida, renderiza unaAppealSectionque muestra un formulario de envío (llama aappealRestriction), un estado "pendiente" o un estado "denegado" con las notas del administrador, segúnmyLatestAppeal.statusapps/frontend-admin/src/app/moderation/appeals/page.tsx(/moderation/appeals) — cola de revisión de administrador, paginada, protegida del lado del cliente porMODERATE_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
-
isVerified—verification.resolver.js; el frontendProfilePage.tsx/PublicProfilePage.tsxlo lee y renderiza la insignia de verificación -
getVerificationStatus— resolver conectado;VerificationRequestPage.tsxlo consulta y leeverificationStatus/verificationCategory/verifiedAt/verificationRequestedAtpara impulsar su interfaz -
verificationBadgeInfo— resolver conectado enuser-verification.resolver.js; no se encontró ningún consumidor en el frontend -
requestVerification—VerificationRequestPage.tsxlo llama directamente -
adminGetPendingVerificationRequests— la página/verificationdefrontend-adminlo 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 degrantVerification, 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]defrontend-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 poradminVerifyUseren su lugar -
appealRestriction/getAppealStatus/myLatestAppeal— construidos en este pase, implementación real sobreuser.accountStatus.appealRestriction/myLatestAppealson consumidos porAccountStatusPage.tsx;getAppealStatus(una sola apelación por id) no tiene ningún consumidor directo en el frontend — el frontend usamyLatestAppealen su lugar -
adminGetAppeals/adminReviewAppeal— construidos en este pase, conectados a la cola de revisión/moderation/appealsdefrontend-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:
| Campo | Descripción |
|---|---|
verificationCategory | La categoría que se solicita (por ejemplo, creator, business, public_figure) |
verificationDocuments | Arreglo de URL de S3 que apuntan a los documentos de respaldo subidos |
verificationNotes | Explicació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.accountStatus — suspended/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.