package web import ( "encoding/json" "net/http" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // handleDRRecipeDownload serves the assembled secret-free DR recipe for a customer as a JSON download // (SPIKE-dr-recipe-2026-06-16). The recipe is PLAINTEXT because it carries NO secrets — only the // reconstruction scaffolding (guest sizing, drive inventory, PVE storage, PBS coordinates, app // inventory + storage bindings). Operator (dashboard-auth) only; no decrypt, nothing to redact. func (s *Server) handleDRRecipeDownload(w http.ResponseWriter, r *http.Request, customerID string) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } rec, err := s.store.GetDRRecipe(customerID) if err != nil { s.logger.Printf("[ERROR] DR-recipe lookup failed for %s: %v", customerID, err) http.Error(w, "Internal error", http.StatusInternalServerError) return } if rec == nil { http.Error(w, "No DR recipe for this customer yet (awaiting a host-report + controller report)", http.StatusNotFound) return } assembled, err := store.AssembleDRRecipe(rec) if err != nil { s.logger.Printf("[ERROR] DR-recipe assemble failed for %s: %v", customerID, err) http.Error(w, "Internal error", http.StatusInternalServerError) return } out, err := json.MarshalIndent(assembled, "", " ") if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Disposition", "attachment; filename=\"dr-recipe-"+safeFilename(customerID)+".json\"") w.Write(out) s.logger.Printf("[INFO] DR-recipe downloaded for customer %s (v%d)", customerID, assembled.RecipeVersion) } // safeFilename keeps a customer id safe for a Content-Disposition filename (alnum/-/_ only). func safeFilename(s string) string { out := make([]rune, 0, len(s)) for _, r := range s { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_': out = append(out, r) default: out = append(out, '_') } } if len(out) == 0 { return "customer" } return string(out) }