init
This commit is contained in:
265
server/handlers.go
Normal file
265
server/handlers.go
Normal file
@@ -0,0 +1,265 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user