Files
galaxy-game/backend/docs/admin-console.md
T
Ilia Denisov ecfb2d3351
Tests · Go / test (push) Successful in 1m58s
feat(admin-console): Stage 4 — games & runtimes domain
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.
2026-05-31 20:25:28 +02:00

7.2 KiB

Operator console (/_gm)

The operator console is a server-rendered web UI for the platform's admin operations. It is the human-facing counterpart to the JSON admin API under /api/v1/admin/*: both call the same service layer, but the console renders HTML pages an operator drives in a browser, while the JSON API stays internal to the deployment for programmatic and test use.

Design choices

  • Server-rendered, no client framework. Pages are rendered with the standard library's html/template. Navigation is by ordinary links and query parameters; every state change is an HTML form POST answered with a Post/Redirect/Get redirect. There is no build step, no JavaScript framework, and no separate asset pipeline — a single embedded stylesheet under /_gm/assets/.
  • Reuses the existing admin auth. The console mounts behind the same basicauth.Middleware(admin.Service) verifier that gates /api/v1/admin/*, so there is one credential store (admin_accounts, bcrypt-12) and no second secret to manage.
  • Lives in the backend. The backend owns the admin domain and the data, so rendering there lets the console call the service layer directly. The gateway stays a thin proxy.

Request path

Browser ── /_gm/* ──► edge Caddy ──► gateway (public listener)
   gateway: anti-abuse `admin` class (per-IP rate limit, body + method limits)
            └─► reverse proxy ──► backend  /_gm/*
   backend: basicauth.Middleware(admin.Service)
            └─► CSRF guard (state-changing methods)
                └─► console handler ──► admin service layer ──► html/template

The gateway preserves the inbound Host and relays the backend's 401 Basic Auth challenge unchanged, so the browser shows its native credential dialog. The gateway adds only the edge anti-abuse layer; authentication and every state change are enforced by the backend. The gateway answers 502 when the backend is unreachable. See the gateway README "Operator Console Proxy" section for the admin route-class env vars.

Components (package internal/adminconsole)

The package is framework-agnostic (no gin) so it unit-tests in isolation:

  • Renderer — parses the embedded layout plus one content page per route and renders a named page wrapped in the shared layout. Rendering goes through an intermediate buffer, so a template failure never emits a partial document.
  • CSRF — issues and verifies the stateless anti-CSRF token: HMAC-SHA256 over the authenticated username, keyed by BACKEND_ADMIN_CONSOLE_CSRF_KEY. When the key is unset a per-process random key is used (secure, but forms reset on restart and do not validate across replicas — set a shared key for multi-replica deployments).
  • Assets — the embedded stylesheet filesystem served under /_gm/assets/.

The gin glue (route group, Basic Auth, the CSRF guard middleware, the per-page handlers) lives in internal/server/handlers_admin_console.go and internal/server/router.go (registerAdminConsoleRoutes).

CSRF protection

Because the console is sessionless (HTTP Basic Auth, whose credentials the browser replays automatically), state-changing requests are double-guarded:

  1. A stateless per-operator token (_csrf form field) that a cross-site page cannot read or forge.
  2. A same-origin Origin/Referer check (when the browser sends one), which relies on the gateway preserving the inbound Host.

Safe methods (GET/HEAD/OPTIONS) pass without a token.

Monitoring

The dashboard is the console landing page. It surfaces backend-visible operational state — service health, game-runtime status, and queue depths — read through the existing service and persistence layers. Richer cross-service metrics are out of scope for the console itself: the /metrics Prometheus exporters on backend and gateway are wired and enabled in the dev deployment so a future Prometheus + Grafana stack can scrape them without code changes.

Pages

Path Method Purpose
/_gm, /_gm/ GET Dashboard: health, runtime/mail/notification status, queues.
/_gm/assets/* GET Embedded stylesheet.
/_gm/users GET Paginated account list.
/_gm/users/{id} GET Account detail: profile, entitlement, active sanctions.
/_gm/users/{id}/block POST Apply a permanent block (reason required).
/_gm/users/{id}/entitlement POST Set the entitlement tier.
/_gm/users/{id}/soft-delete POST Soft-delete the account (cascades).
/_gm/games GET/POST Paginated game list; POST creates a public game.
/_gm/games/{id} GET Game detail with the runtime snapshot.
/_gm/games/{id}/force-start POST Force-start the game.
/_gm/games/{id}/force-stop POST Force-stop the game.
/_gm/games/{id}/ban-member POST Ban a member (user id + reason).
/_gm/games/{id}/runtime/restart POST Restart the engine container.
/_gm/games/{id}/runtime/patch POST Patch the runtime to a target version.
/_gm/games/{id}/runtime/force-next-turn POST Force the next turn now.
/_gm/engine-versions GET/POST Version registry; POST registers a version.
/_gm/engine-versions/{ver}/disable POST Disable a registered version.

Each page reuses the same service layer as the corresponding /api/v1/admin/* JSON endpoint; the console adds no business logic. Collection-mutating POSTs are mounted on the collection path (POST /_gm/games, POST /_gm/engine-versions) so a static action segment never collides with a path parameter in the gin router. Unblocking a user is not yet available because the JSON admin API exposes no remove-sanction endpoint.

Configuration

Variable Where Notes
BACKEND_ADMIN_CONSOLE_CSRF_KEY backend CSRF token key; unset → per-process random key.
BACKEND_ADMIN_BOOTSTRAP_USER backend Bootstrap operator account (shared with the JSON admin API).
BACKEND_ADMIN_BOOTSTRAP_PASSWORD backend Bootstrap operator password.
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_ADMIN_* gateway admin route-class rate-limit and body budgets.