Phase 1: Control Service skeleton (Go, SQLite, session login, Docker, Caddy)

- Go service with its own multi-stage Dockerfile and docker-compose.yml; runs
  as container webrestrict-control on port 8791.
- SQLite schema for the full AGENTS.md §7 data model via versioned migrations
  (PRAGMA user_version); audit log is append-only.
- Configuration from the process environment; the real env file is gitignored
  and a template is committed. No secrets in the repo.
- Separate session login (signed HMAC cookie + CSRF) for Sam.
- GET /health; HTMX + server-rendered dashboard styled with the Family Home Lab
  tokens.css (copied) and Inter fonts; dark mode; phone-friendly.
- Caddy vhost for webrestrict.lab.audasmedia.com.au applied on .35 (Let's
  Encrypt cert issued); route recorded in deploy/caddy/webrestrict.caddy.
- Unit tests for config, auth and store.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-13 14:11:38 +11:00
parent 5368225f23
commit c83c9df0a3
29 changed files with 2119 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
package config
import (
"log/slog"
"testing"
"time"
)
func TestLoadDefaultsAndValidation(t *testing.T) {
t.Setenv("CONTROL_SESSION_SECRET", "0123456789abcdef")
t.Setenv("CONTROL_ADMIN_PASSWORD", "hunter2")
t.Setenv("CONTROL_ADDR", "")
t.Setenv("CONTROL_DB_PATH", "")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Addr != ":8791" {
t.Errorf("Addr = %q, want :8791", cfg.Addr)
}
if cfg.DBPath != "/data/control.db" {
t.Errorf("DBPath = %q, want /data/control.db", cfg.DBPath)
}
if cfg.AdminUsername != "sam" {
t.Errorf("AdminUsername = %q, want sam", cfg.AdminUsername)
}
if cfg.SessionTTL != 12*time.Hour {
t.Errorf("SessionTTL = %v, want 12h", cfg.SessionTTL)
}
if cfg.LogLevel != slog.LevelInfo {
t.Errorf("LogLevel = %v, want info", cfg.LogLevel)
}
if !cfg.CookieSecure {
t.Error("CookieSecure = false, want true by default")
}
}
func TestLoadRequiresSecretAndPassword(t *testing.T) {
t.Setenv("CONTROL_SESSION_SECRET", "")
t.Setenv("CONTROL_ADMIN_PASSWORD", "hunter2")
if _, err := Load(); err == nil {
t.Error("expected error when session secret is missing")
}
t.Setenv("CONTROL_SESSION_SECRET", "0123456789abcdef")
t.Setenv("CONTROL_ADMIN_PASSWORD", "")
t.Setenv("CONTROL_ADMIN_PASSWORD_HASH", "")
if _, err := Load(); err == nil {
t.Error("expected error when no admin password is set")
}
}