27916bbe61
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.
68 lines
1.5 KiB
Go
68 lines
1.5 KiB
Go
package adminconsole
|
|
|
|
import (
|
|
"bytes"
|
|
"io/fs"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRendererRendersDashboard(t *testing.T) {
|
|
renderer, err := NewRenderer()
|
|
if err != nil {
|
|
t.Fatalf("NewRenderer: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = renderer.Render(&buf, "dashboard", PageData{
|
|
Title: "Dashboard",
|
|
Username: "ops-bob",
|
|
ActiveNav: "dashboard",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
|
|
out := buf.String()
|
|
for _, want := range []string{
|
|
"<!DOCTYPE html>",
|
|
"Dashboard",
|
|
"ops-bob",
|
|
`href="/_gm/users"`,
|
|
"/_gm/assets/console.css",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("rendered page missing %q\n--- page ---\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRendererUnknownPage(t *testing.T) {
|
|
renderer := MustNewRenderer()
|
|
if err := renderer.Render(&bytes.Buffer{}, "does-not-exist", PageData{}); err == nil {
|
|
t.Fatal("expected an error rendering an unknown page")
|
|
}
|
|
}
|
|
|
|
func TestRendererEscapesUsername(t *testing.T) {
|
|
renderer := MustNewRenderer()
|
|
|
|
var buf bytes.Buffer
|
|
if err := renderer.Render(&buf, "dashboard", PageData{Username: "<script>evil</script>"}); err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
if strings.Contains(buf.String(), "<script>evil</script>") {
|
|
t.Error("username was not HTML-escaped in the rendered page")
|
|
}
|
|
}
|
|
|
|
func TestAssetsContainsStylesheet(t *testing.T) {
|
|
fsys, err := Assets()
|
|
if err != nil {
|
|
t.Fatalf("Assets: %v", err)
|
|
}
|
|
if _, err := fs.Stat(fsys, "console.css"); err != nil {
|
|
t.Fatalf("console.css missing from embedded assets: %v", err)
|
|
}
|
|
}
|