controller: customer-claim password gate v0.122.0 (closes DRILL-day0-vm F-4/F-5)

The customer sets + owns the dashboard password via a hub-emailed one-time
claim code. An unclaimed box (code hash present, no password) serves ONLY the
claim page — every other route → claim page (302) or 401, so a Day-0 box is
never open on the internet. A set password disables the gate (auth wins).
Reset rides the same code engine (login "Elfelejtett jelszó"). Legacy-open
(no password, no hash) shows a red transition banner until the hub delivers a
hash. Report ACK caches the code state idempotently by generation; report
carries claimed (set-only). --print-reset-code root escape hatch. Requires
hub v0.50.0. Gate-coverage signature test + 4 red-proofs proven.
This commit is contained in:
2026-07-12 18:42:39 +02:00
parent dec6fef20d
commit 3cf49c7fd5
18 changed files with 1117 additions and 1 deletions
+25 -1
View File
@@ -51,7 +51,24 @@ func (s *Server) authEnabled() bool {
// RequireAuth returns middleware that checks for valid session or shows login.
func (s *Server) RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip auth if no password is configured
// Customer-claim gate (v0.122.0, F-4): an unclaimed box with a delivered code hash and no
// password serves ONLY the claim page + its assets; everything else → claim page / 401.
// The claim routes (/claim, /claim/request-new-code) are handled by the mux — let them
// through so serveClaimGate only intercepts the GATED paths. A set password disables the
// gate entirely (claimGateActive returns false → the normal auth path below runs).
if s.claimGateActive() {
if claimPageAllowedPath(r.URL.Path) {
next.ServeHTTP(w, r)
return
}
if s.isDebug() {
s.logger.Printf("[DEBUG] [web] claim gate: intercepting %s %s (unclaimed)", r.Method, r.URL.Path)
}
s.serveClaimGate(w, r)
return
}
// Skip auth if no password is configured (legacy-open transition state, or claim disabled).
if !s.authEnabled() {
if s.isDebug() {
s.logger.Printf("[DEBUG] [web] auth: no password configured, passing through %s %s", r.Method, r.URL.Path)
@@ -65,6 +82,13 @@ func (s *Server) RequireAuth(next http.Handler) http.Handler {
return
}
// Claim/reset routes stay reachable pre-auth even on a claimed box: they are the RESET
// entry (code-gated internally). Static assets for the page too.
if r.URL.Path == "/claim" || r.URL.Path == "/claim/request-new-code" || strings.HasPrefix(r.URL.Path, "/static/") {
next.ServeHTTP(w, r)
return
}
if r.URL.Path == "/login" && r.Method == http.MethodPost {
s.handleLogin(w, r)
return