|
@@ -1,441 +1,282 @@
|
|
|
#include "smartbotic/webserver/http_server.hpp"
|
|
#include "smartbotic/webserver/http_server.hpp"
|
|
|
|
|
|
|
|
|
|
+#include <nlohmann/json.hpp>
|
|
|
#include <spdlog/spdlog.h>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
|
|
|
|
|
|
+#include <algorithm>
|
|
|
#include <chrono>
|
|
#include <chrono>
|
|
|
|
|
+#include <cstring>
|
|
|
#include <filesystem>
|
|
#include <filesystem>
|
|
|
#include <fstream>
|
|
#include <fstream>
|
|
|
-#include <tuple>
|
|
|
|
|
-#include <utility>
|
|
|
|
|
|
|
+#include <random>
|
|
|
|
|
+#include <sstream>
|
|
|
|
|
|
|
|
namespace smartbotic::webserver {
|
|
namespace smartbotic::webserver {
|
|
|
|
|
|
|
|
// ============================================================================
|
|
// ============================================================================
|
|
|
-// WebSocketSession Implementation
|
|
|
|
|
|
|
+// WebSocketServer Implementation
|
|
|
// ============================================================================
|
|
// ============================================================================
|
|
|
|
|
|
|
|
-WebSocketSession::WebSocketSession(tcp::socket socket, const HttpServerConfig& config)
|
|
|
|
|
- : ws_(std::move(socket)), config_(config) {}
|
|
|
|
|
|
|
+// Static instance pointer for callback access
|
|
|
|
|
+WebSocketServer* WebSocketServer::instance_ = nullptr;
|
|
|
|
|
|
|
|
-WebSocketSession::~WebSocketSession() {
|
|
|
|
|
- spdlog::debug("WebSocket session destroyed");
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void WebSocketSession::Run(http::request<http::string_body> req) {
|
|
|
|
|
- DoAccept(std::move(req));
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void WebSocketSession::Send(const std::string& message) {
|
|
|
|
|
- auto msg = std::make_shared<std::string const>(message);
|
|
|
|
|
|
|
+namespace {
|
|
|
|
|
|
|
|
- std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
|
- queue_.push_back(msg);
|
|
|
|
|
|
|
+auto GenerateSessionId() -> std::string {
|
|
|
|
|
+ static std::random_device rd;
|
|
|
|
|
+ static std::mt19937 gen(rd());
|
|
|
|
|
+ static std::uniform_int_distribution<> dis(0, 15);
|
|
|
|
|
+ static const char* hex = "0123456789abcdef";
|
|
|
|
|
|
|
|
- if (queue_.size() > 1) {
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ std::string uuid;
|
|
|
|
|
+ uuid.reserve(36);
|
|
|
|
|
+ for (int i = 0; i < 36; ++i) {
|
|
|
|
|
+ if (i == 8 || i == 13 || i == 18 || i == 23) {
|
|
|
|
|
+ uuid += '-';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uuid += hex[dis(gen)];
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- ws_.async_write(asio::buffer(*queue_.front()),
|
|
|
|
|
- beast::bind_front_handler(&WebSocketSession::OnWrite, shared_from_this()));
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void WebSocketSession::Close() {
|
|
|
|
|
- beast::error_code ec;
|
|
|
|
|
- ws_.close(websocket::close_code::normal, ec);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-auto WebSocketSession::IsOpen() const -> bool {
|
|
|
|
|
- return ws_.is_open();
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void WebSocketSession::SetMessageHandler(WebSocketMessageHandler handler) {
|
|
|
|
|
- message_handler_ = std::move(handler);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void WebSocketSession::DoAccept(http::request<http::string_body> req) { // NOLINT(performance-unnecessary-value-param)
|
|
|
|
|
- ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::server));
|
|
|
|
|
-
|
|
|
|
|
- ws_.set_option(websocket::stream_base::decorator(
|
|
|
|
|
- [](websocket::response_type& res) { res.set(http::field::server, "SmartBotic-Server/0.1.0"); }));
|
|
|
|
|
-
|
|
|
|
|
- ws_.async_accept(req, beast::bind_front_handler(&WebSocketSession::OnAccept, shared_from_this()));
|
|
|
|
|
|
|
+ return uuid;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void WebSocketSession::OnAccept(beast::error_code ec) {
|
|
|
|
|
- if (ec) {
|
|
|
|
|
- spdlog::error("WebSocket accept error: {}", ec.message());
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+} // namespace
|
|
|
|
|
|
|
|
- spdlog::info("WebSocket connection established");
|
|
|
|
|
- DoRead();
|
|
|
|
|
|
|
+// LWS protocol definition - first protocol handles HTTP and is the default for WS
|
|
|
|
|
+static lws_protocols protocols[] = {
|
|
|
|
|
+ {
|
|
|
|
|
+ "http", // Default protocol name for HTTP/WS upgrade
|
|
|
|
|
+ WebSocketServer::WsCallback,
|
|
|
|
|
+ 0, // per_session_data_size - we manage our own
|
|
|
|
|
+ 65536, // rx buffer size
|
|
|
|
|
+ 0, // id
|
|
|
|
|
+ nullptr, // user
|
|
|
|
|
+ 65536 // tx_packet_size
|
|
|
|
|
+ },
|
|
|
|
|
+ LWS_PROTOCOL_LIST_TERM
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+WebSocketServer::WebSocketServer(uint16_t port, const std::string& path)
|
|
|
|
|
+ : port_(port), path_(path) {
|
|
|
|
|
+ instance_ = this;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void WebSocketSession::DoRead() {
|
|
|
|
|
- ws_.async_read(buffer_, beast::bind_front_handler(&WebSocketSession::OnRead, shared_from_this()));
|
|
|
|
|
|
|
+WebSocketServer::~WebSocketServer() {
|
|
|
|
|
+ Stop();
|
|
|
|
|
+ instance_ = nullptr;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void WebSocketSession::OnRead(beast::error_code ec, std::size_t bytes_transferred) {
|
|
|
|
|
- if (ec == websocket::error::closed) {
|
|
|
|
|
- spdlog::info("WebSocket connection closed by client");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (ec) {
|
|
|
|
|
- spdlog::error("WebSocket read error: {}", ec.message());
|
|
|
|
|
|
|
+void WebSocketServer::Start() {
|
|
|
|
|
+ if (running_.load()) {
|
|
|
|
|
+ spdlog::warn("WebSocket server already running");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- std::string message = beast::buffers_to_string(buffer_.data());
|
|
|
|
|
- buffer_.consume(buffer_.size());
|
|
|
|
|
|
|
+ spdlog::info("Starting WebSocket server on port {}", port_);
|
|
|
|
|
|
|
|
- spdlog::debug("WebSocket received: {} bytes", bytes_transferred);
|
|
|
|
|
|
|
+ lws_context_creation_info info{};
|
|
|
|
|
+ std::memset(&info, 0, sizeof(info));
|
|
|
|
|
|
|
|
- if (message_handler_) {
|
|
|
|
|
- message_handler_(*this, message);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ info.port = port_;
|
|
|
|
|
+ info.protocols = protocols;
|
|
|
|
|
+ info.gid = -1;
|
|
|
|
|
+ info.uid = -1;
|
|
|
|
|
+ info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
|
|
|
|
|
+ info.vhost_name = "smartbotic-ws";
|
|
|
|
|
|
|
|
- DoRead();
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ // Suppress verbose lws logging
|
|
|
|
|
+ lws_set_log_level(LLL_ERR | LLL_WARN, nullptr);
|
|
|
|
|
|
|
|
-void WebSocketSession::OnWrite(beast::error_code ec, std::size_t bytes_transferred) {
|
|
|
|
|
- if (ec) {
|
|
|
|
|
- spdlog::error("WebSocket write error: {}", ec.message());
|
|
|
|
|
|
|
+ context_ = lws_create_context(&info);
|
|
|
|
|
+ if (context_ == nullptr) {
|
|
|
|
|
+ spdlog::error("Failed to create libwebsocket context");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- spdlog::debug("WebSocket sent: {} bytes", bytes_transferred);
|
|
|
|
|
-
|
|
|
|
|
- std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
|
- queue_.erase(queue_.begin());
|
|
|
|
|
-
|
|
|
|
|
- if (!queue_.empty()) {
|
|
|
|
|
- ws_.async_write(asio::buffer(*queue_.front()),
|
|
|
|
|
- beast::bind_front_handler(&WebSocketSession::OnWrite, shared_from_this()));
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// ============================================================================
|
|
|
|
|
-// HttpSession Implementation
|
|
|
|
|
-// ============================================================================
|
|
|
|
|
-
|
|
|
|
|
-HttpSession::HttpSession(tcp::socket socket, const HttpServerConfig& config,
|
|
|
|
|
- std::function<void(std::shared_ptr<WebSocketSession>)> ws_connect_handler,
|
|
|
|
|
- std::function<void(std::shared_ptr<WebSocketSession>)> ws_disconnect_handler)
|
|
|
|
|
- : stream_(std::move(socket)),
|
|
|
|
|
- config_(config),
|
|
|
|
|
- ws_connect_handler_(std::move(ws_connect_handler)),
|
|
|
|
|
- ws_disconnect_handler_(std::move(ws_disconnect_handler)) {}
|
|
|
|
|
-
|
|
|
|
|
-HttpSession::~HttpSession() {
|
|
|
|
|
- spdlog::debug("HTTP session destroyed");
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void HttpSession::Run() {
|
|
|
|
|
- asio::dispatch(stream_.get_executor(), beast::bind_front_handler(&HttpSession::DoRead, shared_from_this()));
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ running_.store(true);
|
|
|
|
|
+ eventLoopThread_ = std::thread(&WebSocketServer::RunEventLoop, this);
|
|
|
|
|
|
|
|
-void HttpSession::DoRead() {
|
|
|
|
|
- req_ = {};
|
|
|
|
|
- stream_.expires_after(std::chrono::seconds(30));
|
|
|
|
|
- http::async_read(stream_, buffer_, req_, beast::bind_front_handler(&HttpSession::OnRead, shared_from_this()));
|
|
|
|
|
|
|
+ spdlog::info("WebSocket server started on port {}", port_);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void HttpSession::OnRead(beast::error_code ec, std::size_t bytes_transferred) {
|
|
|
|
|
- std::ignore = bytes_transferred;
|
|
|
|
|
-
|
|
|
|
|
- if (ec == http::error::end_of_stream) {
|
|
|
|
|
- std::ignore = stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (ec) {
|
|
|
|
|
- spdlog::error("HTTP read error: {}", ec.message());
|
|
|
|
|
|
|
+void WebSocketServer::Stop() {
|
|
|
|
|
+ if (!running_.load()) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Capture request info for logging
|
|
|
|
|
- request_start_ = std::chrono::steady_clock::now();
|
|
|
|
|
- request_method_ = std::string(req_.method_string());
|
|
|
|
|
- request_target_ = std::string(req_.target());
|
|
|
|
|
-
|
|
|
|
|
- HandleRequest(std::move(req_));
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void HttpSession::HandleRequest(http::request<http::string_body>&& req) {
|
|
|
|
|
- std::string target(req.target());
|
|
|
|
|
|
|
+ spdlog::info("Stopping WebSocket server...");
|
|
|
|
|
+ running_.store(false);
|
|
|
|
|
|
|
|
- // Handle CORS preflight
|
|
|
|
|
- if (req.method() == http::verb::options && config_.cors.enabled) {
|
|
|
|
|
- auto res = http::response<http::string_body>{http::status::no_content, req.version()};
|
|
|
|
|
- res.set(http::field::server, "SmartBotic-Server/0.1.0");
|
|
|
|
|
- ApplyCorsHeaders(res);
|
|
|
|
|
- res.prepare_payload();
|
|
|
|
|
- SendResponse(std::move(res));
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ // Cancel the event loop to wake up lws_service() immediately
|
|
|
|
|
+ // This is async-signal-safe and can be called from signal handlers
|
|
|
|
|
+ if (context_ != nullptr) {
|
|
|
|
|
+ lws_cancel_service(context_);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Check for WebSocket upgrade
|
|
|
|
|
- if (websocket::is_upgrade(req) && target == config_.ws_path) {
|
|
|
|
|
- spdlog::info("WebSocket upgrade request for {}", target);
|
|
|
|
|
-
|
|
|
|
|
- auto ws_session = std::make_shared<WebSocketSession>(stream_.release_socket(), config_);
|
|
|
|
|
-
|
|
|
|
|
- if (ws_connect_handler_) {
|
|
|
|
|
- ws_connect_handler_(ws_session);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- ws_session->Run(std::move(req));
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ if (eventLoopThread_.joinable()) {
|
|
|
|
|
+ eventLoopThread_.join();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- http::response<http::string_body> res;
|
|
|
|
|
-
|
|
|
|
|
- // Route API requests
|
|
|
|
|
- if (target.starts_with("/api/")) {
|
|
|
|
|
- res = HandleApiRequest(req);
|
|
|
|
|
- }
|
|
|
|
|
- // Serve static files for WebUI
|
|
|
|
|
- else {
|
|
|
|
|
- res = HandleStaticFile(target);
|
|
|
|
|
|
|
+ if (context_ != nullptr) {
|
|
|
|
|
+ lws_context_destroy(context_);
|
|
|
|
|
+ context_ = nullptr;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- res.version(req.version());
|
|
|
|
|
- res.set(http::field::server, "SmartBotic-Server/0.1.0");
|
|
|
|
|
-
|
|
|
|
|
- if (config_.cors.enabled) {
|
|
|
|
|
- ApplyCorsHeaders(res);
|
|
|
|
|
|
|
+ {
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(connectionsMutex_);
|
|
|
|
|
+ connections_.clear();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- res.prepare_payload();
|
|
|
|
|
- SendResponse(std::move(res));
|
|
|
|
|
|
|
+ spdlog::info("WebSocket server stopped");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-auto HttpSession::HandleStaticFile(const std::string& target) -> http::response<http::string_body> {
|
|
|
|
|
- std::string path = target;
|
|
|
|
|
-
|
|
|
|
|
- // Default to index.html for root
|
|
|
|
|
- if (path == "/" || path.empty()) {
|
|
|
|
|
- path = "/index.html";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Security: prevent directory traversal
|
|
|
|
|
- if (path.find("..") != std::string::npos) {
|
|
|
|
|
- return MakeErrorResponse(http::status::forbidden, "Invalid path");
|
|
|
|
|
|
|
+void WebSocketServer::RunEventLoop() {
|
|
|
|
|
+ while (running_.load()) {
|
|
|
|
|
+ lws_service(context_, 50); // 50ms timeout
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- std::filesystem::path file_path = config_.webui_path + path;
|
|
|
|
|
-
|
|
|
|
|
- // If path is a directory, try index.html
|
|
|
|
|
- if (std::filesystem::is_directory(file_path)) {
|
|
|
|
|
- file_path /= "index.html";
|
|
|
|
|
|
|
+void WebSocketServer::Broadcast(const std::string& message) {
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(connectionsMutex_);
|
|
|
|
|
+ for (auto& [wsi, conn] : connections_) {
|
|
|
|
|
+ QueueMessage(conn.get(), message);
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- if (!std::filesystem::exists(file_path)) {
|
|
|
|
|
- // For SPA routing, return index.html for non-file paths
|
|
|
|
|
- std::filesystem::path index_path = config_.webui_path + "/index.html";
|
|
|
|
|
- if (std::filesystem::exists(index_path)) {
|
|
|
|
|
- file_path = index_path;
|
|
|
|
|
- } else {
|
|
|
|
|
- return MakeErrorResponse(http::status::not_found, "File not found");
|
|
|
|
|
|
|
+void WebSocketServer::SendToSession(const std::string& sessionId, const std::string& message) {
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(connectionsMutex_);
|
|
|
|
|
+ for (auto& [wsi, conn] : connections_) {
|
|
|
|
|
+ if (conn->sessionId == sessionId) {
|
|
|
|
|
+ QueueMessage(conn.get(), message);
|
|
|
|
|
+ break;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // Read file content
|
|
|
|
|
- std::ifstream file(file_path, std::ios::binary);
|
|
|
|
|
- if (!file) {
|
|
|
|
|
- return MakeErrorResponse(http::status::internal_server_error, "Failed to read file");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
|
|
|
-
|
|
|
|
|
- http::response<http::string_body> res{http::status::ok, 11};
|
|
|
|
|
- res.set(http::field::content_type, GetMimeType(file_path.string()));
|
|
|
|
|
- res.body() = std::move(content);
|
|
|
|
|
-
|
|
|
|
|
- return res;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-auto HttpSession::HandleApiRequest(http::request<http::string_body>& req) -> http::response<http::string_body> {
|
|
|
|
|
- std::string target(req.target());
|
|
|
|
|
-
|
|
|
|
|
- // Health check endpoint
|
|
|
|
|
- if (target == "/api/health" && req.method() == http::verb::get) {
|
|
|
|
|
- http::response<http::string_body> res{http::status::ok, req.version()};
|
|
|
|
|
- res.set(http::field::content_type, "application/json");
|
|
|
|
|
- res.body() = R"({"status":"healthy","service":"smartbotic-server"})";
|
|
|
|
|
- return res;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Version endpoint
|
|
|
|
|
- if (target == "/api/version" && req.method() == http::verb::get) {
|
|
|
|
|
- http::response<http::string_body> res{http::status::ok, req.version()};
|
|
|
|
|
- res.set(http::field::content_type, "application/json");
|
|
|
|
|
- res.body() = R"({"version":"0.1.0","name":"SmartBotic Web Server"})";
|
|
|
|
|
- return res;
|
|
|
|
|
|
|
+void WebSocketServer::SendToSubscribers(const std::string& subscription_key, const std::string& message) {
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(connectionsMutex_);
|
|
|
|
|
+ for (auto& [wsi, conn] : connections_) {
|
|
|
|
|
+ // Check if this connection is subscribed and authenticated
|
|
|
|
|
+ if (!conn->authenticated) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ const auto& subs = conn->subscriptions;
|
|
|
|
|
+ if (std::find(subs.begin(), subs.end(), subscription_key) != subs.end()) {
|
|
|
|
|
+ QueueMessage(conn.get(), message);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // Default: not found
|
|
|
|
|
- return MakeErrorResponse(http::status::not_found, "API endpoint not found");
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-auto HttpSession::MakeErrorResponse(
|
|
|
|
|
- http::status status, const std::string& message) // NOLINT(readability-convert-member-functions-to-static)
|
|
|
|
|
- -> http::response<http::string_body> {
|
|
|
|
|
- http::response<http::string_body> res{status, 11};
|
|
|
|
|
- res.set(http::field::content_type, "application/json");
|
|
|
|
|
- res.body() = R"({"error":")" + message + R"("})";
|
|
|
|
|
- return res;
|
|
|
|
|
|
|
+auto WebSocketServer::ConnectionCount() const -> size_t {
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(connectionsMutex_);
|
|
|
|
|
+ return connections_.size();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void HttpSession::ApplyCorsHeaders(http::response<http::string_body>& res) {
|
|
|
|
|
- if (!config_.cors.enabled) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Combine allowed origins
|
|
|
|
|
- std::string origins;
|
|
|
|
|
- for (size_t i = 0; i < config_.cors.allowed_origins.size(); ++i) {
|
|
|
|
|
- if (i > 0) {
|
|
|
|
|
- origins += ", ";
|
|
|
|
|
- }
|
|
|
|
|
- origins += config_.cors.allowed_origins[i];
|
|
|
|
|
- }
|
|
|
|
|
- res.set(http::field::access_control_allow_origin, origins);
|
|
|
|
|
-
|
|
|
|
|
- // Combine allowed methods
|
|
|
|
|
- std::string methods;
|
|
|
|
|
- for (size_t i = 0; i < config_.cors.allowed_methods.size(); ++i) {
|
|
|
|
|
- if (i > 0) {
|
|
|
|
|
- methods += ", ";
|
|
|
|
|
- }
|
|
|
|
|
- methods += config_.cors.allowed_methods[i];
|
|
|
|
|
- }
|
|
|
|
|
- res.set(http::field::access_control_allow_methods, methods);
|
|
|
|
|
|
|
+void WebSocketServer::SetMessageHandler(WebSocketMessageHandler handler) {
|
|
|
|
|
+ messageHandler_ = std::move(handler);
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // Combine allowed headers
|
|
|
|
|
- std::string headers;
|
|
|
|
|
- for (size_t i = 0; i < config_.cors.allowed_headers.size(); ++i) {
|
|
|
|
|
- if (i > 0) {
|
|
|
|
|
- headers += ", ";
|
|
|
|
|
- }
|
|
|
|
|
- headers += config_.cors.allowed_headers[i];
|
|
|
|
|
- }
|
|
|
|
|
- res.set(http::field::access_control_allow_headers, headers);
|
|
|
|
|
|
|
+void WebSocketServer::HandleConnect(lws* wsi) {
|
|
|
|
|
+ auto conn = std::make_unique<WsConnection>();
|
|
|
|
|
+ conn->wsi = wsi;
|
|
|
|
|
+ conn->sessionId = GenerateSessionId();
|
|
|
|
|
|
|
|
- if (config_.cors.allow_credentials) {
|
|
|
|
|
- res.set(http::field::access_control_allow_credentials, "true");
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ spdlog::info("WebSocket client connected (session: {})", conn->sessionId);
|
|
|
|
|
|
|
|
- res.set(http::field::access_control_max_age, std::to_string(config_.cors.max_age));
|
|
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(connectionsMutex_);
|
|
|
|
|
+ connections_[wsi] = std::move(conn);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-auto HttpSession::GetMimeType(const std::string& path)
|
|
|
|
|
- -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
|
|
|
|
- auto ext_pos = path.rfind('.');
|
|
|
|
|
- if (ext_pos == std::string::npos) {
|
|
|
|
|
- return "application/octet-stream";
|
|
|
|
|
|
|
+void WebSocketServer::HandleDisconnect(lws* wsi) {
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(connectionsMutex_);
|
|
|
|
|
+ auto it = connections_.find(wsi);
|
|
|
|
|
+ if (it != connections_.end()) {
|
|
|
|
|
+ spdlog::info("WebSocket client disconnected (session: {})", it->second->sessionId);
|
|
|
|
|
+ connections_.erase(it);
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- std::string ext = path.substr(ext_pos);
|
|
|
|
|
-
|
|
|
|
|
- if (ext == ".html" || ext == ".htm") {
|
|
|
|
|
- return "text/html";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".css") {
|
|
|
|
|
- return "text/css";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".js") {
|
|
|
|
|
- return "application/javascript";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".json") {
|
|
|
|
|
- return "application/json";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".png") {
|
|
|
|
|
- return "image/png";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".jpg" || ext == ".jpeg") {
|
|
|
|
|
- return "image/jpeg";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".gif") {
|
|
|
|
|
- return "image/gif";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".svg") {
|
|
|
|
|
- return "image/svg+xml";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".ico") {
|
|
|
|
|
- return "image/x-icon";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".woff") {
|
|
|
|
|
- return "font/woff";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".woff2") {
|
|
|
|
|
- return "font/woff2";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".ttf") {
|
|
|
|
|
- return "font/ttf";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".eot") {
|
|
|
|
|
- return "application/vnd.ms-fontobject";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".txt") {
|
|
|
|
|
- return "text/plain";
|
|
|
|
|
- }
|
|
|
|
|
- if (ext == ".xml") {
|
|
|
|
|
- return "application/xml";
|
|
|
|
|
|
|
+void WebSocketServer::HandleMessage(lws* wsi, const std::string& message) {
|
|
|
|
|
+ WsConnection* conn = nullptr;
|
|
|
|
|
+ {
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(connectionsMutex_);
|
|
|
|
|
+ auto it = connections_.find(wsi);
|
|
|
|
|
+ if (it != connections_.end()) {
|
|
|
|
|
+ conn = it->second.get();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- if (ext == ".pdf") {
|
|
|
|
|
- return "application/pdf";
|
|
|
|
|
|
|
+ // Call handler outside the lock to avoid deadlock with SendToSession
|
|
|
|
|
+ if (conn && messageHandler_) {
|
|
|
|
|
+ messageHandler_(conn, message);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return "application/octet-stream";
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void HttpSession::SendResponse(http::response<http::string_body>&& res) {
|
|
|
|
|
- auto sp = std::make_shared<http::response<http::string_body>>(std::move(res));
|
|
|
|
|
- auto status = sp->result();
|
|
|
|
|
- auto self = shared_from_this();
|
|
|
|
|
|
|
+void WebSocketServer::QueueMessage(WsConnection* conn, const std::string& message) {
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(conn->sendMutex);
|
|
|
|
|
|
|
|
- http::async_write(stream_, *sp, [self, sp, status](beast::error_code ec, std::size_t bytes_transferred) {
|
|
|
|
|
- self->LogRequest(status);
|
|
|
|
|
- self->OnWrite(ec, bytes_transferred, sp->need_eof());
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ // Prepend LWS_PRE bytes for libwebsockets
|
|
|
|
|
+ conn->sendBuffer.resize(LWS_PRE + message.size());
|
|
|
|
|
+ std::memcpy(conn->sendBuffer.data() + LWS_PRE, message.data(), message.size());
|
|
|
|
|
|
|
|
-void HttpSession::LogRequest(http::status status) {
|
|
|
|
|
- auto now = std::chrono::steady_clock::now();
|
|
|
|
|
- auto duration_us = std::chrono::duration_cast<std::chrono::microseconds>(now - request_start_).count();
|
|
|
|
|
|
|
+ lws_callback_on_writable(conn->wsi);
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // Format duration as ms with microsecond precision
|
|
|
|
|
- double duration_ms = static_cast<double>(duration_us) / 1000.0;
|
|
|
|
|
|
|
+auto WebSocketServer::WsCallback(lws* wsi, lws_callback_reasons reason,
|
|
|
|
|
+ void* user, void* in, size_t len) -> int {
|
|
|
|
|
+ if (instance_ == nullptr) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- auto status_int = static_cast<unsigned>(status);
|
|
|
|
|
|
|
+ switch (reason) {
|
|
|
|
|
+ case LWS_CALLBACK_PROTOCOL_INIT:
|
|
|
|
|
+ spdlog::debug("WebSocket protocol initialized");
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ case LWS_CALLBACK_ESTABLISHED:
|
|
|
|
|
+ spdlog::debug("WebSocket connection established");
|
|
|
|
|
+ instance_->HandleConnect(wsi);
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ case LWS_CALLBACK_CLOSED:
|
|
|
|
|
+ spdlog::debug("WebSocket connection closed");
|
|
|
|
|
+ instance_->HandleDisconnect(wsi);
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ case LWS_CALLBACK_RECEIVE: {
|
|
|
|
|
+ std::string message(static_cast<char*>(in), len);
|
|
|
|
|
+ spdlog::debug("WebSocket received {} bytes", len);
|
|
|
|
|
+ instance_->HandleMessage(wsi, message);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Log at appropriate level based on status code
|
|
|
|
|
- if (status_int >= 500) {
|
|
|
|
|
- spdlog::error("{} {} {} {:.2f}ms", request_method_, request_target_, status_int, duration_ms);
|
|
|
|
|
- } else if (status_int >= 400) {
|
|
|
|
|
- spdlog::warn("{} {} {} {:.2f}ms", request_method_, request_target_, status_int, duration_ms);
|
|
|
|
|
- } else {
|
|
|
|
|
- spdlog::info("{} {} {} {:.2f}ms", request_method_, request_target_, status_int, duration_ms);
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ case LWS_CALLBACK_SERVER_WRITEABLE: {
|
|
|
|
|
+ std::lock_guard<std::mutex> lock(instance_->connectionsMutex_);
|
|
|
|
|
+ auto it = instance_->connections_.find(wsi);
|
|
|
|
|
+ if (it != instance_->connections_.end()) {
|
|
|
|
|
+ auto& conn = it->second;
|
|
|
|
|
+ std::lock_guard<std::mutex> sendLock(conn->sendMutex);
|
|
|
|
|
+ if (conn->sendBuffer.size() > LWS_PRE) {
|
|
|
|
|
+ size_t msgLen = conn->sendBuffer.size() - LWS_PRE;
|
|
|
|
|
+ lws_write(wsi, conn->sendBuffer.data() + LWS_PRE, msgLen, LWS_WRITE_TEXT);
|
|
|
|
|
+ conn->sendBuffer.clear();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-void HttpSession::OnWrite(beast::error_code ec, std::size_t bytes_transferred, bool close) {
|
|
|
|
|
- std::ignore = bytes_transferred;
|
|
|
|
|
|
|
+ case LWS_CALLBACK_HTTP:
|
|
|
|
|
+ // Return non-zero to close the connection for non-WebSocket HTTP requests
|
|
|
|
|
+ return -1;
|
|
|
|
|
|
|
|
- if (ec) {
|
|
|
|
|
- spdlog::error("HTTP write error: {}", ec.message());
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
|
|
|
|
|
+ // Allow the connection
|
|
|
|
|
+ return 0;
|
|
|
|
|
|
|
|
- if (close) {
|
|
|
|
|
- beast::error_code ec_close;
|
|
|
|
|
- std::ignore = stream_.socket().shutdown(tcp::socket::shutdown_send, ec_close);
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ default:
|
|
|
|
|
+ break;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- DoRead();
|
|
|
|
|
|
|
+ return 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ============================================================================
|
|
// ============================================================================
|
|
@@ -443,7 +284,10 @@ void HttpSession::OnWrite(beast::error_code ec, std::size_t bytes_transferred, b
|
|
|
// ============================================================================
|
|
// ============================================================================
|
|
|
|
|
|
|
|
HttpServer::HttpServer(HttpServerConfig config)
|
|
HttpServer::HttpServer(HttpServerConfig config)
|
|
|
- : config_(std::move(config)), ioc_(1), acceptor_(ioc_), db_client_(std::make_unique<DatabaseClient>(config_)) {}
|
|
|
|
|
|
|
+ : config_(std::move(config)),
|
|
|
|
|
+ httpServer_(std::make_unique<httplib::Server>()),
|
|
|
|
|
+ dbClient_(std::make_unique<DatabaseClient>(config_)),
|
|
|
|
|
+ authService_(std::make_unique<AuthService>(config_.jwt)) {}
|
|
|
|
|
|
|
|
HttpServer::~HttpServer() {
|
|
HttpServer::~HttpServer() {
|
|
|
if (running_.load()) {
|
|
if (running_.load()) {
|
|
@@ -474,53 +318,45 @@ auto HttpServer::Start(bool require_database) -> bool {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- try {
|
|
|
|
|
- auto const kAddress = asio::ip::make_address(config_.address);
|
|
|
|
|
- tcp::endpoint endpoint{kAddress, config_.port};
|
|
|
|
|
-
|
|
|
|
|
- beast::error_code ec;
|
|
|
|
|
-
|
|
|
|
|
- acceptor_.open(endpoint.protocol(), ec);
|
|
|
|
|
- if (ec) {
|
|
|
|
|
- spdlog::error("Failed to open acceptor: {}", ec.message());
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- acceptor_.set_option(asio::socket_base::reuse_address(true), ec);
|
|
|
|
|
- if (ec) {
|
|
|
|
|
- spdlog::error("Failed to set reuse_address: {}", ec.message());
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- acceptor_.bind(endpoint, ec);
|
|
|
|
|
- if (ec) {
|
|
|
|
|
- spdlog::error("Failed to bind to {}: {}", config_.GetListenAddress(), ec.message());
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- acceptor_.listen(asio::socket_base::max_listen_connections, ec);
|
|
|
|
|
- if (ec) {
|
|
|
|
|
- spdlog::error("Failed to listen: {}", ec.message());
|
|
|
|
|
|
|
+ // Initialize services (only if database is connected)
|
|
|
|
|
+ if (IsDatabaseConnected()) {
|
|
|
|
|
+ if (!InitializeServices()) {
|
|
|
|
|
+ spdlog::error("Failed to initialize services - server startup aborted");
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- running_.store(true);
|
|
|
|
|
- DoAccept();
|
|
|
|
|
|
|
+ // Setup routes
|
|
|
|
|
+ SetupRoutes();
|
|
|
|
|
+
|
|
|
|
|
+ // Start WebSocket server on separate port
|
|
|
|
|
+ wsServer_ = std::make_unique<WebSocketServer>(config_.ws_port, config_.ws_path);
|
|
|
|
|
+
|
|
|
|
|
+ // Setup WebSocket message handler using WsHandler if available
|
|
|
|
|
+ if (wsHandler_) {
|
|
|
|
|
+ wsServer_->SetMessageHandler([this](WsConnection* conn, const std::string& message) {
|
|
|
|
|
+ wsHandler_->HandleMessage(conn, message,
|
|
|
|
|
+ [this](WsConnection* c, const std::string& response) {
|
|
|
|
|
+ wsServer_->SendToSession(c->sessionId, response);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ spdlog::info("WebSocket message handler configured with WsHandler");
|
|
|
|
|
+ } else if (wsMessageHandler_) {
|
|
|
|
|
+ wsServer_->SetMessageHandler(wsMessageHandler_);
|
|
|
|
|
+ }
|
|
|
|
|
+ wsServer_->Start();
|
|
|
|
|
|
|
|
- // Start the I/O context in a thread
|
|
|
|
|
- threads_.emplace_back([this]() { ioc_.run(); });
|
|
|
|
|
|
|
+ // Start HTTP server in a separate thread
|
|
|
|
|
+ running_.store(true);
|
|
|
|
|
+ httpThread_ = std::thread(&HttpServer::RunHttpServer, this);
|
|
|
|
|
|
|
|
- spdlog::info("HTTP server started successfully on {}", config_.GetListenAddress());
|
|
|
|
|
- spdlog::info("WebSocket endpoint: ws://{}{}", config_.GetListenAddress(), config_.ws_path);
|
|
|
|
|
- spdlog::info("Static files: {}", config_.webui_path);
|
|
|
|
|
- spdlog::info("Database: {} ({})", config_.database_address,
|
|
|
|
|
- IsDatabaseConnected() ? "connected" : "not connected");
|
|
|
|
|
|
|
+ spdlog::info("HTTP server started successfully on {}", config_.GetListenAddress());
|
|
|
|
|
+ spdlog::info("WebSocket server: ws://{}:{}{}", config_.address, config_.ws_port, config_.ws_path);
|
|
|
|
|
+ spdlog::info("Static files: {}", config_.webui_path);
|
|
|
|
|
+ spdlog::info("Database: {} ({})", config_.database_address,
|
|
|
|
|
+ IsDatabaseConnected() ? "connected" : "not connected");
|
|
|
|
|
|
|
|
- return true;
|
|
|
|
|
- } catch (const std::exception& e) {
|
|
|
|
|
- spdlog::error("Failed to start HTTP server: {}", e.what());
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void HttpServer::Stop() {
|
|
void HttpServer::Stop() {
|
|
@@ -529,129 +365,103 @@ void HttpServer::Stop() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
spdlog::info("Stopping HTTP server...");
|
|
spdlog::info("Stopping HTTP server...");
|
|
|
-
|
|
|
|
|
running_.store(false);
|
|
running_.store(false);
|
|
|
|
|
|
|
|
- // Disconnect from database
|
|
|
|
|
- if (db_client_) {
|
|
|
|
|
- db_client_->Disconnect();
|
|
|
|
|
|
|
+ // Stop WebSocket server first (this uses lws_cancel_service which is async-signal-safe)
|
|
|
|
|
+ if (wsServer_) {
|
|
|
|
|
+ wsServer_->Stop();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Close all WebSocket sessions
|
|
|
|
|
- {
|
|
|
|
|
- std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
|
|
|
|
|
- for (const auto& session : ws_sessions_) {
|
|
|
|
|
- if (session->IsOpen()) {
|
|
|
|
|
- session->Close();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- ws_sessions_.clear();
|
|
|
|
|
|
|
+ // Stop HTTP server - this makes listen() return
|
|
|
|
|
+ if (httpServer_) {
|
|
|
|
|
+ httpServer_->stop();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Stop accepting new connections
|
|
|
|
|
- beast::error_code ec;
|
|
|
|
|
- std::ignore = acceptor_.close(ec);
|
|
|
|
|
-
|
|
|
|
|
- // Stop the I/O context
|
|
|
|
|
- ioc_.stop();
|
|
|
|
|
-
|
|
|
|
|
- // Wait for threads to finish
|
|
|
|
|
- for (auto& thread : threads_) {
|
|
|
|
|
- if (thread.joinable()) {
|
|
|
|
|
- thread.join();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Disconnect from database
|
|
|
|
|
+ if (dbClient_) {
|
|
|
|
|
+ dbClient_->Disconnect();
|
|
|
}
|
|
}
|
|
|
- threads_.clear();
|
|
|
|
|
|
|
|
|
|
- spdlog::info("HTTP server stopped");
|
|
|
|
|
|
|
+ // NOTE: Don't join httpThread_ here - let Wait() handle it
|
|
|
|
|
+ // This avoids race condition when Stop() is called from signal handler
|
|
|
|
|
+ // while main thread is blocked in Wait()
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("HTTP server stop requested");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void HttpServer::Wait() {
|
|
void HttpServer::Wait() {
|
|
|
- for (auto& thread : threads_) {
|
|
|
|
|
- if (thread.joinable()) {
|
|
|
|
|
- thread.join();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (httpThread_.joinable()) {
|
|
|
|
|
+ httpThread_.join();
|
|
|
}
|
|
}
|
|
|
|
|
+ spdlog::info("HTTP server stopped");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void HttpServer::BroadcastWebSocket(const std::string& message) {
|
|
void HttpServer::BroadcastWebSocket(const std::string& message) {
|
|
|
- std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
|
|
|
|
|
- for (const auto& session : ws_sessions_) {
|
|
|
|
|
- if (session->IsOpen()) {
|
|
|
|
|
- session->Send(message);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (wsServer_) {
|
|
|
|
|
+ wsServer_->Broadcast(message);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void HttpServer::SetWebSocketMessageHandler(WebSocketMessageHandler handler) {
|
|
|
|
|
- ws_message_handler_ = std::move(handler);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-auto HttpServer::GetWebSocketClientCount() const -> size_t {
|
|
|
|
|
- std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
|
|
|
|
|
- return ws_sessions_.size();
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void HttpServer::DoAccept() {
|
|
|
|
|
- acceptor_.async_accept(ioc_, beast::bind_front_handler(&HttpServer::OnAccept, this));
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void HttpServer::OnAccept(beast::error_code ec, tcp::socket socket) {
|
|
|
|
|
- if (ec) {
|
|
|
|
|
- if (running_.load()) {
|
|
|
|
|
- spdlog::error("Accept error: {}", ec.message());
|
|
|
|
|
- }
|
|
|
|
|
|
|
+void HttpServer::BroadcastDocumentEvent(const std::string& workspace_id,
|
|
|
|
|
+ const std::string& collection,
|
|
|
|
|
+ DocumentAction action,
|
|
|
|
|
+ const std::string& document_id,
|
|
|
|
|
+ const nlohmann::json& data) {
|
|
|
|
|
+ if (!wsServer_ || !wsHandler_) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- auto endpoint = socket.remote_endpoint(ec);
|
|
|
|
|
- if (!ec) {
|
|
|
|
|
- spdlog::debug("New connection from {}:{}", endpoint.address().to_string(), endpoint.port());
|
|
|
|
|
|
|
+ std::string subscription_key = WsHandler::BuildSubscriptionKey(workspace_id, collection);
|
|
|
|
|
+
|
|
|
|
|
+ // Build the event JSON
|
|
|
|
|
+ nlohmann::json event = {
|
|
|
|
|
+ {"type", "document"},
|
|
|
|
|
+ {"action", DocumentActionToString(action)},
|
|
|
|
|
+ {"workspace_id", workspace_id},
|
|
|
|
|
+ {"collection", collection},
|
|
|
|
|
+ {"document_id", document_id}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Include data for create/update, exclude for delete
|
|
|
|
|
+ if (action != DocumentAction::Delete && !data.is_null()) {
|
|
|
|
|
+ event["data"] = data;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- auto session = std::make_shared<HttpSession>(
|
|
|
|
|
- std::move(socket), config_, [this](std::shared_ptr<WebSocketSession> ws) { OnWebSocketConnect(std::move(ws)); },
|
|
|
|
|
- [this](std::shared_ptr<WebSocketSession> ws) { OnWebSocketDisconnect(std::move(ws)); });
|
|
|
|
|
|
|
+ std::string event_str = event.dump();
|
|
|
|
|
|
|
|
- session->Run();
|
|
|
|
|
|
|
+ spdlog::debug("Broadcasting document event: {} {} in {} (key={})",
|
|
|
|
|
+ DocumentActionToString(action), document_id, collection, subscription_key);
|
|
|
|
|
|
|
|
- if (running_.load()) {
|
|
|
|
|
- DoAccept();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ wsServer_->SendToSubscribers(subscription_key, event_str);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void HttpServer::OnWebSocketConnect(const std::shared_ptr<WebSocketSession>& session) {
|
|
|
|
|
- if (ws_message_handler_) {
|
|
|
|
|
- session->SetMessageHandler(ws_message_handler_);
|
|
|
|
|
|
|
+void HttpServer::SetWebSocketMessageHandler(WebSocketMessageHandler handler) {
|
|
|
|
|
+ wsMessageHandler_ = std::move(handler);
|
|
|
|
|
+ if (wsServer_) {
|
|
|
|
|
+ wsServer_->SetMessageHandler(wsMessageHandler_);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
|
|
|
|
|
- ws_sessions_.insert(session);
|
|
|
|
|
- spdlog::info("WebSocket client connected (total: {})", ws_sessions_.size());
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void HttpServer::OnWebSocketDisconnect(const std::shared_ptr<WebSocketSession>& session) {
|
|
|
|
|
- std::lock_guard<std::mutex> lock(ws_sessions_mutex_);
|
|
|
|
|
- ws_sessions_.erase(session);
|
|
|
|
|
- spdlog::info("WebSocket client disconnected (total: {})", ws_sessions_.size());
|
|
|
|
|
|
|
+auto HttpServer::GetWebSocketClientCount() const -> size_t {
|
|
|
|
|
+ return wsServer_ ? wsServer_->ConnectionCount() : 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
auto HttpServer::IsDatabaseConnected() const -> bool {
|
|
auto HttpServer::IsDatabaseConnected() const -> bool {
|
|
|
- return db_client_ && db_client_->IsConnected();
|
|
|
|
|
|
|
+ return dbClient_ && dbClient_->IsConnected();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
auto HttpServer::ConnectToDatabase() -> bool {
|
|
auto HttpServer::ConnectToDatabase() -> bool {
|
|
|
- if (!db_client_) {
|
|
|
|
|
- db_client_ = std::make_unique<DatabaseClient>(config_);
|
|
|
|
|
|
|
+ if (!dbClient_) {
|
|
|
|
|
+ dbClient_ = std::make_unique<DatabaseClient>(config_);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Try to connect
|
|
// Try to connect
|
|
|
- if (!db_client_->Connect()) {
|
|
|
|
|
|
|
+ if (!dbClient_->Connect()) {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Perform a health check to verify the connection is working
|
|
// Perform a health check to verify the connection is working
|
|
|
- auto health = db_client_->HealthCheck();
|
|
|
|
|
|
|
+ auto health = dbClient_->HealthCheck();
|
|
|
if (!health.healthy) {
|
|
if (!health.healthy) {
|
|
|
spdlog::error("Database health check failed: {}", health.message);
|
|
spdlog::error("Database health check failed: {}", health.message);
|
|
|
return false;
|
|
return false;
|
|
@@ -661,4 +471,4069 @@ auto HttpServer::ConnectToDatabase() -> bool {
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+auto HttpServer::InitializeServices() -> bool {
|
|
|
|
|
+ // Initialize UserService
|
|
|
|
|
+ userService_ = std::make_unique<UserService>(*dbClient_);
|
|
|
|
|
+ if (!userService_->Initialize()) {
|
|
|
|
|
+ spdlog::error("Failed to initialize UserService");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ spdlog::info("UserService initialized successfully");
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize WorkspaceService
|
|
|
|
|
+ workspaceService_ = std::make_unique<WorkspaceService>(*dbClient_);
|
|
|
|
|
+ if (!workspaceService_->Initialize()) {
|
|
|
|
|
+ spdlog::error("Failed to initialize WorkspaceService");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ spdlog::info("WorkspaceService initialized successfully");
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize GroupService
|
|
|
|
|
+ groupService_ = std::make_unique<GroupService>(*dbClient_);
|
|
|
|
|
+ if (!groupService_->Initialize()) {
|
|
|
|
|
+ spdlog::error("Failed to initialize GroupService");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ spdlog::info("GroupService initialized successfully");
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize MembershipService
|
|
|
|
|
+ membershipService_ = std::make_unique<MembershipService>(*dbClient_);
|
|
|
|
|
+ if (!membershipService_->Initialize()) {
|
|
|
|
|
+ spdlog::error("Failed to initialize MembershipService");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ spdlog::info("MembershipService initialized successfully");
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize ApiKeyService
|
|
|
|
|
+ apiKeyService_ = std::make_unique<ApiKeyService>(*dbClient_);
|
|
|
|
|
+ if (!apiKeyService_->Initialize()) {
|
|
|
|
|
+ spdlog::error("Failed to initialize ApiKeyService");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ spdlog::info("ApiKeyService initialized successfully");
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize CollectionService
|
|
|
|
|
+ collectionService_ = std::make_unique<CollectionService>(*dbClient_);
|
|
|
|
|
+ spdlog::info("CollectionService initialized successfully");
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize DocumentService
|
|
|
|
|
+ documentService_ = std::make_unique<DocumentService>(*dbClient_);
|
|
|
|
|
+ spdlog::info("DocumentService initialized successfully");
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize ViewService
|
|
|
|
|
+ viewService_ = std::make_unique<ViewService>(*dbClient_);
|
|
|
|
|
+ if (!viewService_->Initialize()) {
|
|
|
|
|
+ spdlog::error("Failed to initialize ViewService");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ spdlog::info("ViewService initialized successfully");
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize WsHandler for WebSocket real-time updates
|
|
|
|
|
+ wsHandler_ = std::make_unique<WsHandler>(*authService_);
|
|
|
|
|
+ spdlog::info("WsHandler initialized successfully");
|
|
|
|
|
+
|
|
|
|
|
+ // Note: WebSocket message handler is set up in Start() after wsServer_ is created
|
|
|
|
|
+
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::SetupRoutes() {
|
|
|
|
|
+ // Pre-routing handler for CORS
|
|
|
|
|
+ httpServer_->set_pre_routing_handler([this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Apply CORS headers if enabled
|
|
|
|
|
+ if (config_.cors.enabled) {
|
|
|
|
|
+ // Combine allowed origins
|
|
|
|
|
+ std::string origins;
|
|
|
|
|
+ for (size_t i = 0; i < config_.cors.allowed_origins.size(); ++i) {
|
|
|
|
|
+ if (i > 0) origins += ", ";
|
|
|
|
|
+ origins += config_.cors.allowed_origins[i];
|
|
|
|
|
+ }
|
|
|
|
|
+ res.set_header("Access-Control-Allow-Origin", origins);
|
|
|
|
|
+
|
|
|
|
|
+ // Combine allowed methods
|
|
|
|
|
+ std::string methods;
|
|
|
|
|
+ for (size_t i = 0; i < config_.cors.allowed_methods.size(); ++i) {
|
|
|
|
|
+ if (i > 0) methods += ", ";
|
|
|
|
|
+ methods += config_.cors.allowed_methods[i];
|
|
|
|
|
+ }
|
|
|
|
|
+ res.set_header("Access-Control-Allow-Methods", methods);
|
|
|
|
|
+
|
|
|
|
|
+ // Combine allowed headers
|
|
|
|
|
+ std::string headers;
|
|
|
|
|
+ for (size_t i = 0; i < config_.cors.allowed_headers.size(); ++i) {
|
|
|
|
|
+ if (i > 0) headers += ", ";
|
|
|
|
|
+ headers += config_.cors.allowed_headers[i];
|
|
|
|
|
+ }
|
|
|
|
|
+ res.set_header("Access-Control-Allow-Headers", headers);
|
|
|
|
|
+
|
|
|
|
|
+ if (config_.cors.allow_credentials) {
|
|
|
|
|
+ res.set_header("Access-Control-Allow-Credentials", "true");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.set_header("Access-Control-Max-Age", std::to_string(config_.cors.max_age));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.set_header("Server", "SmartBotic-Server/0.1.0");
|
|
|
|
|
+
|
|
|
|
|
+ // Handle CORS preflight
|
|
|
|
|
+ if (req.method == "OPTIONS") {
|
|
|
|
|
+ res.status = 204;
|
|
|
|
|
+ return httplib::Server::HandlerResponse::Handled;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return httplib::Server::HandlerResponse::Unhandled;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Health check endpoint
|
|
|
|
|
+ httpServer_->Get("/api/health", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleHealth(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Version endpoint
|
|
|
|
|
+ httpServer_->Get("/api/version", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleVersion(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Bootstrap endpoint - creates first admin user if no users exist
|
|
|
|
|
+ httpServer_->Post("/api/bootstrap", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleBootstrap(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Setup authentication routes
|
|
|
|
|
+ SetupAuthRoutes();
|
|
|
|
|
+
|
|
|
|
|
+ // Setup user management routes
|
|
|
|
|
+ SetupUserRoutes();
|
|
|
|
|
+
|
|
|
|
|
+ // Setup API key routes (under /api/users/:id/api-keys)
|
|
|
|
|
+ SetupApiKeyRoutes();
|
|
|
|
|
+
|
|
|
|
|
+ // Setup membership routes (before workspace routes since they're more specific)
|
|
|
|
|
+ SetupMembershipRoutes();
|
|
|
|
|
+
|
|
|
|
|
+ // Setup document routes (most specific - before collections)
|
|
|
|
|
+ SetupDocumentRoutes();
|
|
|
|
|
+
|
|
|
|
|
+ // Setup view routes (schema definitions)
|
|
|
|
|
+ SetupViewRoutes();
|
|
|
|
|
+
|
|
|
|
|
+ // Setup collection routes (before workspace routes since they're more specific)
|
|
|
|
|
+ SetupCollectionRoutes();
|
|
|
|
|
+
|
|
|
|
|
+ // Setup group management routes (before workspace routes since they're more specific)
|
|
|
|
|
+ SetupGroupRoutes();
|
|
|
|
|
+
|
|
|
|
|
+ // Setup workspace management routes
|
|
|
|
|
+ SetupWorkspaceRoutes();
|
|
|
|
|
+
|
|
|
|
|
+ // Mount static file directory for WebUI
|
|
|
|
|
+ if (std::filesystem::exists(config_.webui_path)) {
|
|
|
|
|
+ httpServer_->set_mount_point("/", config_.webui_path);
|
|
|
|
|
+ spdlog::info("Mounted static files from: {}", config_.webui_path);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ spdlog::warn("WebUI path does not exist: {}", config_.webui_path);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Custom error handler for 404 (only if response body is empty)
|
|
|
|
|
+ httpServer_->set_error_handler([](const httplib::Request& /*req*/, httplib::Response& res) {
|
|
|
|
|
+ // Only set default error message if body hasn't been set by a handler
|
|
|
|
|
+ if (res.body.empty()) {
|
|
|
|
|
+ res.set_content(R"({"error":"Not found"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Logger for requests
|
|
|
|
|
+ httpServer_->set_logger([](const httplib::Request& req, const httplib::Response& res) {
|
|
|
|
|
+ auto status = res.status;
|
|
|
|
|
+ if (status >= 500) {
|
|
|
|
|
+ spdlog::error("{} {} {}", req.method, req.path, status);
|
|
|
|
|
+ } else if (status >= 400) {
|
|
|
|
|
+ spdlog::warn("{} {} {}", req.method, req.path, status);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ spdlog::info("{} {} {}", req.method, req.path, status);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::RunHttpServer() {
|
|
|
|
|
+ if (!httpServer_->listen(config_.address, config_.port)) {
|
|
|
|
|
+ spdlog::error("Failed to start HTTP server on {}", config_.GetListenAddress());
|
|
|
|
|
+ running_.store(false);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleHealth(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ res.set_content(R"({"status":"healthy","service":"smartbotic-server"})", "application/json");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleVersion(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ res.set_content(R"({"version":"0.1.0","name":"SmartBotic Web Server"})", "application/json");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleBootstrap(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Bootstrap endpoint - creates first admin user if no users exist
|
|
|
|
|
+ if (!userService_) {
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"User service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if any users exist
|
|
|
|
|
+ auto users = userService_->ListUsers();
|
|
|
|
|
+ if (!users.users.empty()) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"System already bootstrapped - users exist"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ nlohmann::json body;
|
|
|
|
|
+ try {
|
|
|
|
|
+ body = nlohmann::json::parse(req.body);
|
|
|
|
|
+ } catch (...) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Validate required fields
|
|
|
|
|
+ if (!body.contains("email") || !body.contains("password")) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Email and password are required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CreateUserRequest user_request;
|
|
|
|
|
+ user_request.email = body["email"].get<std::string>();
|
|
|
|
|
+ user_request.password = body["password"].get<std::string>();
|
|
|
|
|
+ user_request.name = body.value("name", "Admin");
|
|
|
|
|
+
|
|
|
|
|
+ // Create the admin user
|
|
|
|
|
+ auto result = userService_->CreateUser(user_request);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response;
|
|
|
|
|
+ error_response["error"] = result.error;
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Create a default workspace
|
|
|
|
|
+ if (workspaceService_) {
|
|
|
|
|
+ CreateWorkspaceRequest ws_request;
|
|
|
|
|
+ ws_request.name = "Default Workspace";
|
|
|
|
|
+ ws_request.settings["default"] = true;
|
|
|
|
|
+ auto ws_result = workspaceService_->CreateWorkspace(ws_request);
|
|
|
|
|
+
|
|
|
|
|
+ // Add user to workspace
|
|
|
|
|
+ if (ws_result.success && ws_result.workspace && membershipService_) {
|
|
|
|
|
+ AddMemberRequest member_request;
|
|
|
|
|
+ member_request.workspace_id = ws_result.workspace->id;
|
|
|
|
|
+ member_request.user_id = result.user->id;
|
|
|
|
|
+ member_request.group_ids = {};
|
|
|
|
|
+ [[maybe_unused]] auto member_result = membershipService_->AddMember(member_request);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return success with user info
|
|
|
|
|
+ nlohmann::json response;
|
|
|
|
|
+ response["success"] = true;
|
|
|
|
|
+ response["message"] = "System bootstrapped successfully";
|
|
|
|
|
+ response["user"]["id"] = result.user->id;
|
|
|
|
|
+ response["user"]["email"] = result.user->email;
|
|
|
|
|
+ response["user"]["name"] = result.user->name;
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("System bootstrapped with admin user: {}", user_request.email);
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto HttpServer::GetMimeType(const std::string& path) -> std::string {
|
|
|
|
|
+ auto ext_pos = path.rfind('.');
|
|
|
|
|
+ if (ext_pos == std::string::npos) {
|
|
|
|
|
+ return "application/octet-stream";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string ext = path.substr(ext_pos);
|
|
|
|
|
+
|
|
|
|
|
+ static const std::unordered_map<std::string, std::string> mime_types = {
|
|
|
|
|
+ {".html", "text/html"},
|
|
|
|
|
+ {".htm", "text/html"},
|
|
|
|
|
+ {".css", "text/css"},
|
|
|
|
|
+ {".js", "application/javascript"},
|
|
|
|
|
+ {".json", "application/json"},
|
|
|
|
|
+ {".png", "image/png"},
|
|
|
|
|
+ {".jpg", "image/jpeg"},
|
|
|
|
|
+ {".jpeg", "image/jpeg"},
|
|
|
|
|
+ {".gif", "image/gif"},
|
|
|
|
|
+ {".svg", "image/svg+xml"},
|
|
|
|
|
+ {".ico", "image/x-icon"},
|
|
|
|
|
+ {".woff", "font/woff"},
|
|
|
|
|
+ {".woff2", "font/woff2"},
|
|
|
|
|
+ {".ttf", "font/ttf"},
|
|
|
|
|
+ {".eot", "application/vnd.ms-fontobject"},
|
|
|
|
|
+ {".txt", "text/plain"},
|
|
|
|
|
+ {".xml", "application/xml"},
|
|
|
|
|
+ {".pdf", "application/pdf"},
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ auto it = mime_types.find(ext);
|
|
|
|
|
+ if (it != mime_types.end()) {
|
|
|
|
|
+ return it->second;
|
|
|
|
|
+ }
|
|
|
|
|
+ return "application/octet-stream";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// Authentication Routes Implementation
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::SetupAuthRoutes() {
|
|
|
|
|
+ // POST /api/auth/login - Authenticate user and return JWT
|
|
|
|
|
+ httpServer_->Post("/api/auth/login", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleAuthLogin(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // POST /api/auth/refresh - Refresh access token
|
|
|
|
|
+ httpServer_->Post("/api/auth/refresh", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleAuthRefresh(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // POST /api/auth/logout - Invalidate refresh token
|
|
|
|
|
+ httpServer_->Post("/api/auth/logout", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleAuthLogout(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/auth/me - Get current authenticated user info
|
|
|
|
|
+ httpServer_->Get("/api/auth/me", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleAuthMe(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Authentication routes registered: /api/auth/login, /api/auth/refresh, /api/auth/logout, /api/auth/me");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleAuthLogin(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ auto body = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ if (!body.contains("email") || !body.contains("password")) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Missing email or password"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string email = body["email"].get<std::string>();
|
|
|
|
|
+ std::string password = body["password"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ if (config_.jwt.secret.empty()) {
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"JWT secret not configured"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!userService_) {
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"User service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Verify user credentials
|
|
|
|
|
+ auto user_result = userService_->VerifyCredentials(email, password);
|
|
|
|
|
+ if (!user_result.success || !user_result.user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid email or password"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const auto& user = *user_result.user;
|
|
|
|
|
+
|
|
|
|
|
+ // Get user's workspace memberships
|
|
|
|
|
+ std::vector<std::string> workspace_ids;
|
|
|
|
|
+ if (membershipService_) {
|
|
|
|
|
+ auto memberships = membershipService_->ListUserMemberships(user.id);
|
|
|
|
|
+ for (const auto& m : memberships.members) {
|
|
|
|
|
+ workspace_ids.push_back(m.workspace_id);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Determine user groups (superadmin is a special case - first user is superadmin)
|
|
|
|
|
+ std::vector<std::string> groups;
|
|
|
|
|
+ auto all_users = userService_->ListUsers();
|
|
|
|
|
+ if (all_users.users.size() == 1 ||
|
|
|
|
|
+ (!all_users.users.empty() && all_users.users[0].id == user.id)) {
|
|
|
|
|
+ // First user is superadmin
|
|
|
|
|
+ groups.push_back("superadmin");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto tokens = authService_->GenerateTokens(user.id, email, workspace_ids, groups);
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = {
|
|
|
|
|
+ {"access_token", tokens.access_token},
|
|
|
|
|
+ {"refresh_token", tokens.refresh_token},
|
|
|
|
|
+ {"token_type", "Bearer"},
|
|
|
|
|
+ {"expires_in", tokens.access_expires_in}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("User logged in: {}", email);
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Login error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleAuthRefresh(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ auto body = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ if (!body.contains("refresh_token")) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Missing refresh_token"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string refresh_token = body["refresh_token"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ auto new_tokens = authService_->RefreshAccessToken(refresh_token);
|
|
|
|
|
+ if (!new_tokens) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid or expired refresh token"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = {
|
|
|
|
|
+ {"access_token", new_tokens->access_token},
|
|
|
|
|
+ {"refresh_token", new_tokens->refresh_token},
|
|
|
|
|
+ {"token_type", "Bearer"},
|
|
|
|
|
+ {"expires_in", new_tokens->access_expires_in}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::debug("Token refreshed successfully");
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Token refresh error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleAuthLogout(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ auto body = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ if (!body.contains("refresh_token")) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Missing refresh_token"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string refresh_token = body["refresh_token"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ if (authService_->InvalidateRefreshToken(refresh_token)) {
|
|
|
|
|
+ spdlog::debug("User logged out successfully");
|
|
|
|
|
+ res.set_content(R"({"message":"Logged out successfully"})", "application/json");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid refresh token"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Logout error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleAuthMe(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build response with user info from token
|
|
|
|
|
+ nlohmann::json response = {
|
|
|
|
|
+ {"id", auth_user->user_id},
|
|
|
|
|
+ {"email", auth_user->email},
|
|
|
|
|
+ {"groups", auth_user->groups},
|
|
|
|
|
+ {"workspace_ids", auth_user->workspace_ids}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Try to get additional user info from database
|
|
|
|
|
+ if (userService_) {
|
|
|
|
|
+ auto user_result = userService_->GetUser(auth_user->user_id);
|
|
|
|
|
+ if (user_result.success && user_result.user) {
|
|
|
|
|
+ response["name"] = user_result.user->name;
|
|
|
|
|
+ response["created_at"] = user_result.user->created_at;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user is superadmin based on groups
|
|
|
|
|
+ response["is_superadmin"] = IsSuperadmin(*auth_user);
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto HttpServer::AuthenticateRequest(const httplib::Request& req) -> std::optional<AuthUser> {
|
|
|
|
|
+ std::string auth_token;
|
|
|
|
|
+
|
|
|
|
|
+ // First check X-API-Key header (direct API key)
|
|
|
|
|
+ auto api_key_header = req.get_header_value("X-API-Key");
|
|
|
|
|
+ if (!api_key_header.empty()) {
|
|
|
|
|
+ auth_token = api_key_header;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Check Authorization header
|
|
|
|
|
+ auto auth_header = req.get_header_value("Authorization");
|
|
|
|
|
+ if (auth_header.empty()) {
|
|
|
|
|
+ return std::nullopt;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto token = AuthService::ExtractBearerToken(auth_header);
|
|
|
|
|
+ if (!token) {
|
|
|
|
|
+ return std::nullopt;
|
|
|
|
|
+ }
|
|
|
|
|
+ auth_token = *token;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if this is an API key (starts with "sk_")
|
|
|
|
|
+ if (auth_token.starts_with("sk_")) {
|
|
|
|
|
+ if (!apiKeyService_) {
|
|
|
|
|
+ spdlog::warn("API key authentication attempted but service not available");
|
|
|
|
|
+ return std::nullopt;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto validation = apiKeyService_->ValidateApiKey(auth_token);
|
|
|
|
|
+ if (!validation.valid || !validation.api_key) {
|
|
|
|
|
+ spdlog::debug("API key validation failed: {}", validation.error);
|
|
|
|
|
+ return std::nullopt;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if key is expired
|
|
|
|
|
+ if (validation.api_key->IsExpired()) {
|
|
|
|
|
+ spdlog::debug("API key expired");
|
|
|
|
|
+ return std::nullopt;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Update last used timestamp (fire and forget)
|
|
|
|
|
+ apiKeyService_->UpdateLastUsed(validation.api_key->id);
|
|
|
|
|
+
|
|
|
|
|
+ // Build AuthUser from API key
|
|
|
|
|
+ AuthUser auth_user;
|
|
|
|
|
+ auth_user.user_id = validation.api_key->user_id;
|
|
|
|
|
+
|
|
|
|
|
+ // Use API key permissions as groups
|
|
|
|
|
+ auth_user.groups = validation.api_key->permissions;
|
|
|
|
|
+
|
|
|
|
|
+ // Try to fetch additional user info
|
|
|
|
|
+ if (userService_) {
|
|
|
|
|
+ auto user_result = userService_->GetUser(validation.api_key->user_id);
|
|
|
|
|
+ if (user_result.success && user_result.user) {
|
|
|
|
|
+ auth_user.email = user_result.user->email;
|
|
|
|
|
+
|
|
|
|
|
+ // Fetch workspace memberships for the user
|
|
|
|
|
+ if (membershipService_) {
|
|
|
|
|
+ auto memberships = membershipService_->ListUserMemberships(user_result.user->id);
|
|
|
|
|
+ for (const auto& m : memberships.members) {
|
|
|
|
|
+ auth_user.workspace_ids.push_back(m.workspace_id);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ spdlog::debug("API key owner user not found in database: {}", validation.api_key->user_id);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return auth_user;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Otherwise, treat as JWT token
|
|
|
|
|
+ auto validation = authService_->ValidateAccessToken(auth_token);
|
|
|
|
|
+ if (!validation.valid) {
|
|
|
|
|
+ spdlog::debug("Token validation failed: {}", validation.error);
|
|
|
|
|
+ return std::nullopt;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return validation.user;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// User Management Routes Implementation
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::SetupUserRoutes() {
|
|
|
|
|
+ // POST /api/users - Create a new user (superadmin only)
|
|
|
|
|
+ httpServer_->Post("/api/users", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleCreateUser(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/users - List all users (superadmin only)
|
|
|
|
|
+ httpServer_->Get("/api/users", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleListUsers(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/users/:id - Get a specific user
|
|
|
|
|
+ httpServer_->Get(R"(/api/users/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleGetUser(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // PATCH /api/users/:id - Update a user
|
|
|
|
|
+ httpServer_->Patch(R"(/api/users/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleUpdateUser(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // DELETE /api/users/:id - Soft delete a user (superadmin only)
|
|
|
|
|
+ httpServer_->Delete(R"(/api/users/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleDeleteUser(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("User management routes registered: /api/users");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto HttpServer::IsSuperadmin(const AuthUser& user) -> bool {
|
|
|
|
|
+ return std::find(user.groups.begin(), user.groups.end(), "superadmin") != user.groups.end();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleCreateUser(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Only superadmins can create users
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if UserService is available
|
|
|
|
|
+ if (!userService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"User service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto body = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ CreateUserRequest create_req;
|
|
|
|
|
+ if (body.contains("email")) {
|
|
|
|
|
+ create_req.email = body["email"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (body.contains("password")) {
|
|
|
|
|
+ create_req.password = body["password"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (body.contains("name")) {
|
|
|
|
|
+ create_req.name = body["name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = userService_->CreateUser(create_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return created user (without password_hash)
|
|
|
|
|
+ nlohmann::json user_json = {
|
|
|
|
|
+ {"id", result.user->id},
|
|
|
|
|
+ {"email", result.user->email},
|
|
|
|
|
+ {"name", result.user->name},
|
|
|
|
|
+ {"created_at", result.user->created_at},
|
|
|
|
|
+ {"updated_at", result.user->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.status = 201;
|
|
|
|
|
+ res.set_content(user_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Create user error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleListUsers(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Only superadmins can list all users
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if UserService is available
|
|
|
|
|
+ if (!userService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"User service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse query parameters
|
|
|
|
|
+ int limit = 100;
|
|
|
|
|
+ int offset = 0;
|
|
|
|
|
+ bool include_deleted = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (req.has_param("limit")) {
|
|
|
|
|
+ limit = std::stoi(req.get_param_value("limit"));
|
|
|
|
|
+ if (limit < 1 || limit > 1000) {
|
|
|
|
|
+ limit = 100;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (req.has_param("offset")) {
|
|
|
|
|
+ offset = std::stoi(req.get_param_value("offset"));
|
|
|
|
|
+ if (offset < 0) {
|
|
|
|
|
+ offset = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (req.has_param("include_deleted")) {
|
|
|
|
|
+ include_deleted = req.get_param_value("include_deleted") == "true";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = userService_->ListUsers(limit, offset, include_deleted);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build response
|
|
|
|
|
+ nlohmann::json users_array = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& user : result.users) {
|
|
|
|
|
+ users_array.push_back({
|
|
|
|
|
+ {"id", user.id},
|
|
|
|
|
+ {"email", user.email},
|
|
|
|
|
+ {"name", user.name},
|
|
|
|
|
+ {"created_at", user.created_at},
|
|
|
|
|
+ {"updated_at", user.updated_at},
|
|
|
|
|
+ {"deleted_at", user.deleted_at}
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = {
|
|
|
|
|
+ {"users", users_array},
|
|
|
|
|
+ {"total_count", result.total_count},
|
|
|
|
|
+ {"limit", limit},
|
|
|
|
|
+ {"offset", offset}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("List users error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleGetUser(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if UserService is available
|
|
|
|
|
+ if (!userService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"User service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Extract user ID from path
|
|
|
|
|
+ std::string user_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Users can only get their own info, unless superadmin
|
|
|
|
|
+ if (auth_user->user_id != user_id && !IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - can only access your own user data"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = userService_->GetUser(user_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return user (without password_hash)
|
|
|
|
|
+ nlohmann::json user_json = {
|
|
|
|
|
+ {"id", result.user->id},
|
|
|
|
|
+ {"email", result.user->email},
|
|
|
|
|
+ {"name", result.user->name},
|
|
|
|
|
+ {"created_at", result.user->created_at},
|
|
|
|
|
+ {"updated_at", result.user->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(user_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Get user error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleUpdateUser(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if UserService is available
|
|
|
|
|
+ if (!userService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"User service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Extract user ID from path
|
|
|
|
|
+ std::string user_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Users can only update their own info, unless superadmin
|
|
|
|
|
+ if (auth_user->user_id != user_id && !IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - can only update your own user data"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto body = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ UpdateUserRequest update_req;
|
|
|
|
|
+ if (body.contains("email")) {
|
|
|
|
|
+ update_req.email = body["email"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (body.contains("password")) {
|
|
|
|
|
+ update_req.password = body["password"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (body.contains("name")) {
|
|
|
|
|
+ update_req.name = body["name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = userService_->UpdateUser(user_id, update_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return updated user (without password_hash)
|
|
|
|
|
+ nlohmann::json user_json = {
|
|
|
|
|
+ {"id", result.user->id},
|
|
|
|
|
+ {"email", result.user->email},
|
|
|
|
|
+ {"name", result.user->name},
|
|
|
|
|
+ {"created_at", result.user->created_at},
|
|
|
|
|
+ {"updated_at", result.user->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(user_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Update user error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleDeleteUser(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Only superadmins can delete users
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if UserService is available
|
|
|
|
|
+ if (!userService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"User service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Extract user ID from path
|
|
|
|
|
+ std::string user_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ auto result = userService_->DeleteUser(user_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(R"({"message":"User deleted successfully"})", "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Delete user error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// Workspace Management Routes Implementation
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::SetupWorkspaceRoutes() {
|
|
|
|
|
+ // POST /api/workspaces - Create a new workspace (superadmin only)
|
|
|
|
|
+ httpServer_->Post("/api/workspaces", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleCreateWorkspace(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces - List workspaces (filtered by user membership for non-superadmin)
|
|
|
|
|
+ httpServer_->Get("/api/workspaces", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleListWorkspaces(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces/:id - Get a specific workspace
|
|
|
|
|
+ httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleGetWorkspace(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // PATCH /api/workspaces/:id - Update a workspace
|
|
|
|
|
+ httpServer_->Patch(R"(/api/workspaces/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleUpdateWorkspace(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // DELETE /api/workspaces/:id - Soft delete a workspace (superadmin only)
|
|
|
|
|
+ httpServer_->Delete(R"(/api/workspaces/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleDeleteWorkspace(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Workspace management routes registered: /api/workspaces");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleCreateWorkspace(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Only superadmins can create workspaces
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if WorkspaceService is available
|
|
|
|
|
+ if (!workspaceService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto body = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ CreateWorkspaceRequest create_req;
|
|
|
|
|
+ if (body.contains("name")) {
|
|
|
|
|
+ create_req.name = body["name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (body.contains("settings")) {
|
|
|
|
|
+ create_req.settings = body["settings"];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = workspaceService_->CreateWorkspace(create_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return created workspace
|
|
|
|
|
+ nlohmann::json workspace_json = {
|
|
|
|
|
+ {"id", result.workspace->id},
|
|
|
|
|
+ {"name", result.workspace->name},
|
|
|
|
|
+ {"settings", result.workspace->settings},
|
|
|
|
|
+ {"created_at", result.workspace->created_at},
|
|
|
|
|
+ {"updated_at", result.workspace->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.status = 201;
|
|
|
|
|
+ res.set_content(workspace_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Create workspace error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleListWorkspaces(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if WorkspaceService is available
|
|
|
|
|
+ if (!workspaceService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ WorkspaceListResult result;
|
|
|
|
|
+
|
|
|
|
|
+ // Superadmins can see all workspaces, others only see their memberships
|
|
|
|
|
+ if (IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ // Parse query parameters
|
|
|
|
|
+ int limit = 100;
|
|
|
|
|
+ int offset = 0;
|
|
|
|
|
+ bool include_deleted = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (req.has_param("limit")) {
|
|
|
|
|
+ limit = std::stoi(req.get_param_value("limit"));
|
|
|
|
|
+ if (limit < 1 || limit > 1000) {
|
|
|
|
|
+ limit = 100;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (req.has_param("offset")) {
|
|
|
|
|
+ offset = std::stoi(req.get_param_value("offset"));
|
|
|
|
|
+ if (offset < 0) {
|
|
|
|
|
+ offset = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (req.has_param("include_deleted")) {
|
|
|
|
|
+ include_deleted = req.get_param_value("include_deleted") == "true";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result = workspaceService_->ListWorkspaces(limit, offset, include_deleted);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Non-superadmin: filter by user's workspace memberships
|
|
|
|
|
+ result = workspaceService_->ListWorkspacesByIds(auth_user->workspace_ids, false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build response
|
|
|
|
|
+ nlohmann::json workspaces_array = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& workspace : result.workspaces) {
|
|
|
|
|
+ nlohmann::json ws_json = {
|
|
|
|
|
+ {"id", workspace.id},
|
|
|
|
|
+ {"name", workspace.name},
|
|
|
|
|
+ {"settings", workspace.settings},
|
|
|
|
|
+ {"created_at", workspace.created_at},
|
|
|
|
|
+ {"updated_at", workspace.updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+ if (IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ ws_json["deleted_at"] = workspace.deleted_at;
|
|
|
|
|
+ }
|
|
|
|
|
+ workspaces_array.push_back(ws_json);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = {
|
|
|
|
|
+ {"workspaces", workspaces_array},
|
|
|
|
|
+ {"total_count", result.total_count}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("List workspaces error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleGetWorkspace(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if WorkspaceService is available
|
|
|
|
|
+ if (!workspaceService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Extract workspace ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Non-superadmins can only access workspaces they're members of
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ auto it = std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id);
|
|
|
|
|
+ if (it == auth_user->workspace_ids.end()) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - not a member of this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return workspace
|
|
|
|
|
+ nlohmann::json workspace_json = {
|
|
|
|
|
+ {"id", result.workspace->id},
|
|
|
|
|
+ {"name", result.workspace->name},
|
|
|
|
|
+ {"settings", result.workspace->settings},
|
|
|
|
|
+ {"created_at", result.workspace->created_at},
|
|
|
|
|
+ {"updated_at", result.workspace->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(workspace_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Get workspace error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleUpdateWorkspace(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Only superadmins can update workspaces
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if WorkspaceService is available
|
|
|
|
|
+ if (!workspaceService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Extract workspace ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ auto body = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ UpdateWorkspaceRequest update_req;
|
|
|
|
|
+ if (body.contains("name")) {
|
|
|
|
|
+ update_req.name = body["name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (body.contains("settings")) {
|
|
|
|
|
+ update_req.settings = body["settings"];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = workspaceService_->UpdateWorkspace(workspace_id, update_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return updated workspace
|
|
|
|
|
+ nlohmann::json workspace_json = {
|
|
|
|
|
+ {"id", result.workspace->id},
|
|
|
|
|
+ {"name", result.workspace->name},
|
|
|
|
|
+ {"settings", result.workspace->settings},
|
|
|
|
|
+ {"created_at", result.workspace->created_at},
|
|
|
|
|
+ {"updated_at", result.workspace->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(workspace_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Update workspace error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleDeleteWorkspace(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Only superadmins can delete workspaces
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if WorkspaceService is available
|
|
|
|
|
+ if (!workspaceService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Extract workspace ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ auto result = workspaceService_->DeleteWorkspace(workspace_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(R"({"message":"Workspace deleted successfully"})", "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Delete workspace error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// Group Management Routes Implementation
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::SetupGroupRoutes() {
|
|
|
|
|
+ // POST /api/workspaces/:wid/groups - Create a new group in workspace
|
|
|
|
|
+ httpServer_->Post(R"(/api/workspaces/([a-zA-Z0-9\-]+)/groups)", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleCreateGroup(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces/:wid/groups - List groups in workspace
|
|
|
|
|
+ httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/groups)", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleListGroups(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces/:wid/groups/:id - Get a specific group
|
|
|
|
|
+ httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/groups/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleGetGroup(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // PATCH /api/workspaces/:wid/groups/:id - Update a group
|
|
|
|
|
+ httpServer_->Patch(R"(/api/workspaces/([a-zA-Z0-9\-]+)/groups/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleUpdateGroup(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // DELETE /api/workspaces/:wid/groups/:id - Delete a group
|
|
|
|
|
+ httpServer_->Delete(R"(/api/workspaces/([a-zA-Z0-9\-]+)/groups/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleDeleteGroup(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Group management routes registered: /api/workspaces/:wid/groups");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleCreateGroup(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace (superadmin or member)
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ auto it = std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id);
|
|
|
|
|
+ if (it == auth_user->workspace_ids.end()) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - not a member of this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // Only superadmins can create groups
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required to create groups"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if GroupService is available
|
|
|
|
|
+ if (!groupService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Group service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto body = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ CreateGroupRequest create_req;
|
|
|
|
|
+ create_req.workspace_id = workspace_id;
|
|
|
|
|
+ if (body.contains("name")) {
|
|
|
|
|
+ create_req.name = body["name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (body.contains("permissions") && body["permissions"].is_array()) {
|
|
|
|
|
+ for (const auto& perm : body["permissions"]) {
|
|
|
|
|
+ if (perm.is_string()) {
|
|
|
|
|
+ create_req.permissions.push_back(perm.get<std::string>());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = groupService_->CreateGroup(create_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return created group
|
|
|
|
|
+ nlohmann::json group_json = {
|
|
|
|
|
+ {"id", result.group->id},
|
|
|
|
|
+ {"workspace_id", result.group->workspace_id},
|
|
|
|
|
+ {"name", result.group->name},
|
|
|
|
|
+ {"permissions", result.group->permissions},
|
|
|
|
|
+ {"created_at", result.group->created_at},
|
|
|
|
|
+ {"updated_at", result.group->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.status = 201;
|
|
|
|
|
+ res.set_content(group_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Create group error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleListGroups(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ auto it = std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id);
|
|
|
|
|
+ if (it == auth_user->workspace_ids.end()) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - not a member of this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if GroupService is available
|
|
|
|
|
+ if (!groupService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Group service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse query parameters
|
|
|
|
|
+ int limit = 100;
|
|
|
|
|
+ int offset = 0;
|
|
|
|
|
+ bool include_deleted = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (req.has_param("limit")) {
|
|
|
|
|
+ limit = std::stoi(req.get_param_value("limit"));
|
|
|
|
|
+ if (limit < 1 || limit > 1000) {
|
|
|
|
|
+ limit = 100;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (req.has_param("offset")) {
|
|
|
|
|
+ offset = std::stoi(req.get_param_value("offset"));
|
|
|
|
|
+ if (offset < 0) {
|
|
|
|
|
+ offset = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (IsSuperadmin(*auth_user) && req.has_param("include_deleted")) {
|
|
|
|
|
+ include_deleted = req.get_param_value("include_deleted") == "true";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = groupService_->ListGroups(workspace_id, limit, offset, include_deleted);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build response
|
|
|
|
|
+ nlohmann::json groups_array = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& group : result.groups) {
|
|
|
|
|
+ nlohmann::json grp_json = {
|
|
|
|
|
+ {"id", group.id},
|
|
|
|
|
+ {"workspace_id", group.workspace_id},
|
|
|
|
|
+ {"name", group.name},
|
|
|
|
|
+ {"permissions", group.permissions},
|
|
|
|
|
+ {"created_at", group.created_at},
|
|
|
|
|
+ {"updated_at", group.updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+ if (IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ grp_json["deleted_at"] = group.deleted_at;
|
|
|
|
|
+ }
|
|
|
|
|
+ groups_array.push_back(grp_json);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = {
|
|
|
|
|
+ {"groups", groups_array},
|
|
|
|
|
+ {"total_count", result.total_count}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("List groups error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleGetGroup(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and group ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string group_id = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ auto it = std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id);
|
|
|
|
|
+ if (it == auth_user->workspace_ids.end()) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - not a member of this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if GroupService is available
|
|
|
|
|
+ if (!groupService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Group service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto result = groupService_->GetGroup(group_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Verify group belongs to the workspace
|
|
|
|
|
+ if (result.group->workspace_id != workspace_id) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Group not found in this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return group
|
|
|
|
|
+ nlohmann::json group_json = {
|
|
|
|
|
+ {"id", result.group->id},
|
|
|
|
|
+ {"workspace_id", result.group->workspace_id},
|
|
|
|
|
+ {"name", result.group->name},
|
|
|
|
|
+ {"permissions", result.group->permissions},
|
|
|
|
|
+ {"created_at", result.group->created_at},
|
|
|
|
|
+ {"updated_at", result.group->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(group_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Get group error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleUpdateGroup(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and group ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string group_id = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Only superadmins can update groups
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if GroupService is available
|
|
|
|
|
+ if (!groupService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Group service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // First verify group belongs to the workspace
|
|
|
|
|
+ auto existing = groupService_->GetGroup(group_id);
|
|
|
|
|
+ if (!existing.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", existing.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (existing.group->workspace_id != workspace_id) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Group not found in this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto body = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ UpdateGroupRequest update_req;
|
|
|
|
|
+ if (body.contains("name")) {
|
|
|
|
|
+ update_req.name = body["name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (body.contains("permissions") && body["permissions"].is_array()) {
|
|
|
|
|
+ std::vector<std::string> perms;
|
|
|
|
|
+ for (const auto& perm : body["permissions"]) {
|
|
|
|
|
+ if (perm.is_string()) {
|
|
|
|
|
+ perms.push_back(perm.get<std::string>());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ update_req.permissions = perms;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = groupService_->UpdateGroup(group_id, update_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return updated group
|
|
|
|
|
+ nlohmann::json group_json = {
|
|
|
|
|
+ {"id", result.group->id},
|
|
|
|
|
+ {"workspace_id", result.group->workspace_id},
|
|
|
|
|
+ {"name", result.group->name},
|
|
|
|
|
+ {"permissions", result.group->permissions},
|
|
|
|
|
+ {"created_at", result.group->created_at},
|
|
|
|
|
+ {"updated_at", result.group->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(group_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Update group error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleDeleteGroup(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and group ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string group_id = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Only superadmins can delete groups
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if GroupService is available
|
|
|
|
|
+ if (!groupService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Group service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // First verify group belongs to the workspace
|
|
|
|
|
+ auto existing = groupService_->GetGroup(group_id);
|
|
|
|
|
+ if (!existing.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", existing.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (existing.group->workspace_id != workspace_id) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Group not found in this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = groupService_->DeleteGroup(group_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(R"({"message":"Group deleted successfully"})", "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Delete group error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// Membership Routes
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::SetupMembershipRoutes() {
|
|
|
|
|
+ // POST /api/workspaces/:wid/members - Add a user to workspace
|
|
|
|
|
+ httpServer_->Post(R"(/api/workspaces/([a-zA-Z0-9\-]+)/members)", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleAddMember(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces/:wid/members - List members in workspace
|
|
|
|
|
+ httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/members)", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleListMembers(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces/:wid/members/:userId - Get a specific member
|
|
|
|
|
+ httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/members/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleGetMember(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // PUT /api/workspaces/:wid/members/:userId - Update user's groups in workspace
|
|
|
|
|
+ httpServer_->Put(R"(/api/workspaces/([a-zA-Z0-9\-]+)/members/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleUpdateMember(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // DELETE /api/workspaces/:wid/members/:userId - Remove user from workspace
|
|
|
|
|
+ httpServer_->Delete(R"(/api/workspaces/([a-zA-Z0-9\-]+)/members/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleRemoveMember(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Membership routes registered: /api/workspaces/:wid/members");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleAddMember(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Only superadmins can add members
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if MembershipService is available
|
|
|
|
|
+ if (!membershipService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Membership service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ auto json = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ AddMemberRequest add_req;
|
|
|
|
|
+ add_req.workspace_id = workspace_id;
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("user_id") && json["user_id"].is_string()) {
|
|
|
|
|
+ add_req.user_id = json["user_id"].get<std::string>();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"user_id is required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("group_ids") && json["group_ids"].is_array()) {
|
|
|
|
|
+ for (const auto& item : json["group_ids"]) {
|
|
|
|
|
+ if (item.is_string()) {
|
|
|
|
|
+ add_req.group_ids.push_back(item.get<std::string>());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = membershipService_->AddMember(add_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return created membership
|
|
|
|
|
+ nlohmann::json member_json = {
|
|
|
|
|
+ {"id", result.member->id},
|
|
|
|
|
+ {"workspace_id", result.member->workspace_id},
|
|
|
|
|
+ {"user_id", result.member->user_id},
|
|
|
|
|
+ {"group_ids", result.member->group_ids},
|
|
|
|
|
+ {"created_at", result.member->created_at},
|
|
|
|
|
+ {"updated_at", result.member->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.status = 201;
|
|
|
|
|
+ res.set_content(member_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::exception& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", "Invalid JSON: " + std::string(e.what())}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Add member error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleListMembers(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Check access to workspace (superadmin or member)
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ auto it = std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id);
|
|
|
|
|
+ if (it == auth_user->workspace_ids.end()) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - not a member of this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if MembershipService is available
|
|
|
|
|
+ if (!membershipService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Membership service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Get query parameters
|
|
|
|
|
+ int limit = 100;
|
|
|
|
|
+ int offset = 0;
|
|
|
|
|
+ bool include_deleted = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (req.has_param("limit")) {
|
|
|
|
|
+ limit = std::stoi(req.get_param_value("limit"));
|
|
|
|
|
+ if (limit < 1 || limit > 1000) {
|
|
|
|
|
+ limit = 100;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (req.has_param("offset")) {
|
|
|
|
|
+ offset = std::stoi(req.get_param_value("offset"));
|
|
|
|
|
+ if (offset < 0) {
|
|
|
|
|
+ offset = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (IsSuperadmin(*auth_user) && req.has_param("include_deleted")) {
|
|
|
|
|
+ include_deleted = req.get_param_value("include_deleted") == "true";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = membershipService_->ListMembers(workspace_id, limit, offset, include_deleted);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build response with user details
|
|
|
|
|
+ nlohmann::json members_array = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& member : result.members) {
|
|
|
|
|
+ nlohmann::json member_json = {
|
|
|
|
|
+ {"id", member.id},
|
|
|
|
|
+ {"workspace_id", member.workspace_id},
|
|
|
|
|
+ {"user_id", member.user_id},
|
|
|
|
|
+ {"group_ids", member.group_ids},
|
|
|
|
|
+ {"created_at", member.created_at},
|
|
|
|
|
+ {"updated_at", member.updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+ if (IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ member_json["deleted_at"] = member.deleted_at;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Fetch user details if UserService is available
|
|
|
|
|
+ if (userService_) {
|
|
|
|
|
+ auto user_result = userService_->GetUser(member.user_id);
|
|
|
|
|
+ if (user_result.success && user_result.user) {
|
|
|
|
|
+ member_json["user"] = {
|
|
|
|
|
+ {"id", user_result.user->id},
|
|
|
|
|
+ {"email", user_result.user->email},
|
|
|
|
|
+ {"name", user_result.user->name},
|
|
|
|
|
+ {"created_at", user_result.user->created_at}
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ members_array.push_back(member_json);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = {
|
|
|
|
|
+ {"members", members_array},
|
|
|
|
|
+ {"total_count", result.total_count}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("List members error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleGetMember(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and user ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string user_id = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Check access to workspace (superadmin or member)
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ auto it = std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id);
|
|
|
|
|
+ if (it == auth_user->workspace_ids.end()) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - not a member of this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if MembershipService is available
|
|
|
|
|
+ if (!membershipService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Membership service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto result = membershipService_->GetMembershipByUser(workspace_id, user_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return membership
|
|
|
|
|
+ nlohmann::json member_json = {
|
|
|
|
|
+ {"id", result.member->id},
|
|
|
|
|
+ {"workspace_id", result.member->workspace_id},
|
|
|
|
|
+ {"user_id", result.member->user_id},
|
|
|
|
|
+ {"group_ids", result.member->group_ids},
|
|
|
|
|
+ {"created_at", result.member->created_at},
|
|
|
|
|
+ {"updated_at", result.member->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(member_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Get member error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleUpdateMember(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and user ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string user_id = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Only superadmins can update memberships
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if MembershipService is available
|
|
|
|
|
+ if (!membershipService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Membership service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ auto json = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ UpdateMemberRequest update_req;
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("group_ids") && json["group_ids"].is_array()) {
|
|
|
|
|
+ std::vector<std::string> group_ids;
|
|
|
|
|
+ for (const auto& item : json["group_ids"]) {
|
|
|
|
|
+ if (item.is_string()) {
|
|
|
|
|
+ group_ids.push_back(item.get<std::string>());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ update_req.group_ids = group_ids;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = membershipService_->UpdateMembership(workspace_id, user_id, update_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return updated membership
|
|
|
|
|
+ nlohmann::json member_json = {
|
|
|
|
|
+ {"id", result.member->id},
|
|
|
|
|
+ {"workspace_id", result.member->workspace_id},
|
|
|
|
|
+ {"user_id", result.member->user_id},
|
|
|
|
|
+ {"group_ids", result.member->group_ids},
|
|
|
|
|
+ {"created_at", result.member->created_at},
|
|
|
|
|
+ {"updated_at", result.member->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(member_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::exception& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", "Invalid JSON: " + std::string(e.what())}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Update member error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleRemoveMember(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and user ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string user_id = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Only superadmins can remove members
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - superadmin required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if MembershipService is available
|
|
|
|
|
+ if (!membershipService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Membership service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto result = membershipService_->RemoveMember(workspace_id, user_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(R"({"message":"Member removed successfully"})", "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Remove member error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// API Key Routes
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::SetupApiKeyRoutes() {
|
|
|
|
|
+ // POST /api/users/:id/api-keys - Create an API key for a user
|
|
|
|
|
+ httpServer_->Post(R"(/api/users/([a-zA-Z0-9\-]+)/api-keys)", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleCreateApiKey(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/users/:id/api-keys - List user's API keys
|
|
|
|
|
+ httpServer_->Get(R"(/api/users/([a-zA-Z0-9\-]+)/api-keys)", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleListApiKeys(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // DELETE /api/users/:id/api-keys/:keyId - Revoke an API key
|
|
|
|
|
+ httpServer_->Delete(R"(/api/users/([a-zA-Z0-9\-]+)/api-keys/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleRevokeApiKey(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("API key routes registered: /api/users/:id/api-keys");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleCreateApiKey(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract user ID from path
|
|
|
|
|
+ std::string target_user_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Check permissions: superadmin can create for any user, others only for themselves
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user) && auth_user->user_id != target_user_id) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - can only create API keys for yourself"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if ApiKeyService is available
|
|
|
|
|
+ if (!apiKeyService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"API key service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ auto json = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ CreateApiKeyRequest create_req;
|
|
|
|
|
+ create_req.user_id = target_user_id;
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("name") && json["name"].is_string()) {
|
|
|
|
|
+ create_req.name = json["name"].get<std::string>();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"name is required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("permissions") && json["permissions"].is_array()) {
|
|
|
|
|
+ for (const auto& item : json["permissions"]) {
|
|
|
|
|
+ if (item.is_string()) {
|
|
|
|
|
+ std::string perm = item.get<std::string>();
|
|
|
|
|
+ // Users can only grant permissions they have (unless superadmin)
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ // For now, allow all permissions - proper permission checking
|
|
|
|
|
+ // would require looking up user's actual permissions
|
|
|
|
|
+ }
|
|
|
|
|
+ create_req.permissions.push_back(perm);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("expires_at") && json["expires_at"].is_string()) {
|
|
|
|
|
+ create_req.expires_at = json["expires_at"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = apiKeyService_->CreateApiKey(create_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return created key info INCLUDING the raw key (only time it's returned!)
|
|
|
|
|
+ nlohmann::json key_json = {
|
|
|
|
|
+ {"id", result.api_key->id},
|
|
|
|
|
+ {"user_id", result.api_key->user_id},
|
|
|
|
|
+ {"name", result.api_key->name},
|
|
|
|
|
+ {"key_prefix", result.api_key->key_prefix},
|
|
|
|
|
+ {"key", result.raw_key}, // THE RAW KEY - only returned once!
|
|
|
|
|
+ {"permissions", result.api_key->permissions},
|
|
|
|
|
+ {"created_at", result.api_key->created_at},
|
|
|
|
|
+ {"expires_at", result.api_key->expires_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.status = 201;
|
|
|
|
|
+ res.set_content(key_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::exception& e) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", "Invalid JSON: " + std::string(e.what())}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Create API key error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleListApiKeys(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract user ID from path
|
|
|
|
|
+ std::string target_user_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Check permissions: superadmin can list for any user, others only for themselves
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user) && auth_user->user_id != target_user_id) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - can only list your own API keys"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if ApiKeyService is available
|
|
|
|
|
+ if (!apiKeyService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"API key service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Check for include_revoked parameter (superadmin only)
|
|
|
|
|
+ bool include_revoked = false;
|
|
|
|
|
+ if (IsSuperadmin(*auth_user) && req.has_param("include_revoked")) {
|
|
|
|
|
+ include_revoked = req.get_param_value("include_revoked") == "true";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = apiKeyService_->ListApiKeys(target_user_id, include_revoked);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build response (note: raw keys and hashes are NOT included)
|
|
|
|
|
+ nlohmann::json keys_array = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& key : result.api_keys) {
|
|
|
|
|
+ nlohmann::json key_json = {
|
|
|
|
|
+ {"id", key.id},
|
|
|
|
|
+ {"user_id", key.user_id},
|
|
|
|
|
+ {"name", key.name},
|
|
|
|
|
+ {"key_prefix", key.key_prefix}, // Only prefix, not full key
|
|
|
|
|
+ {"permissions", key.permissions},
|
|
|
|
|
+ {"created_at", key.created_at},
|
|
|
|
|
+ {"updated_at", key.updated_at},
|
|
|
|
|
+ {"last_used_at", key.last_used_at},
|
|
|
|
|
+ {"expires_at", key.expires_at}
|
|
|
|
|
+ };
|
|
|
|
|
+ if (IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ key_json["deleted_at"] = key.deleted_at;
|
|
|
|
|
+ }
|
|
|
|
|
+ keys_array.push_back(key_json);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = {
|
|
|
|
|
+ {"api_keys", keys_array},
|
|
|
|
|
+ {"total_count", result.total_count}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("List API keys error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleRevokeApiKey(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract user ID and key ID from path
|
|
|
|
|
+ std::string target_user_id = req.matches[1].str();
|
|
|
|
|
+ std::string key_id = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Check permissions: superadmin can revoke for any user, others only for themselves
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user) && auth_user->user_id != target_user_id) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - can only revoke your own API keys"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if ApiKeyService is available
|
|
|
|
|
+ if (!apiKeyService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"API key service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto result = apiKeyService_->RevokeApiKey(key_id, target_user_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(R"({"message":"API key revoked successfully"})", "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Revoke API key error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// Collection Routes
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::SetupCollectionRoutes() {
|
|
|
|
|
+ // POST /api/workspaces/:wid/collections - Create a collection
|
|
|
|
|
+ httpServer_->Post(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections)", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleCreateCollection(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces/:wid/collections - List collections
|
|
|
|
|
+ httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections)", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleListCollections(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces/:wid/collections/:name - Get a collection
|
|
|
|
|
+ httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleGetCollection(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // PUT /api/workspaces/:wid/collections/:name - Update a collection
|
|
|
|
|
+ httpServer_->Put(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleUpdateCollection(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // DELETE /api/workspaces/:wid/collections/:name - Drop a collection
|
|
|
|
|
+ httpServer_->Delete(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleDropCollection(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Collection routes registered: /api/workspaces/:wid/collections");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleCreateCollection(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists and user has access
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace (superadmin has access to all)
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if CollectionService is available
|
|
|
|
|
+ if (!collectionService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Collection service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ auto json = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ CreateCollectionRequest create_req;
|
|
|
|
|
+ create_req.workspace_id = workspace_id;
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("name") && json["name"].is_string()) {
|
|
|
|
|
+ create_req.name = json["name"].get<std::string>();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"name is required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse settings
|
|
|
|
|
+ if (json.contains("schema") && json["schema"].is_string()) {
|
|
|
|
|
+ create_req.settings.schema = json["schema"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("encrypted_fields") && json["encrypted_fields"].is_array()) {
|
|
|
|
|
+ for (const auto& item : json["encrypted_fields"]) {
|
|
|
|
|
+ if (item.is_string()) {
|
|
|
|
|
+ create_req.settings.encrypted_fields.push_back(item.get<std::string>());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("ttl_seconds") && json["ttl_seconds"].is_number_integer()) {
|
|
|
|
|
+ create_req.settings.ttl_seconds = json["ttl_seconds"].get<int64_t>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = collectionService_->CreateCollection(create_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return created collection info
|
|
|
|
|
+ nlohmann::json coll_json = {
|
|
|
|
|
+ {"name", result.collection->name},
|
|
|
|
|
+ {"workspace_id", result.collection->workspace_id},
|
|
|
|
|
+ {"document_count", result.collection->document_count},
|
|
|
|
|
+ {"size_bytes", result.collection->size_bytes},
|
|
|
|
|
+ {"created_at", result.collection->created_at},
|
|
|
|
|
+ {"settings", {
|
|
|
|
|
+ {"schema", result.collection->settings.schema},
|
|
|
|
|
+ {"encrypted_fields", result.collection->settings.encrypted_fields},
|
|
|
|
|
+ {"ttl_seconds", result.collection->settings.ttl_seconds}
|
|
|
|
|
+ }}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.status = 201;
|
|
|
|
|
+ res.set_content(coll_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::exception& e) {
|
|
|
|
|
+ spdlog::warn("Create collection JSON parse error: {}", e.what());
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Create collection error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleListCollections(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists and user has access
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if CollectionService is available
|
|
|
|
|
+ if (!collectionService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Collection service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Check if superadmin wants to include system collections
|
|
|
|
|
+ bool include_system = IsSuperadmin(*auth_user) &&
|
|
|
|
|
+ req.has_param("include_system") &&
|
|
|
|
|
+ req.get_param_value("include_system") == "true";
|
|
|
|
|
+
|
|
|
|
|
+ auto result = collectionService_->ListCollections(workspace_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json collections_array = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& coll : result.collections) {
|
|
|
|
|
+ // Check if collection name starts with underscore (system collection)
|
|
|
|
|
+ bool is_system = !coll.name.empty() && coll.name[0] == '_';
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json coll_json = {
|
|
|
|
|
+ {"name", coll.name},
|
|
|
|
|
+ {"workspace_id", coll.workspace_id},
|
|
|
|
|
+ {"document_count", coll.document_count},
|
|
|
|
|
+ {"size_bytes", coll.size_bytes},
|
|
|
|
|
+ {"created_at", coll.created_at},
|
|
|
|
|
+ {"updated_at", coll.updated_at},
|
|
|
|
|
+ {"is_system", is_system},
|
|
|
|
|
+ {"settings", {
|
|
|
|
|
+ {"schema", coll.settings.schema},
|
|
|
|
|
+ {"encrypted_fields", coll.settings.encrypted_fields},
|
|
|
|
|
+ {"ttl_seconds", coll.settings.ttl_seconds}
|
|
|
|
|
+ }}
|
|
|
|
|
+ };
|
|
|
|
|
+ collections_array.push_back(coll_json);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // If superadmin requested system collections, add global system collections
|
|
|
|
|
+ if (include_system) {
|
|
|
|
|
+ auto system_result = collectionService_->ListSystemCollections();
|
|
|
|
|
+ if (system_result.success) {
|
|
|
|
|
+ for (const auto& coll : system_result.collections) {
|
|
|
|
|
+ nlohmann::json coll_json = {
|
|
|
|
|
+ {"name", coll.name},
|
|
|
|
|
+ {"workspace_id", ""},
|
|
|
|
|
+ {"document_count", coll.document_count},
|
|
|
|
|
+ {"size_bytes", coll.size_bytes},
|
|
|
|
|
+ {"created_at", coll.created_at},
|
|
|
|
|
+ {"updated_at", coll.updated_at},
|
|
|
|
|
+ {"is_system", true},
|
|
|
|
|
+ {"settings", nlohmann::json::object()}
|
|
|
|
|
+ };
|
|
|
|
|
+ collections_array.push_back(coll_json);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = {
|
|
|
|
|
+ {"collections", collections_array},
|
|
|
|
|
+ {"total_count", static_cast<int64_t>(collections_array.size())}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("List collections error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleGetCollection(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and collection name from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string collection_name = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists and user has access
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if CollectionService is available
|
|
|
|
|
+ if (!collectionService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Collection service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto result = collectionService_->GetCollection(workspace_id, collection_name);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json coll_json = {
|
|
|
|
|
+ {"name", result.collection->name},
|
|
|
|
|
+ {"workspace_id", result.collection->workspace_id},
|
|
|
|
|
+ {"document_count", result.collection->document_count},
|
|
|
|
|
+ {"size_bytes", result.collection->size_bytes},
|
|
|
|
|
+ {"created_at", result.collection->created_at},
|
|
|
|
|
+ {"updated_at", result.collection->updated_at},
|
|
|
|
|
+ {"settings", {
|
|
|
|
|
+ {"schema", result.collection->settings.schema},
|
|
|
|
|
+ {"encrypted_fields", result.collection->settings.encrypted_fields},
|
|
|
|
|
+ {"ttl_seconds", result.collection->settings.ttl_seconds}
|
|
|
|
|
+ }}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(coll_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Get collection error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleUpdateCollection(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and collection name from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string collection_name = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists and user has access
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if CollectionService is available
|
|
|
|
|
+ if (!collectionService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Collection service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ auto json = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ // Get existing settings first
|
|
|
|
|
+ auto get_result = collectionService_->GetCollection(workspace_id, collection_name);
|
|
|
|
|
+ if (!get_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Collection not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CollectionSettings settings = get_result.collection->settings;
|
|
|
|
|
+
|
|
|
|
|
+ // Update only provided fields
|
|
|
|
|
+ if (json.contains("schema") && json["schema"].is_string()) {
|
|
|
|
|
+ settings.schema = json["schema"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("encrypted_fields") && json["encrypted_fields"].is_array()) {
|
|
|
|
|
+ settings.encrypted_fields.clear();
|
|
|
|
|
+ for (const auto& item : json["encrypted_fields"]) {
|
|
|
|
|
+ if (item.is_string()) {
|
|
|
|
|
+ settings.encrypted_fields.push_back(item.get<std::string>());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("ttl_seconds") && json["ttl_seconds"].is_number_integer()) {
|
|
|
|
|
+ settings.ttl_seconds = json["ttl_seconds"].get<int64_t>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = collectionService_->UpdateCollection(workspace_id, collection_name, settings);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json coll_json = {
|
|
|
|
|
+ {"name", result.collection->name},
|
|
|
|
|
+ {"workspace_id", result.collection->workspace_id},
|
|
|
|
|
+ {"document_count", result.collection->document_count},
|
|
|
|
|
+ {"size_bytes", result.collection->size_bytes},
|
|
|
|
|
+ {"created_at", result.collection->created_at},
|
|
|
|
|
+ {"updated_at", result.collection->updated_at},
|
|
|
|
|
+ {"settings", {
|
|
|
|
|
+ {"schema", result.collection->settings.schema},
|
|
|
|
|
+ {"encrypted_fields", result.collection->settings.encrypted_fields},
|
|
|
|
|
+ {"ttl_seconds", result.collection->settings.ttl_seconds}
|
|
|
|
|
+ }}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(coll_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::exception& e) {
|
|
|
|
|
+ spdlog::warn("Update collection JSON parse error: {}", e.what());
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Update collection error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// Document Routes
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::SetupDocumentRoutes() {
|
|
|
|
|
+ // POST /api/workspaces/:wid/collections/:name/documents - Create a document
|
|
|
|
|
+ httpServer_->Post(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*)/documents)", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleCreateDocument(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces/:wid/collections/:name/documents - List documents
|
|
|
|
|
+ httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*)/documents)", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleListDocuments(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces/:wid/collections/:name/documents/:id - Get a document
|
|
|
|
|
+ httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*)/documents/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleGetDocument(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // PUT /api/workspaces/:wid/collections/:name/documents/:id - Update a document
|
|
|
|
|
+ httpServer_->Put(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*)/documents/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleUpdateDocument(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // DELETE /api/workspaces/:wid/collections/:name/documents/:id - Delete a document
|
|
|
|
|
+ httpServer_->Delete(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*)/documents/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleDeleteDocument(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Document routes registered: /api/workspaces/:wid/collections/:name/documents");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleCreateDocument(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and collection name from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string collection_name = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists and user has access
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if DocumentService is available
|
|
|
|
|
+ if (!documentService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Document service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ auto json = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ CreateDocumentRequest create_req;
|
|
|
|
|
+ create_req.workspace_id = workspace_id;
|
|
|
|
|
+ create_req.collection = collection_name;
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("id") && json["id"].is_string()) {
|
|
|
|
|
+ create_req.id = json["id"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("data") && json["data"].is_object()) {
|
|
|
|
|
+ create_req.data = json["data"];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Treat entire body as data if no "data" field
|
|
|
|
|
+ create_req.data = json;
|
|
|
|
|
+ create_req.data.erase("id"); // Remove id from data
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = documentService_->CreateDocument(create_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Return created document info
|
|
|
|
|
+ nlohmann::json doc_json = {
|
|
|
|
|
+ {"id", result.document->id},
|
|
|
|
|
+ {"collection", result.document->collection},
|
|
|
|
|
+ {"workspace_id", result.document->workspace_id},
|
|
|
|
|
+ {"data", result.document->data},
|
|
|
|
|
+ {"created_at", result.document->created_at},
|
|
|
|
|
+ {"updated_at", result.document->updated_at},
|
|
|
|
|
+ {"version", result.document->version}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Broadcast document create event to WebSocket subscribers
|
|
|
|
|
+ BroadcastDocumentEvent(workspace_id, collection_name, DocumentAction::Create,
|
|
|
|
|
+ result.document->id, result.document->data);
|
|
|
|
|
+
|
|
|
|
|
+ res.status = 201;
|
|
|
|
|
+ res.set_content(doc_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::exception& e) {
|
|
|
|
|
+ spdlog::warn("Create document JSON parse error: {}", e.what());
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Create document error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleListDocuments(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and collection name from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string collection_name = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists and user has access
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if DocumentService is available
|
|
|
|
|
+ if (!documentService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Document service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ DocumentQuery query;
|
|
|
|
|
+ query.workspace_id = workspace_id;
|
|
|
|
|
+ query.collection = collection_name;
|
|
|
|
|
+
|
|
|
|
|
+ // Parse query parameters
|
|
|
|
|
+ if (req.has_param("limit")) {
|
|
|
|
|
+ query.limit = std::stoi(req.get_param_value("limit"));
|
|
|
|
|
+ if (query.limit < 1) query.limit = 1;
|
|
|
|
|
+ if (query.limit > 1000) query.limit = 1000;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (req.has_param("offset")) {
|
|
|
|
|
+ query.offset = std::stoi(req.get_param_value("offset"));
|
|
|
|
|
+ if (query.offset < 0) query.offset = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (req.has_param("sort")) {
|
|
|
|
|
+ query.sort_field = req.get_param_value("sort");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (req.has_param("order")) {
|
|
|
|
|
+ query.sort_ascending = (req.get_param_value("order") != "desc");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (req.has_param("filter")) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ query.filter = nlohmann::json::parse(req.get_param_value("filter"));
|
|
|
|
|
+ } catch (...) {
|
|
|
|
|
+ // Invalid filter JSON - ignore
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = documentService_->ListDocuments(query);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json docs_array = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& doc : result.documents) {
|
|
|
|
|
+ nlohmann::json doc_json = {
|
|
|
|
|
+ {"id", doc.id},
|
|
|
|
|
+ {"collection", doc.collection},
|
|
|
|
|
+ {"workspace_id", doc.workspace_id},
|
|
|
|
|
+ {"data", doc.data},
|
|
|
|
|
+ {"created_at", doc.created_at},
|
|
|
|
|
+ {"updated_at", doc.updated_at},
|
|
|
|
|
+ {"version", doc.version}
|
|
|
|
|
+ };
|
|
|
|
|
+ docs_array.push_back(doc_json);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = {
|
|
|
|
|
+ {"documents", docs_array},
|
|
|
|
|
+ {"total_count", result.total_count}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("List documents error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleGetDocument(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID, collection name, and document ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string collection_name = req.matches[2].str();
|
|
|
|
|
+ std::string document_id = req.matches[3].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists and user has access
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if DocumentService is available
|
|
|
|
|
+ if (!documentService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Document service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto result = documentService_->GetDocument(workspace_id, collection_name, document_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json doc_json = {
|
|
|
|
|
+ {"id", result.document->id},
|
|
|
|
|
+ {"collection", result.document->collection},
|
|
|
|
|
+ {"workspace_id", result.document->workspace_id},
|
|
|
|
|
+ {"data", result.document->data},
|
|
|
|
|
+ {"created_at", result.document->created_at},
|
|
|
|
|
+ {"updated_at", result.document->updated_at},
|
|
|
|
|
+ {"version", result.document->version}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(doc_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Get document error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleUpdateDocument(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID, collection name, and document ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string collection_name = req.matches[2].str();
|
|
|
|
|
+ std::string document_id = req.matches[3].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists and user has access
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if DocumentService is available
|
|
|
|
|
+ if (!documentService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Document service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ auto json = nlohmann::json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ UpdateDocumentRequest update_req;
|
|
|
|
|
+ update_req.workspace_id = workspace_id;
|
|
|
|
|
+ update_req.collection = collection_name;
|
|
|
|
|
+ update_req.id = document_id;
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("data") && json["data"].is_object()) {
|
|
|
|
|
+ update_req.data = json["data"];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Treat entire body as data if no "data" field
|
|
|
|
|
+ update_req.data = json;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("merge") && json["merge"].is_boolean()) {
|
|
|
|
|
+ update_req.merge = json["merge"].get<bool>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("expected_version") && json["expected_version"].is_number_integer()) {
|
|
|
|
|
+ update_req.expected_version = json["expected_version"].get<int64_t>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = documentService_->UpdateDocument(update_req);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json doc_json = {
|
|
|
|
|
+ {"id", result.document->id},
|
|
|
|
|
+ {"collection", result.document->collection},
|
|
|
|
|
+ {"workspace_id", result.document->workspace_id},
|
|
|
|
|
+ {"data", result.document->data},
|
|
|
|
|
+ {"created_at", result.document->created_at},
|
|
|
|
|
+ {"updated_at", result.document->updated_at},
|
|
|
|
|
+ {"version", result.document->version}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Broadcast document update event to WebSocket subscribers
|
|
|
|
|
+ BroadcastDocumentEvent(workspace_id, collection_name, DocumentAction::Update,
|
|
|
|
|
+ result.document->id, result.document->data);
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(doc_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const nlohmann::json::exception& e) {
|
|
|
|
|
+ spdlog::warn("Update document JSON parse error: {}", e.what());
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Update document error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleDeleteDocument(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID, collection name, and document ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string collection_name = req.matches[2].str();
|
|
|
|
|
+ std::string document_id = req.matches[3].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists and user has access
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if DocumentService is available
|
|
|
|
|
+ if (!documentService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Document service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto result = documentService_->DeleteDocument(workspace_id, collection_name, document_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Broadcast document delete event to WebSocket subscribers
|
|
|
|
|
+ BroadcastDocumentEvent(workspace_id, collection_name, DocumentAction::Delete,
|
|
|
|
|
+ document_id, nlohmann::json::object());
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(R"({"message":"Document deleted successfully"})", "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Delete document error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleDropCollection(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and collection name from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string collection_name = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists and user has access
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace (for collections, maybe require superadmin?)
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if CollectionService is available
|
|
|
|
|
+ if (!collectionService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"Collection service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Check for force parameter
|
|
|
|
|
+ bool force = false;
|
|
|
|
|
+ if (req.has_param("force")) {
|
|
|
|
|
+ force = req.get_param_value("force") == "true";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = collectionService_->DropCollection(workspace_id, collection_name, force);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(R"({"message":"Collection dropped successfully"})", "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Drop collection error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// View Routes
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::SetupViewRoutes() {
|
|
|
|
|
+ // POST /api/workspaces/:workspace_id/views - Create a new view
|
|
|
|
|
+ httpServer_->Post(R"(/api/workspaces/([^/]+)/views)",
|
|
|
|
|
+ [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleCreateView(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces/:workspace_id/views - List all views
|
|
|
|
|
+ httpServer_->Get(R"(/api/workspaces/([^/]+)/views)",
|
|
|
|
|
+ [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleListViews(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // GET /api/workspaces/:workspace_id/views/:view_id - Get a specific view
|
|
|
|
|
+ httpServer_->Get(R"(/api/workspaces/([^/]+)/views/([^/]+))",
|
|
|
|
|
+ [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleGetView(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // PATCH /api/workspaces/:workspace_id/views/:view_id - Update a view
|
|
|
|
|
+ httpServer_->Patch(R"(/api/workspaces/([^/]+)/views/([^/]+))",
|
|
|
|
|
+ [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleUpdateView(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // DELETE /api/workspaces/:workspace_id/views/:view_id - Delete a view
|
|
|
|
|
+ httpServer_->Delete(R"(/api/workspaces/([^/]+)/views/([^/]+))",
|
|
|
|
|
+ [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ HandleDeleteView(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("View routes configured");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleCreateView(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if ViewService is available
|
|
|
|
|
+ if (!viewService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"View service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ nlohmann::json body;
|
|
|
|
|
+ try {
|
|
|
|
|
+ body = nlohmann::json::parse(req.body);
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& /*e*/) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Validate required fields
|
|
|
|
|
+ if (!body.contains("name") || !body["name"].is_string()) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"name is required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!body.contains("collection_name") || !body["collection_name"].is_string()) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"collection_name is required"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ CreateViewRequest request;
|
|
|
|
|
+ request.workspace_id = workspace_id;
|
|
|
|
|
+ request.name = body["name"].get<std::string>();
|
|
|
|
|
+ request.collection_name = body["collection_name"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ // Parse schema if provided
|
|
|
|
|
+ if (body.contains("schema") && body["schema"].is_object()) {
|
|
|
|
|
+ const auto& schema_json = body["schema"];
|
|
|
|
|
+ if (schema_json.contains("title")) {
|
|
|
|
|
+ request.schema.title = schema_json.value("title", "");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (schema_json.contains("description")) {
|
|
|
|
|
+ request.schema.description = schema_json.value("description", "");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (schema_json.contains("layout")) {
|
|
|
|
|
+ request.schema.layout = schema_json["layout"];
|
|
|
|
|
+ }
|
|
|
|
|
+ if (schema_json.contains("fields") && schema_json["fields"].is_array()) {
|
|
|
|
|
+ for (const auto& field_json : schema_json["fields"]) {
|
|
|
|
|
+ SchemaField field;
|
|
|
|
|
+ field.name = field_json.value("name", "");
|
|
|
|
|
+ field.type = StringToFieldType(field_json.value("type", "text"));
|
|
|
|
|
+ field.label = field_json.value("label", "");
|
|
|
|
|
+ field.description = field_json.value("description", "");
|
|
|
|
|
+ field.required = field_json.value("required", false);
|
|
|
|
|
+ field.display_order = field_json.value("display_order", 0);
|
|
|
|
|
+ field.widget = field_json.value("widget", "");
|
|
|
|
|
+ field.group = field_json.value("group", "");
|
|
|
|
|
+ field.default_value = field_json.value("default_value", nlohmann::json());
|
|
|
|
|
+ field.options = field_json.value("options", nlohmann::json());
|
|
|
|
|
+ field.reference_collection = field_json.value("reference_collection", "");
|
|
|
|
|
+ field.computed_expression = field_json.value("computed_expression", "");
|
|
|
|
|
+ request.schema.fields.push_back(field);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse settings if provided
|
|
|
|
|
+ if (body.contains("settings") && body["settings"].is_object()) {
|
|
|
|
|
+ const auto& settings_json = body["settings"];
|
|
|
|
|
+ request.settings.is_default = settings_json.value("is_default", false);
|
|
|
|
|
+ request.settings.show_in_sidebar = settings_json.value("show_in_sidebar", true);
|
|
|
|
|
+ request.settings.icon = settings_json.value("icon", "");
|
|
|
|
|
+ request.settings.filters = settings_json.value("filters", nlohmann::json::object());
|
|
|
|
|
+ request.settings.sort = settings_json.value("sort", nlohmann::json::object());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = viewService_->CreateView(request);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build response with view info
|
|
|
|
|
+ nlohmann::json view_json = {
|
|
|
|
|
+ {"id", result.view->id},
|
|
|
|
|
+ {"workspace_id", result.view->workspace_id},
|
|
|
|
|
+ {"name", result.view->name},
|
|
|
|
|
+ {"collection_name", result.view->collection_name},
|
|
|
|
|
+ {"created_at", result.view->created_at},
|
|
|
|
|
+ {"updated_at", result.view->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Add schema
|
|
|
|
|
+ nlohmann::json schema_json = {
|
|
|
|
|
+ {"title", result.view->schema.title},
|
|
|
|
|
+ {"description", result.view->schema.description},
|
|
|
|
|
+ {"layout", result.view->schema.layout}
|
|
|
|
|
+ };
|
|
|
|
|
+ nlohmann::json fields_json = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& field : result.view->schema.fields) {
|
|
|
|
|
+ nlohmann::json field_json = {
|
|
|
|
|
+ {"name", field.name},
|
|
|
|
|
+ {"type", FieldTypeToString(field.type)},
|
|
|
|
|
+ {"label", field.label},
|
|
|
|
|
+ {"description", field.description},
|
|
|
|
|
+ {"required", field.required},
|
|
|
|
|
+ {"display_order", field.display_order},
|
|
|
|
|
+ {"widget", field.widget},
|
|
|
|
|
+ {"group", field.group}
|
|
|
|
|
+ };
|
|
|
|
|
+ if (!field.default_value.is_null()) {
|
|
|
|
|
+ field_json["default_value"] = field.default_value;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!field.options.is_null()) {
|
|
|
|
|
+ field_json["options"] = field.options;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!field.reference_collection.empty()) {
|
|
|
|
|
+ field_json["reference_collection"] = field.reference_collection;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!field.computed_expression.empty()) {
|
|
|
|
|
+ field_json["computed_expression"] = field.computed_expression;
|
|
|
|
|
+ }
|
|
|
|
|
+ fields_json.push_back(field_json);
|
|
|
|
|
+ }
|
|
|
|
|
+ schema_json["fields"] = fields_json;
|
|
|
|
|
+ view_json["schema"] = schema_json;
|
|
|
|
|
+
|
|
|
|
|
+ // Add settings
|
|
|
|
|
+ view_json["settings"] = {
|
|
|
|
|
+ {"is_default", result.view->settings.is_default},
|
|
|
|
|
+ {"show_in_sidebar", result.view->settings.show_in_sidebar},
|
|
|
|
|
+ {"icon", result.view->settings.icon},
|
|
|
|
|
+ {"filters", result.view->settings.filters},
|
|
|
|
|
+ {"sort", result.view->settings.sort}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.status = 201;
|
|
|
|
|
+ res.set_content(view_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Create view error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleListViews(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if ViewService is available
|
|
|
|
|
+ if (!viewService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"View service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ ViewListResult result;
|
|
|
|
|
+
|
|
|
|
|
+ // Check if filtering by collection
|
|
|
|
|
+ if (req.has_param("collection")) {
|
|
|
|
|
+ std::string collection_name = req.get_param_value("collection");
|
|
|
|
|
+ result = viewService_->ListViewsForCollection(workspace_id, collection_name);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result = viewService_->ListViews(workspace_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json views_json = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& view : result.views) {
|
|
|
|
|
+ nlohmann::json view_json = {
|
|
|
|
|
+ {"id", view.id},
|
|
|
|
|
+ {"workspace_id", view.workspace_id},
|
|
|
|
|
+ {"name", view.name},
|
|
|
|
|
+ {"collection_name", view.collection_name},
|
|
|
|
|
+ {"created_at", view.created_at},
|
|
|
|
|
+ {"updated_at", view.updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Add schema summary (fields count and title)
|
|
|
|
|
+ view_json["schema"] = {
|
|
|
|
|
+ {"title", view.schema.title},
|
|
|
|
|
+ {"field_count", view.schema.fields.size()}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Add settings
|
|
|
|
|
+ view_json["settings"] = {
|
|
|
|
|
+ {"is_default", view.settings.is_default},
|
|
|
|
|
+ {"show_in_sidebar", view.settings.show_in_sidebar},
|
|
|
|
|
+ {"icon", view.settings.icon}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ views_json.push_back(view_json);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = {{"views", views_json}};
|
|
|
|
|
+ res.set_content(response.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("List views error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleGetView(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and view ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string view_id = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if ViewService is available
|
|
|
|
|
+ if (!viewService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"View service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto result = viewService_->GetView(workspace_id, view_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success || !result.view) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build full response with view info
|
|
|
|
|
+ nlohmann::json view_json = {
|
|
|
|
|
+ {"id", result.view->id},
|
|
|
|
|
+ {"workspace_id", result.view->workspace_id},
|
|
|
|
|
+ {"name", result.view->name},
|
|
|
|
|
+ {"collection_name", result.view->collection_name},
|
|
|
|
|
+ {"created_at", result.view->created_at},
|
|
|
|
|
+ {"updated_at", result.view->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Add schema
|
|
|
|
|
+ nlohmann::json schema_json = {
|
|
|
|
|
+ {"title", result.view->schema.title},
|
|
|
|
|
+ {"description", result.view->schema.description},
|
|
|
|
|
+ {"layout", result.view->schema.layout}
|
|
|
|
|
+ };
|
|
|
|
|
+ nlohmann::json fields_json = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& field : result.view->schema.fields) {
|
|
|
|
|
+ nlohmann::json field_json = {
|
|
|
|
|
+ {"name", field.name},
|
|
|
|
|
+ {"type", FieldTypeToString(field.type)},
|
|
|
|
|
+ {"label", field.label},
|
|
|
|
|
+ {"description", field.description},
|
|
|
|
|
+ {"required", field.required},
|
|
|
|
|
+ {"display_order", field.display_order},
|
|
|
|
|
+ {"widget", field.widget},
|
|
|
|
|
+ {"group", field.group}
|
|
|
|
|
+ };
|
|
|
|
|
+ if (!field.default_value.is_null()) {
|
|
|
|
|
+ field_json["default_value"] = field.default_value;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!field.options.is_null()) {
|
|
|
|
|
+ field_json["options"] = field.options;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!field.reference_collection.empty()) {
|
|
|
|
|
+ field_json["reference_collection"] = field.reference_collection;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!field.computed_expression.empty()) {
|
|
|
|
|
+ field_json["computed_expression"] = field.computed_expression;
|
|
|
|
|
+ }
|
|
|
|
|
+ fields_json.push_back(field_json);
|
|
|
|
|
+ }
|
|
|
|
|
+ schema_json["fields"] = fields_json;
|
|
|
|
|
+ view_json["schema"] = schema_json;
|
|
|
|
|
+
|
|
|
|
|
+ // Add settings
|
|
|
|
|
+ view_json["settings"] = {
|
|
|
|
|
+ {"is_default", result.view->settings.is_default},
|
|
|
|
|
+ {"show_in_sidebar", result.view->settings.show_in_sidebar},
|
|
|
|
|
+ {"icon", result.view->settings.icon},
|
|
|
|
|
+ {"filters", result.view->settings.filters},
|
|
|
|
|
+ {"sort", result.view->settings.sort}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(view_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Get view error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleUpdateView(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and view ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string view_id = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if ViewService is available
|
|
|
|
|
+ if (!viewService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"View service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse request body
|
|
|
|
|
+ nlohmann::json body;
|
|
|
|
|
+ try {
|
|
|
|
|
+ body = nlohmann::json::parse(req.body);
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& /*e*/) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ UpdateViewRequest request;
|
|
|
|
|
+ request.workspace_id = workspace_id;
|
|
|
|
|
+ request.id = view_id;
|
|
|
|
|
+
|
|
|
|
|
+ if (body.contains("name") && body["name"].is_string()) {
|
|
|
|
|
+ request.name = body["name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (body.contains("collection_name") && body["collection_name"].is_string()) {
|
|
|
|
|
+ request.collection_name = body["collection_name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse schema if provided
|
|
|
|
|
+ if (body.contains("schema") && body["schema"].is_object()) {
|
|
|
|
|
+ ViewSchema schema;
|
|
|
|
|
+ const auto& schema_json = body["schema"];
|
|
|
|
|
+ schema.title = schema_json.value("title", "");
|
|
|
|
|
+ schema.description = schema_json.value("description", "");
|
|
|
|
|
+ schema.layout = schema_json.value("layout", nlohmann::json::object());
|
|
|
|
|
+
|
|
|
|
|
+ if (schema_json.contains("fields") && schema_json["fields"].is_array()) {
|
|
|
|
|
+ for (const auto& field_json : schema_json["fields"]) {
|
|
|
|
|
+ SchemaField field;
|
|
|
|
|
+ field.name = field_json.value("name", "");
|
|
|
|
|
+ field.type = StringToFieldType(field_json.value("type", "text"));
|
|
|
|
|
+ field.label = field_json.value("label", "");
|
|
|
|
|
+ field.description = field_json.value("description", "");
|
|
|
|
|
+ field.required = field_json.value("required", false);
|
|
|
|
|
+ field.display_order = field_json.value("display_order", 0);
|
|
|
|
|
+ field.widget = field_json.value("widget", "");
|
|
|
|
|
+ field.group = field_json.value("group", "");
|
|
|
|
|
+ field.default_value = field_json.value("default_value", nlohmann::json());
|
|
|
|
|
+ field.options = field_json.value("options", nlohmann::json());
|
|
|
|
|
+ field.reference_collection = field_json.value("reference_collection", "");
|
|
|
|
|
+ field.computed_expression = field_json.value("computed_expression", "");
|
|
|
|
|
+ schema.fields.push_back(field);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ request.schema = schema;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse settings if provided
|
|
|
|
|
+ if (body.contains("settings") && body["settings"].is_object()) {
|
|
|
|
|
+ ViewSettings settings;
|
|
|
|
|
+ const auto& settings_json = body["settings"];
|
|
|
|
|
+ settings.is_default = settings_json.value("is_default", false);
|
|
|
|
|
+ settings.show_in_sidebar = settings_json.value("show_in_sidebar", true);
|
|
|
|
|
+ settings.icon = settings_json.value("icon", "");
|
|
|
|
|
+ settings.filters = settings_json.value("filters", nlohmann::json::object());
|
|
|
|
|
+ settings.sort = settings_json.value("sort", nlohmann::json::object());
|
|
|
|
|
+ request.settings = settings;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = viewService_->UpdateView(request);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success || !result.view) {
|
|
|
|
|
+ res.status = 400;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build response with updated view info
|
|
|
|
|
+ nlohmann::json view_json = {
|
|
|
|
|
+ {"id", result.view->id},
|
|
|
|
|
+ {"workspace_id", result.view->workspace_id},
|
|
|
|
|
+ {"name", result.view->name},
|
|
|
|
|
+ {"collection_name", result.view->collection_name},
|
|
|
|
|
+ {"created_at", result.view->created_at},
|
|
|
|
|
+ {"updated_at", result.view->updated_at}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Add schema
|
|
|
|
|
+ nlohmann::json schema_json = {
|
|
|
|
|
+ {"title", result.view->schema.title},
|
|
|
|
|
+ {"description", result.view->schema.description},
|
|
|
|
|
+ {"layout", result.view->schema.layout}
|
|
|
|
|
+ };
|
|
|
|
|
+ nlohmann::json fields_json = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& field : result.view->schema.fields) {
|
|
|
|
|
+ nlohmann::json field_json = {
|
|
|
|
|
+ {"name", field.name},
|
|
|
|
|
+ {"type", FieldTypeToString(field.type)},
|
|
|
|
|
+ {"label", field.label},
|
|
|
|
|
+ {"description", field.description},
|
|
|
|
|
+ {"required", field.required},
|
|
|
|
|
+ {"display_order", field.display_order},
|
|
|
|
|
+ {"widget", field.widget},
|
|
|
|
|
+ {"group", field.group}
|
|
|
|
|
+ };
|
|
|
|
|
+ if (!field.default_value.is_null()) {
|
|
|
|
|
+ field_json["default_value"] = field.default_value;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!field.options.is_null()) {
|
|
|
|
|
+ field_json["options"] = field.options;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!field.reference_collection.empty()) {
|
|
|
|
|
+ field_json["reference_collection"] = field.reference_collection;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!field.computed_expression.empty()) {
|
|
|
|
|
+ field_json["computed_expression"] = field.computed_expression;
|
|
|
|
|
+ }
|
|
|
|
|
+ fields_json.push_back(field_json);
|
|
|
|
|
+ }
|
|
|
|
|
+ schema_json["fields"] = fields_json;
|
|
|
|
|
+ view_json["schema"] = schema_json;
|
|
|
|
|
+
|
|
|
|
|
+ // Add settings
|
|
|
|
|
+ view_json["settings"] = {
|
|
|
|
|
+ {"is_default", result.view->settings.is_default},
|
|
|
|
|
+ {"show_in_sidebar", result.view->settings.show_in_sidebar},
|
|
|
|
|
+ {"icon", result.view->settings.icon},
|
|
|
|
|
+ {"filters", result.view->settings.filters},
|
|
|
|
|
+ {"sort", result.view->settings.sort}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(view_json.dump(), "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Update view error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void HttpServer::HandleDeleteView(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ // Authenticate the request
|
|
|
|
|
+ auto auth_user = AuthenticateRequest(req);
|
|
|
|
|
+ if (!auth_user) {
|
|
|
|
|
+ res.status = 401;
|
|
|
|
|
+ res.set_content(R"({"error":"Unauthorized"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract workspace ID and view ID from path
|
|
|
|
|
+ std::string workspace_id = req.matches[1].str();
|
|
|
|
|
+ std::string view_id = req.matches[2].str();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify workspace exists
|
|
|
|
|
+ auto ws_result = workspaceService_->GetWorkspace(workspace_id);
|
|
|
|
|
+ if (!ws_result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ res.set_content(R"({"error":"Workspace not found"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if user has access to this workspace
|
|
|
|
|
+ if (!IsSuperadmin(*auth_user)) {
|
|
|
|
|
+ bool has_access = false;
|
|
|
|
|
+ for (const auto& wid : auth_user->workspace_ids) {
|
|
|
|
|
+ if (wid == workspace_id) {
|
|
|
|
|
+ has_access = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_access) {
|
|
|
|
|
+ res.status = 403;
|
|
|
|
|
+ res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if ViewService is available
|
|
|
|
|
+ if (!viewService_) {
|
|
|
|
|
+ res.status = 503;
|
|
|
|
|
+ res.set_content(R"({"error":"View service not available"})", "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ auto result = viewService_->DeleteView(workspace_id, view_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ res.status = 404;
|
|
|
|
|
+ nlohmann::json error_response = {{"error", result.error}};
|
|
|
|
|
+ res.set_content(error_response.dump(), "application/json");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.set_content(R"({"message":"View deleted successfully"})", "application/json");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Delete view error: {}", e.what());
|
|
|
|
|
+ res.status = 500;
|
|
|
|
|
+ res.set_content(R"({"error":"Internal server error"})", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
} // namespace smartbotic::webserver
|
|
} // namespace smartbotic::webserver
|