Files
cloud-ip-validator/internal/dashboard/server.go
T
ayurishchevandClaude Sonnet 5.5 debf2afed2 Add authentication: admin/agent bearer tokens for the API, login for the dashboard
control-api: every route now carries a mandatory access level (admin / agent /
open) in a route table. All /api/v1/admin/* require the admin token; the
write calls of validator-agent and prober (self-check, events, results,
complete) require a separate static agent token; register, heartbeat and
fetching the assignment stay open. Tokens come from env vars, are compared in
constant time and never logged. An empty token leaves that level open with a
startup warning (backward compatible).

validator-agent / prober: apiclient sends the agent token only to control-api.

admin-dashboard: login/password (from env) with a stateless HMAC session
cookie, Origin-based CSRF check, per-IP brute-force throttle, HX-Redirect for
htmx polls, logout in the sidebar; the dashboard calls control-api with the
admin token. Login page layout fixed after review.

Also: env plumbing in docker-compose/rxprod-compose/systemd/config examples,
e2e script with token assertions, tests, docs (API, SETUP, USAGE, DASHBOARD,
README), plan and review under docs/changes/, bin/ rebuilt with new
SHA256SUMS.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:35:24 +03:00

73 lines
2.0 KiB
Go

// Package dashboard implements the admin dashboard's HTTP surface: a
// server-rendered (html/template + htmx + Alpine.js) web UI giving full
// coverage of control-api's /api/v1/admin/* API. It never talks to
// internal/db directly and has no state of its own — every page and
// fragment is computed fresh, on each request, from control-api's API via
// the client in client.go.
package dashboard
import (
"html/template"
"log/slog"
"net/http"
"time"
)
type Config struct {
ControlAPIBaseURL string
ControlAPITimeout time.Duration
LastCompletedCount int
OverviewPollIntervalS int
// ControlAPIToken is the admin bearer token sent to control-api. Empty =
// no Authorization header.
ControlAPIToken string
// Username/Password are the single dashboard administrator's login. If
// either is empty, login is DISABLED (every page is open).
Username string
Password string
// SessionSecret is the HMAC key for session cookies; empty = random per
// process start (sessions are lost on restart).
SessionSecret string
// SessionTTL is the session lifetime (default 8h).
SessionTTL time.Duration
}
type Server struct {
CA *client
Cfg Config
tmpl *template.Template
Log *slog.Logger
auth *authState
}
func New(cfg Config, log *slog.Logger) (*Server, error) {
tmpl, err := parseTemplates()
if err != nil {
return nil, err
}
ca := newClient(cfg.ControlAPIBaseURL, cfg.ControlAPITimeout)
ca.token = cfg.ControlAPIToken
return &Server{
CA: ca,
Cfg: cfg,
tmpl: tmpl,
Log: log,
auth: newAuthState(cfg, log),
}, nil
}
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
s.routes(mux)
// Never log cookies, Authorization or form bodies here.
return loggingMiddleware(s.Log, s.authMiddleware(mux))
}
func loggingMiddleware(log *slog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
log.Debug("request", "method", r.Method, "path", r.URL.Path)
})
}