From ad9ee0d10dfaae9c6960a6a2d7a9f1cd2f98d591 Mon Sep 17 00:00:00 2001 From: DBT Date: Thu, 26 Feb 2026 12:30:38 +0000 Subject: [PATCH] webui config: add schema validation and risky-field confirmation on save --- pkg/nodes/registry_server.go | 74 ++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pkg/nodes/registry_server.go b/pkg/nodes/registry_server.go index 9b9f9c0..e7f96fa 100644 --- a/pkg/nodes/registry_server.go +++ b/pkg/nodes/registry_server.go @@ -2,6 +2,7 @@ package nodes import ( "bufio" + "bytes" "context" "encoding/json" "fmt" @@ -18,6 +19,8 @@ import ( "strings" "syscall" "time" + + cfgpkg "clawgo/pkg/config" ) type RegistryServer struct { @@ -263,6 +266,61 @@ func (s *RegistryServer) handleWebUIConfig(w http.ResponseWriter, r *http.Reques http.Error(w, "invalid json", http.StatusBadRequest) return } + confirmRisky, _ := body["confirm_risky"].(bool) + delete(body, "confirm_risky") + + oldCfgRaw, _ := os.ReadFile(s.configPath) + var oldMap map[string]interface{} + _ = json.Unmarshal(oldCfgRaw, &oldMap) + + riskyPaths := []string{ + "channels.telegram.token", + "channels.telegram.allow_from", + "channels.telegram.allow_chats", + "providers.proxy.base_url", + "providers.proxy.api_key", + "gateway.token", + "gateway.port", + } + changedRisky := make([]string, 0) + for _, p := range riskyPaths { + if fmt.Sprintf("%v", getPathValue(oldMap, p)) != fmt.Sprintf("%v", getPathValue(body, p)) { + changedRisky = append(changedRisky, p) + } + } + if len(changedRisky) > 0 && !confirmRisky { + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "ok": false, + "error": "risky fields changed; confirmation required", + "requires_confirm": true, + "changed_fields": changedRisky, + }) + return + } + + candidate, err := json.Marshal(body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + cfg := cfgpkg.DefaultConfig() + dec := json.NewDecoder(bytes.NewReader(candidate)) + dec.DisallowUnknownFields() + if err := dec.Decode(cfg); err != nil { + http.Error(w, "config schema validation failed: "+err.Error(), http.StatusBadRequest) + return + } + if errs := cfgpkg.Validate(cfg); len(errs) > 0 { + list := make([]string, 0, len(errs)) + for _, e := range errs { + list = append(list, e.Error()) + } + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "config validation failed", "details": list}) + return + } + b, err := json.MarshalIndent(body, "", " ") if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -288,6 +346,22 @@ func (s *RegistryServer) handleWebUIConfig(w http.ResponseWriter, r *http.Reques } } +func getPathValue(m map[string]interface{}, path string) interface{} { + if m == nil || strings.TrimSpace(path) == "" { + return nil + } + parts := strings.Split(path, ".") + var cur interface{} = m + for _, p := range parts { + node, ok := cur.(map[string]interface{}) + if !ok { + return nil + } + cur = node[p] + } + return cur +} + func (s *RegistryServer) handleWebUIUpload(w http.ResponseWriter, r *http.Request) { if !s.checkAuth(r) { http.Error(w, "unauthorized", http.StatusUnauthorized)