Problem
Bench publishes reviews of named, sitting judges, so the moderation pipeline is the product's legal and trust backbone. Right now the pipeline has one working surface (the pending-review queue) and gaps everywhere else: flags go into a black hole, comments and photos reach the public with no admin removal path, claims to a judge's identity carry no evidence, and account creation is cheap to script. Together these make harassment, doxxing, and impersonation both easy to produce and hard to clean up.
Findings
- Flags are write-only.
review_flagsrows are inserted (app/Actions/Reviews/FlagReviewAction.ts:105-113) but no admin route, action, view, or store ever reads or resolves them; status is permanentlyopen. Because moderators are emailed only on the first open flag (FlagReviewAction.ts:93-118), each review emits exactly one email ever — miss it and 50 later flags are silent. - Comments auto-publish with no takedown.
app/Actions/Reviews/CommentSubmitAction.ts:88-96insertsstatus: 'published'; no admin comment route exists anywhere inroutes/, no email-verification gate applies, andFlagReviewActioncovers reviews only. Removal currently requires direct DB surgery. - Photos bypass moderation entirely.
review_photoshas no status column (migration0000000005),UploadReviewPhotosAction.ts:44-62checks ownership but never review status — so photos can be attached to an already-published review without resetting it to pending (unlike text edits,UpdateMyReviewAction.ts:117) — the upload route has no throttle (routes/api.ts:166-169), and the adminReviewsTablerenders no images, so moderators approve blind. - No bulk moderation. Only single-
{id}PATCH/DELETE routes exist (routes/api.ts:385-396);UpdateReviewStatusAction.ts:35accepts one id and sends an author email inline per call, making moderator throughput the publication bottleneck for a pending-by-default queue. - Judge-claim flow captures zero identity evidence.
ClaimJudgeProfileAction.ts:24-29,62-72accepts onlyjudgeId; the credential queue (CredentialQueueIndexAction.ts:22-32) shows admins nothing to verify against, yet approval unlocks official judge responses (SubmitJudgeResponseAction.ts:43-54) — a mistaken approval is public impersonation of a judge. - Registration relies on IP throttle alone.
routes/api.ts:71-74hasthrottle:5,10mand nothing else: no CAPTCHA/Turnstile, honeypot, or disposable-email blocking anywhere in app/config/resources, so verified throwaway accounts are trivially scriptable.
Acceptance criteria
GET /api/admin/flags(open-first) andPATCH /api/admin/flags/{id}(dismiss/actioned, withlogModeration) exist; open-flag counts appear inReviewIndexActionrows; flags auto-close on review reject/delete so the first-open-flag email re-arms- Admins can set a comment's status to
rejectedvia an authed admin endpoint, andCommentSubmitActionrequires a verified email - Photo upload to a published review resets it to
pending; photo thumbnails render in the admin reviews queue; the upload route is throttled (e.g.throttle:10,10m) - Review status updates accept an
ids[]batch with per-row moderation logging, andReviewsTable.stxgains checkboxes plus a bulk publish/reject bar - Judge claims require an evidence field (court email verification loop, or at minimum free-text + document reference) that is displayed in the credential queue beside approve/reject
- Registration includes a CAPTCHA/Turnstile check and a disposable-email-domain blocklist ahead of
register()
From the 2026-06-10 production-readiness audit.