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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user