#include "smartbotic/webserver/http_server.hpp" #include "smartbotic/webserver/permissions.hpp" #include #include #include #include #include #include #include #include #include namespace smartbotic::webserver { // ============================================================================ // WebSocketServer Implementation // ============================================================================ // Static instance pointer for callback access WebSocketServer* WebSocketServer::instance_ = nullptr; namespace { 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"; 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)]; } } return uuid; } } // namespace // 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; } WebSocketServer::~WebSocketServer() { Stop(); instance_ = nullptr; } void WebSocketServer::Start() { if (running_.load()) { spdlog::warn("WebSocket server already running"); return; } spdlog::info("Starting WebSocket server on port {}", port_); lws_context_creation_info info{}; std::memset(&info, 0, sizeof(info)); 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"; // Suppress verbose lws logging lws_set_log_level(LLL_ERR | LLL_WARN, nullptr); context_ = lws_create_context(&info); if (context_ == nullptr) { spdlog::error("Failed to create libwebsocket context"); return; } running_.store(true); eventLoopThread_ = std::thread(&WebSocketServer::RunEventLoop, this); spdlog::info("WebSocket server started on port {}", port_); } void WebSocketServer::Stop() { if (!running_.load()) { return; } spdlog::info("Stopping WebSocket server..."); running_.store(false); // 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_); } if (eventLoopThread_.joinable()) { eventLoopThread_.join(); } if (context_ != nullptr) { lws_context_destroy(context_); context_ = nullptr; } { std::lock_guard lock(connectionsMutex_); connections_.clear(); } spdlog::info("WebSocket server stopped"); } void WebSocketServer::RunEventLoop() { while (running_.load()) { lws_service(context_, 50); // 50ms timeout } } void WebSocketServer::Broadcast(const std::string& message) { std::lock_guard lock(connectionsMutex_); for (auto& [wsi, conn] : connections_) { QueueMessage(conn.get(), message); } } void WebSocketServer::SendToSession(const std::string& sessionId, const std::string& message) { std::lock_guard lock(connectionsMutex_); for (auto& [wsi, conn] : connections_) { if (conn->sessionId == sessionId) { QueueMessage(conn.get(), message); break; } } } void WebSocketServer::SendToSubscribers(const std::string& subscription_key, const std::string& message) { std::lock_guard 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); } } } auto WebSocketServer::ConnectionCount() const -> size_t { std::lock_guard lock(connectionsMutex_); return connections_.size(); } void WebSocketServer::SetMessageHandler(WebSocketMessageHandler handler) { messageHandler_ = std::move(handler); } void WebSocketServer::HandleConnect(lws* wsi) { auto conn = std::make_unique(); conn->wsi = wsi; conn->sessionId = GenerateSessionId(); spdlog::info("WebSocket client connected (session: {})", conn->sessionId); std::lock_guard lock(connectionsMutex_); connections_[wsi] = std::move(conn); } void WebSocketServer::HandleDisconnect(lws* wsi) { std::lock_guard lock(connectionsMutex_); auto it = connections_.find(wsi); if (it != connections_.end()) { spdlog::info("WebSocket client disconnected (session: {})", it->second->sessionId); connections_.erase(it); } } void WebSocketServer::HandleMessage(lws* wsi, const std::string& message) { WsConnection* conn = nullptr; { std::lock_guard lock(connectionsMutex_); auto it = connections_.find(wsi); if (it != connections_.end()) { conn = it->second.get(); } } // Call handler outside the lock to avoid deadlock with SendToSession if (conn && messageHandler_) { messageHandler_(conn, message); } } void WebSocketServer::QueueMessage(WsConnection* conn, const std::string& message) { std::lock_guard lock(conn->sendMutex); // Prepend LWS_PRE bytes for libwebsockets conn->sendBuffer.resize(LWS_PRE + message.size()); std::memcpy(conn->sendBuffer.data() + LWS_PRE, message.data(), message.size()); lws_callback_on_writable(conn->wsi); } auto WebSocketServer::WsCallback(lws* wsi, lws_callback_reasons reason, void* user, void* in, size_t len) -> int { if (instance_ == nullptr) { return 0; } 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(in), len); spdlog::debug("WebSocket received {} bytes", len); instance_->HandleMessage(wsi, message); break; } case LWS_CALLBACK_SERVER_WRITEABLE: { std::lock_guard lock(instance_->connectionsMutex_); auto it = instance_->connections_.find(wsi); if (it != instance_->connections_.end()) { auto& conn = it->second; std::lock_guard 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; } case LWS_CALLBACK_HTTP: // Return non-zero to close the connection for non-WebSocket HTTP requests return -1; case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION: // Allow the connection return 0; default: break; } return 0; } // ============================================================================ // HttpServer Implementation // ============================================================================ HttpServer::HttpServer(HttpServerConfig config) : config_(std::move(config)), httpServer_(std::make_unique()), dbClient_(std::make_unique(config_)), authService_(std::make_unique(config_.jwt)) {} HttpServer::~HttpServer() { if (running_.load()) { Stop(); } } auto HttpServer::Start(bool require_database) -> bool { if (running_.load()) { spdlog::warn("Server already running"); return true; } spdlog::info("Starting HTTP server on {}", config_.GetListenAddress()); // Connect to database first (fail-fast if required) if (require_database) { spdlog::info("Connecting to database at {} (fail-fast mode enabled)", config_.database_address); if (!ConnectToDatabase()) { spdlog::error("Failed to connect to database - server startup aborted"); return false; } } else { // Try to connect but don't fail if unavailable spdlog::info("Connecting to database at {} (optional mode)", config_.database_address); if (!ConnectToDatabase()) { spdlog::warn("Database connection failed - server will start without database connectivity"); } } // Initialize services (only if database is connected) if (IsDatabaseConnected()) { if (!InitializeServices()) { spdlog::error("Failed to initialize services - server startup aborted"); return false; } } // Setup routes SetupRoutes(); // Start WebSocket server on separate port wsServer_ = std::make_unique(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 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 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; } void HttpServer::Stop() { if (!running_.load()) { return; } spdlog::info("Stopping HTTP server..."); running_.store(false); // Stop WebSocket server first (this uses lws_cancel_service which is async-signal-safe) if (wsServer_) { wsServer_->Stop(); } // Stop HTTP server - this makes listen() return if (httpServer_) { httpServer_->stop(); } // Disconnect from database if (dbClient_) { dbClient_->Disconnect(); } // 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() { if (httpThread_.joinable()) { httpThread_.join(); } spdlog::info("HTTP server stopped"); } void HttpServer::BroadcastWebSocket(const std::string& message) { if (wsServer_) { wsServer_->Broadcast(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; } 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; } std::string event_str = event.dump(); spdlog::debug("Broadcasting document event: {} {} in {} (key={})", DocumentActionToString(action), document_id, collection, subscription_key); wsServer_->SendToSubscribers(subscription_key, event_str); } void HttpServer::SetWebSocketMessageHandler(WebSocketMessageHandler handler) { wsMessageHandler_ = std::move(handler); if (wsServer_) { wsServer_->SetMessageHandler(wsMessageHandler_); } } auto HttpServer::GetWebSocketClientCount() const -> size_t { return wsServer_ ? wsServer_->ConnectionCount() : 0; } auto HttpServer::IsDatabaseConnected() const -> bool { return dbClient_ && dbClient_->IsConnected(); } auto HttpServer::ConnectToDatabase() -> bool { if (!dbClient_) { dbClient_ = std::make_unique(config_); } // Try to connect if (!dbClient_->Connect()) { return false; } // Perform a health check to verify the connection is working auto health = dbClient_->HealthCheck(); if (!health.healthy) { spdlog::error("Database health check failed: {}", health.message); return false; } spdlog::info("Database health check passed (latency: {}ms)", health.latency.count()); return true; } auto HttpServer::InitializeServices() -> bool { // Initialize UserService userService_ = std::make_unique(*dbClient_); if (!userService_->Initialize()) { spdlog::error("Failed to initialize UserService"); return false; } spdlog::info("UserService initialized successfully"); // Initialize WorkspaceService workspaceService_ = std::make_unique(*dbClient_); if (!workspaceService_->Initialize()) { spdlog::error("Failed to initialize WorkspaceService"); return false; } spdlog::info("WorkspaceService initialized successfully"); // Initialize GroupService groupService_ = std::make_unique(*dbClient_); if (!groupService_->Initialize()) { spdlog::error("Failed to initialize GroupService"); return false; } spdlog::info("GroupService initialized successfully"); // Initialize MembershipService membershipService_ = std::make_unique(*dbClient_); if (!membershipService_->Initialize()) { spdlog::error("Failed to initialize MembershipService"); return false; } spdlog::info("MembershipService initialized successfully"); // Initialize ApiKeyService apiKeyService_ = std::make_unique(*dbClient_); if (!apiKeyService_->Initialize()) { spdlog::error("Failed to initialize ApiKeyService"); return false; } spdlog::info("ApiKeyService initialized successfully"); // Initialize CollectionService collectionService_ = std::make_unique(*dbClient_); spdlog::info("CollectionService initialized successfully"); // Initialize DocumentService documentService_ = std::make_unique(*dbClient_); spdlog::info("DocumentService initialized successfully"); // Initialize ViewService viewService_ = std::make_unique(*dbClient_); if (!viewService_->Initialize()) { spdlog::error("Failed to initialize ViewService"); return false; } spdlog::info("ViewService initialized successfully"); // Initialize PageService pageService_ = std::make_unique(*dbClient_); if (!pageService_->Initialize()) { spdlog::error("Failed to initialize PageService"); return false; } spdlog::info("PageService initialized successfully"); // Initialize AuthorizationService authorizationService_ = std::make_unique(*groupService_, *collectionService_); if (!authorizationService_->Initialize()) { spdlog::error("Failed to initialize AuthorizationService"); return false; } spdlog::info("AuthorizationService initialized successfully"); // Initialize WsHandler for WebSocket real-time updates wsHandler_ = std::make_unique(*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 page routes (page builder) SetupPageRoutes(); // 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(); user_request.password = body["password"].get(); 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; } // Get the superadmin group to assign to the first user std::string superadmin_group_id; if (groupService_) { auto sa_group = groupService_->GetSuperadminGroup(); if (sa_group.success && sa_group.group) { superadmin_group_id = sa_group.group->id; spdlog::info("Found superadmin group: {}", superadmin_group_id); } else { spdlog::warn("Superadmin group not found - first user will not have admin privileges"); } } // 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 with superadmin group 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; // Assign the superadmin group to the first user if (!superadmin_group_id.empty()) { member_request.group_ids = {superadmin_group_id}; spdlog::info("Assigning superadmin group to bootstrap user"); } else { 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 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 password = body["password"].get(); 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 workspace_ids; if (membershipService_) { auto memberships = membershipService_->ListUserMemberships(user.id); for (const auto& m : memberships.members) { workspace_ids.push_back(m.workspace_id); } } // Collect user's groups from memberships // Note: Superadmin status is determined by group membership, not by special logic // The first user gets superadmin group assigned during bootstrap std::vector groups; if (membershipService_) { auto memberships = membershipService_->ListUserMemberships(user.id); for (const auto& m : memberships.members) { for (const auto& gid : m.group_ids) { if (std::find(groups.begin(), groups.end(), gid) == groups.end()) { groups.push_back(gid); } } } } 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(); 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(); 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 { 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; } // Verify the user still exists in the database // This handles cases where the database was reset or user was deleted if (userService_ && validation.user) { auto user_result = userService_->GetUser(validation.user->user_id); if (!user_result.success || !user_result.user) { spdlog::debug("Token user no longer exists in database: {}", validation.user->user_id); 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 { // Delegate to authorization service for proper permission checking // This maintains backward compatibility while using the new RBAC system if (authorizationService_) { return authorizationService_->IsSuperadmin(user); } // Fallback to old behavior if authorization service not available return std::find(user.groups.begin(), user.groups.end(), "superadmin") != user.groups.end(); } auto HttpServer::ResolveUserName(const std::string& user_id) -> std::string { if (user_id.empty() || !userService_) { return ""; } auto result = userService_->GetUser(user_id, true); if (result.success && result.user) { return result.user->name; } return ""; } 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; } // Check permission to create users if (!authorizationService_ || !authorizationService_->HasPermission(*auth_user, smartbotic::permissions::kUsersCreate)) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:users:create permission"})", "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; create_req.actor_id = auth_user->user_id; if (body.contains("email")) { create_req.email = body["email"].get(); } if (body.contains("password")) { create_req.password = body["password"].get(); } if (body.contains("name")) { create_req.name = body["name"].get(); } 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; } // Requires system:users:read permission if (!authorizationService_->HasPermission(*auth_user, permissions::kUsersRead)) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:users:read permission"})", "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) { nlohmann::json user_obj = { {"id", user.id}, {"email", user.email}, {"name", user.name}, {"created_at", user.created_at}, {"updated_at", user.updated_at}, {"deleted_at", user.deleted_at}, {"created_by", user.created_by}, {"updated_by", user.updated_by}, {"created_by_name", ResolveUserName(user.created_by)}, {"updated_by_name", ResolveUserName(user.updated_by)} }; users_array.push_back(user_obj); } 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, or require system:users:read permission bool is_own = (auth_user->user_id == user_id); if (!is_own && !authorizationService_->HasPermission(*auth_user, permissions::kUsersRead)) { res.status = 403; res.set_content(R"({"error":"Forbidden - can only access your own user data or requires system:users:read permission"})", "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, or require system:users:update permission bool is_own = (auth_user->user_id == user_id); if (!is_own && !authorizationService_->HasPermission(*auth_user, permissions::kUsersUpdate)) { res.status = 403; res.set_content(R"({"error":"Forbidden - can only update your own user data or requires system:users:update permission"})", "application/json"); return; } auto body = nlohmann::json::parse(req.body); UpdateUserRequest update_req; update_req.actor_id = auth_user->user_id; if (body.contains("email")) { update_req.email = body["email"].get(); } if (body.contains("password")) { update_req.password = body["password"].get(); } if (body.contains("name")) { update_req.name = body["name"].get(); } 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; } // Requires system:users:delete permission if (!authorizationService_->HasPermission(*auth_user, permissions::kUsersDelete)) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:users:delete permission"})", "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; } // Requires system:workspaces:create permission if (!authorizationService_->HasPermission(*auth_user, permissions::kWorkspacesCreate)) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:workspaces:create permission"})", "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; create_req.actor_id = auth_user->user_id; if (body.contains("name")) { create_req.name = body["name"].get(); } 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; bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kWorkspacesRead); // Users with system:workspaces:read can see all workspaces, others only see their memberships if (has_system_access) { // 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-privileged user: 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}, {"created_by", workspace.created_by}, {"updated_by", workspace.updated_by}, {"created_by_name", ResolveUserName(workspace.created_by)}, {"updated_by_name", ResolveUserName(workspace.updated_by)} }; if (has_system_access) { 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(); // Check access: must be member OR have system:workspaces:read permission bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kWorkspacesRead); if (!is_member && !has_system_access) { 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; } // Extract workspace ID from path (needed for IsWorkspaceAdmin check) std::string workspace_id = req.matches[1].str(); // Requires system:workspaces:update permission OR workspace admin bool can_update = authorizationService_->HasPermission(*auth_user, permissions::kWorkspacesUpdate) || authorizationService_->IsWorkspaceAdmin(*auth_user, workspace_id); if (!can_update) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:workspaces:update permission or workspace admin"})", "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); UpdateWorkspaceRequest update_req; update_req.actor_id = auth_user->user_id; if (body.contains("name")) { update_req.name = body["name"].get(); } 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; } // Requires system:workspaces:delete permission if (!authorizationService_->HasPermission(*auth_user, permissions::kWorkspacesDelete)) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:workspaces:delete permission"})", "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(); // Requires system:groups:create permission OR workspace group management permission bool can_create = authorizationService_->HasPermission(*auth_user, permissions::kGroupsCreate) || authorizationService_->CanManageWorkspaceGroups(*auth_user, workspace_id); if (!can_create) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:groups:create permission or workspace group management"})", "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; create_req.actor_id = auth_user->user_id; if (body.contains("name")) { create_req.name = body["name"].get(); } 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()); } } } 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 access: must be member OR have system:groups:read permission bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kGroupsRead); if (!is_member && !has_system_access) { 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 (has_system_access && req.has_param("include_deleted")) { include_deleted = req.get_param_value("include_deleted") == "true"; } bool include_system = has_system_access && req.has_param("include_system") && req.get_param_value("include_system") == "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(); // Add global system groups first if requested if (include_system) { auto global_result = groupService_->ListSystemGroups(); if (global_result.success) { for (const auto& group : global_result.groups) { nlohmann::json grp_json = { {"id", group.id}, {"workspace_id", group.workspace_id}, {"name", group.name}, {"permissions", group.permissions}, {"is_system", group.is_system}, {"parent_group_id", group.parent_group_id}, {"created_at", group.created_at}, {"updated_at", group.updated_at}, {"deleted_at", group.deleted_at}, {"created_by", group.created_by}, {"updated_by", group.updated_by}, {"created_by_name", ResolveUserName(group.created_by)}, {"updated_by_name", ResolveUserName(group.updated_by)} }; groups_array.push_back(grp_json); } } } for (const auto& group : result.groups) { nlohmann::json grp_json = { {"id", group.id}, {"workspace_id", group.workspace_id}, {"name", group.name}, {"permissions", group.permissions}, {"is_system", group.is_system}, {"parent_group_id", group.parent_group_id}, {"created_at", group.created_at}, {"updated_at", group.updated_at}, {"created_by", group.created_by}, {"updated_by", group.updated_by}, {"created_by_name", ResolveUserName(group.created_by)}, {"updated_by_name", ResolveUserName(group.updated_by)} }; if (has_system_access) { 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 access: must be member OR have system:groups:read permission bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kGroupsRead); if (!is_member && !has_system_access) { 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(); // Requires system:groups:update permission OR workspace group management permission bool can_update = authorizationService_->HasPermission(*auth_user, permissions::kGroupsUpdate) || authorizationService_->CanManageWorkspaceGroups(*auth_user, workspace_id); if (!can_update) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:groups:update permission or workspace group management"})", "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; update_req.actor_id = auth_user->user_id; if (body.contains("name")) { update_req.name = body["name"].get(); } if (body.contains("permissions") && body["permissions"].is_array()) { std::vector perms; for (const auto& perm : body["permissions"]) { if (perm.is_string()) { perms.push_back(perm.get()); } } 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(); // Requires system:groups:delete permission OR workspace group management permission bool can_delete = authorizationService_->HasPermission(*auth_user, permissions::kGroupsDelete) || authorizationService_->CanManageWorkspaceGroups(*auth_user, workspace_id); if (!can_delete) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:groups:delete permission or workspace group management"})", "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(); // Requires system:memberships:create permission OR workspace member management permission bool can_add = authorizationService_->HasPermission(*auth_user, permissions::kMembershipsCreate) || authorizationService_->CanManageWorkspaceMembers(*auth_user, workspace_id); if (!can_add) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:memberships:create permission or workspace member management"})", "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(); } 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()); } } } 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: must be member OR have system:memberships:read permission bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kMembershipsRead); if (!is_member && !has_system_access) { 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 (has_system_access && 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 (has_system_access) { 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: must be member OR have system:memberships:read permission bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kMembershipsRead); if (!is_member && !has_system_access) { 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(); // Requires system:memberships:update permission OR workspace member management permission bool can_update = authorizationService_->HasPermission(*auth_user, permissions::kMembershipsUpdate) || authorizationService_->CanManageWorkspaceMembers(*auth_user, workspace_id); if (!can_update) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:memberships:update permission or workspace member management"})", "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 group_ids; for (const auto& item : json["group_ids"]) { if (item.is_string()) { group_ids.push_back(item.get()); } } 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(); // Requires system:memberships:delete permission OR workspace member management permission bool can_remove = authorizationService_->HasPermission(*auth_user, permissions::kMembershipsDelete) || authorizationService_->CanManageWorkspaceMembers(*auth_user, workspace_id); if (!can_remove) { res.status = 403; res.set_content(R"({"error":"Forbidden - requires system:memberships:delete permission or workspace member management"})", "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: own OR system:api_keys:create permission bool is_own = (auth_user->user_id == target_user_id); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kApiKeysCreate); if (!is_own && !has_system_access) { res.status = 403; res.set_content(R"({"error":"Forbidden - can only create API keys for yourself or requires system:api_keys:create permission"})", "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(); } 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(); // Users can only grant permissions they have (unless they have system access) if (!has_system_access) { // 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(); } 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: own OR system:api_keys:read permission bool is_own = (auth_user->user_id == target_user_id); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kApiKeysRead); if (!is_own && !has_system_access) { res.status = 403; res.set_content(R"({"error":"Forbidden - can only list your own API keys or requires system:api_keys:read permission"})", "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 (requires system access) bool include_revoked = false; if (has_system_access && 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 (has_system_access) { 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: own OR system:api_keys:delete permission bool is_own = (auth_user->user_id == target_user_id); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kApiKeysDelete); if (!is_own && !has_system_access) { res.status = 403; res.set_content(R"({"error":"Forbidden - can only revoke your own API keys or requires system:api_keys:delete permission"})", "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; } // Requires system:collections:create permission OR workspace membership with manage_collections bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kCollectionsCreate); if (!is_member && !has_system_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(); } 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(); } 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()); } } } if (json.contains("ttl_seconds") && json["ttl_seconds"].is_number_integer()) { create_req.settings.ttl_seconds = json["ttl_seconds"].get(); } 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 access: must be member OR have system:collections:read permission bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kCollectionsRead); if (!is_member && !has_system_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 user with system access wants to include system collections bool include_system = has_system_access && 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}, {"created_by", coll.created_by}, {"updated_by", coll.updated_by}, {"created_by_name", ResolveUserName(coll.created_by)}, {"updated_by_name", ResolveUserName(coll.updated_by)}, {"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}, {"created_by", coll.created_by}, {"updated_by", coll.updated_by}, {"created_by_name", ResolveUserName(coll.created_by)}, {"updated_by_name", ResolveUserName(coll.updated_by)}, {"settings", nlohmann::json::object()} }; collections_array.push_back(coll_json); } } } nlohmann::json response = { {"collections", collections_array}, {"total_count", static_cast(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 access: must be member OR have system:collections:read permission bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kCollectionsRead); if (!is_member && !has_system_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; } // Requires system:collections:update permission OR workspace membership bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kCollectionsUpdate); if (!is_member && !has_system_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(); } 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()); } } } if (json.contains("ttl_seconds") && json["ttl_seconds"].is_number_integer()) { settings.ttl_seconds = json["ttl_seconds"].get(); } 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); }); // PATCH /api/workspaces/:wid/collections/:name/documents/:id - Update a document (partial) httpServer_->Patch(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 document create permission using AuthorizationService if (!authorizationService_->CanCreateDocument(*auth_user, workspace_id, collection_name)) { res.status = 403; res.set_content(R"({"error":"Forbidden - no permission to create documents in this collection"})", "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; create_req.user_id = auth_user->user_id; // Set user for _created_by tracking if (json.contains("id") && json["id"].is_string()) { create_req.id = json["id"].get(); } 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 collection read permission using AuthorizationService if (!authorizationService_->CanReadCollection(*auth_user, workspace_id, collection_name)) { res.status = 403; res.set_content(R"({"error":"Forbidden - no permission to read documents in this collection"})", "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) { // Resolve user names for _created_by and _updated_by nlohmann::json enriched_data = doc.data; if (doc.data.contains("_created_by") && doc.data["_created_by"].is_string()) { std::string created_by_name = ResolveUserName(doc.data["_created_by"].get()); if (!created_by_name.empty()) { enriched_data["_created_by_name"] = created_by_name; } } if (doc.data.contains("_updated_by") && doc.data["_updated_by"].is_string()) { std::string updated_by_name = ResolveUserName(doc.data["_updated_by"].get()); if (!updated_by_name.empty()) { enriched_data["_updated_by_name"] = updated_by_name; } } nlohmann::json doc_json = { {"id", doc.id}, {"collection", doc.collection}, {"workspace_id", doc.workspace_id}, {"data", enriched_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 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; } // Check document read permission with ownership std::string doc_owner = result.document->data.value("_created_by", ""); if (!authorizationService_->CanReadDocument(*auth_user, workspace_id, collection_name, doc_owner)) { res.status = 403; res.set_content(R"({"error":"Forbidden - no permission to read this document"})", "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 DocumentService is available if (!documentService_) { res.status = 503; res.set_content(R"({"error":"Document service not available"})", "application/json"); return; } try { // First get the document to check ownership auto existing = documentService_->GetDocument(workspace_id, collection_name, document_id); if (!existing.success) { res.status = 404; nlohmann::json error_response = {{"error", existing.error}}; res.set_content(error_response.dump(), "application/json"); return; } // Check document write permission with ownership std::string doc_owner = existing.document->data.value("_created_by", ""); if (!authorizationService_->CanWriteDocument(*auth_user, workspace_id, collection_name, doc_owner)) { res.status = 403; res.set_content(R"({"error":"Forbidden - no permission to update this document"})", "application/json"); return; } // 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; update_req.user_id = auth_user->user_id; // Set user for _updated_by tracking 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(); } if (json.contains("expected_version") && json["expected_version"].is_number_integer()) { update_req.expected_version = json["expected_version"].get(); } 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 DocumentService is available if (!documentService_) { res.status = 503; res.set_content(R"({"error":"Document service not available"})", "application/json"); return; } try { // First get the document to check ownership auto existing = documentService_->GetDocument(workspace_id, collection_name, document_id); if (!existing.success) { res.status = 404; nlohmann::json error_response = {{"error", existing.error}}; res.set_content(error_response.dump(), "application/json"); return; } // Check document delete permission with ownership std::string doc_owner = existing.document->data.value("_created_by", ""); if (!authorizationService_->CanDeleteDocument(*auth_user, workspace_id, collection_name, doc_owner)) { res.status = 403; res.set_content(R"({"error":"Forbidden - no permission to delete this document"})", "application/json"); return; } 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; } // Requires system:collections:delete permission OR workspace membership bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kCollectionsDelete); if (!is_member && !has_system_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; } // Requires system:views:create permission OR workspace membership bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kViewsCreate); if (!is_member && !has_system_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(); request.collection_name = body["collection_name"].get(); // 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.show_create_button = settings_json.value("show_create_button", 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()); request.settings.quick_create_mode = settings_json.value("quick_create_mode", "modal"); request.settings.quick_edit_mode = settings_json.value("quick_edit_mode", "modal"); request.settings.show_edit_button = settings_json.value("show_edit_button", true); if (settings_json.contains("quick_create_fields") && settings_json["quick_create_fields"].is_array()) { for (const auto& field : settings_json["quick_create_fields"]) { if (field.is_string()) { request.settings.quick_create_fields.push_back(field.get()); } } } if (settings_json.contains("quick_edit_fields") && settings_json["quick_edit_fields"].is_array()) { for (const auto& field : settings_json["quick_edit_fields"]) { if (field.is_string()) { request.settings.quick_edit_fields.push_back(field.get()); } } } } 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 nlohmann::json create_settings_json = { {"is_default", result.view->settings.is_default}, {"show_in_sidebar", result.view->settings.show_in_sidebar}, {"show_create_button", result.view->settings.show_create_button}, {"show_edit_button", result.view->settings.show_edit_button}, {"icon", result.view->settings.icon}, {"filters", result.view->settings.filters}, {"sort", result.view->settings.sort}, {"quick_create_mode", result.view->settings.quick_create_mode}, {"quick_edit_mode", result.view->settings.quick_edit_mode} }; if (!result.view->settings.quick_create_fields.empty()) { create_settings_json["quick_create_fields"] = result.view->settings.quick_create_fields; } if (!result.view->settings.quick_edit_fields.empty()) { create_settings_json["quick_edit_fields"] = result.view->settings.quick_edit_fields; } view_json["settings"] = create_settings_json; 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 access: must be member OR have system:views:read permission bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kViewsRead); if (!is_member && !has_system_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 nlohmann::json settings_json = { {"is_default", view.settings.is_default}, {"show_in_sidebar", view.settings.show_in_sidebar}, {"show_create_button", view.settings.show_create_button}, {"show_edit_button", view.settings.show_edit_button}, {"icon", view.settings.icon}, {"quick_create_mode", view.settings.quick_create_mode}, {"quick_edit_mode", view.settings.quick_edit_mode} }; if (!view.settings.quick_create_fields.empty()) { settings_json["quick_create_fields"] = view.settings.quick_create_fields; } if (!view.settings.quick_edit_fields.empty()) { settings_json["quick_edit_fields"] = view.settings.quick_edit_fields; } view_json["settings"] = settings_json; 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 access: must be member OR have system:views:read permission bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kViewsRead); if (!is_member && !has_system_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 nlohmann::json settings_json = { {"is_default", result.view->settings.is_default}, {"show_in_sidebar", result.view->settings.show_in_sidebar}, {"show_create_button", result.view->settings.show_create_button}, {"show_edit_button", result.view->settings.show_edit_button}, {"icon", result.view->settings.icon}, {"filters", result.view->settings.filters}, {"sort", result.view->settings.sort}, {"quick_create_mode", result.view->settings.quick_create_mode}, {"quick_edit_mode", result.view->settings.quick_edit_mode} }; if (!result.view->settings.quick_create_fields.empty()) { settings_json["quick_create_fields"] = result.view->settings.quick_create_fields; } if (!result.view->settings.quick_edit_fields.empty()) { settings_json["quick_edit_fields"] = result.view->settings.quick_edit_fields; } view_json["settings"] = settings_json; 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; } // Requires system:views:update permission OR workspace membership bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kViewsUpdate); if (!is_member && !has_system_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(); } if (body.contains("collection_name") && body["collection_name"].is_string()) { request.collection_name = body["collection_name"].get(); } // 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.show_create_button = settings_json.value("show_create_button", 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()); settings.quick_create_mode = settings_json.value("quick_create_mode", "modal"); settings.quick_edit_mode = settings_json.value("quick_edit_mode", "modal"); settings.show_edit_button = settings_json.value("show_edit_button", true); if (settings_json.contains("quick_create_fields") && settings_json["quick_create_fields"].is_array()) { for (const auto& field : settings_json["quick_create_fields"]) { if (field.is_string()) { settings.quick_create_fields.push_back(field.get()); } } } if (settings_json.contains("quick_edit_fields") && settings_json["quick_edit_fields"].is_array()) { for (const auto& field : settings_json["quick_edit_fields"]) { if (field.is_string()) { settings.quick_edit_fields.push_back(field.get()); } } } 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 nlohmann::json settings_json = { {"is_default", result.view->settings.is_default}, {"show_in_sidebar", result.view->settings.show_in_sidebar}, {"show_create_button", result.view->settings.show_create_button}, {"show_edit_button", result.view->settings.show_edit_button}, {"icon", result.view->settings.icon}, {"filters", result.view->settings.filters}, {"sort", result.view->settings.sort}, {"quick_create_mode", result.view->settings.quick_create_mode}, {"quick_edit_mode", result.view->settings.quick_edit_mode} }; if (!result.view->settings.quick_create_fields.empty()) { settings_json["quick_create_fields"] = result.view->settings.quick_create_fields; } if (!result.view->settings.quick_edit_fields.empty()) { settings_json["quick_edit_fields"] = result.view->settings.quick_edit_fields; } view_json["settings"] = settings_json; 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; } // Requires system:views:delete permission OR workspace membership bool is_member = (std::find(auth_user->workspace_ids.begin(), auth_user->workspace_ids.end(), workspace_id) != auth_user->workspace_ids.end()); bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kViewsDelete); if (!is_member && !has_system_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"); } } // ============================================================================ // Page Routes // ============================================================================ void HttpServer::SetupPageRoutes() { // POST /api/workspaces/:workspace_id/pages - Create a new page httpServer_->Post(R"(/api/workspaces/([^/]+)/pages$)", [this](const httplib::Request& req, httplib::Response& res) { HandleCreatePage(req, res); }); // GET /api/workspaces/:workspace_id/pages - List all pages httpServer_->Get(R"(/api/workspaces/([^/]+)/pages$)", [this](const httplib::Request& req, httplib::Response& res) { HandleListPages(req, res); }); // GET /api/workspaces/:workspace_id/pages/sidebar - List sidebar pages httpServer_->Get(R"(/api/workspaces/([^/]+)/pages/sidebar$)", [this](const httplib::Request& req, httplib::Response& res) { HandleListSidebarPages(req, res); }); // GET /api/workspaces/:workspace_id/pages/slug/:slug - Get page by slug httpServer_->Get(R"(/api/workspaces/([^/]+)/pages/slug/([^/]+)$)", [this](const httplib::Request& req, httplib::Response& res) { HandleGetPageBySlug(req, res); }); // GET /api/workspaces/:workspace_id/pages/:page_id - Get a specific page httpServer_->Get(R"(/api/workspaces/([^/]+)/pages/([^/]+)$)", [this](const httplib::Request& req, httplib::Response& res) { HandleGetPage(req, res); }); // PATCH /api/workspaces/:workspace_id/pages/:page_id - Update a page httpServer_->Patch(R"(/api/workspaces/([^/]+)/pages/([^/]+)$)", [this](const httplib::Request& req, httplib::Response& res) { HandleUpdatePage(req, res); }); // PATCH /api/workspaces/:workspace_id/pages/:page_id/share - Update page sharing httpServer_->Patch(R"(/api/workspaces/([^/]+)/pages/([^/]+)/share$)", [this](const httplib::Request& req, httplib::Response& res) { HandleUpdatePageSharing(req, res); }); // DELETE /api/workspaces/:workspace_id/pages/:page_id - Delete a page httpServer_->Delete(R"(/api/workspaces/([^/]+)/pages/([^/]+)$)", [this](const httplib::Request& req, httplib::Response& res) { HandleDeletePage(req, res); }); spdlog::info("Page routes configured"); } void HttpServer::HandleCreatePage(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 permission to create pages if (!authorizationService_->CanCreatePage(*auth_user, workspace_id)) { res.status = 403; res.set_content(R"({"error":"Forbidden - insufficient permissions to create pages"})", "application/json"); return; } // Check if PageService is available if (!pageService_) { res.status = 503; res.set_content(R"({"error":"Page 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; } try { CreatePageRequest request; request.workspace_id = workspace_id; request.name = body["name"].get(); request.slug = body.value("slug", ""); request.created_by = auth_user->user_id; // Set owner to current user // Parse layout if provided if (body.contains("layout") && body["layout"].is_object()) { const auto& layout_json = body["layout"]; request.layout.version = layout_json.value("version", 1); request.layout.grid_columns = layout_json.value("grid_columns", 12); if (layout_json.contains("components") && layout_json["components"].is_array()) { for (const auto& comp_json : layout_json["components"]) { LayoutComponent comp; comp.id = comp_json.value("id", ""); comp.type = comp_json.value("type", ""); comp.config = comp_json.value("config", nlohmann::json::object()); if (comp_json.contains("position") && comp_json["position"].is_object()) { const auto& pos = comp_json["position"]; comp.position.x = pos.value("x", 0); comp.position.y = pos.value("y", 0); comp.position.width = pos.value("width", 12); comp.position.height = pos.value("height", 1); } request.layout.components.push_back(comp); } } } // Parse settings if provided if (body.contains("settings") && body["settings"].is_object()) { const auto& settings_json = body["settings"]; request.settings.show_in_sidebar = settings_json.value("show_in_sidebar", true); request.settings.icon = settings_json.value("icon", ""); request.settings.menu_order = settings_json.value("menu_order", 0); } auto result = pageService_->CreatePage(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 page info nlohmann::json page_json = { {"id", result.page->id}, {"workspace_id", result.page->workspace_id}, {"name", result.page->name}, {"slug", result.page->slug}, {"created_at", result.page->created_at}, {"updated_at", result.page->updated_at}, {"created_by", result.page->created_by}, {"shared_with_groups", result.page->shared_with_groups} }; // Add layout nlohmann::json layout_json = { {"version", result.page->layout.version}, {"grid_columns", result.page->layout.grid_columns} }; nlohmann::json components_json = nlohmann::json::array(); for (const auto& comp : result.page->layout.components) { nlohmann::json comp_json = { {"id", comp.id}, {"type", comp.type}, {"position", { {"x", comp.position.x}, {"y", comp.position.y}, {"width", comp.position.width}, {"height", comp.position.height} }}, {"config", comp.config} }; components_json.push_back(comp_json); } layout_json["components"] = components_json; page_json["layout"] = layout_json; // Add settings page_json["settings"] = { {"show_in_sidebar", result.page->settings.show_in_sidebar}, {"icon", result.page->settings.icon}, {"menu_order", result.page->settings.menu_order} }; res.status = 201; res.set_content(page_json.dump(), "application/json"); } catch (const std::exception& e) { spdlog::error("Create page error: {}", e.what()); res.status = 500; res.set_content(R"({"error":"Internal server error"})", "application/json"); } } void HttpServer::HandleListPages(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 PageService is available if (!pageService_) { res.status = 503; res.set_content(R"({"error":"Page service not available"})", "application/json"); return; } try { auto result = pageService_->ListPages(workspace_id); if (!result.success) { res.status = 500; nlohmann::json error_response = {{"error", result.error}}; res.set_content(error_response.dump(), "application/json"); return; } // Filter pages based on user permissions nlohmann::json pages_json = nlohmann::json::array(); for (const auto& page : result.pages) { // Check if user can view this page if (!authorizationService_->CanViewPage(*auth_user, workspace_id, page.created_by, page.shared_with_groups)) { continue; // Skip pages user cannot view } nlohmann::json page_json = { {"id", page.id}, {"workspace_id", page.workspace_id}, {"name", page.name}, {"slug", page.slug}, {"created_at", page.created_at}, {"updated_at", page.updated_at}, {"created_by", page.created_by}, {"shared_with_groups", page.shared_with_groups}, {"settings", { {"show_in_sidebar", page.settings.show_in_sidebar}, {"icon", page.settings.icon}, {"menu_order", page.settings.menu_order} }} }; pages_json.push_back(page_json); } nlohmann::json response = {{"pages", pages_json}}; res.set_content(response.dump(), "application/json"); } catch (const std::exception& e) { spdlog::error("List pages error: {}", e.what()); res.status = 500; res.set_content(R"({"error":"Internal server error"})", "application/json"); } } void HttpServer::HandleListSidebarPages(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 PageService is available if (!pageService_) { res.status = 503; res.set_content(R"({"error":"Page service not available"})", "application/json"); return; } try { auto result = pageService_->ListSidebarPages(workspace_id); if (!result.success) { res.status = 500; nlohmann::json error_response = {{"error", result.error}}; res.set_content(error_response.dump(), "application/json"); return; } // Filter pages based on user permissions nlohmann::json pages_json = nlohmann::json::array(); for (const auto& page : result.pages) { // Check if user can view this page if (!authorizationService_->CanViewPage(*auth_user, workspace_id, page.created_by, page.shared_with_groups)) { continue; // Skip pages user cannot view } nlohmann::json page_json = { {"id", page.id}, {"workspace_id", page.workspace_id}, {"name", page.name}, {"slug", page.slug}, {"created_by", page.created_by}, {"settings", { {"show_in_sidebar", page.settings.show_in_sidebar}, {"icon", page.settings.icon}, {"menu_order", page.settings.menu_order} }} }; pages_json.push_back(page_json); } nlohmann::json response = {{"pages", pages_json}}; res.set_content(response.dump(), "application/json"); } catch (const std::exception& e) { spdlog::error("List sidebar pages error: {}", e.what()); res.status = 500; res.set_content(R"({"error":"Internal server error"})", "application/json"); } } void HttpServer::HandleGetPageBySlug(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 slug from path std::string workspace_id = req.matches[1].str(); std::string slug = req.matches[2].str(); // Check if PageService is available if (!pageService_) { res.status = 503; res.set_content(R"({"error":"Page service not available"})", "application/json"); return; } try { auto result = pageService_->GetPageBySlug(workspace_id, slug); if (!result.success) { res.status = 404; nlohmann::json error_response = {{"error", result.error}}; res.set_content(error_response.dump(), "application/json"); return; } // Check if user can view this page if (!authorizationService_->CanViewPage(*auth_user, workspace_id, result.page->created_by, result.page->shared_with_groups)) { res.status = 403; res.set_content(R"({"error":"Forbidden - no access to this page"})", "application/json"); return; } // Build full response with layout nlohmann::json page_json = { {"id", result.page->id}, {"workspace_id", result.page->workspace_id}, {"name", result.page->name}, {"slug", result.page->slug}, {"created_at", result.page->created_at}, {"updated_at", result.page->updated_at}, {"created_by", result.page->created_by}, {"shared_with_groups", result.page->shared_with_groups} }; // Add layout nlohmann::json layout_json = { {"version", result.page->layout.version}, {"grid_columns", result.page->layout.grid_columns} }; nlohmann::json components_json = nlohmann::json::array(); for (const auto& comp : result.page->layout.components) { nlohmann::json comp_json = { {"id", comp.id}, {"type", comp.type}, {"position", { {"x", comp.position.x}, {"y", comp.position.y}, {"width", comp.position.width}, {"height", comp.position.height} }}, {"config", comp.config} }; components_json.push_back(comp_json); } layout_json["components"] = components_json; page_json["layout"] = layout_json; // Add settings page_json["settings"] = { {"show_in_sidebar", result.page->settings.show_in_sidebar}, {"icon", result.page->settings.icon}, {"menu_order", result.page->settings.menu_order} }; res.set_content(page_json.dump(), "application/json"); } catch (const std::exception& e) { spdlog::error("Get page by slug error: {}", e.what()); res.status = 500; res.set_content(R"({"error":"Internal server error"})", "application/json"); } } void HttpServer::HandleGetPage(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 page ID from path std::string workspace_id = req.matches[1].str(); std::string page_id = req.matches[2].str(); // Check if PageService is available if (!pageService_) { res.status = 503; res.set_content(R"({"error":"Page service not available"})", "application/json"); return; } try { auto result = pageService_->GetPage(workspace_id, page_id); if (!result.success) { res.status = 404; nlohmann::json error_response = {{"error", result.error}}; res.set_content(error_response.dump(), "application/json"); return; } // Check if user can view this page if (!authorizationService_->CanViewPage(*auth_user, workspace_id, result.page->created_by, result.page->shared_with_groups)) { res.status = 403; res.set_content(R"({"error":"Forbidden - no access to this page"})", "application/json"); return; } // Build full response with layout nlohmann::json page_json = { {"id", result.page->id}, {"workspace_id", result.page->workspace_id}, {"name", result.page->name}, {"slug", result.page->slug}, {"created_at", result.page->created_at}, {"updated_at", result.page->updated_at}, {"created_by", result.page->created_by}, {"shared_with_groups", result.page->shared_with_groups} }; // Add layout nlohmann::json layout_json = { {"version", result.page->layout.version}, {"grid_columns", result.page->layout.grid_columns} }; nlohmann::json components_json = nlohmann::json::array(); for (const auto& comp : result.page->layout.components) { nlohmann::json comp_json = { {"id", comp.id}, {"type", comp.type}, {"position", { {"x", comp.position.x}, {"y", comp.position.y}, {"width", comp.position.width}, {"height", comp.position.height} }}, {"config", comp.config} }; components_json.push_back(comp_json); } layout_json["components"] = components_json; page_json["layout"] = layout_json; // Add settings page_json["settings"] = { {"show_in_sidebar", result.page->settings.show_in_sidebar}, {"icon", result.page->settings.icon}, {"menu_order", result.page->settings.menu_order} }; res.set_content(page_json.dump(), "application/json"); } catch (const std::exception& e) { spdlog::error("Get page error: {}", e.what()); res.status = 500; res.set_content(R"({"error":"Internal server error"})", "application/json"); } } void HttpServer::HandleUpdatePage(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 page ID from path std::string workspace_id = req.matches[1].str(); std::string page_id = req.matches[2].str(); // Check if PageService is available if (!pageService_) { res.status = 503; res.set_content(R"({"error":"Page service not available"})", "application/json"); return; } // Get existing page to check ownership auto existing = pageService_->GetPage(workspace_id, page_id); if (!existing.success || !existing.page) { res.status = 404; res.set_content(R"({"error":"Page not found"})", "application/json"); return; } // Check permission to edit if (!authorizationService_->CanEditPage(*auth_user, workspace_id, existing.page->created_by)) { res.status = 403; res.set_content(R"({"error":"Forbidden - insufficient permissions to edit this page"})", "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 { UpdatePageRequest request; request.workspace_id = workspace_id; request.id = page_id; if (body.contains("name") && body["name"].is_string()) { request.name = body["name"].get(); } if (body.contains("slug") && body["slug"].is_string()) { request.slug = body["slug"].get(); } // Parse layout if provided if (body.contains("layout") && body["layout"].is_object()) { PageLayout layout; const auto& layout_json = body["layout"]; layout.version = layout_json.value("version", 1); layout.grid_columns = layout_json.value("grid_columns", 12); if (layout_json.contains("components") && layout_json["components"].is_array()) { for (const auto& comp_json : layout_json["components"]) { LayoutComponent comp; comp.id = comp_json.value("id", ""); comp.type = comp_json.value("type", ""); comp.config = comp_json.value("config", nlohmann::json::object()); if (comp_json.contains("position") && comp_json["position"].is_object()) { const auto& pos = comp_json["position"]; comp.position.x = pos.value("x", 0); comp.position.y = pos.value("y", 0); comp.position.width = pos.value("width", 12); comp.position.height = pos.value("height", 1); } layout.components.push_back(comp); } } request.layout = layout; } // Parse settings if provided if (body.contains("settings") && body["settings"].is_object()) { PageSettings settings; const auto& settings_json = body["settings"]; settings.show_in_sidebar = settings_json.value("show_in_sidebar", true); settings.icon = settings_json.value("icon", ""); settings.menu_order = settings_json.value("menu_order", 0); request.settings = settings; } auto result = pageService_->UpdatePage(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 page info nlohmann::json page_json = { {"id", result.page->id}, {"workspace_id", result.page->workspace_id}, {"name", result.page->name}, {"slug", result.page->slug}, {"created_at", result.page->created_at}, {"updated_at", result.page->updated_at}, {"created_by", result.page->created_by}, {"shared_with_groups", result.page->shared_with_groups} }; // Add layout nlohmann::json layout_json = { {"version", result.page->layout.version}, {"grid_columns", result.page->layout.grid_columns} }; nlohmann::json components_json = nlohmann::json::array(); for (const auto& comp : result.page->layout.components) { nlohmann::json comp_json = { {"id", comp.id}, {"type", comp.type}, {"position", { {"x", comp.position.x}, {"y", comp.position.y}, {"width", comp.position.width}, {"height", comp.position.height} }}, {"config", comp.config} }; components_json.push_back(comp_json); } layout_json["components"] = components_json; page_json["layout"] = layout_json; // Add settings page_json["settings"] = { {"show_in_sidebar", result.page->settings.show_in_sidebar}, {"icon", result.page->settings.icon}, {"menu_order", result.page->settings.menu_order} }; res.set_content(page_json.dump(), "application/json"); } catch (const std::exception& e) { spdlog::error("Update page error: {}", e.what()); res.status = 500; res.set_content(R"({"error":"Internal server error"})", "application/json"); } } void HttpServer::HandleUpdatePageSharing(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 page ID from path std::string workspace_id = req.matches[1].str(); std::string page_id = req.matches[2].str(); // Check if PageService is available if (!pageService_) { res.status = 503; res.set_content(R"({"error":"Page service not available"})", "application/json"); return; } // Get existing page to check ownership auto existing = pageService_->GetPage(workspace_id, page_id); if (!existing.success || !existing.page) { res.status = 404; res.set_content(R"({"error":"Page not found"})", "application/json"); return; } // Check permission to share if (!authorizationService_->CanSharePage(*auth_user, workspace_id, existing.page->created_by)) { res.status = 403; res.set_content(R"({"error":"Forbidden - insufficient permissions to share this page"})", "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 shared_with_groups field if (!body.contains("shared_with_groups") || !body["shared_with_groups"].is_array()) { res.status = 400; res.set_content(R"({"error":"shared_with_groups array is required"})", "application/json"); return; } try { std::vector shared_with_groups; for (const auto& group : body["shared_with_groups"]) { if (group.is_string()) { shared_with_groups.push_back(group.get()); } } auto result = pageService_->UpdatePageSharing(workspace_id, page_id, shared_with_groups); if (!result.success) { res.status = 400; nlohmann::json error_response = {{"error", result.error}}; res.set_content(error_response.dump(), "application/json"); return; } // Build response nlohmann::json response = { {"id", result.page->id}, {"shared_with_groups", result.page->shared_with_groups}, {"message", "Page sharing updated successfully"} }; res.set_content(response.dump(), "application/json"); } catch (const std::exception& e) { spdlog::error("Update page sharing error: {}", e.what()); res.status = 500; res.set_content(R"({"error":"Internal server error"})", "application/json"); } } void HttpServer::HandleDeletePage(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 page ID from path std::string workspace_id = req.matches[1].str(); std::string page_id = req.matches[2].str(); // Check if PageService is available if (!pageService_) { res.status = 503; res.set_content(R"({"error":"Page service not available"})", "application/json"); return; } // Get existing page to check ownership auto existing = pageService_->GetPage(workspace_id, page_id); if (!existing.success || !existing.page) { res.status = 404; res.set_content(R"({"error":"Page not found"})", "application/json"); return; } // Check permission to delete if (!authorizationService_->CanDeletePage(*auth_user, workspace_id, existing.page->created_by)) { res.status = 403; res.set_content(R"({"error":"Forbidden - insufficient permissions to delete this page"})", "application/json"); return; } try { auto result = pageService_->DeletePage(workspace_id, page_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":"Page deleted successfully"})", "application/json"); } catch (const std::exception& e) { spdlog::error("Delete page error: {}", e.what()); res.status = 500; res.set_content(R"({"error":"Internal server error"})", "application/json"); } } } // namespace smartbotic::webserver