3cf49c7fd5
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.
54 lines
2.1 KiB
Go
54 lines
2.1 KiB
Go
package report
|
|
|
|
import "log"
|
|
|
|
// Customer-claim arc (v0.122.0, F-4) — the ACK-side of the claim-code delivery. The hub serves
|
|
// the ACTIVE code's bcrypt hash + monotonic generation on every report ACK of a managed
|
|
// customer; this reconciler caches it into settings.json IDEMPOTENTLY BY GENERATION (the
|
|
// offsite-descriptor guard shape: an unchanged generation never rewrites, a hub outage never
|
|
// clears — set/refresh only, exactly like the escrow confirmer's one-way rules). The gate itself
|
|
// (internal/web) reads the cached state; nothing here decides gating.
|
|
|
|
// ClaimStatus mirrors the hub ACK's `claim` object (nil when the hub has no claim row / old hub).
|
|
type ClaimStatus struct {
|
|
CodeHash string `json:"code_hash"`
|
|
Generation int `json:"generation"`
|
|
IssuedAt string `json:"issued_at"` // RFC3339
|
|
}
|
|
|
|
// ClaimSettings is the settings surface the sync needs (satisfied by *settings.Settings).
|
|
type ClaimSettings interface {
|
|
GetClaimCode() (hash string, generation int, issuedAt string)
|
|
SetClaimCode(hash string, generation int, issuedAt string) error
|
|
}
|
|
|
|
// ClaimSync applies one ACK's claim status to the settings cache.
|
|
type ClaimSync struct {
|
|
Settings ClaimSettings
|
|
Logger *log.Logger
|
|
}
|
|
|
|
func (c *ClaimSync) logf(f string, a ...any) {
|
|
if c.Logger != nil {
|
|
c.Logger.Printf(f, a...)
|
|
}
|
|
}
|
|
|
|
// Reconcile caches a newer-generation code state; same-or-older generations and nil/empty
|
|
// statuses are no-ops (a rotation is the ONLY thing that moves the cache — no write-back, no
|
|
// clearing on hub silence).
|
|
func (c *ClaimSync) Reconcile(cs *ClaimStatus) {
|
|
if cs == nil || cs.CodeHash == "" || cs.Generation <= 0 {
|
|
return
|
|
}
|
|
_, curGen, _ := c.Settings.GetClaimCode()
|
|
if cs.Generation <= curGen {
|
|
return // idempotent: this generation (or a newer one) is already cached
|
|
}
|
|
if err := c.Settings.SetClaimCode(cs.CodeHash, cs.Generation, cs.IssuedAt); err != nil {
|
|
c.logf("[ERROR] [claim-sync] caching hub claim code (gen %d) failed (retries next ACK): %v", cs.Generation, err)
|
|
return
|
|
}
|
|
c.logf("[INFO] [claim-sync] hub claim code cached (generation %d) — hash first 8: %.8s…", cs.Generation, cs.CodeHash)
|
|
}
|