NextMyDay/internal/core/server/setupflow.go
Claus Lohmar 6d421b61fd feat(setup): gate first run with a printed setup code and a first-run page
The real topology is an LXC container (11020) behind Proxmox + Zoraxy, so
the SSH tunnel is impractical: it lands on the container gateway, not
loopback. D-025 is re-revised and D-044 adds the general gate.

- core/setup: a Gate (gate.go) holding the 128-bit setup code (generated at
  first run, or pinned by config `setup_code`) plus a signed unlock cookie
  (`myday_setup`) that carries a keyed one-way FINGERPRINT of the code, never
  the code itself. The code is single-use (retired on the first success) and
  the comparison ignores case/separators.
- The unlock action (`POST /setup?do=setup&action=unlock`) is CSRF-protected
  (A15), rate-limited (the /setup A3 class) and audited; a wrong or absent
  code gets the same generic 403 page.
- The door (server.Deps.SetupGate): in setup mode the wizard is served to
  EITHER the direct tunnel (gate a) OR an unlocked request (gate b); `GET /`
  and an un-unlocked `GET /setup` render a friendly first-run page with the
  code form — never a bare 404 — and every other route stays 404.
- cmd/myday prints an unmistakable startup banner: setup mode, the wizard
  URL, the code, where to read it (journalctl / pct exec 11020) and the
  restart that adopts the persisted secrets.
- httpx.Signer.Fingerprint backs the unlock cookie; config gains `setup_code`.

Tests cover the boilerplate at `/` and `/setup` (200, not 404), the unlock
(code → wizard; wrong/absent → generic, 429 under the limit), single-use, the
CSRF requirement, access-log redaction, and that a non-/setup route is still
404. Gate green (build · vet · test -race); live-smoked against the binary.
2026-10-15 06:33:13 +00:00

42 lines
1.7 KiB
Go

package server
import (
"net/http"
"git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/setup"
)
// setupModule is the `do` module name for the SPEC-005 wizard steps. The
// wizard is a reserved level-1 workflow (`/setup`) whose steps are actions.
const setupModule = "setup"
// RegisterSetupFlow registers the SPEC-005 `/setup` wizard: the page (the core
// `setup` workflow) and the action module (unlock · domain.save · admin.create ·
// admin.enroll.* · admin.totp.* · finish). The door gates them to setup mode
// and verifies the pre-session CSRF token (A15) before dispatch.
//
// `unlock` is the D-044 code gate: the door lets it through without the tunnel
// or an existing unlock, because it is what grants the unlock.
func RegisterSetupFlow(reg *MapRegistry, a *setup.Actions) {
reg.RegisterCore("setup", adaptSetup(a.Page))
reg.RegisterModule(NewModule(setupModule).
AddAction("unlock", "unlock", adaptSetup(a.Unlock)).
AddAction("domain.save", "domain.save", adaptSetup(a.DomainSave)).
AddAction("admin.create", "admin.create", adaptSetup(a.AdminCreate)).
// Step 2 is TOTP only (D-041) — there is no admin.enroll.* action.
AddAction("admin.totp.begin", "admin.totp.begin", adaptSetup(a.TOTPBegin)).
AddAction("admin.totp.verify", "admin.totp.verify", adaptSetup(a.TOTPVerify)).
AddAction("finish", "finish", adaptSetup(a.Finish)))
}
// setupFunc is a wizard handler. The door resolves the workflow; the wizard
// never parses a raw path.
type setupFunc func(w http.ResponseWriter, r *http.Request) error
// adaptSetup adapts a wizard handler to the door's Handler signature.
func adaptSetup(f setupFunc) Handler {
return func(w http.ResponseWriter, r *http.Request, _ *RequestContext) error {
return f(w, r)
}
}