hub v0.163.0: a rebuilt box gets the household's sealed package back (R-1176); one run per daily job on clock-change days (R-1183); a scoped abandonment purge (R-1182); the new-link page says the old box is still linked (R-1203)
gates / gates (push) Successful in 13m23s
gates / gates (push) Successful in 13m23s
This commit is contained in:
1 parent
e07b077ac2
commit
9688e76efd
16 files changed
+1018
-29
No files matched your search
@@ -2,6 +2,8 @@ package web
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@@ -97,7 +99,7 @@ func bindClientIP(r *http.Request) string {
|
||||
// --- the page ---
|
||||
|
||||
type bindPageData struct {
|
||||
State string // "form" | "success" | "expired" | "consumed" | "locked" | "resent"
|
||||
State string // "form" | "success" | "expired" | "consumed" | "locked" | "resent" | "oldbox" (R-1203)
|
||||
Token string // echoed into the form action (the capability itself; already in the URL)
|
||||
Failed bool // generic factor-check failure (form state only)
|
||||
// Lang is the language to render in. It is the CUSTOMER'S CREATION-TIME language and nothing
|
||||
@@ -334,6 +336,9 @@ const bindPageHTML = `<!DOCTYPE html>
|
||||
<p class="lead">{{T "bind.consumed.lead"}}</p>
|
||||
<p>{{T "bind.consumed.body"}}</p>
|
||||
<form method="POST" action="/bind/{{.Token}}/resend"><p class="hint">{{T "bind.resend.hint"}}</p><button type="submit">{{T "bind.resend.button"}}</button></form>
|
||||
{{else if eq .State "oldbox"}}
|
||||
<p class="lead">{{T "bind.oldbox.lead"}}</p>
|
||||
<p>{{T "bind.oldbox.body"}}</p>
|
||||
{{else if eq .State "resent"}}
|
||||
<p class="lead">{{T "bind.resent.lead"}}</p>
|
||||
<p>{{T "bind.resent.body"}}</p>
|
||||
@@ -368,7 +373,8 @@ const bindPageHTML = `<!DOCTYPE html>
|
||||
const bindResendEvery = time.Hour
|
||||
|
||||
func (s *Server) handleBindResend(w http.ResponseWriter, token string) {
|
||||
defer s.renderBind(w, http.StatusOK, bindPageData{State: "resent", Lang: i18n.Default})
|
||||
page := bindPageData{State: "resent", Lang: i18n.Default}
|
||||
defer func() { s.renderBind(w, http.StatusOK, page) }()
|
||||
tok, err := s.store.SelfBindTokenByHash(selfBindHash(token))
|
||||
if err != nil || tok == nil {
|
||||
return
|
||||
@@ -377,6 +383,35 @@ func (s *Server) handleBindResend(w http.ResponseWriter, token string) {
|
||||
if !tok.Expired(now) && !tok.Consumed() {
|
||||
return // a live link needs no replacement
|
||||
}
|
||||
// R-1203 (2026-10-21): a household whose old box is still linked waited for a mail that never came (rehearsal
|
||||
// 2026-10-20 B7, 17 minutes, then a phone call). The link holder is told so in plain words, and the operator gets
|
||||
// the host name to delete. This answers only a REAL used or expired link — an unknown token still gets the same
|
||||
// „resent" page, so a stranger learns nothing; the holder of a real link is the household (it came to their mailbox).
|
||||
if h, herr := s.store.GetHostByCustomer(tok.CustomerID); herr == nil && h != nil {
|
||||
page = bindPageData{State: "oldbox", Lang: s.store.CustomerLanguage(tok.CustomerID)}
|
||||
s.bindResendMu.Lock()
|
||||
key := "oldbox:" + tok.CustomerID
|
||||
last := s.bindResendAt[key]
|
||||
if now.Sub(last) < bindResendEvery {
|
||||
s.bindResendMu.Unlock()
|
||||
return
|
||||
}
|
||||
if s.bindResendAt == nil {
|
||||
s.bindResendAt = map[string]time.Time{}
|
||||
}
|
||||
s.bindResendAt[key] = now
|
||||
s.bindResendMu.Unlock()
|
||||
s.logger.Printf("[WARN] self-bind: %s asked for a new link but host %s is still linked — no link sent; the operator is told", tok.CustomerID, h.HostID)
|
||||
if s.emit != nil {
|
||||
details, _ := json.Marshal(map[string]any{"host_id": h.HostID})
|
||||
s.emit(tok.CustomerID, eventBindOldHostLinked, "warning",
|
||||
fmt.Sprintf("Customer %s asked for a new box link, but host %s is still linked to it, so no link was sent. "+
|
||||
"If the household's old box is gone (a rebuild), delete host %s on the Hosts page, then send a new link from the customer page. "+
|
||||
"The household was told the old box is still linked and to write to Felhom.", tok.CustomerID, h.HostID, h.HostID),
|
||||
string(details), "hub")
|
||||
}
|
||||
return
|
||||
}
|
||||
s.bindResendMu.Lock()
|
||||
last := s.bindResendAt[tok.CustomerID]
|
||||
if now.Sub(last) < bindResendEvery {
|
||||
@@ -391,3 +426,6 @@ func (s *Server) handleBindResend(w http.ResponseWriter, token string) {
|
||||
s.bindResendMu.Unlock()
|
||||
s.autoMintSelfBindIfWaiting(tok.CustomerID, "fresh link asked on an expired or used link")
|
||||
}
|
||||
|
||||
// eventBindOldHostLinked (R-1203): operator-only, registered in notify.operatorOnlyEvents in the same commit.
|
||||
const eventBindOldHostLinked = "bind_old_host_linked"
|
||||
Reference in new issue
Block a user