feat(admin-console): Stage 1 — pipe + skeleton behind the gateway
Tests · Go / test (push) Successful in 2m0s

Add the server-rendered operator console at /_gm, exposed publicly through
the gateway behind the existing admin_accounts Basic Auth.

Backend:
- new internal/adminconsole package (html/template Renderer, stateless HMAC
  CSRF signer, embedded stylesheet)
- /_gm route group reusing basicauth.Middleware(admin.Service) + a CSRF guard
  (per-operator token + same-origin check); dashboard landing page
- BACKEND_ADMIN_CONSOLE_CSRF_KEY config (per-process random fallback)

Gateway:
- new "admin" public route class (per-IP rate limit, body + GET/HEAD/POST
  method limits) classifying /_gm traffic
- reverse proxy to the backend /_gm surface, preserving Host and relaying the
  backend 401 Basic Auth challenge; 502 when the backend is unreachable
- GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_ADMIN_* config

dev-deploy:
- Caddy routes /_gm/* to the gateway
- bootstrap admin + stable CSRF key; enable Prometheus /metrics exporters on
  backend and gateway (forward-compat for a future Prometheus/Grafana stack)

Docs: ARCHITECTURE 14.1/16, FUNCTIONAL 10.2.1 (+ru mirror), backend and
gateway READMEs, new backend/docs/admin-console.md.

Tests: renderer + CSRF unit tests; backend router auth/render/asset/CSRF;
gateway classifier, proxy forwarding/Host/401/405/413/429/502.
This commit is contained in:
Ilia Denisov
2026-05-31 19:50:15 +02:00
parent 5d2f2bfc26
commit 27916bbe61
28 changed files with 1319 additions and 3 deletions
@@ -234,6 +234,8 @@ func publicRoutePolicyForClass(policy config.PublicHTTPAntiAbuseConfig, class Pu
return policy.BrowserBootstrap
case PublicRouteClassBrowserAsset:
return policy.BrowserAsset
case PublicRouteClassAdmin:
return policy.Admin
default:
return policy.PublicMisc
}
@@ -252,6 +254,8 @@ func publicAuthIdentityPolicyForPath(requestPath string, policy config.PublicHTT
func allowedMethodsForRequestShape(r *http.Request) []string {
switch {
case isAdminConsolePath(r.URL.Path):
return []string{http.MethodGet, http.MethodHead, http.MethodPost}
case isPublicAuthPath(r.URL.Path):
return []string{http.MethodPost}
case isProbePath(r.URL.Path):
@@ -284,6 +288,17 @@ func isPublicAuthPath(requestPath string) bool {
}
}
// isAdminConsoleRequest reports whether r targets the operator console surface.
func isAdminConsoleRequest(r *http.Request) bool {
return isAdminConsolePath(r.URL.Path)
}
// isAdminConsolePath reports whether requestPath is the admin console root
// (`/_gm`) or any path beneath it (`/_gm/...`).
func isAdminConsolePath(requestPath string) bool {
return requestPath == "/_gm" || strings.HasPrefix(requestPath, "/_gm/")
}
func isProbePath(requestPath string) bool {
switch requestPath {
case "/healthz", "/readyz":