Revisión de apelaciones
Los administradores revisan apelaciones de restricción — solicitudes de usuarios suspendidos o baneados que piden que se levante la restricción. Esta es la mitad orientada al administrador de Verificación → Apelaciones de restricción; la mitad orientada al usuario (enviar una apelación desde Settings → Account status) está documentada allí. Ambas se construyeron en este pase — antes appealRestriction/getAppealStatus existían solo como stubs rotos, sin ninguna superficie de revisión para administradores.
Checklist de implementación
- Listar apelaciones pendientes (
adminGetAppeals) — por defectostatus: 'pending', más antiguas primero; paginado (20 por página) - Revisar una apelación — aprobar (
adminReviewAppealcondecision: 'approved') efectivamente levanta la restricción al llamar a la lógica real deunsuspendUser/unbanUserenuser-moderation.manager.js(eligiendo la que coincida con elaccountStatusactual del usuario), no solo marca la apelación como revisada - Revisar una apelación — denegar (
adminReviewAppealcondecision: 'denied') permite al administrador agregar notas explicando la decisión (el camponoteses opcional, la API no lo exige); la restricción permanece vigente y las notas, si las hay, se muestran de vuelta al usuario en su propia página de Account status
No hay una acción de revisión en lote — las apelaciones se revisan una a la vez.
Dónde vive esto
Backend
apps/backend/graphql/types/admin/appeal-admin.type.js—adminGetAppeals,adminReviewAppeal, compartiendo el tipoAppealStatusdeclarado enappeal.type.js(el mismo patrón queCoinCashoutsiendo compartido entre los archivos de tipo de payout del cliente y del administrador)apps/backend/graphql/resolvers/admin/appeal-admin.resolver.js— ambas operaciones están protegidas por el permisoMODERATE_CONTENT(reutilizado en lugar de agregar una nueva clave de permiso — el precedente existente más cercano es la cola de revisión de verificación de identidad, que tiene la misma forma de "el usuario envía algo, el administrador lo revisa")apps/backend/managers/user-managers/appeal.manager.js—getAppeals/reviewAppeal; construido sobre el sistema de restricción realuser.accountStatus/suspensionReason/suspendedUntilusado poruser-moderation.manager.js, deliberadamente no el sistema separado y muertoisRestricted/restrictedUntilderestrictions-limits.manager.jsapps/backend/data-access-services/user/appeal.access-service.js— acceso a la tablaappeal, una tabla nueva dedicada (migración20260718000000-create-appeal.js, indexada por(user_id, status))
Frontend — apps/frontend-admin/src/app/moderation/appeals/page.tsx (/moderation/appeals), protegido del lado del cliente por MODERATE_CONTENT, con la entrada de navegación mostrada condicionalmente en AdminLayout.tsx. Una tabla paginada muestra el usuario de cada apelación, el estado actual de su cuenta (con el motivo de suspensión/baneo si existe), y un motivo de apelación truncado; un botón "Review" abre un modal con el motivo completo y las acciones Approve/Deny — Deny revela un área de texto opcional para notas antes de confirmar la denegación (nada obliga a llenarla).
Referencia de GraphQL
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 }
}
}