#include "server.hpp" #include "errors.hpp" #include "json_http.hpp" #include #include #include namespace svapi { ApiServer::ApiServer(ServerDeps deps) : d_(std::move(deps)) {} static bool keyExpired(const ApiKey& k) { if (!k.scope) return false; auto now = (uint64_t)std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); return k.scope->expired(now); } std::optional resolveKey(ServerDeps& d, const httplib::Request& req) { if (auto it = req.headers.find("Authorization"); it != req.headers.end()) if (auto tok = bearerToken(it->second)) if (auto k = d.keys.lookup(*tok)) { if (!keyExpired(*k)) return k; return std::nullopt; } if (auto it = req.headers.find("Cookie"); it != req.headers.end()) if (auto c = cookieValue(it->second, "svapi_session")) if (auto keyVal = d.sessions.keyFor(*c, std::chrono::system_clock::now())) if (auto k = d.keys.lookup(*keyVal)) { if (!keyExpired(*k)) return k; return std::nullopt; } return std::nullopt; } ApiKey requireKey(ServerDeps& d, const httplib::Request& req) { if (auto k = resolveKey(d, req)) return *k; throw ApiError(ErrCode::Unauthorized, "unauthorized", "missing or invalid credentials"); } void requireAdmin(const ApiKey& k) { if (!k.admin) throw ApiError(ErrCode::Forbidden, "forbidden", "admin privilege required"); } void requireProjectAccess(const ApiKey& k, const std::string& project) { if (!k.canAccess(project)) throw ApiError(ErrCode::Forbidden, "forbidden", "key not granted project '" + project + "'"); } void ApiServer::registerRoutes() { svr_.set_exception_handler([](const httplib::Request&, httplib::Response& res, std::exception_ptr ep) { try { std::rethrow_exception(ep); } catch (const ApiError& e) { sendJson(res, httpStatus(e.code), errorBody(e.slug, e.what())); } catch (const std::exception& e) { sendJson(res, 500, errorBody("internal", e.what())); } }); // SPA history fallback: a hard navigation / refresh / direct URL to a client-side // route (e.g. /login, /settings) reaches the server as a 404 (no such static file). // Serve index.html so React Router can handle it. Excludes API/meta routes and asset // requests (paths with a file extension), which should keep their real 404. svr_.set_error_handler([this](const httplib::Request& req, httplib::Response& res) { if (res.status == 404 && req.method == "GET" && !d_.webuiDir.empty() && req.path.rfind("/api/", 0) != 0 && req.path.rfind("/ui/", 0) != 0 && req.path.rfind("/docs", 0) != 0 && req.path != "/openapi.json" && req.path != "/llms.txt" && req.path != "/healthz" && req.path != "/readyz" && req.path.find('.') == std::string::npos) { std::ifstream f(d_.webuiDir + "/index.html", std::ios::binary); if (f) { std::stringstream ss; ss << f.rdbuf(); res.set_content(ss.str(), "text/html"); res.status = 200; return httplib::Server::HandlerResponse::Handled; } } return httplib::Server::HandlerResponse::Unhandled; }); // Authenticate /api/* (authorization is per-handler). svr_.set_pre_routing_handler([this](const httplib::Request& req, httplib::Response& res) { if (req.method != "OPTIONS" && req.path.rfind("/api/", 0) == 0) { if (!resolveKey(d_, req)) { sendJson(res, 401, errorBody("unauthorized", "missing or invalid credentials")); return httplib::Server::HandlerResponse::Handled; } } return httplib::Server::HandlerResponse::Unhandled; }); svr_.set_post_routing_handler([this](const httplib::Request& req, httplib::Response& res) { auto snap = d_.settings.snapshot(); std::string responseOrigin; if (!snap->corsOrigins.empty() && snap->corsOrigins[0] == "*") { responseOrigin = "*"; } else if (auto it = req.headers.find("Origin"); it != req.headers.end()) { for (const auto& a : snap->corsOrigins) if (a == it->second) { responseOrigin = it->second; break; } } if (!responseOrigin.empty()) { res.set_header("Access-Control-Allow-Origin", responseOrigin); if (responseOrigin != "*") res.set_header("Vary", "Origin"); } res.set_header("Access-Control-Allow-Headers", "Authorization, Content-Type"); res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); }); svr_.Options(R"(.*)", [](const httplib::Request&, httplib::Response& res) { res.status = 204; }); registerMetaRoutes(*this); registerWebuiAuthRoutes(*this); registerProjectRoutes(*this); registerKeyRoutes(*this); registerCollectionRoutes(*this); registerDocumentRoutes(*this); registerVectorRoutes(*this); registerStatsRoutes(*this); registerSettingsRoutes(*this); if (!d_.shareDir.empty()) svr_.set_mount_point("/docs", d_.shareDir + "/docs"); if (!d_.webuiDir.empty()) svr_.set_mount_point("/", d_.webuiDir); } int ApiServer::bindToAnyPort(const std::string& host) { return svr_.bind_to_any_port(host); } void ApiServer::listenAfterBind() { svr_.listen_after_bind(); } bool ApiServer::listen(const std::string& host, uint16_t port) { return svr_.listen(host, port); } void ApiServer::stop() { svr_.stop(); } } // namespace svapi