hub: F-15 instant reset codes — reset-request response carries the rotated {code_hash, generation, issued_at} (same shape/guarantee as the report ACK); claim-resent flash now states the operator-resend ACK lag
Claude-Session: https://claude.ai/code/session_01GzammAMzsJTgpQHqxwM2bC
This commit is contained in:
@@ -125,6 +125,49 @@ func TestClaimResetRequest_SelfScoped(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// v0.52.0 (take-two F-15): the reset-request response carries the ACTIVE code state — the freshly
|
||||
// ROTATED hash + generation matching the stored row — so the box accepts the emailed code
|
||||
// immediately instead of waiting for the next report ACK. Never a plaintext code.
|
||||
func TestClaimResetRequest_ResponseCarriesRotatedHash(t *testing.T) {
|
||||
h, st, _ := newTestHandler(t)
|
||||
withClaimEngine(t, h, st)
|
||||
st.SaveCustomerConfig(&store.CustomerConfig{
|
||||
CustomerID: "c1", RetrievalPassword: "pw", APIKey: "KEY1", ConfigJSON: "{}", Email: "a@b.hu",
|
||||
})
|
||||
do(h, http.MethodPost, "/report", globalKey, `{"customer_id":"c1"}`) // issues gen 1
|
||||
before, _ := st.GetClaim("c1")
|
||||
|
||||
rr := do(h, http.MethodPost, "/claim/reset-request", "KEY1", `{"customer_id":"c1"}`)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("reset request = %d (%s)", rr.Code, rr.Body.String())
|
||||
}
|
||||
var resp struct {
|
||||
Claim *struct {
|
||||
CodeHash string `json:"code_hash"`
|
||||
Generation int `json:"generation"`
|
||||
IssuedAt string `json:"issued_at"`
|
||||
} `json:"claim"`
|
||||
}
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
after, _ := st.GetClaim("c1")
|
||||
if resp.Claim == nil {
|
||||
t.Fatalf("reset-request response carries no claim object: %s", rr.Body.String())
|
||||
}
|
||||
if resp.Claim.Generation != before.Generation+1 || resp.Claim.Generation != after.Generation {
|
||||
t.Fatalf("response generation = %d, want the rotated one (stored %d, pre-rotation %d)",
|
||||
resp.Claim.Generation, after.Generation, before.Generation)
|
||||
}
|
||||
if resp.Claim.CodeHash != after.CodeHash || resp.Claim.CodeHash == before.CodeHash {
|
||||
t.Fatalf("response hash must be the freshly stored one (matches-stored=%v, still-old=%v)",
|
||||
resp.Claim.CodeHash == after.CodeHash, resp.Claim.CodeHash == before.CodeHash)
|
||||
}
|
||||
if !strings.HasPrefix(resp.Claim.CodeHash, "$2") || resp.Claim.IssuedAt == "" {
|
||||
t.Fatalf("response claim malformed: %+v (want bcrypt hash + issued_at)", resp.Claim)
|
||||
}
|
||||
}
|
||||
|
||||
// The generated controller.yaml bakes the claim hash (Day-0 gate-from-first-boot) and NEVER a
|
||||
// plaintext code; without a claim state the web section is unchanged.
|
||||
func TestConfiggen_BakesClaimHashOnly(t *testing.T) {
|
||||
|
||||
@@ -1403,9 +1403,21 @@ func (h *Handler) handleClaimResetRequest(w http.ResponseWriter, r *http.Request
|
||||
} else {
|
||||
h.logger.Printf("[INFO] claim reset-request for %s: reset code emailed to the registered address", payload.CustomerID)
|
||||
}
|
||||
// v0.52.0 (take-two F-15): serve the ACTIVE code state in the response — same shape and same
|
||||
// bcrypt-only guarantee as the report ACK — so the box accepts the emailed code the moment it
|
||||
// lands instead of waiting for the next ACK (~15 min). Served on every authorized outcome: on
|
||||
// a cap-reached refusal it is the unrotated row (a controller-side no-op by generation).
|
||||
resp := map[string]interface{}{"status": "ok"}
|
||||
if cs, err := h.store.GetClaim(payload.CustomerID); err == nil && cs != nil {
|
||||
resp["claim"] = map[string]interface{}{
|
||||
"code_hash": cs.CodeHash,
|
||||
"generation": cs.Generation,
|
||||
"issued_at": cs.IssuedAt.UTC().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
// allowedEventTypes lists all valid event_type values the Hub accepts.
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
{{else if eq .Flash "blocked"}}Customer blocked — hidden from Dashboard.
|
||||
{{else if eq .Flash "unblocked"}}Customer unblocked — visible on Dashboard again.
|
||||
{{else if eq .Flash "log_tail_requested"}}Log tail requested — the controller delivers it on its next report cycle (a few minutes). A customer-visible event line was recorded.
|
||||
{{else if eq .Flash "claim-resent"}}Code re-sent to the registered address. A kód a doboz következő jelentésekor (~15 percen belül) aktiválódik.
|
||||
{{else if eq .Flash "claim-resend-failed"}}Claim code resend FAILED — check the hub log (email delivery / send error).
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user