NextMyDay/internal/core/server/setupflow.go
Claus Lohmar c7229e23ff feat(setup): gate setup with the config toggle and an optional code
D-045 — the setup gate is now `config.setup`, not the peer/SSH tunnel:

- setup: true      -> /setup serves the wizard (the code form first when
                      `setup_code` is set, via the existing D-044 gate);
                      GET / points at the wizard; every other route -> 404.
- setup: false + no admin -> LOCKED: GET / and /setup render an "enable
                      setup" boilerplate; every other route -> 404; the
                      wizard is not claimable.
- completion       -> writes `setup: false` and creates the admin.

`setup_code` is now OPTIONAL: blank => Gate.Required() false => the toggle
alone admits; set => required (reusing the unlock cookie/fingerprint/
redaction/rate-limit). The peer/tunnel gate is retired: `httpx.
SetupRequestAllowed` and `httpx.IsLoopbackIP` are deleted; the XFF/XFP
handling is kept for the client IP and the Secure/__Host- cookies + HSTS
(A6/A11/D-043). The !bootstrapped() fail-safe becomes the locked predicate,
latched at startup.

New door seams: Deps.LockedMode and SetupGate.{Required,CodeForm,Landing,
Locked}; the Gate renders three first-run pages. The startup banner states
the mode and prints the code only when required.

Tests cover the three states, the boilerplate, the code optionality and the
unlock mechanics; live-smoked end to end (locked / wizard / code form /
full bootstrap -> setup: false).
2026-10-15 06:47:33 +00:00

43 lines
1.8 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
// (`config.setup`, D-045) and verifies the pre-session CSRF token (A15) before
// dispatch.
//
// `unlock` is the D-044 code gate: when a `setup_code` is set the door lets it
// through without 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)
}
}