hub v0.3.5: Recovery endpoint + customer_blocked in report response

- New GET /api/v1/recovery/{customer_id}: returns generated controller.yaml
  and infra backup in a single response for disaster recovery.
  Auth via X-Retrieval-Password header.
- Report response now includes customer_blocked: true when customer
  status is "blocked" — controllers use this to detect standing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 12:38:57 +01:00
parent 9048e1adad
commit 4ec1b7d712
3 changed files with 114 additions and 6 deletions
+80 -1
View File
@@ -109,6 +109,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else {
h.handleCustomer(w, r, customerID)
}
case r.Method == http.MethodGet && strings.HasPrefix(path, "/recovery/"):
customerID := strings.TrimPrefix(path, "/recovery/")
h.handleRecovery(w, r, customerID)
case r.Method == http.MethodGet && strings.HasPrefix(path, "/config/"):
customerID := strings.TrimPrefix(path, "/config/")
h.handleConfigRetrieve(w, r, customerID)
@@ -152,8 +155,17 @@ func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) {
}
h.logger.Printf("[INFO] Received report from %s (%d bytes)", payload.CustomerID, len(body))
// Build response with optional customer_blocked flag
resp := map[string]interface{}{"status": "ok"}
if custCfg, err := h.store.GetCustomerConfig(payload.CustomerID); err == nil && custCfg != nil {
if custCfg.Status == "blocked" {
resp["customer_blocked"] = true
}
}
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.
@@ -552,6 +564,73 @@ func (h *Handler) handleInfraBackupGet(w http.ResponseWriter, r *http.Request, c
w.Write(data)
}
// handleRecovery returns both the generated controller.yaml and the infra backup for disaster recovery.
// Auth: X-Retrieval-Password header (same as config retrieval).
func (h *Handler) handleRecovery(w http.ResponseWriter, r *http.Request, customerID string) {
if customerID == "" {
http.Error(w, "Missing customer_id", http.StatusBadRequest)
return
}
password := r.Header.Get("X-Retrieval-Password")
if password == "" {
http.Error(w, "Unauthorized: X-Retrieval-Password header required", http.StatusUnauthorized)
return
}
cfg, err := h.store.GetCustomerConfig(customerID)
if err != nil {
h.logger.Printf("[ERROR] Recovery: failed to get customer config for %s: %v", customerID, err)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
if cfg == nil {
http.Error(w, "Not found", http.StatusNotFound)
return
}
if subtle.ConstantTimeCompare([]byte(password), []byte(cfg.RetrievalPassword)) != 1 {
http.Error(w, "Unauthorized: invalid password", http.StatusUnauthorized)
return
}
// Generate controller.yaml
var configYAML string
if h.templateProvider != nil {
yamlOutput, err := configgen.Generate(h.templateProvider.Template(), cfg)
if err != nil {
h.logger.Printf("[ERROR] Recovery: failed to generate config for %s: %v", customerID, err)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
configYAML = yamlOutput
}
// Fetch infra backup (optional — may not exist for new customers)
var infraBackup json.RawMessage
hasInfraBackup := false
if data, err := h.store.GetInfraBackup(customerID); err == nil && data != nil {
infraBackup = data
hasInfraBackup = true
}
resp := struct {
CustomerID string `json:"customer_id"`
ConfigYAML string `json:"config_yaml"`
InfraBackup json.RawMessage `json:"infra_backup"`
HasInfraBackup bool `json:"has_infra_backup"`
}{
CustomerID: customerID,
ConfigYAML: configYAML,
InfraBackup: infraBackup,
HasInfraBackup: hasInfraBackup,
}
h.logger.Printf("[INFO] Recovery data downloaded for customer %s (has_infra_backup=%v)", customerID, hasInfraBackup)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// handleConfigRetrieve returns a generated controller.yaml for a customer.
// Auth: X-Retrieval-Password header (not Bearer token).
func (h *Handler) handleConfigRetrieve(w http.ResponseWriter, r *http.Request, customerID string) {