#include "app.hpp" #include "stores/user_store.hpp" #include "stores/workspace_store.hpp" #include "stores/invitation_store.hpp" #include "stores/assistant_store.hpp" #include "stores/calendar_store.hpp" #include "stores/settings_store.hpp" #include "services/auth_service.hpp" #include "services/workspace_service.hpp" #include "services/invitation_service.hpp" #include "services/assistant_service.hpp" #include "services/calendar_service.hpp" #include "routes/auth_routes.hpp" #include "routes/user_routes.hpp" #include "routes/workspace_routes.hpp" #include "routes/invitation_routes.hpp" #include "routes/assistant_routes.hpp" #include "routes/calendar_routes.hpp" #include "routes/settings_routes.hpp" #include #include #include #include namespace smartbotic::microbit { App::App(const json& config) : config_(config) { bindAddress_ = ConfigLoader::get(config_, "http.bind_address", "0.0.0.0"); httpPort_ = ConfigLoader::get(config_, "http.port", 8090); staticFilesPath_ = ConfigLoader::get(config_, "http.static_files.path", "webui/dist"); } App::~App() { stop(); } void App::connectDatabase() { smartbotic::database::Client::Config dbConfig; dbConfig.address = ConfigLoader::get(config_, "database.rpc_address", "localhost:9004"); dbConfig.timeoutMs = ConfigLoader::get(config_, "database.timeout_ms", 5000); dbConfig.maxRetries = ConfigLoader::get(config_, "database.max_retries", 3); db_ = std::make_unique(dbConfig); if (!db_->connect()) { throw std::runtime_error("Failed to connect to database at " + dbConfig.address); } spdlog::info("Connected to database at {}", dbConfig.address); } void App::initializeComponents() { // Auth middleware auth::AuthConfig authConfig; authConfig.enabled = ConfigLoader::get(config_, "auth.enabled", true); authConfig.jwtSecret = ConfigLoader::get(config_, "auth.jwt_secret", ""); authConfig.accessTokenLifetimeSec = ConfigLoader::get(config_, "auth.access_token_lifetime_sec", 900); authConfig.refreshTokenLifetimeSec = ConfigLoader::get(config_, "auth.refresh_token_lifetime_sec", 604800); authConfig.minPasswordLength = ConfigLoader::get(config_, "auth.password_policy.min_length", 8); authConfig.requireNumber = ConfigLoader::get(config_, "auth.password_policy.require_number", true); authConfig.requireSpecial = ConfigLoader::get(config_, "auth.password_policy.require_special", false); if (authConfig.jwtSecret.empty() || authConfig.jwtSecret == "change-me-in-production") { spdlog::warn("JWT secret not configured or using default. Set JWT_SECRET environment variable."); } authMiddleware_ = std::make_unique(authConfig); // SMTP client smtp::SmtpConfig smtpConfig; smtpConfig.host = ConfigLoader::get(config_, "smtp.host", ""); smtpConfig.port = ConfigLoader::get(config_, "smtp.port", 587); smtpConfig.username = ConfigLoader::get(config_, "smtp.username", ""); smtpConfig.password = ConfigLoader::get(config_, "smtp.password", ""); smtpConfig.fromAddress = ConfigLoader::get(config_, "smtp.from_address", "noreply@smartbotics.ai"); smtpConfig.fromName = ConfigLoader::get(config_, "smtp.from_name", "Smartbotic"); smtpConfig.useTls = ConfigLoader::get(config_, "smtp.use_tls", true); smtpClient_ = std::make_unique(smtpConfig); // CallerAI client callerai::CallerAIConfig calleraiConfig; calleraiConfig.apiUrl = ConfigLoader::get(config_, "callerai.api_url", "http://localhost:8080"); calleraiConfig.apiKey = ConfigLoader::get(config_, "callerai.api_key", ""); calleraiConfig.timeoutSec = ConfigLoader::get(config_, "callerai.timeout_sec", 30); calleraiClient_ = std::make_unique(calleraiConfig); // Stores userStore_ = std::make_unique(db_.get()); workspaceStore_ = std::make_unique(db_.get()); invitationStore_ = std::make_unique(db_.get()); assistantStore_ = std::make_unique(db_.get()); calendarStore_ = std::make_unique(db_.get()); settingsStore_ = std::make_unique(db_.get()); // Services authService_ = std::make_unique(userStore_.get(), authMiddleware_.get()); workspaceService_ = std::make_unique(workspaceStore_.get()); invitationService_ = std::make_unique( invitationStore_.get(), workspaceStore_.get(), userStore_.get(), smtpClient_.get()); assistantService_ = std::make_unique(assistantStore_.get(), calleraiClient_.get()); calendarService_ = std::make_unique(calendarStore_.get()); } void App::setupCors(httplib::Server& svr) { bool corsEnabled = ConfigLoader::get(config_, "cors.enabled", true); if (!corsEnabled) return; svr.Options(R"(/api/.*)", [](const httplib::Request&, httplib::Response& res) { res.set_header("Access-Control-Allow-Origin", "*"); res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); res.set_header("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Requested-With"); res.set_header("Access-Control-Max-Age", "86400"); res.status = 204; }); svr.set_post_routing_handler([](const httplib::Request&, httplib::Response& res) { res.set_header("Access-Control-Allow-Origin", "*"); }); } void App::setupRoutes(httplib::Server& svr) { // Health check svr.Get("/api/v1/health", [](const httplib::Request&, httplib::Response& res) { res.set_content(R"({"status":"ok"})", "application/json"); }); // Auth middleware svr.set_pre_routing_handler([this](const httplib::Request& req, httplib::Response& res) { if (req.method == "OPTIONS") { return httplib::Server::HandlerResponse::Unhandled; } if (req.path.find("/api/") != 0) { return httplib::Server::HandlerResponse::Unhandled; } if (authMiddleware_->isPublicPath(req.path)) { return httplib::Server::HandlerResponse::Unhandled; } if (!authMiddleware_->authenticate(req, res)) { return httplib::Server::HandlerResponse::Handled; } return httplib::Server::HandlerResponse::Unhandled; }); // Register all routes setupAuthRoutes(svr, *this); setupUserRoutes(svr, *this); setupWorkspaceRoutes(svr, *this); setupInvitationRoutes(svr, *this); setupAssistantRoutes(svr, *this); setupCalendarRoutes(svr, *this); setupSettingsRoutes(svr, *this); } void App::start() { if (running_) return; spdlog::info("Starting Smartbotic MicroBit v{}", "0.1.0"); connectDatabase(); initializeComponents(); httpServer_ = std::make_unique(); setupCors(*httpServer_); setupRoutes(*httpServer_); // Serve static files bool serveStatic = ConfigLoader::get(config_, "http.static_files.enabled", true); if (serveStatic && std::filesystem::exists(staticFilesPath_)) { // Disable caching for index.html so browser always gets the latest version httpServer_->set_file_request_handler([](const httplib::Request& req, httplib::Response& res) { if (req.path == "/" || req.path == "/index.html") { res.set_header("Cache-Control", "no-cache, no-store, must-revalidate"); } }); httpServer_->set_mount_point("/", staticFilesPath_); spdlog::info("Serving static files from {}", staticFilesPath_); // SPA fallback: serve index.html for non-API routes that don't match a file auto indexPath = std::filesystem::path(staticFilesPath_) / "index.html"; if (std::filesystem::exists(indexPath)) { httpServer_->set_error_handler([indexPath](const httplib::Request& req, httplib::Response& res) { if (res.status == 404 && req.path.substr(0, 5) != "/api/") { std::ifstream ifs(indexPath); if (ifs) { std::string body((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); res.set_content(body, "text/html"); res.status = 200; } } }); } } running_ = true; httpThread_ = std::thread([this]() { spdlog::info("HTTP server listening on {}:{}", bindAddress_, httpPort_); httpServer_->listen(bindAddress_, httpPort_); }); } void App::stop() { if (!running_) return; running_ = false; if (httpServer_) { httpServer_->stop(); } if (httpThread_.joinable()) { httpThread_.join(); } spdlog::info("Smartbotic MicroBit stopped"); } } // namespace smartbotic::microbit