Problem
A judge-review platform is a magnet for harassment and defamation actors, and right now the only admin response to an abusive account is DELETE /api/admin/users/{id} — a hard delete that destroys the abuser's content (the evidence) and frees their email for instant re-registration. The abuse-response loop is fully open: delete → re-register → repeat. There is no way to suspend an account, no account-state check anywhere in auth, and no audit trail of the action. This is a launch blocker.
Findings
- No account-state column exists. The users table has only
name/email/password/role_label/credential_*/timestamps — nostatus,banned_at, orsuspended_at(database/migrations/0000000035-create-users-table.sql). No ban/suspension table exists either. - Middleware checks nothing account-level.
app/Middleware/Auth.tsvalidates only token/session validity andapp/Middleware/Admin.tsonly checks the admin role; a grep forbanned|suspend|disabled|lockedacross both yields zero hits. A banned user (if the concept existed) would still authenticate. - The only moderation tool is a hard delete that destroys evidence. Admin user routes are index/update/role/delete only (
routes/api.ts:356-377);app/Actions/Admin/Users/UpdateUserAction.ts:33-47patches name/email only;app/Actions/Admin/Users/DeleteUserAction.ts:62-83performs an explicit hard delete with a manual multi-table cascade, wiping the user's reviews along with the row. - A deleted email is instantly re-registrable.
POST /auth/register(routes/api.ts:71) runs the framework default, which checks only email shape, password length, and email uniqueness (storage/framework/core/auth/src/register.ts:14-60). Deleting an abuser frees their email seconds later; no blocklist exists anywhere inapp/,config/, or the vendored auth core.
Acceptance criteria
- Migration adds a
banned_at(orstatus) column tousers POST /api/admin/users/{id}/suspend(auth + admin gated) setsbanned_atand writes amoderation_logsentry recording actor, target, and reasonLoginActionrejects credentials for any user withbanned_atsetAuth.user()resolution / auth middleware rejects existing tokens for banned users, so suspension takes effect on live sessions- Suspension keeps the user row in place, so the existing duplicate-email check in registration blocks re-registration with the banned email (verified by test: register with a banned email → 409)
- Tests cover suspend endpoint, login rejection, in-flight token rejection, and re-registration blocking
From the 2026-06-10 production-readiness audit.