Files
AbstractWizard/server/handlers.go
2026-02-15 08:49:27 +00:00

266 lines
7.8 KiB
Go

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)
}