package api import ( "context" "encoding/json" "net/http" "strings" "time" "github.com/go-chi/chi/v5" "github.com/kindredsystems/silo/internal/auth" ) // HandleGetAllSettings returns the full config grouped by module with secrets redacted. func (s *Server) HandleGetAllSettings(w http.ResponseWriter, r *http.Request) { resp := map[string]any{ "core": s.buildCoreSettings(), "schemas": s.buildSchemasSettings(), "storage": s.buildStorageSettings(r.Context()), "database": s.buildDatabaseSettings(r.Context()), "auth": s.buildAuthSettings(), "projects": map[string]any{"enabled": s.modules.IsEnabled("projects")}, "audit": map[string]any{"enabled": s.modules.IsEnabled("audit")}, "odoo": s.buildOdooSettings(), "freecad": s.buildFreecadSettings(), "jobs": s.buildJobsSettings(), "dag": map[string]any{"enabled": s.modules.IsEnabled("dag")}, } writeJSON(w, http.StatusOK, resp) } // HandleGetModuleSettings returns settings for a single module. func (s *Server) HandleGetModuleSettings(w http.ResponseWriter, r *http.Request) { module := chi.URLParam(r, "module") var settings any switch module { case "core": settings = s.buildCoreSettings() case "schemas": settings = s.buildSchemasSettings() case "storage": settings = s.buildStorageSettings(r.Context()) case "database": settings = s.buildDatabaseSettings(r.Context()) case "auth": settings = s.buildAuthSettings() case "projects": settings = map[string]any{"enabled": s.modules.IsEnabled("projects")} case "audit": settings = map[string]any{"enabled": s.modules.IsEnabled("audit")} case "odoo": settings = s.buildOdooSettings() case "freecad": settings = s.buildFreecadSettings() case "jobs": settings = s.buildJobsSettings() case "dag": settings = map[string]any{"enabled": s.modules.IsEnabled("dag")} default: writeError(w, http.StatusNotFound, "not_found", "Unknown module: "+module) return } writeJSON(w, http.StatusOK, settings) } // HandleUpdateModuleSettings handles module toggle and config overrides. func (s *Server) HandleUpdateModuleSettings(w http.ResponseWriter, r *http.Request) { module := chi.URLParam(r, "module") // Validate module exists if s.modules.Get(module) == nil { writeError(w, http.StatusNotFound, "not_found", "Unknown module: "+module) return } var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "invalid_json", err.Error()) return } user := auth.UserFromContext(r.Context()) username := "system" if user != nil { username = user.Username } var updated []string restartRequired := false // Handle module toggle if enabledVal, ok := body["enabled"]; ok { enabled, ok := enabledVal.(bool) if !ok { writeError(w, http.StatusBadRequest, "invalid_value", "'enabled' must be a boolean") return } if err := s.modules.SetEnabled(module, enabled); err != nil { writeError(w, http.StatusBadRequest, "toggle_failed", err.Error()) return } if err := s.settings.SetModuleState(r.Context(), module, enabled, username); err != nil { s.logger.Error().Err(err).Str("module", module).Msg("failed to persist module state") writeError(w, http.StatusInternalServerError, "persist_failed", "Failed to save module state") return } updated = append(updated, module+".enabled") } // Handle config overrides (future use — persisted but not hot-reloaded) for key, value := range body { if key == "enabled" { continue } fullKey := module + "." + key if err := s.settings.SetOverride(r.Context(), fullKey, value, username); err != nil { s.logger.Error().Err(err).Str("key", fullKey).Msg("failed to persist setting override") writeError(w, http.StatusInternalServerError, "persist_failed", "Failed to save setting: "+key) return } updated = append(updated, fullKey) // These namespaces require a restart to take effect if strings.HasPrefix(fullKey, "database.") || strings.HasPrefix(fullKey, "storage.") || strings.HasPrefix(fullKey, "server.") || strings.HasPrefix(fullKey, "schemas.") { restartRequired = true } } writeJSON(w, http.StatusOK, map[string]any{ "updated": updated, "restart_required": restartRequired, }) // Publish SSE event s.broker.Publish("settings.changed", mustMarshal(map[string]any{ "module": module, "changed_keys": updated, "updated_by": username, })) } // HandleTestModuleConnectivity tests external connectivity for a module. func (s *Server) HandleTestModuleConnectivity(w http.ResponseWriter, r *http.Request) { module := chi.URLParam(r, "module") start := time.Now() var success bool var message string switch module { case "database": if err := s.db.Pool().Ping(r.Context()); err != nil { success = false message = "Database ping failed: " + err.Error() } else { success = true message = "Database connection OK" } case "storage": if s.storage == nil { success = false message = "Storage not configured" } else if err := s.storage.Ping(r.Context()); err != nil { success = false message = "Storage ping failed: " + err.Error() } else { success = true message = "Storage connection OK" } case "auth", "odoo": success = false message = "Connectivity test not implemented for " + module default: writeError(w, http.StatusBadRequest, "not_testable", "No connectivity test available for module: "+module) return } latency := time.Since(start).Milliseconds() writeJSON(w, http.StatusOK, map[string]any{ "success": success, "message": message, "latency_ms": latency, }) } // --- build helpers (read config, redact secrets) --- func redact(s string) string { if s == "" { return "" } return "****" } func (s *Server) buildCoreSettings() map[string]any { return map[string]any{ "enabled": true, "host": s.cfg.Server.Host, "port": s.cfg.Server.Port, "base_url": s.cfg.Server.BaseURL, "readonly": s.cfg.Server.ReadOnly, } } func (s *Server) buildSchemasSettings() map[string]any { return map[string]any{ "enabled": true, "directory": s.cfg.Schemas.Directory, "default": s.cfg.Schemas.Default, "count": len(s.schemas), } } func (s *Server) buildStorageSettings(ctx context.Context) map[string]any { result := map[string]any{ "enabled": true, "backend": "filesystem", "root_dir": s.cfg.Storage.Filesystem.RootDir, } if s.storage != nil { if err := s.storage.Ping(ctx); err != nil { result["status"] = "unavailable" } else { result["status"] = "ok" } } else { result["status"] = "not_configured" } return result } func (s *Server) buildDatabaseSettings(ctx context.Context) map[string]any { result := map[string]any{ "enabled": true, "host": s.cfg.Database.Host, "port": s.cfg.Database.Port, "name": s.cfg.Database.Name, "user": s.cfg.Database.User, "password": redact(s.cfg.Database.Password), "sslmode": s.cfg.Database.SSLMode, "max_connections": s.cfg.Database.MaxConnections, } if err := s.db.Pool().Ping(ctx); err != nil { result["status"] = "unavailable" } else { result["status"] = "ok" } return result } func (s *Server) buildAuthSettings() map[string]any { return map[string]any{ "enabled": s.modules.IsEnabled("auth"), "session_secret": redact(s.cfg.Auth.SessionSecret), "local": map[string]any{ "enabled": s.cfg.Auth.Local.Enabled, "default_admin_username": s.cfg.Auth.Local.DefaultAdminUsername, "default_admin_password": redact(s.cfg.Auth.Local.DefaultAdminPassword), }, "ldap": map[string]any{ "enabled": s.cfg.Auth.LDAP.Enabled, "url": s.cfg.Auth.LDAP.URL, "base_dn": s.cfg.Auth.LDAP.BaseDN, "bind_dn": s.cfg.Auth.LDAP.BindDN, "bind_password": redact(s.cfg.Auth.LDAP.BindPassword), }, "oidc": map[string]any{ "enabled": s.cfg.Auth.OIDC.Enabled, "issuer_url": s.cfg.Auth.OIDC.IssuerURL, "client_id": s.cfg.Auth.OIDC.ClientID, "client_secret": redact(s.cfg.Auth.OIDC.ClientSecret), "redirect_url": s.cfg.Auth.OIDC.RedirectURL, }, } } func (s *Server) buildOdooSettings() map[string]any { return map[string]any{ "enabled": s.modules.IsEnabled("odoo"), "url": s.cfg.Odoo.URL, "database": s.cfg.Odoo.Database, "username": s.cfg.Odoo.Username, "api_key": redact(s.cfg.Odoo.APIKey), } } func (s *Server) buildFreecadSettings() map[string]any { return map[string]any{ "enabled": s.modules.IsEnabled("freecad"), "uri_scheme": s.cfg.FreeCAD.URIScheme, "executable": s.cfg.FreeCAD.Executable, } } func (s *Server) buildJobsSettings() map[string]any { return map[string]any{ "enabled": s.modules.IsEnabled("jobs"), "directory": s.cfg.Jobs.Directory, "runner_timeout": s.cfg.Jobs.RunnerTimeout, "job_timeout_check": s.cfg.Jobs.JobTimeoutCheck, "default_priority": s.cfg.Jobs.DefaultPriority, "definitions_count": len(s.jobDefs), } }