hub: customer-claim password arc parts 1+2 — code engine, emails, ACK, configgen bake, UI (v0.50.0)

Closes DRILL-day0-vm F-4 hub-side: per-customer claim state (customer_claims,
bcrypt-only custody), the claim engine (issue at real config retrieve = Day-0
bake; first-report issue for live boxes; resend rotates generation; reset
rate-limited 3/day), three Hungarian emails via the dispatcher, report-ACK
claim object {code_hash, generation, issued_at} + set-only claimed ingest,
web.claim_code_* baked into generated controller.yaml, Setup-tab status chip
+ resend button, POST /api/v1/claim/reset-request (self-scoped), claim_lockout
event allowlisted. 13 new tests; full repo green.

Claude-Session: https://claude.ai/code/session_01NptTCFtu7dz2Ru89qHRagN
This commit is contained in:
2026-07-12 18:12:48 +02:00
parent b904477ed9
commit 6b40eb8619
14 changed files with 1103 additions and 7 deletions
+36 -1
View File
@@ -338,6 +338,10 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
// the same configFormData the standalone chrome renders. Zero-valued (and never rendered)
// when the customer has no config.
ConfigForm configFormView
// Claim (v0.50.0, customer-claim arc): the dashboard claim state for the Setup-tab card —
// nil when no code has been issued yet (pre-arc / never-pulled customer).
Claim *store.ClaimState
}
pendingSet := make(map[string]bool, len(pendingTails))
@@ -427,6 +431,11 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
data.ConfigForm = s.configFormData(r, false, cfg, nil, "")
}
// Claim state (v0.50.0) for the Setup-tab access card (nil-safe: no row → no card content).
if cs, err := s.store.GetClaim(customerID); err == nil {
data.Claim = cs
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.templates.ExecuteTemplate(w, "customer_unified.html", data); err != nil {
s.logger.Printf("[ERROR] Template render: %v", err)
@@ -615,6 +624,29 @@ func (s *Server) handleConfigUpdate(w http.ResponseWriter, r *http.Request, cust
http.Redirect(w, r, "/customers/"+customerID+"?flash=updated#tab=edit", http.StatusSeeOther)
}
// handleClaimResend (v0.50.0, customer-claim arc) rotates the claim/reset code and re-sends it to
// the REGISTERED customer address — the operator "Kód újraküldése" / "Visszaállító kód küldése"
// button. The old code stops verifying immediately (single active code); the fresh hash reaches
// the box on its next report ACK (no config bump needed). No plaintext is ever rendered or logged.
func (s *Server) handleClaimResend(w http.ResponseWriter, r *http.Request, customerID string) {
if s.claimEngine == nil {
http.Error(w, "Claim engine is not configured on this hub", http.StatusBadGateway)
return
}
cfg, err := s.store.GetCustomerConfig(customerID)
if err != nil || cfg == nil {
http.NotFound(w, r)
return
}
if err := s.claimEngine.Resend(cfg); err != nil {
s.logger.Printf("[ERROR] claim resend for %s: %v", customerID, err)
http.Redirect(w, r, "/customers/"+customerID+"?flash=claim-resend-failed#tab=setup", http.StatusSeeOther)
return
}
s.logger.Printf("[INFO] claim code re-sent for %s (operator resend; generation rotated)", customerID)
http.Redirect(w, r, "/customers/"+customerID+"?flash=claim-resent#tab=setup", http.StatusSeeOther)
}
// handleOffsiteReissue (F4) resets the customer's offsite credential and stores a fresh one-time password —
// the explicit operator recovery for a consumed-password dead-end (fresh-guest DR, consumed-but-failed
// install). Scoped to the resource labelled for THIS customer (the provisioner refuses unless exactly one).
@@ -724,7 +756,10 @@ func (s *Server) handleConfigPreview(w http.ResponseWriter, r *http.Request, cus
templateYAML = s.templateFetcher.Template()
}
yamlOutput, err := configgen.Generate(templateYAML, cfg)
// Claim arc (v0.50.0): the preview BAKES an existing claim hash (so it matches what a box
// would pull) but never ISSUES one — issuing + emailing belongs to the real config retrieve.
claimState, _ := s.store.GetClaim(customerID)
yamlOutput, err := configgen.Generate(templateYAML, cfg, claimState)
if err != nil {
s.logger.Printf("[ERROR] Failed to generate preview for %s: %v", customerID, err)
http.Error(w, "Generation error: "+err.Error(), http.StatusInternalServerError)