#include "errors.hpp" #include "json_http.hpp" #include "server.hpp" namespace svapi { void registerSettingsRoutes(ApiServer& s) { auto& svr = s.raw(); ServerDeps* d = &s.deps(); svr.Get("/api/v1/settings", [d](const httplib::Request& req, httplib::Response& res) { requireAdmin(requireKey(*d, req)); auto snap = d->settings.snapshot(); nlohmann::json j = snap->toJson(); bool keySet = !snap->openaiApiKey.empty(); j.erase("openai_api_key"); j["openai_api_key_set"] = keySet; bool captchaSecretSet = !snap->captchaSecret.empty(); j.erase("captcha_secret"); j["captcha_secret_set"] = captchaSecretSet; sendJson(res, 200, j); }); svr.Put("/api/v1/settings", [d](const httplib::Request& req, httplib::Response& res) { requireAdmin(requireKey(*d, req)); auto body = bodyJson(req); // Start from the current snapshot (preserve unspecified fields). Settings updated = *d->settings.snapshot(); if (body.contains("openai_api_base")) updated.openaiApiBase = body["openai_api_base"].get(); // Only overwrite the key if a non-empty string is supplied. if (body.contains("openai_api_key")) { std::string k = body["openai_api_key"].get(); if (!k.empty()) updated.openaiApiKey = k; } if (body.contains("default_embedding_model")) updated.defaultEmbeddingModel = body["default_embedding_model"].get(); if (body.contains("cors_origins") && body["cors_origins"].is_array()) updated.corsOrigins = body["cors_origins"].get>(); if (body.contains("session_ttl_minutes")) updated.sessionTtlMinutes = body["session_ttl_minutes"].get(); if (body.contains("webui_enabled")) updated.webuiEnabled = body["webui_enabled"].get(); if (body.contains("default_project")) updated.defaultProject = body["default_project"].get(); if (body.contains("embedding_connect_timeout_sec")) updated.embeddingConnectTimeoutSec = body["embedding_connect_timeout_sec"].get(); if (body.contains("embedding_read_timeout_sec")) updated.embeddingReadTimeoutSec = body["embedding_read_timeout_sec"].get(); if (body.contains("embedding_cache_size")) updated.embeddingCacheSize = body["embedding_cache_size"].get(); if (body.contains("embedding_cache_ttl_sec")) updated.embeddingCacheTtlSec = body["embedding_cache_ttl_sec"].get(); if (body.contains("embedding_cache_max_bytes")) updated.embeddingCacheMaxBytes = body["embedding_cache_max_bytes"].get(); if (body.contains("embedding_cache_normalize")) updated.embeddingCacheNormalize = body["embedding_cache_normalize"].get(); if (body.contains("embedding_client_pool_size")) updated.embeddingClientPoolSize = body["embedding_client_pool_size"].get(); if (body.contains("captcha_provider")) updated.captchaProvider = body["captcha_provider"].get(); // Only overwrite the secret if a non-empty string is supplied (mirror openai_api_key handling). if (body.contains("captcha_secret")) { std::string cs = body["captcha_secret"].get(); if (!cs.empty()) updated.captchaSecret = cs; } if (body.contains("captcha_verify_url")) updated.captchaVerifyUrl = body["captcha_verify_url"].get(); d->settings.save(updated); sendJson(res, 200, {{"ok", true}}); }); // Server-global read-only lock (not per project). GET reflects the DB's own // authoritative state - the DB can also enter read-only by itself after a // failed/degraded recovery, not only via an operator's PUT here, so `reason` // (when non-empty) tells the caller which it was. svr.Get(R"(/api/v1/admin/readonly)", [d](const httplib::Request& req, httplib::Response& res) { requireAdmin(requireKey(*d, req)); auto st = d->db.client().getReadOnlyStatus(); nlohmann::json j{{"readonly", st.readOnly}, {"scope", "server"}}; if (!st.reason.empty()) j["reason"] = st.reason; sendJson(res, 200, j); }); svr.Put(R"(/api/v1/admin/readonly)", [d](const httplib::Request& req, httplib::Response& res) { requireAdmin(requireKey(*d, req)); auto body = bodyJson(req); if (!body.is_object() || !body.contains("readonly") || !body["readonly"].is_boolean()) throw ApiError(ErrCode::Unprocessable, "validation", "readonly (boolean) is required"); const bool ro = body["readonly"].get(); if (!d->db.client().setReadOnly(ro)) throw ApiError(ErrCode::Unavailable, "db_error", "failed to set read-only mode"); auto st = d->db.client().getReadOnlyStatus(); nlohmann::json j{{"readonly", st.readOnly}, {"scope", "server"}}; if (!st.reason.empty()) j["reason"] = st.reason; sendJson(res, 200, j); }); } } // namespace svapi