Admin Account Moderation API
Admin endpoints for account moderation — browsing mentee accounts, and suspending,
banning, or reinstating any account. Implements admin-brd.md §6, expressed on the
existing is_active / is_banned columns (no new user columns).
The surface is split to match the platform's mentor/mentee vocabulary:
-
/admin/mentees— role-scoped browsing of mentee accounts (this document) -
/admin/mentors— mentor application browsing and lifecycle (existing, keyed by profile id; application suspension there is independent of account moderation) -
/admin/accounts— the moderation actions, role-agnostic. A target may be a mentee, a mentor, or both — the account is moderated once, whatever roles it holds. -
Base URL:
/api/v1/admin -
Auth: Sanctum bearer token —
Authorization: Bearer {token} -
Role: all endpoints require the
adminrole — non-admins receive403
Moderation rules
- Suspend sets
is_active = false. Indefinite — there is no auto-expiry; only an admin reinstate lifts it. - Ban sets
is_banned = true. Indefinite; only an admin reinstate lifts it. - Reinstate restores
is_active = true, is_banned = falseand lifts either state. - Suspend and ban both revoke every Sanctum token, killing open sessions immediately,
and block login (
401). Moderated accounts are excluded from feed suggestions. - A reason is required for every action and is emailed to the user
(
account.suspended/account.banned/account.reinstated— mail only, since a moderated user cannot log in to read an in-app notification). - Every action writes an
admin_audit_logsentry in the same transaction as the state change — actor, before/after flags, reason, IP, user agent. If the audit write fails, the action rolls back. Who/when/why lives only in the audit log, never on theuserstable. - Policy guards: an admin cannot moderate themselves, and cannot moderate another
admin— both return403. - Account moderation is independent of the mentor application lifecycle — banning a
mentor's account does not change their
approval_status, and vice versa. - Banning does not touch the account's content — content takedown is a separate,
separately-audited decision (
admin-brd.md§7, pending).
Account status is derived from the two flags (ban wins):
is_banned |
is_active |
account_status |
Login |
|---|---|---|---|
false |
true |
active |
✅ |
false |
false |
suspended |
❌ |
true |
— | banned |
❌ |
Response envelope
Success
{ "status": "success", "message": "…", "data": { } }
Error
{ "status": "error", "message": "…", "data": { } }
Status codes
| Code | Meaning |
|---|---|
| 200 | OK (list, show, action applied) |
| 401 | Missing/invalid token |
| 403 | Caller lacks the admin role, or the policy forbids the target (self / another admin) |
| 404 | Account not found (or not a mentee, on the mentee routes) |
| 422 | Validation error, or illegal state transition |
| 500 | Server error |
Admin account object
{
"id": "9b6f…",
"full_name": "John Doe",
"username": "johndoe",
"email": "john@example.com",
"avatar_url": null,
"is_active": true,
"is_banned": false,
"account_status": "active",
"account_status_label": "Active",
"email_verified_at": "2026-07-01T09:00:00+00:00",
"roles": ["mentee"],
"mentee_profile": { "…mentee profile object…" },
"moderation_history": [
{
"id": "8a2c…",
"action": "user.banned",
"action_label": "User Banned",
"reason": "Fraudulent activity.",
"changes": {
"before": { "is_active": true, "is_banned": false },
"after": { "is_active": true, "is_banned": true }
},
"ip_address": "203.0.113.7",
"admin": { "id": "7c1d…", "full_name": "Admin One", "username": "admin1" },
"created_at": "2026-07-23T10:15:00+00:00"
}
],
"created_at": "2026-07-01T09:00:00+00:00",
"updated_at": "2026-07-23T10:15:00+00:00"
}
account_status/account_status_labelare derived from the flags (table above).moderation_historyis present on mentee detail and action responses — the 20 most recent audit entries against this account, newest first, each with the acting admin.mentee_profileis present when the relation is loaded (mentee detail).
1. List mentees
GET /api/v1/admin/mentees
Returns accounts holding the mentee role, newest first, paginated. A dual-role user
(mentor who is also a mentee) appears here too; accounts without the mentee role — e.g.
mentor-only or admin accounts — do not.
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
status |
string | — | Filter: active, suspended, or banned. Invalid values → 422. |
search |
string | — | Matches full_name, username, or email (partial) |
date_from |
date | — | Registered on/after this date (Y-m-d). Invalid dates → 422. |
date_to |
date | — | Registered on/before this date; must be ≥ date_from |
per_page |
int | 15 | Page size (max 100) |
Example
GET /api/v1/admin/mentees?status=banned&search=john&date_from=2026-07-01&per_page=20
200 Response
{
"status": "success",
"message": "Mentees retrieved.",
"data": {
"mentees": [ { "…admin account object…" } ],
"pagination": { "total": 42, "per_page": 20, "current_page": 1, "last_page": 3 }
}
}
Errors: 401 unauthenticated · 403 not an admin
2. Show a mentee
GET /api/v1/admin/mentees/{userId}
Returns the account with its mentee_profile and moderation_history embedded, so prior
admin actions are visible without a second request. 404 if the user does not exist or
does not hold the mentee role.
200 Response
{
"status": "success",
"message": "Mentee details retrieved.",
"data": { "mentee": { "…admin account object with moderation_history…" } }
}
Errors: 401 unauthenticated · 403 not an admin · 404 not found / not a mentee
3. Suspend an account
PATCH /api/v1/admin/accounts/{userId}/suspend
Role-agnostic — works for mentees and mentors alike. Sets is_active = false, revokes all
tokens, writes a user.suspended audit entry, and emails the user with the reason.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
reason |
string | yes | Max 1000 chars — A reason is required to suspend an account. |
Example
{ "reason": "Repeated abusive behaviour." }
200 Response
{
"status": "success",
"message": "Account suspended.",
"data": { "account": { "…admin account object, account_status: suspended…" } }
}
Errors
| Code | When |
|---|---|
| 422 | reason missing; account already suspended — This account is already suspended.; account is banned — This account is banned. Reinstate it before applying a suspension. |
| 403 | Target is the caller or another admin — You cannot moderate this account. |
| 404 | Account not found |
| 401 | Unauthenticated |
4. Ban an account
PATCH /api/v1/admin/accounts/{userId}/ban
Sets is_banned = true, revokes all tokens, writes a user.banned audit entry, and emails
the user with the reason. A suspended account can be banned (escalation).
Body
| Field | Type | Required | Notes |
|---|---|---|---|
reason |
string | yes | Max 1000 chars — A reason is required to ban an account. |
200 Response
{
"status": "success",
"message": "Account banned.",
"data": { "account": { "…admin account object, account_status: banned…" } }
}
Errors
| Code | When |
|---|---|
| 422 | reason missing; account already banned — This account is already banned. |
| 403 | Target is the caller or another admin |
| 404 | Account not found |
| 401 | Unauthenticated |
5. Reinstate an account
PATCH /api/v1/admin/accounts/{userId}/reinstate
Restores is_active = true, is_banned = false (lifts a suspension or a ban), writes a
user.reinstated audit entry, and emails the user. Tokens are not restored — the user
logs in again normally.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
reason |
string | yes | Max 1000 chars — A reason is required to reinstate an account. |
200 Response
{
"status": "success",
"message": "Account reinstated.",
"data": { "account": { "…admin account object, account_status: active…" } }
}
Errors
| Code | When |
|---|---|
| 422 | reason missing; account not moderated — This account is neither suspended nor banned. |
| 403 | Target is the caller or another admin |
| 404 | Account not found |
| 401 | Unauthenticated |
Related
GET /api/v1/admin/mentors— mentor application browsing and lifecycle. The list acceptsstatus(approval status),account_status(active/suspended/banned),search(legal name + applicant name/username/email),date_from/date_to, andper_page. The application detail (GET /admin/mentors/{profileId}) embeds the same admin account object asdata.account— withmoderation_history— so a mentor's account state is visible there; to moderate it, take theuser.idand call the/admin/accounts/{userId}/*actions above.admin-brd.md§6 (User Account Moderation), §8 (Admin Audit Log) — note the §6.2account_statuscolumn proposal was not adopted; moderation state stays on the existingis_active/is_bannedbooleans, withUserAccountStatusderived in code.- Admin Verification Document API — mentor KYC document review.