#include "settings_routes.hpp" #include "../app.hpp" #include "../stores/settings_store.hpp" #include "../stores/workspace_store.hpp" #include #include #include #include #include #include #include #include namespace smartbotic::microbit { using json = nlohmann::json; // Helper: check if user is owner of any workspace static bool isOwnerOfAnyWorkspace(App& app, const std::string& userId) { auto workspaces = app.workspaceStore()->listUserWorkspaces(userId); for (const auto& ws : workspaces) { auto member = app.workspaceStore()->getMember(ws.id, userId); if (member && member->role == "owner") { return true; } } return false; } // Helper: mask sensitive fields in settings static json maskSensitiveFields(const json& settings) { json masked = settings; // List of keys whose values should be masked static const std::vector sensitiveKeys = { "smtp_password", "callerai_api_key", "api_key", "secret", "password" }; for (const auto& key : sensitiveKeys) { if (masked.contains(key) && masked[key].is_string()) { std::string val = masked[key].get(); if (val.size() > 4) { masked[key] = std::string(val.size() - 4, '*') + val.substr(val.size() - 4); } else if (!val.empty()) { masked[key] = "****"; } } } return masked; } void setupSettingsRoutes(httplib::Server& svr, App& app) { // GET /api/v1/settings -- get global settings (owner of any workspace) svr.Get("/api/v1/settings", [&app](const httplib::Request& req, httplib::Response& res) { try { auto authCtx = app.authMiddleware()->getAuthContext(); if (!authCtx) { res.status = 401; res.set_content(R"({"error":"Unauthorized"})", "application/json"); return; } if (!isOwnerOfAnyWorkspace(app, authCtx->userId)) { res.status = 403; res.set_content(json{{"error", "Forbidden: requires owner role in at least one workspace"}}.dump(), "application/json"); return; } auto settings = app.settingsStore()->getGlobalSettings(); json settingsJson = settings.value_or(json::object()); // Mask sensitive fields before returning json masked = maskSensitiveFields(settingsJson); res.status = 200; res.set_content(json{{"settings", masked}}.dump(), "application/json"); } catch (const std::exception& e) { spdlog::error("Get settings error: {}", e.what()); res.status = 500; res.set_content(json{{"error", "Internal server error"}}.dump(), "application/json"); } }); // PUT /api/v1/settings -- update global settings (owner) svr.Put("/api/v1/settings", [&app](const httplib::Request& req, httplib::Response& res) { try { auto authCtx = app.authMiddleware()->getAuthContext(); if (!authCtx) { res.status = 401; res.set_content(R"({"error":"Unauthorized"})", "application/json"); return; } if (!isOwnerOfAnyWorkspace(app, authCtx->userId)) { res.status = 403; res.set_content(json{{"error", "Forbidden: requires owner role in at least one workspace"}}.dump(), "application/json"); return; } auto body = json::parse(req.body); bool updated = app.settingsStore()->updateGlobalSettings(body); if (!updated) { res.status = 500; res.set_content(json{{"error", "Failed to update settings"}}.dump(), "application/json"); return; } // Return the updated (masked) settings auto settings = app.settingsStore()->getGlobalSettings(); json settingsJson = settings.value_or(json::object()); json masked = maskSensitiveFields(settingsJson); res.status = 200; res.set_content(json{{"settings", masked}}.dump(), "application/json"); } catch (const json::parse_error& e) { res.status = 400; res.set_content(json{{"error", "Invalid JSON"}}.dump(), "application/json"); } catch (const std::invalid_argument& e) { res.status = 400; res.set_content(json{{"error", e.what()}}.dump(), "application/json"); } catch (const std::exception& e) { spdlog::error("Update settings error: {}", e.what()); res.status = 500; res.set_content(json{{"error", "Internal server error"}}.dump(), "application/json"); } }); // POST /api/v1/settings/test-smtp -- test SMTP connection (owner) svr.Post("/api/v1/settings/test-smtp", [&app](const httplib::Request& req, httplib::Response& res) { try { auto authCtx = app.authMiddleware()->getAuthContext(); if (!authCtx) { res.status = 401; res.set_content(R"({"error":"Unauthorized"})", "application/json"); return; } if (!isOwnerOfAnyWorkspace(app, authCtx->userId)) { res.status = 403; res.set_content(json{{"error", "Forbidden: requires owner role in at least one workspace"}}.dump(), "application/json"); return; } if (!app.smtpClient()) { throw std::invalid_argument("SMTP client is not configured"); } bool success = app.smtpClient()->testConnection(); if (success) { res.status = 200; res.set_content(json{{"message", "SMTP connection successful"}}.dump(), "application/json"); } else { res.status = 500; res.set_content(json{{"error", "SMTP connection failed"}}.dump(), "application/json"); } } catch (const std::invalid_argument& e) { res.status = 400; res.set_content(json{{"error", e.what()}}.dump(), "application/json"); } catch (const std::exception& e) { spdlog::error("Test SMTP error: {}", e.what()); res.status = 500; res.set_content(json{{"error", "Internal server error"}}.dump(), "application/json"); } }); // POST /api/v1/settings/test-callerai -- test CallerAI connection (owner) svr.Post("/api/v1/settings/test-callerai", [&app](const httplib::Request& req, httplib::Response& res) { try { auto authCtx = app.authMiddleware()->getAuthContext(); if (!authCtx) { res.status = 401; res.set_content(R"({"error":"Unauthorized"})", "application/json"); return; } if (!isOwnerOfAnyWorkspace(app, authCtx->userId)) { res.status = 403; res.set_content(json{{"error", "Forbidden: requires owner role in at least one workspace"}}.dump(), "application/json"); return; } if (!app.calleraiClient()) { throw std::invalid_argument("CallerAI client is not configured"); } bool success = app.calleraiClient()->testConnection(); if (success) { res.status = 200; res.set_content(json{{"message", "CallerAI connection successful"}}.dump(), "application/json"); } else { res.status = 500; res.set_content(json{{"error", "CallerAI connection failed"}}.dump(), "application/json"); } } catch (const std::invalid_argument& e) { res.status = 400; res.set_content(json{{"error", e.what()}}.dump(), "application/json"); } catch (const std::exception& e) { spdlog::error("Test CallerAI error: {}", e.what()); res.status = 500; res.set_content(json{{"error", "Internal server error"}}.dump(), "application/json"); } }); } } // namespace smartbotic::microbit