Kaynağa Gözat

feat(settings): captcha provider/secret/verify-url (secret masked)

Add captchaProvider/captchaSecret/captchaVerifyUrl fields to Settings
with fromJson/toJson round-trip (keys: captcha_provider, captcha_secret,
captcha_verify_url). GET /api/v1/settings masks captcha_secret and adds
captcha_secret_set bool. PUT /api/v1/settings merges all three; secret
only overwritten on non-empty input, mirroring openai_api_key handling.
Tests: defaults are empty strings; provider/url round-trip; secret round-trip.
Fszontagh 1 ay önce
ebeveyn
işleme
c45d4b6cbc
4 değiştirilmiş dosya ile 49 ekleme ve 1 silme
  1. 12 0
      src/handlers/settings.cpp
  2. 7 1
      src/settings.cpp
  3. 4 0
      src/settings.hpp
  4. 26 0
      tests/test_settings.cpp

+ 12 - 0
src/handlers/settings.cpp

@@ -12,6 +12,9 @@ void registerSettingsRoutes(ApiServer& s) {
         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);
     });
 
@@ -51,6 +54,15 @@ void registerSettingsRoutes(ApiServer& s) {
             updated.embeddingCacheNormalize = body["embedding_cache_normalize"].get<bool>();
         if (body.contains("embedding_client_pool_size"))
             updated.embeddingClientPoolSize = body["embedding_client_pool_size"].get<uint32_t>();
+        if (body.contains("captcha_provider"))
+            updated.captchaProvider = body["captcha_provider"].get<std::string>();
+        // 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<std::string>();
+            if (!cs.empty()) updated.captchaSecret = cs;
+        }
+        if (body.contains("captcha_verify_url"))
+            updated.captchaVerifyUrl = body["captcha_verify_url"].get<std::string>();
         d->settings.save(updated);
         sendJson(res, 200, {{"ok", true}});
     });

+ 7 - 1
src/settings.cpp

@@ -19,6 +19,9 @@ Settings Settings::fromJson(const nlohmann::json& j) {
     s.embeddingCacheMaxBytes     = j.value("embedding_cache_max_bytes",   s.embeddingCacheMaxBytes);
     s.embeddingCacheNormalize    = j.value("embedding_cache_normalize",   s.embeddingCacheNormalize);
     s.embeddingClientPoolSize    = j.value("embedding_client_pool_size",  s.embeddingClientPoolSize);
+    s.captchaProvider            = j.value("captcha_provider",   s.captchaProvider);
+    s.captchaSecret              = j.value("captcha_secret",     s.captchaSecret);
+    s.captchaVerifyUrl           = j.value("captcha_verify_url", s.captchaVerifyUrl);
     return s;
 }
 
@@ -33,7 +36,10 @@ nlohmann::json Settings::toJson() const {
             {"embedding_cache_ttl_sec",     embeddingCacheTtlSec},
             {"embedding_cache_max_bytes",   embeddingCacheMaxBytes},
             {"embedding_cache_normalize",   embeddingCacheNormalize},
-            {"embedding_client_pool_size",  embeddingClientPoolSize}};
+            {"embedding_client_pool_size",  embeddingClientPoolSize},
+            {"captcha_provider",   captchaProvider},
+            {"captcha_secret",     captchaSecret},
+            {"captcha_verify_url", captchaVerifyUrl}};
 }
 
 } // namespace svapi

+ 4 - 0
src/settings.hpp

@@ -22,6 +22,10 @@ struct Settings {
     bool        embeddingCacheNormalize    = false;      // opt-in trim+lowercase
     uint32_t    embeddingClientPoolSize    = 4;          // keep-alive clients per upstream origin
 
+    std::string captchaProvider;   // "" = disabled | "turnstile" | "hcaptcha"
+    std::string captchaSecret;     // masked in GET
+    std::string captchaVerifyUrl;  // full verify endpoint
+
     static Settings fromJson(const nlohmann::json& j);
     nlohmann::json  toJson() const;
 };

+ 26 - 0
tests/test_settings.cpp

@@ -31,6 +31,32 @@ TEST(Settings, EmbeddingTimeoutRoundTrip) {
     EXPECT_EQ(s2.embeddingReadTimeoutSec, 120u);
 }
 
+TEST(Settings, CaptchaDefaults) {
+    Settings s = Settings::fromJson(nlohmann::json::object());
+    EXPECT_EQ(s.captchaProvider, "");
+    EXPECT_EQ(s.captchaSecret, "");
+    EXPECT_EQ(s.captchaVerifyUrl, "");
+}
+
+TEST(Settings, CaptchaProviderAndUrlRoundTrip) {
+    nlohmann::json j{{"captcha_provider", "turnstile"},
+                     {"captcha_verify_url", "https://challenges.cloudflare.com/turnstile/v0/siteverify"}};
+    Settings s = Settings::fromJson(j);
+    EXPECT_EQ(s.captchaProvider, "turnstile");
+    EXPECT_EQ(s.captchaVerifyUrl, "https://challenges.cloudflare.com/turnstile/v0/siteverify");
+    Settings s2 = Settings::fromJson(s.toJson());
+    EXPECT_EQ(s2.captchaProvider, "turnstile");
+    EXPECT_EQ(s2.captchaVerifyUrl, "https://challenges.cloudflare.com/turnstile/v0/siteverify");
+}
+
+TEST(Settings, CaptchaSecretRoundTrip) {
+    nlohmann::json j{{"captcha_secret", "mysecret"}};
+    Settings s = Settings::fromJson(j);
+    EXPECT_EQ(s.captchaSecret, "mysecret");
+    Settings s2 = Settings::fromJson(s.toJson());
+    EXPECT_EQ(s2.captchaSecret, "mysecret");
+}
+
 TEST(ApiKey, JsonRoundTripAndPublicMasks) {
     ApiKey k = ApiKey::fromJson(nlohmann::json::parse(
         R"({"key":"abcdef0123456789","label":"n8n","projects":["p1","p2"],"admin":false,"created_at":7})"));