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
+65
View File
@@ -28,6 +28,17 @@ type Settings struct {
// Auth
PasswordHash string `json:"password_hash,omitempty"` // bcrypt hash, overrides controller.yaml
// Customer-claim arc (v0.122.0, F-4). Claimed is SET-ONLY (a claim or reset completed at
// least once — never cleared). ClaimCode* cache the freshest hub-delivered code state (report
// ACK; beats controller.yaml when its generation is newer). ClaimConsumedGeneration records
// the last code generation successfully consumed — a code of a consumed generation is dead
// even if its hash still matches (single-use).
Claimed bool `json:"claimed,omitempty"`
ClaimCodeHash string `json:"claim_code_hash,omitempty"`
ClaimCodeGeneration int `json:"claim_code_generation,omitempty"`
ClaimCodeIssuedAt string `json:"claim_code_issued_at,omitempty"` // RFC3339
ClaimConsumedGeneration int `json:"claim_consumed_generation,omitempty"`
// Notification preferences (Phase 2 — define struct now, leave empty)
Notifications *NotificationPrefs `json:"notifications,omitempty"`
@@ -405,6 +416,60 @@ func (s *Settings) SetPasswordHash(hash string) error {
return s.save()
}
// ── Customer-claim arc (v0.122.0) ──────────────────────────────────────────────
// GetClaimed reports whether this box has completed a claim (set-only).
func (s *Settings) GetClaimed() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.Claimed
}
// SetClaimed marks the box claimed (never un-claims) and saves.
func (s *Settings) SetClaimed() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.Claimed {
return nil
}
s.Claimed = true
return s.save()
}
// GetClaimCode returns the cached hub-delivered code state (hash, generation, issuedAt RFC3339).
func (s *Settings) GetClaimCode() (hash string, generation int, issuedAt string) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.ClaimCodeHash, s.ClaimCodeGeneration, s.ClaimCodeIssuedAt
}
// SetClaimCode caches a hub-delivered code state (idempotent by generation — the caller guards).
func (s *Settings) SetClaimCode(hash string, generation int, issuedAt string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.ClaimCodeHash = hash
s.ClaimCodeGeneration = generation
s.ClaimCodeIssuedAt = issuedAt
return s.save()
}
// GetClaimConsumedGeneration returns the last successfully consumed code generation.
func (s *Settings) GetClaimConsumedGeneration() int {
s.mu.RLock()
defer s.mu.RUnlock()
return s.ClaimConsumedGeneration
}
// SetClaimConsumedGeneration records a consumed code generation (single-use enforcement).
func (s *Settings) SetClaimConsumedGeneration(gen int) error {
s.mu.Lock()
defer s.mu.Unlock()
if gen > s.ClaimConsumedGeneration {
s.ClaimConsumedGeneration = gen
}
return s.save()
}
// GetDBValidations returns a copy of the cached DB validations.
func (s *Settings) GetDBValidations() map[string]DBValidationCache {
s.mu.RLock()