package server import ( "encoding/json" "io" "net/http" "os" "AbstractWizard/config" ) func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { relPath := r.PathValue("path") fullPath, err := config.FullPath(s.cfg.ConfigDir, relPath) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } data, err := os.ReadFile(fullPath) if os.IsNotExist(err) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "file not found"}) return } if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to read file"}) return } format, err := config.DetectFormat(fullPath) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } parsed, err := config.Parse(data, format) if err != nil { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) return } s.audit.Log(r.RemoteAddr, "read", relPath, "config read") writeJSON(w, http.StatusOK, parsed) } func (s *Server) handlePutConfig(w http.ResponseWriter, r *http.Request) { if s.GetMode() == ModeReadOnly { writeJSON(w, http.StatusForbidden, map[string]string{"error": "server is in readonly mode"}) return } relPath := r.PathValue("path") fullPath, err := config.FullPath(s.cfg.ConfigDir, relPath) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } body, err := io.ReadAll(io.LimitReader(r.Body, 10<<20)) // 10 MB limit if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read request body"}) return } format, err := config.DetectFormat(fullPath) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } // Validate that the body is valid config if _, err := config.Parse(body, format); err != nil { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) return } version, err := config.CreateBackup(s.cfg.ConfigDir, relPath, s.cfg.MaxBackups) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "backup failed: " + err.Error()}) return } if err := config.AtomicWrite(fullPath, body, 0o644); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "write failed: " + err.Error()}) return } s.audit.Log(r.RemoteAddr, "replace", relPath, "full replace") resp := map[string]string{"status": "ok"} if version != "" { resp["backup_version"] = version } writeJSON(w, http.StatusOK, resp) } func (s *Server) handlePatchConfig(w http.ResponseWriter, r *http.Request) { if s.GetMode() == ModeReadOnly { writeJSON(w, http.StatusForbidden, map[string]string{"error": "server is in readonly mode"}) return } relPath := r.PathValue("path") fullPath, err := config.FullPath(s.cfg.ConfigDir, relPath) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } format, err := config.DetectFormat(fullPath) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } // Read existing config existing := make(map[string]any) data, err := os.ReadFile(fullPath) if err != nil && !os.IsNotExist(err) { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to read existing file"}) return } if err == nil { existing, err = config.Parse(data, format) if err != nil { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "existing file is invalid: " + err.Error()}) return } } // Parse patch body body, err := io.ReadAll(io.LimitReader(r.Body, 10<<20)) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read request body"}) return } patch, err := config.Parse(body, format) if err != nil { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) return } merged := config.DeepMerge(existing, patch) out, err := config.Serialize(merged, format) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "serialization failed"}) return } version, err := config.CreateBackup(s.cfg.ConfigDir, relPath, s.cfg.MaxBackups) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "backup failed: " + err.Error()}) return } if err := config.AtomicWrite(fullPath, out, 0o644); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "write failed"}) return } s.audit.Log(r.RemoteAddr, "patch", relPath, "deep merge update") resp := map[string]string{"status": "ok"} if version != "" { resp["backup_version"] = version } writeJSON(w, http.StatusOK, resp) } func (s *Server) handleListBackups(w http.ResponseWriter, r *http.Request) { relPath := r.PathValue("path") backups, err := config.ListBackups(s.cfg.ConfigDir, relPath) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } type backupResponse struct { Version string `json:"version"` Timestamp string `json:"timestamp"` } var resp []backupResponse for _, b := range backups { resp = append(resp, backupResponse{Version: b.Version, Timestamp: b.Timestamp}) } if resp == nil { resp = []backupResponse{} } s.audit.Log(r.RemoteAddr, "list_backups", relPath, "listed backups") writeJSON(w, http.StatusOK, resp) } func (s *Server) handleRollback(w http.ResponseWriter, r *http.Request) { if s.GetMode() == ModeReadOnly { writeJSON(w, http.StatusForbidden, map[string]string{"error": "server is in readonly mode"}) return } relPath := r.PathValue("path") var body struct { Version string `json:"version"` } if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if body.Version == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "version is required"}) return } // Backup current state before rollback if _, err := config.CreateBackup(s.cfg.ConfigDir, relPath, s.cfg.MaxBackups); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "pre-rollback backup failed: " + err.Error()}) return } if err := config.Rollback(s.cfg.ConfigDir, relPath, body.Version); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "rollback failed: " + err.Error()}) return } s.audit.Log(r.RemoteAddr, "rollback", relPath, "rolled back to "+body.Version) writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "restored_version": body.Version}) } func (s *Server) handleGetMode(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"mode": s.GetMode().String()}) } func (s *Server) handleSetMode(w http.ResponseWriter, r *http.Request) { var body struct { Mode string `json:"mode"` } if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } mode, ok := ParseMode(body.Mode) if !ok { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid mode; must be 'init' or 'readonly'"}) return } s.SetMode(mode) s.audit.Log(r.RemoteAddr, "set_mode", "", "mode changed to "+body.Mode) writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "mode": mode.String()}) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) }