266 lines
7.8 KiB
Go
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)
|
|
}
|