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.
42 lines
1.7 KiB
Go
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)
|
|
}
|
|
}
|