R-415 fixed on hub main (no duplicate/nested/felhom.eu customer domain; R-138 option B); R-762 closed (9202 proven, catalog eec9a0d); R-905 opened (wger static in backups); decision sheet D10; 130 -> 129
gates / gates (push) Successful in 3m55s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-08 10:19:39 +02:00
parent 78121aa475
commit d9147b02de
18 changed files with 818 additions and 9 deletions
+38
View File
@@ -741,6 +741,18 @@ func (s *Server) handleConfigCreate(w http.ResponseWriter, r *http.Request) {
return
}
// R-415 / R-138 option B: a domain equal to, inside or containing another customer's, or under felhom.eu, is refused
// BEFORE anything is generated or provisioned.
if msg := s.domainConflictMessage(customerID, r.FormValue("domain")); msg != "" {
s.renderConfigForm(w, r, true, &store.CustomerConfig{
CustomerID: customerID,
CustomerName: r.FormValue("customer_name"),
Domain: r.FormValue("domain"),
Email: r.FormValue("email"),
}, nil, msg)
return
}
// Generate credentials.
//
// R-597: the Owner passphrase follows the language the operator is choosing ON THIS FORM, not
@@ -861,6 +873,13 @@ func (s *Server) handleConfigUpdate(w http.ResponseWriter, r *http.Request, cust
s.renderConfigForm(w, r, false, cfg, submitted, "Display Name and Domain are required.")
return
}
// R-415 / R-138 option B — the same guard on edit (a customer's own current domain never conflicts with itself).
if msg := s.domainConflictMessage(customerID, cfg.Domain); msg != "" {
var submitted map[string]interface{}
_ = json.Unmarshal([]byte(buildConfigJSON(r)), &submitted)
s.renderConfigForm(w, r, false, cfg, submitted, msg)
return
}
cfg.ConfigJSON = buildConfigJSON(r)
@@ -1769,3 +1788,22 @@ func (s *Server) handleGeoDisable(w http.ResponseWriter, r *http.Request, custom
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Geo-restriction removed from Cloudflare."})
}
// domainConflictMessage is the operator's sentence for a refused domain ("" = allowed). A store error refuses too
// (fail closed: the save can be retried; a duplicate domain cannot be undone once boxes use it).
func (s *Server) domainConflictMessage(customerID, domain string) string {
other, err := s.store.DomainConflict(customerID, domain)
if err != nil {
s.logger.Printf("[ERROR] domain check for %s failed: %v — save refused", customerID, err)
return "The domain could not be checked against the other customers — nothing was saved. Try again."
}
switch other {
case "":
return ""
case "felhom.eu":
return fmt.Sprintf("Domain %q is under felhom.eu — every customer has their own domain (01 §7). Nothing was saved.", strings.TrimSpace(domain))
default:
s.logger.Printf("[WARN] domain %q for %s refused: it overlaps customer %s's domain (R-415)", strings.TrimSpace(domain), customerID, other)
return fmt.Sprintf("Domain %q equals, contains or lies under the domain of customer %q — every customer has their own domain (01 §7). Nothing was saved.", strings.TrimSpace(domain), other)
}
}