Skip to main content

Appeals Review

Admins review restriction appeals — requests from suspended or banned users asking to have the restriction lifted. This is the admin-facing half of Verification → Restriction appeals; the user-facing half (submitting an appeal from Settings → Account status) is documented there. Both were built this pass — previously appealRestriction/getAppealStatus existed only as broken stubs with no admin review surface at all.

Implementation checklist

  • List pending appeals (adminGetAppeals) — defaults to status: 'pending', oldest first; paginated (20/page)
  • Review an appeal — approve (adminReviewAppeal with decision: 'approved') actually lifts the restriction by calling the real unsuspendUser/unbanUser logic in user-moderation.manager.js (picking whichever matches the user's current accountStatus), not just marking the appeal reviewed
  • Review an appeal — deny (adminReviewAppeal with decision: 'denied') lets the admin add notes explaining the decision (the notes field is optional, not enforced by the API); the restriction stays in place and any notes are shown back to the user on their own Account status page

There is no bulk-review action — appeals are reviewed one at a time.

Where this lives

Backend

Frontendapps/frontend-admin/src/app/moderation/appeals/page.tsx (/moderation/appeals), gated client-side on MODERATE_CONTENT, nav entry conditionally shown in AdminLayout.tsx. A paginated table shows each appeal's user, current account status (with suspension/ban reason if present), and a truncated appeal reason; a "Review" button opens a modal with the full reason and Approve/Deny actions — Deny reveals an optional notes textarea before the denial is confirmed (nothing forces it to be filled in).

GraphQL reference

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 }
}
}