feat(admin-console): server-rendered operator console at /_gm #87
Reference in New Issue
Block a user
Delete Branch "feature/admin-console"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds a server-rendered operator console at
/_gm, exposed through the gateway behind the existingadmin_accountsBasic Auth. The backend already shipped a complete admin JSON API (/api/v1/admin/*) but had no UI and no public path; this PR adds the operator-facing console and routes it through the gateway's edge protections.What's included
adminanti-abuse route class (per-IP rate limit + body/method limits) and a reverse proxy for/_gm/*to the backend, preservingHost, relaying the backend 401 Basic Auth challenge, and answering 502 when the backend is down. Authentication is delegated to the backend.html/template) under the existing admin Basic Auth, with a stateless HMAC CSRF guard + same-origin check. Domains: dashboard/monitoring (opsstatus, built on go-jet), users (block / entitlement / soft-delete), games + runtime + engine versions, operators (admin accounts), and mail + notifications + multi-game broadcast./_gm/*, a bootstrap operator + stable CSRF key, and the Prometheus/metricsexporters enabled (forward-compat for a future Prometheus/Grafana stack).Verification
opsstatus, and an integration test driving/_gmend to end through the real gateway to the backend.Notes
Docs updated: ARCHITECTURE 14.1/16, FUNCTIONAL 10.2.1 (+ru mirror), backend & gateway READMEs, and
backend/docs/admin-console.md.Implemented in 6 CI-green stages on
feature/admin-console.Add the operator console's user-administration pages over the existing *user.Service (no new business logic). - GET /_gm/users paginated account list - GET /_gm/users/{id} account detail: profile, entitlement, sanctions - POST /_gm/users/{id}/block apply permanent_block (reason required) - POST /_gm/users/{id}/entitlement set the entitlement tier - POST /_gm/users/{id}/soft-delete soft-delete the account (cascades) The console depends on a UserAdmin interface (satisfied by *user.Service) so the pages render in tests without a database. All writes flow through the CSRF guard, carry the operator as the audit actor, and answer with a 303 redirect; a generic message page handles not-found, validation, and failure notices. Unblock is intentionally absent — the admin API exposes no remove-sanction endpoint. Tests: list/detail render, not-found, block (with actor/scope/reason assertions), missing-reason 400, bad-CSRF 403, entitlement, soft-delete redirect, and the service-unavailable path. Docs: backend/docs/admin-console.md gains the page inventory.Add the games, runtime, and engine-version pages over the existing lobby, runtime, and engine-version services (no new business logic). - GET/POST /_gm/games list + create public game - GET /_gm/games/{id} detail incl. runtime snapshot - POST /_gm/games/{id}/force-start|stop game state actions - POST /_gm/games/{id}/ban-member ban a member (uuid + reason) - POST /_gm/games/{id}/runtime/restart|patch|force-next-turn - GET/POST /_gm/engine-versions registry + register - POST /_gm/engine-versions/{ver}/disable disable a version Console depends on GameAdmin / RuntimeAdmin / EngineVersionAdmin interfaces (satisfied by the concrete services) so the pages render in tests without a database. Collection-mutating POSTs are mounted on the collection path to avoid a static-vs-param route conflict in gin. Writes flow through the CSRF guard and redirect back; the create form parses datetime-local as UTC. Tests: list/detail (with and without a runtime), create (visibility/owner/time assertions), force-start (+ bad-CSRF), ban-member (+ bad uuid), runtime patch (+ missing version), engine-version list/register/disable, and unavailable. Docs: backend/docs/admin-console.md page inventory extended.Add the operator-management page over *admin.Service (no new business logic). - GET/POST /_gm/operators list + create operator - POST /_gm/operators/{user}/disable|enable toggle access - POST /_gm/operators/{user}/reset-password set a new password Console depends on an OperatorAdmin interface (satisfied by *admin.Service) so the page renders in tests without a database. Create POST is mounted on the collection path; per-row disable/enable/reset are guarded by the CSRF middleware and redirect back. Passwords are never logged. Tests: list render, create (+ username/password assertions), username-taken conflict, disable/enable, reset (+ password assertion), missing-password 400, bad-CSRF 403, and unavailable 503. Docs: backend/docs/admin-console.md page inventory extended.Add the mail, notifications, and broadcast pages over the mail, notification, and diplomail services (no new business logic), completing the operator console. - GET /_gm/mail deliveries (paginated) + dead-letters - GET /_gm/mail/deliveries/{id} delivery detail + attempts - POST /_gm/mail/deliveries/{id}/resend re-enqueue a non-sent delivery - GET /_gm/notifications notifications + dead-letters + malformed - GET/POST /_gm/broadcast multi-game admin diplomatic broadcast Console depends on MailAdmin / NotificationAdmin / DiplomailAdmin interfaces (satisfied by the concrete services); pages render in tests without a database. Delivery detail and dead-letters live under /_gm/mail/deliveries/* and /_gm/mail/... static segments to avoid a param/static route conflict. Resend and broadcast flow through the CSRF guard. Tests: mail page, delivery detail (+ not-found), resend (+ bad-CSRF), notifications overview, broadcast form + send (input assertions) + bad game ids, and unavailable. Plus an integration test that drives /_gm end to end through the real gateway → backend (401 challenge + authenticated dashboard). Docs: backend/docs/admin-console.md page inventory completed.