| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588 |
- #include "smartbotic/webserver/http_server.hpp"
- #include "smartbotic/webserver/permissions.hpp"
- #include <nlohmann/json.hpp>
- #include <spdlog/spdlog.h>
- #include <algorithm>
- #include <chrono>
- #include <cstring>
- #include <filesystem>
- #include <fstream>
- #include <random>
- #include <sstream>
- 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(connectionsMutex_);
- for (auto& [wsi, conn] : connections_) {
- // Check if this connection is subscribed and authenticated
- if (!conn->authenticated) {
- continue;
- }
- const auto& subs = conn->subscriptions;
- if (std::find(subs.begin(), subs.end(), subscription_key) != subs.end()) {
- QueueMessage(conn.get(), message);
- }
- }
- }
- auto WebSocketServer::ConnectionCount() const -> size_t {
- std::lock_guard<std::mutex> lock(connectionsMutex_);
- return connections_.size();
- }
- void WebSocketServer::SetMessageHandler(WebSocketMessageHandler handler) {
- messageHandler_ = std::move(handler);
- }
- void WebSocketServer::HandleConnect(lws* wsi) {
- auto conn = std::make_unique<WsConnection>();
- conn->wsi = wsi;
- conn->sessionId = GenerateSessionId();
- spdlog::info("WebSocket client connected (session: {})", conn->sessionId);
- std::lock_guard<std::mutex> lock(connectionsMutex_);
- connections_[wsi] = std::move(conn);
- }
- void WebSocketServer::HandleDisconnect(lws* wsi) {
- std::lock_guard<std::mutex> lock(connectionsMutex_);
- auto it = connections_.find(wsi);
- if (it != connections_.end()) {
- spdlog::info("WebSocket client disconnected (session: {})", it->second->sessionId);
- connections_.erase(it);
- }
- }
- void WebSocketServer::HandleMessage(lws* wsi, const std::string& message) {
- WsConnection* conn = nullptr;
- {
- std::lock_guard<std::mutex> lock(connectionsMutex_);
- auto it = connections_.find(wsi);
- if (it != connections_.end()) {
- conn = it->second.get();
- }
- }
- // 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<std::mutex> 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<char*>(in), len);
- spdlog::debug("WebSocket received {} bytes", len);
- instance_->HandleMessage(wsi, message);
- break;
- }
- case LWS_CALLBACK_SERVER_WRITEABLE: {
- std::lock_guard<std::mutex> lock(instance_->connectionsMutex_);
- auto it = instance_->connections_.find(wsi);
- if (it != instance_->connections_.end()) {
- auto& conn = it->second;
- std::lock_guard<std::mutex> sendLock(conn->sendMutex);
- if (conn->sendBuffer.size() > LWS_PRE) {
- size_t msgLen = conn->sendBuffer.size() - LWS_PRE;
- lws_write(wsi, conn->sendBuffer.data() + LWS_PRE, msgLen, LWS_WRITE_TEXT);
- conn->sendBuffer.clear();
- }
- }
- break;
- }
- 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<httplib::Server>()),
- dbClient_(std::make_unique<DatabaseClient>(config_)),
- authService_(std::make_unique<AuthService>(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<WebSocketServer>(config_.ws_port, config_.ws_path);
- // Setup WebSocket message handler using WsHandler if available
- if (wsHandler_) {
- wsServer_->SetMessageHandler([this](WsConnection* conn, const std::string& message) {
- wsHandler_->HandleMessage(conn, message,
- [this](WsConnection* c, const std::string& response) {
- wsServer_->SendToSession(c->sessionId, response);
- });
- });
- spdlog::info("WebSocket message handler configured with WsHandler");
- } else if (wsMessageHandler_) {
- wsServer_->SetMessageHandler(wsMessageHandler_);
- }
- wsServer_->Start();
- // Start 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<DatabaseClient>(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<UserService>(*dbClient_);
- if (!userService_->Initialize()) {
- spdlog::error("Failed to initialize UserService");
- return false;
- }
- spdlog::info("UserService initialized successfully");
- // Initialize WorkspaceService
- workspaceService_ = std::make_unique<WorkspaceService>(*dbClient_);
- if (!workspaceService_->Initialize()) {
- spdlog::error("Failed to initialize WorkspaceService");
- return false;
- }
- spdlog::info("WorkspaceService initialized successfully");
- // Initialize GroupService
- groupService_ = std::make_unique<GroupService>(*dbClient_);
- if (!groupService_->Initialize()) {
- spdlog::error("Failed to initialize GroupService");
- return false;
- }
- spdlog::info("GroupService initialized successfully");
- // Initialize MembershipService
- membershipService_ = std::make_unique<MembershipService>(*dbClient_);
- if (!membershipService_->Initialize()) {
- spdlog::error("Failed to initialize MembershipService");
- return false;
- }
- spdlog::info("MembershipService initialized successfully");
- // Initialize ApiKeyService
- apiKeyService_ = std::make_unique<ApiKeyService>(*dbClient_);
- if (!apiKeyService_->Initialize()) {
- spdlog::error("Failed to initialize ApiKeyService");
- return false;
- }
- spdlog::info("ApiKeyService initialized successfully");
- // Initialize CollectionService
- collectionService_ = std::make_unique<CollectionService>(*dbClient_);
- spdlog::info("CollectionService initialized successfully");
- // Initialize DocumentService
- documentService_ = std::make_unique<DocumentService>(*dbClient_);
- spdlog::info("DocumentService initialized successfully");
- // Initialize ViewService
- viewService_ = std::make_unique<ViewService>(*dbClient_);
- if (!viewService_->Initialize()) {
- spdlog::error("Failed to initialize ViewService");
- return false;
- }
- spdlog::info("ViewService initialized successfully");
- // Initialize AuthorizationService
- authorizationService_ = std::make_unique<AuthorizationService>(*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<WsHandler>(*authService_);
- spdlog::info("WsHandler initialized successfully");
- // Note: WebSocket message handler is set up in Start() after wsServer_ is created
- return true;
- }
- void HttpServer::SetupRoutes() {
- // Pre-routing handler for CORS
- httpServer_->set_pre_routing_handler([this](const httplib::Request& req, httplib::Response& res) {
- // Apply CORS headers if enabled
- if (config_.cors.enabled) {
- // Combine allowed origins
- std::string origins;
- for (size_t i = 0; i < config_.cors.allowed_origins.size(); ++i) {
- if (i > 0) origins += ", ";
- origins += config_.cors.allowed_origins[i];
- }
- res.set_header("Access-Control-Allow-Origin", origins);
- // Combine allowed methods
- std::string methods;
- for (size_t i = 0; i < config_.cors.allowed_methods.size(); ++i) {
- if (i > 0) methods += ", ";
- methods += config_.cors.allowed_methods[i];
- }
- res.set_header("Access-Control-Allow-Methods", methods);
- // Combine allowed headers
- std::string headers;
- for (size_t i = 0; i < config_.cors.allowed_headers.size(); ++i) {
- if (i > 0) headers += ", ";
- headers += config_.cors.allowed_headers[i];
- }
- res.set_header("Access-Control-Allow-Headers", headers);
- if (config_.cors.allow_credentials) {
- res.set_header("Access-Control-Allow-Credentials", "true");
- }
- res.set_header("Access-Control-Max-Age", std::to_string(config_.cors.max_age));
- }
- res.set_header("Server", "SmartBotic-Server/0.1.0");
- // Handle CORS preflight
- if (req.method == "OPTIONS") {
- res.status = 204;
- return httplib::Server::HandlerResponse::Handled;
- }
- return httplib::Server::HandlerResponse::Unhandled;
- });
- // Health check endpoint
- httpServer_->Get("/api/health", [this](const httplib::Request& req, httplib::Response& res) {
- HandleHealth(req, res);
- });
- // Version endpoint
- httpServer_->Get("/api/version", [this](const httplib::Request& req, httplib::Response& res) {
- HandleVersion(req, res);
- });
- // Bootstrap endpoint - creates first admin user if no users exist
- httpServer_->Post("/api/bootstrap", [this](const httplib::Request& req, httplib::Response& res) {
- HandleBootstrap(req, res);
- });
- // Setup authentication routes
- SetupAuthRoutes();
- // Setup user management routes
- SetupUserRoutes();
- // Setup API key routes (under /api/users/:id/api-keys)
- SetupApiKeyRoutes();
- // Setup membership routes (before workspace routes since they're more specific)
- SetupMembershipRoutes();
- // Setup document routes (most specific - before collections)
- SetupDocumentRoutes();
- // Setup view routes (schema definitions)
- SetupViewRoutes();
- // Setup collection routes (before workspace routes since they're more specific)
- SetupCollectionRoutes();
- // Setup group management routes (before workspace routes since they're more specific)
- SetupGroupRoutes();
- // Setup workspace management routes
- SetupWorkspaceRoutes();
- // Mount static file directory for WebUI
- if (std::filesystem::exists(config_.webui_path)) {
- httpServer_->set_mount_point("/", config_.webui_path);
- spdlog::info("Mounted static files from: {}", config_.webui_path);
- } else {
- spdlog::warn("WebUI path does not exist: {}", config_.webui_path);
- }
- // Custom error handler for 404 (only if response body is empty)
- httpServer_->set_error_handler([](const httplib::Request& /*req*/, httplib::Response& res) {
- // Only set default error message if body hasn't been set by a handler
- if (res.body.empty()) {
- res.set_content(R"({"error":"Not found"})", "application/json");
- }
- });
- // Logger for requests
- httpServer_->set_logger([](const httplib::Request& req, const httplib::Response& res) {
- auto status = res.status;
- if (status >= 500) {
- spdlog::error("{} {} {}", req.method, req.path, status);
- } else if (status >= 400) {
- spdlog::warn("{} {} {}", req.method, req.path, status);
- } else {
- spdlog::info("{} {} {}", req.method, req.path, status);
- }
- });
- }
- void HttpServer::RunHttpServer() {
- if (!httpServer_->listen(config_.address, config_.port)) {
- spdlog::error("Failed to start HTTP server on {}", config_.GetListenAddress());
- running_.store(false);
- }
- }
- void HttpServer::HandleHealth(const httplib::Request& req, httplib::Response& res) {
- res.set_content(R"({"status":"healthy","service":"smartbotic-server"})", "application/json");
- }
- void HttpServer::HandleVersion(const httplib::Request& req, httplib::Response& res) {
- res.set_content(R"({"version":"0.1.0","name":"SmartBotic Web Server"})", "application/json");
- }
- void HttpServer::HandleBootstrap(const httplib::Request& req, httplib::Response& res) {
- // Bootstrap endpoint - creates first admin user if no users exist
- if (!userService_) {
- res.status = 500;
- res.set_content(R"({"error":"User service not available"})", "application/json");
- return;
- }
- // Check if any users exist
- auto users = userService_->ListUsers();
- if (!users.users.empty()) {
- res.status = 400;
- res.set_content(R"({"error":"System already bootstrapped - users exist"})", "application/json");
- return;
- }
- // Parse request body
- nlohmann::json body;
- try {
- body = nlohmann::json::parse(req.body);
- } catch (...) {
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
- return;
- }
- // Validate required fields
- if (!body.contains("email") || !body.contains("password")) {
- res.status = 400;
- res.set_content(R"({"error":"Email and password are required"})", "application/json");
- return;
- }
- CreateUserRequest user_request;
- user_request.email = body["email"].get<std::string>();
- user_request.password = body["password"].get<std::string>();
- user_request.name = body.value("name", "Admin");
- // Create the admin user
- auto result = userService_->CreateUser(user_request);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response;
- error_response["error"] = result.error;
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Create a default workspace
- if (workspaceService_) {
- CreateWorkspaceRequest ws_request;
- ws_request.name = "Default Workspace";
- ws_request.settings["default"] = true;
- auto ws_result = workspaceService_->CreateWorkspace(ws_request);
- // Add user to workspace
- if (ws_result.success && ws_result.workspace && membershipService_) {
- AddMemberRequest member_request;
- member_request.workspace_id = ws_result.workspace->id;
- member_request.user_id = result.user->id;
- member_request.group_ids = {};
- [[maybe_unused]] auto member_result = membershipService_->AddMember(member_request);
- }
- }
- // Return success with user info
- nlohmann::json response;
- response["success"] = true;
- response["message"] = "System bootstrapped successfully";
- response["user"]["id"] = result.user->id;
- response["user"]["email"] = result.user->email;
- response["user"]["name"] = result.user->name;
- spdlog::info("System bootstrapped with admin user: {}", user_request.email);
- res.set_content(response.dump(), "application/json");
- }
- auto HttpServer::GetMimeType(const std::string& path) -> std::string {
- auto ext_pos = path.rfind('.');
- if (ext_pos == std::string::npos) {
- return "application/octet-stream";
- }
- std::string ext = path.substr(ext_pos);
- static const std::unordered_map<std::string, std::string> mime_types = {
- {".html", "text/html"},
- {".htm", "text/html"},
- {".css", "text/css"},
- {".js", "application/javascript"},
- {".json", "application/json"},
- {".png", "image/png"},
- {".jpg", "image/jpeg"},
- {".jpeg", "image/jpeg"},
- {".gif", "image/gif"},
- {".svg", "image/svg+xml"},
- {".ico", "image/x-icon"},
- {".woff", "font/woff"},
- {".woff2", "font/woff2"},
- {".ttf", "font/ttf"},
- {".eot", "application/vnd.ms-fontobject"},
- {".txt", "text/plain"},
- {".xml", "application/xml"},
- {".pdf", "application/pdf"},
- };
- auto it = mime_types.find(ext);
- if (it != mime_types.end()) {
- return it->second;
- }
- return "application/octet-stream";
- }
- // ============================================================================
- // Authentication Routes Implementation
- // ============================================================================
- void HttpServer::SetupAuthRoutes() {
- // POST /api/auth/login - Authenticate user and return JWT
- httpServer_->Post("/api/auth/login", [this](const httplib::Request& req, httplib::Response& res) {
- HandleAuthLogin(req, res);
- });
- // POST /api/auth/refresh - Refresh access token
- httpServer_->Post("/api/auth/refresh", [this](const httplib::Request& req, httplib::Response& res) {
- HandleAuthRefresh(req, res);
- });
- // POST /api/auth/logout - Invalidate refresh token
- httpServer_->Post("/api/auth/logout", [this](const httplib::Request& req, httplib::Response& res) {
- HandleAuthLogout(req, res);
- });
- // GET /api/auth/me - Get current authenticated user info
- httpServer_->Get("/api/auth/me", [this](const httplib::Request& req, httplib::Response& res) {
- HandleAuthMe(req, res);
- });
- spdlog::info("Authentication routes registered: /api/auth/login, /api/auth/refresh, /api/auth/logout, /api/auth/me");
- }
- void HttpServer::HandleAuthLogin(const httplib::Request& req, httplib::Response& res) {
- try {
- // Parse request body
- auto body = nlohmann::json::parse(req.body);
- if (!body.contains("email") || !body.contains("password")) {
- res.status = 400;
- res.set_content(R"({"error":"Missing email or password"})", "application/json");
- return;
- }
- std::string email = body["email"].get<std::string>();
- std::string password = body["password"].get<std::string>();
- if (config_.jwt.secret.empty()) {
- res.status = 500;
- res.set_content(R"({"error":"JWT secret not configured"})", "application/json");
- return;
- }
- if (!userService_) {
- res.status = 500;
- res.set_content(R"({"error":"User service not available"})", "application/json");
- return;
- }
- // Verify user credentials
- auto user_result = userService_->VerifyCredentials(email, password);
- if (!user_result.success || !user_result.user) {
- res.status = 401;
- res.set_content(R"({"error":"Invalid email or password"})", "application/json");
- return;
- }
- const auto& user = *user_result.user;
- // Get user's workspace memberships
- std::vector<std::string> workspace_ids;
- if (membershipService_) {
- auto memberships = membershipService_->ListUserMemberships(user.id);
- for (const auto& m : memberships.members) {
- workspace_ids.push_back(m.workspace_id);
- }
- }
- // Collect user's groups from memberships
- std::vector<std::string> 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);
- }
- }
- }
- }
- // First registered user is automatically superadmin
- if (groupService_) {
- auto superadmin_group = groupService_->GetSuperadminGroup();
- if (superadmin_group.success && superadmin_group.group) {
- auto all_users = userService_->ListUsers();
- if (!all_users.users.empty() && all_users.users[0].id == user.id) {
- const auto& sa_id = superadmin_group.group->id;
- if (std::find(groups.begin(), groups.end(), sa_id) == groups.end()) {
- groups.push_back(sa_id);
- }
- }
- }
- }
- auto tokens = authService_->GenerateTokens(user.id, email, workspace_ids, groups);
- nlohmann::json response = {
- {"access_token", tokens.access_token},
- {"refresh_token", tokens.refresh_token},
- {"token_type", "Bearer"},
- {"expires_in", tokens.access_expires_in}
- };
- spdlog::info("User logged in: {}", email);
- res.set_content(response.dump(), "application/json");
- } catch (const nlohmann::json::parse_error& e) {
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Login error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleAuthRefresh(const httplib::Request& req, httplib::Response& res) {
- try {
- // Parse request body
- auto body = nlohmann::json::parse(req.body);
- if (!body.contains("refresh_token")) {
- res.status = 400;
- res.set_content(R"({"error":"Missing refresh_token"})", "application/json");
- return;
- }
- std::string refresh_token = body["refresh_token"].get<std::string>();
- auto new_tokens = authService_->RefreshAccessToken(refresh_token);
- if (!new_tokens) {
- res.status = 401;
- res.set_content(R"({"error":"Invalid or expired refresh token"})", "application/json");
- return;
- }
- nlohmann::json response = {
- {"access_token", new_tokens->access_token},
- {"refresh_token", new_tokens->refresh_token},
- {"token_type", "Bearer"},
- {"expires_in", new_tokens->access_expires_in}
- };
- spdlog::debug("Token refreshed successfully");
- res.set_content(response.dump(), "application/json");
- } catch (const nlohmann::json::parse_error& e) {
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Token refresh error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleAuthLogout(const httplib::Request& req, httplib::Response& res) {
- try {
- // Parse request body
- auto body = nlohmann::json::parse(req.body);
- if (!body.contains("refresh_token")) {
- res.status = 400;
- res.set_content(R"({"error":"Missing refresh_token"})", "application/json");
- return;
- }
- std::string refresh_token = body["refresh_token"].get<std::string>();
- if (authService_->InvalidateRefreshToken(refresh_token)) {
- spdlog::debug("User logged out successfully");
- res.set_content(R"({"message":"Logged out successfully"})", "application/json");
- } else {
- res.status = 400;
- res.set_content(R"({"error":"Invalid refresh token"})", "application/json");
- }
- } catch (const nlohmann::json::parse_error& e) {
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Logout error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleAuthMe(const httplib::Request& req, httplib::Response& res) {
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Build response with user info from token
- nlohmann::json response = {
- {"id", auth_user->user_id},
- {"email", auth_user->email},
- {"groups", auth_user->groups},
- {"workspace_ids", auth_user->workspace_ids}
- };
- // Try to get additional user info from database
- if (userService_) {
- auto user_result = userService_->GetUser(auth_user->user_id);
- if (user_result.success && user_result.user) {
- response["name"] = user_result.user->name;
- response["created_at"] = user_result.user->created_at;
- }
- }
- // Check if user is superadmin based on groups
- response["is_superadmin"] = IsSuperadmin(*auth_user);
- res.set_content(response.dump(), "application/json");
- }
- auto HttpServer::AuthenticateRequest(const httplib::Request& req) -> std::optional<AuthUser> {
- std::string auth_token;
- // First check X-API-Key header (direct API key)
- auto api_key_header = req.get_header_value("X-API-Key");
- if (!api_key_header.empty()) {
- auth_token = api_key_header;
- } else {
- // Check Authorization header
- auto auth_header = req.get_header_value("Authorization");
- if (auth_header.empty()) {
- return std::nullopt;
- }
- auto token = AuthService::ExtractBearerToken(auth_header);
- if (!token) {
- return std::nullopt;
- }
- auth_token = *token;
- }
- // Check if this is an API key (starts with "sk_")
- if (auth_token.starts_with("sk_")) {
- if (!apiKeyService_) {
- spdlog::warn("API key authentication attempted but service not available");
- return std::nullopt;
- }
- auto validation = apiKeyService_->ValidateApiKey(auth_token);
- if (!validation.valid || !validation.api_key) {
- spdlog::debug("API key validation failed: {}", validation.error);
- return std::nullopt;
- }
- // Check if key is expired
- if (validation.api_key->IsExpired()) {
- spdlog::debug("API key expired");
- return std::nullopt;
- }
- // Update last used timestamp (fire and forget)
- apiKeyService_->UpdateLastUsed(validation.api_key->id);
- // Build AuthUser from API key
- AuthUser auth_user;
- auth_user.user_id = validation.api_key->user_id;
- // Use API key permissions as groups
- auth_user.groups = validation.api_key->permissions;
- // Try to fetch additional user info
- if (userService_) {
- auto user_result = userService_->GetUser(validation.api_key->user_id);
- if (user_result.success && user_result.user) {
- auth_user.email = user_result.user->email;
- // Fetch workspace memberships for the user
- if (membershipService_) {
- auto memberships = membershipService_->ListUserMemberships(user_result.user->id);
- for (const auto& m : memberships.members) {
- auth_user.workspace_ids.push_back(m.workspace_id);
- }
- }
- } else {
- spdlog::debug("API key owner user not found in database: {}", validation.api_key->user_id);
- }
- }
- return auth_user;
- }
- // Otherwise, treat as JWT token
- auto validation = authService_->ValidateAccessToken(auth_token);
- if (!validation.valid) {
- spdlog::debug("Token validation failed: {}", validation.error);
- return std::nullopt;
- }
- return validation.user;
- }
- // ============================================================================
- // User Management Routes Implementation
- // ============================================================================
- void HttpServer::SetupUserRoutes() {
- // POST /api/users - Create a new user (superadmin only)
- httpServer_->Post("/api/users", [this](const httplib::Request& req, httplib::Response& res) {
- HandleCreateUser(req, res);
- });
- // GET /api/users - List all users (superadmin only)
- httpServer_->Get("/api/users", [this](const httplib::Request& req, httplib::Response& res) {
- HandleListUsers(req, res);
- });
- // GET /api/users/:id - Get a specific user
- httpServer_->Get(R"(/api/users/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
- HandleGetUser(req, res);
- });
- // PATCH /api/users/:id - Update a user
- httpServer_->Patch(R"(/api/users/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
- HandleUpdateUser(req, res);
- });
- // DELETE /api/users/:id - Soft delete a user (superadmin only)
- httpServer_->Delete(R"(/api/users/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
- HandleDeleteUser(req, res);
- });
- spdlog::info("User management routes registered: /api/users");
- }
- auto HttpServer::IsSuperadmin(const AuthUser& user) -> bool {
- // 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<std::string>();
- }
- if (body.contains("password")) {
- create_req.password = body["password"].get<std::string>();
- }
- if (body.contains("name")) {
- create_req.name = body["name"].get<std::string>();
- }
- auto result = userService_->CreateUser(create_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Return created user (without password_hash)
- nlohmann::json user_json = {
- {"id", result.user->id},
- {"email", result.user->email},
- {"name", result.user->name},
- {"created_at", result.user->created_at},
- {"updated_at", result.user->updated_at}
- };
- res.status = 201;
- res.set_content(user_json.dump(), "application/json");
- } catch (const nlohmann::json::parse_error& e) {
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Create user error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleListUsers(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // 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<std::string>();
- }
- if (body.contains("password")) {
- update_req.password = body["password"].get<std::string>();
- }
- if (body.contains("name")) {
- update_req.name = body["name"].get<std::string>();
- }
- auto result = userService_->UpdateUser(user_id, update_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Return updated user (without password_hash)
- nlohmann::json user_json = {
- {"id", result.user->id},
- {"email", result.user->email},
- {"name", result.user->name},
- {"created_at", result.user->created_at},
- {"updated_at", result.user->updated_at}
- };
- res.set_content(user_json.dump(), "application/json");
- } catch (const nlohmann::json::parse_error& e) {
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Update user error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleDeleteUser(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // 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<std::string>();
- }
- if (body.contains("settings")) {
- create_req.settings = body["settings"];
- }
- auto result = workspaceService_->CreateWorkspace(create_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Return created workspace
- nlohmann::json workspace_json = {
- {"id", result.workspace->id},
- {"name", result.workspace->name},
- {"settings", result.workspace->settings},
- {"created_at", result.workspace->created_at},
- {"updated_at", result.workspace->updated_at}
- };
- res.status = 201;
- res.set_content(workspace_json.dump(), "application/json");
- } catch (const nlohmann::json::parse_error& e) {
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Create workspace error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleListWorkspaces(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Check if WorkspaceService is available
- if (!workspaceService_) {
- res.status = 503;
- res.set_content(R"({"error":"Workspace service not available"})", "application/json");
- return;
- }
- try {
- WorkspaceListResult result;
- 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<std::string>();
- }
- if (body.contains("settings")) {
- update_req.settings = body["settings"];
- }
- auto result = workspaceService_->UpdateWorkspace(workspace_id, update_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Return updated workspace
- nlohmann::json workspace_json = {
- {"id", result.workspace->id},
- {"name", result.workspace->name},
- {"settings", result.workspace->settings},
- {"created_at", result.workspace->created_at},
- {"updated_at", result.workspace->updated_at}
- };
- res.set_content(workspace_json.dump(), "application/json");
- } catch (const nlohmann::json::parse_error& e) {
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Update workspace error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleDeleteWorkspace(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // 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<std::string>();
- }
- if (body.contains("permissions") && body["permissions"].is_array()) {
- for (const auto& perm : body["permissions"]) {
- if (perm.is_string()) {
- create_req.permissions.push_back(perm.get<std::string>());
- }
- }
- }
- auto result = groupService_->CreateGroup(create_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Return created group
- nlohmann::json group_json = {
- {"id", result.group->id},
- {"workspace_id", result.group->workspace_id},
- {"name", result.group->name},
- {"permissions", result.group->permissions},
- {"created_at", result.group->created_at},
- {"updated_at", result.group->updated_at}
- };
- res.status = 201;
- res.set_content(group_json.dump(), "application/json");
- } catch (const nlohmann::json::parse_error& e) {
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Create group error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleListGroups(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID from path
- std::string workspace_id = req.matches[1].str();
- // Check 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<std::string>();
- }
- if (body.contains("permissions") && body["permissions"].is_array()) {
- std::vector<std::string> perms;
- for (const auto& perm : body["permissions"]) {
- if (perm.is_string()) {
- perms.push_back(perm.get<std::string>());
- }
- }
- update_req.permissions = perms;
- }
- auto result = groupService_->UpdateGroup(group_id, update_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Return updated group
- nlohmann::json group_json = {
- {"id", result.group->id},
- {"workspace_id", result.group->workspace_id},
- {"name", result.group->name},
- {"permissions", result.group->permissions},
- {"created_at", result.group->created_at},
- {"updated_at", result.group->updated_at}
- };
- res.set_content(group_json.dump(), "application/json");
- } catch (const nlohmann::json::parse_error& e) {
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Update group error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleDeleteGroup(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID and group ID from path
- std::string workspace_id = req.matches[1].str();
- std::string group_id = req.matches[2].str();
- // 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<std::string>();
- } else {
- res.status = 400;
- res.set_content(R"({"error":"user_id is required"})", "application/json");
- return;
- }
- if (json.contains("group_ids") && json["group_ids"].is_array()) {
- for (const auto& item : json["group_ids"]) {
- if (item.is_string()) {
- add_req.group_ids.push_back(item.get<std::string>());
- }
- }
- }
- auto result = membershipService_->AddMember(add_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Return created membership
- nlohmann::json member_json = {
- {"id", result.member->id},
- {"workspace_id", result.member->workspace_id},
- {"user_id", result.member->user_id},
- {"group_ids", result.member->group_ids},
- {"created_at", result.member->created_at},
- {"updated_at", result.member->updated_at}
- };
- res.status = 201;
- res.set_content(member_json.dump(), "application/json");
- } catch (const nlohmann::json::exception& e) {
- res.status = 400;
- nlohmann::json error_response = {{"error", "Invalid JSON: " + std::string(e.what())}};
- res.set_content(error_response.dump(), "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Add member error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleListMembers(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID from path
- std::string workspace_id = req.matches[1].str();
- // Check access: 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<std::string> group_ids;
- for (const auto& item : json["group_ids"]) {
- if (item.is_string()) {
- group_ids.push_back(item.get<std::string>());
- }
- }
- update_req.group_ids = group_ids;
- }
- auto result = membershipService_->UpdateMembership(workspace_id, user_id, update_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Return updated membership
- nlohmann::json member_json = {
- {"id", result.member->id},
- {"workspace_id", result.member->workspace_id},
- {"user_id", result.member->user_id},
- {"group_ids", result.member->group_ids},
- {"created_at", result.member->created_at},
- {"updated_at", result.member->updated_at}
- };
- res.set_content(member_json.dump(), "application/json");
- } catch (const nlohmann::json::exception& e) {
- res.status = 400;
- nlohmann::json error_response = {{"error", "Invalid JSON: " + std::string(e.what())}};
- res.set_content(error_response.dump(), "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Update member error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleRemoveMember(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID and user ID from path
- std::string workspace_id = req.matches[1].str();
- std::string user_id = req.matches[2].str();
- // 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<std::string>();
- } else {
- res.status = 400;
- res.set_content(R"({"error":"name is required"})", "application/json");
- return;
- }
- if (json.contains("permissions") && json["permissions"].is_array()) {
- for (const auto& item : json["permissions"]) {
- if (item.is_string()) {
- std::string perm = item.get<std::string>();
- // Users can only grant permissions they have (unless 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<std::string>();
- }
- auto result = apiKeyService_->CreateApiKey(create_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Return created key info INCLUDING the raw key (only time it's returned!)
- nlohmann::json key_json = {
- {"id", result.api_key->id},
- {"user_id", result.api_key->user_id},
- {"name", result.api_key->name},
- {"key_prefix", result.api_key->key_prefix},
- {"key", result.raw_key}, // THE RAW KEY - only returned once!
- {"permissions", result.api_key->permissions},
- {"created_at", result.api_key->created_at},
- {"expires_at", result.api_key->expires_at}
- };
- res.status = 201;
- res.set_content(key_json.dump(), "application/json");
- } catch (const nlohmann::json::exception& e) {
- res.status = 400;
- nlohmann::json error_response = {{"error", "Invalid JSON: " + std::string(e.what())}};
- res.set_content(error_response.dump(), "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Create API key error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleListApiKeys(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract user ID from path
- std::string target_user_id = req.matches[1].str();
- // Check permissions: 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<std::string>();
- } else {
- res.status = 400;
- res.set_content(R"({"error":"name is required"})", "application/json");
- return;
- }
- // Parse settings
- if (json.contains("schema") && json["schema"].is_string()) {
- create_req.settings.schema = json["schema"].get<std::string>();
- }
- if (json.contains("encrypted_fields") && json["encrypted_fields"].is_array()) {
- for (const auto& item : json["encrypted_fields"]) {
- if (item.is_string()) {
- create_req.settings.encrypted_fields.push_back(item.get<std::string>());
- }
- }
- }
- if (json.contains("ttl_seconds") && json["ttl_seconds"].is_number_integer()) {
- create_req.settings.ttl_seconds = json["ttl_seconds"].get<int64_t>();
- }
- auto result = collectionService_->CreateCollection(create_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Return created collection info
- nlohmann::json coll_json = {
- {"name", result.collection->name},
- {"workspace_id", result.collection->workspace_id},
- {"document_count", result.collection->document_count},
- {"size_bytes", result.collection->size_bytes},
- {"created_at", result.collection->created_at},
- {"settings", {
- {"schema", result.collection->settings.schema},
- {"encrypted_fields", result.collection->settings.encrypted_fields},
- {"ttl_seconds", result.collection->settings.ttl_seconds}
- }}
- };
- res.status = 201;
- res.set_content(coll_json.dump(), "application/json");
- } catch (const nlohmann::json::exception& e) {
- spdlog::warn("Create collection JSON parse error: {}", e.what());
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Create collection error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleListCollections(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID from path
- std::string workspace_id = req.matches[1].str();
- // Verify workspace exists and user has access
- auto ws_result = workspaceService_->GetWorkspace(workspace_id);
- if (!ws_result.success) {
- res.status = 404;
- res.set_content(R"({"error":"Workspace not found"})", "application/json");
- return;
- }
- // Check 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<int64_t>(collections_array.size())}
- };
- res.set_content(response.dump(), "application/json");
- } catch (const std::exception& e) {
- spdlog::error("List collections error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleGetCollection(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID and collection name from path
- std::string workspace_id = req.matches[1].str();
- std::string collection_name = req.matches[2].str();
- // Verify workspace exists and user has access
- auto ws_result = workspaceService_->GetWorkspace(workspace_id);
- if (!ws_result.success) {
- res.status = 404;
- res.set_content(R"({"error":"Workspace not found"})", "application/json");
- return;
- }
- // Check 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<std::string>();
- }
- if (json.contains("encrypted_fields") && json["encrypted_fields"].is_array()) {
- settings.encrypted_fields.clear();
- for (const auto& item : json["encrypted_fields"]) {
- if (item.is_string()) {
- settings.encrypted_fields.push_back(item.get<std::string>());
- }
- }
- }
- if (json.contains("ttl_seconds") && json["ttl_seconds"].is_number_integer()) {
- settings.ttl_seconds = json["ttl_seconds"].get<int64_t>();
- }
- auto result = collectionService_->UpdateCollection(workspace_id, collection_name, settings);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- nlohmann::json coll_json = {
- {"name", result.collection->name},
- {"workspace_id", result.collection->workspace_id},
- {"document_count", result.collection->document_count},
- {"size_bytes", result.collection->size_bytes},
- {"created_at", result.collection->created_at},
- {"updated_at", result.collection->updated_at},
- {"settings", {
- {"schema", result.collection->settings.schema},
- {"encrypted_fields", result.collection->settings.encrypted_fields},
- {"ttl_seconds", result.collection->settings.ttl_seconds}
- }}
- };
- res.set_content(coll_json.dump(), "application/json");
- } catch (const nlohmann::json::exception& e) {
- spdlog::warn("Update collection JSON parse error: {}", e.what());
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Update collection error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- // ============================================================================
- // Document Routes
- // ============================================================================
- void HttpServer::SetupDocumentRoutes() {
- // POST /api/workspaces/:wid/collections/:name/documents - Create a document
- httpServer_->Post(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*)/documents)", [this](const httplib::Request& req, httplib::Response& res) {
- HandleCreateDocument(req, res);
- });
- // GET /api/workspaces/:wid/collections/:name/documents - List documents
- httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*)/documents)", [this](const httplib::Request& req, httplib::Response& res) {
- HandleListDocuments(req, res);
- });
- // GET /api/workspaces/:wid/collections/:name/documents/:id - Get a document
- httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*)/documents/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
- HandleGetDocument(req, res);
- });
- // PUT /api/workspaces/:wid/collections/:name/documents/:id - Update a document
- httpServer_->Put(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*)/documents/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
- HandleUpdateDocument(req, res);
- });
- // DELETE /api/workspaces/:wid/collections/:name/documents/:id - Delete a document
- httpServer_->Delete(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*)/documents/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
- HandleDeleteDocument(req, res);
- });
- spdlog::info("Document routes registered: /api/workspaces/:wid/collections/:name/documents");
- }
- void HttpServer::HandleCreateDocument(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID and collection name from path
- std::string workspace_id = req.matches[1].str();
- std::string collection_name = req.matches[2].str();
- // Verify workspace exists and user has access
- auto ws_result = workspaceService_->GetWorkspace(workspace_id);
- if (!ws_result.success) {
- res.status = 404;
- res.set_content(R"({"error":"Workspace not found"})", "application/json");
- return;
- }
- // Check 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;
- if (json.contains("id") && json["id"].is_string()) {
- create_req.id = json["id"].get<std::string>();
- }
- if (json.contains("data") && json["data"].is_object()) {
- create_req.data = json["data"];
- } else {
- // Treat entire body as data if no "data" field
- create_req.data = json;
- create_req.data.erase("id"); // Remove id from data
- }
- auto result = documentService_->CreateDocument(create_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Return created document info
- nlohmann::json doc_json = {
- {"id", result.document->id},
- {"collection", result.document->collection},
- {"workspace_id", result.document->workspace_id},
- {"data", result.document->data},
- {"created_at", result.document->created_at},
- {"updated_at", result.document->updated_at},
- {"version", result.document->version}
- };
- // Broadcast document create event to WebSocket subscribers
- BroadcastDocumentEvent(workspace_id, collection_name, DocumentAction::Create,
- result.document->id, result.document->data);
- res.status = 201;
- res.set_content(doc_json.dump(), "application/json");
- } catch (const nlohmann::json::exception& e) {
- spdlog::warn("Create document JSON parse error: {}", e.what());
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Create document error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleListDocuments(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID and collection name from path
- std::string workspace_id = req.matches[1].str();
- std::string collection_name = req.matches[2].str();
- // Verify workspace exists and user has access
- auto ws_result = workspaceService_->GetWorkspace(workspace_id);
- if (!ws_result.success) {
- res.status = 404;
- res.set_content(R"({"error":"Workspace not found"})", "application/json");
- return;
- }
- // Check 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) {
- nlohmann::json doc_json = {
- {"id", doc.id},
- {"collection", doc.collection},
- {"workspace_id", doc.workspace_id},
- {"data", doc.data},
- {"created_at", doc.created_at},
- {"updated_at", doc.updated_at},
- {"version", doc.version}
- };
- docs_array.push_back(doc_json);
- }
- nlohmann::json response = {
- {"documents", docs_array},
- {"total_count", result.total_count}
- };
- res.set_content(response.dump(), "application/json");
- } catch (const std::exception& e) {
- spdlog::error("List documents error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleGetDocument(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID, collection name, and document ID from path
- std::string workspace_id = req.matches[1].str();
- std::string collection_name = req.matches[2].str();
- std::string document_id = req.matches[3].str();
- // Verify workspace exists and user has access
- auto ws_result = workspaceService_->GetWorkspace(workspace_id);
- if (!ws_result.success) {
- res.status = 404;
- res.set_content(R"({"error":"Workspace not found"})", "application/json");
- return;
- }
- // Check if 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;
- if (json.contains("data") && json["data"].is_object()) {
- update_req.data = json["data"];
- } else {
- // Treat entire body as data if no "data" field
- update_req.data = json;
- }
- if (json.contains("merge") && json["merge"].is_boolean()) {
- update_req.merge = json["merge"].get<bool>();
- }
- if (json.contains("expected_version") && json["expected_version"].is_number_integer()) {
- update_req.expected_version = json["expected_version"].get<int64_t>();
- }
- auto result = documentService_->UpdateDocument(update_req);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- nlohmann::json doc_json = {
- {"id", result.document->id},
- {"collection", result.document->collection},
- {"workspace_id", result.document->workspace_id},
- {"data", result.document->data},
- {"created_at", result.document->created_at},
- {"updated_at", result.document->updated_at},
- {"version", result.document->version}
- };
- // Broadcast document update event to WebSocket subscribers
- BroadcastDocumentEvent(workspace_id, collection_name, DocumentAction::Update,
- result.document->id, result.document->data);
- res.set_content(doc_json.dump(), "application/json");
- } catch (const nlohmann::json::exception& e) {
- spdlog::warn("Update document JSON parse error: {}", e.what());
- res.status = 400;
- res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Update document error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleDeleteDocument(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID, collection name, and document ID from path
- std::string workspace_id = req.matches[1].str();
- std::string collection_name = req.matches[2].str();
- std::string document_id = req.matches[3].str();
- // Verify workspace exists and user has access
- auto ws_result = workspaceService_->GetWorkspace(workspace_id);
- if (!ws_result.success) {
- res.status = 404;
- res.set_content(R"({"error":"Workspace not found"})", "application/json");
- return;
- }
- // Check if 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<std::string>();
- request.collection_name = body["collection_name"].get<std::string>();
- // Parse schema if provided
- if (body.contains("schema") && body["schema"].is_object()) {
- const auto& schema_json = body["schema"];
- if (schema_json.contains("title")) {
- request.schema.title = schema_json.value("title", "");
- }
- if (schema_json.contains("description")) {
- request.schema.description = schema_json.value("description", "");
- }
- if (schema_json.contains("layout")) {
- request.schema.layout = schema_json["layout"];
- }
- if (schema_json.contains("fields") && schema_json["fields"].is_array()) {
- for (const auto& field_json : schema_json["fields"]) {
- SchemaField field;
- field.name = field_json.value("name", "");
- field.type = StringToFieldType(field_json.value("type", "text"));
- field.label = field_json.value("label", "");
- field.description = field_json.value("description", "");
- field.required = field_json.value("required", false);
- field.display_order = field_json.value("display_order", 0);
- field.widget = field_json.value("widget", "");
- field.group = field_json.value("group", "");
- field.default_value = field_json.value("default_value", nlohmann::json());
- field.options = field_json.value("options", nlohmann::json());
- field.reference_collection = field_json.value("reference_collection", "");
- field.computed_expression = field_json.value("computed_expression", "");
- request.schema.fields.push_back(field);
- }
- }
- }
- // Parse settings if provided
- if (body.contains("settings") && body["settings"].is_object()) {
- const auto& settings_json = body["settings"];
- request.settings.is_default = settings_json.value("is_default", false);
- request.settings.show_in_sidebar = settings_json.value("show_in_sidebar", true);
- request.settings.icon = settings_json.value("icon", "");
- request.settings.filters = settings_json.value("filters", nlohmann::json::object());
- request.settings.sort = settings_json.value("sort", nlohmann::json::object());
- }
- auto result = viewService_->CreateView(request);
- if (!result.success) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Build response with view info
- nlohmann::json view_json = {
- {"id", result.view->id},
- {"workspace_id", result.view->workspace_id},
- {"name", result.view->name},
- {"collection_name", result.view->collection_name},
- {"created_at", result.view->created_at},
- {"updated_at", result.view->updated_at}
- };
- // Add schema
- nlohmann::json schema_json = {
- {"title", result.view->schema.title},
- {"description", result.view->schema.description},
- {"layout", result.view->schema.layout}
- };
- nlohmann::json fields_json = nlohmann::json::array();
- for (const auto& field : result.view->schema.fields) {
- nlohmann::json field_json = {
- {"name", field.name},
- {"type", FieldTypeToString(field.type)},
- {"label", field.label},
- {"description", field.description},
- {"required", field.required},
- {"display_order", field.display_order},
- {"widget", field.widget},
- {"group", field.group}
- };
- if (!field.default_value.is_null()) {
- field_json["default_value"] = field.default_value;
- }
- if (!field.options.is_null()) {
- field_json["options"] = field.options;
- }
- if (!field.reference_collection.empty()) {
- field_json["reference_collection"] = field.reference_collection;
- }
- if (!field.computed_expression.empty()) {
- field_json["computed_expression"] = field.computed_expression;
- }
- fields_json.push_back(field_json);
- }
- schema_json["fields"] = fields_json;
- view_json["schema"] = schema_json;
- // Add settings
- view_json["settings"] = {
- {"is_default", result.view->settings.is_default},
- {"show_in_sidebar", result.view->settings.show_in_sidebar},
- {"icon", result.view->settings.icon},
- {"filters", result.view->settings.filters},
- {"sort", result.view->settings.sort}
- };
- res.status = 201;
- res.set_content(view_json.dump(), "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Create view error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleListViews(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID from path
- std::string workspace_id = req.matches[1].str();
- // Verify workspace exists
- auto ws_result = workspaceService_->GetWorkspace(workspace_id);
- if (!ws_result.success) {
- res.status = 404;
- res.set_content(R"({"error":"Workspace not found"})", "application/json");
- return;
- }
- // Check 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
- view_json["settings"] = {
- {"is_default", view.settings.is_default},
- {"show_in_sidebar", view.settings.show_in_sidebar},
- {"icon", view.settings.icon}
- };
- views_json.push_back(view_json);
- }
- nlohmann::json response = {{"views", views_json}};
- res.set_content(response.dump(), "application/json");
- } catch (const std::exception& e) {
- spdlog::error("List views error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleGetView(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID and view ID from path
- std::string workspace_id = req.matches[1].str();
- std::string view_id = req.matches[2].str();
- // Verify workspace exists
- auto ws_result = workspaceService_->GetWorkspace(workspace_id);
- if (!ws_result.success) {
- res.status = 404;
- res.set_content(R"({"error":"Workspace not found"})", "application/json");
- return;
- }
- // Check 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
- view_json["settings"] = {
- {"is_default", result.view->settings.is_default},
- {"show_in_sidebar", result.view->settings.show_in_sidebar},
- {"icon", result.view->settings.icon},
- {"filters", result.view->settings.filters},
- {"sort", result.view->settings.sort}
- };
- res.set_content(view_json.dump(), "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Get view error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleUpdateView(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID and view ID from path
- std::string workspace_id = req.matches[1].str();
- std::string view_id = req.matches[2].str();
- // Verify workspace exists
- auto ws_result = workspaceService_->GetWorkspace(workspace_id);
- if (!ws_result.success) {
- res.status = 404;
- res.set_content(R"({"error":"Workspace not found"})", "application/json");
- return;
- }
- // 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<std::string>();
- }
- if (body.contains("collection_name") && body["collection_name"].is_string()) {
- request.collection_name = body["collection_name"].get<std::string>();
- }
- // Parse schema if provided
- if (body.contains("schema") && body["schema"].is_object()) {
- ViewSchema schema;
- const auto& schema_json = body["schema"];
- schema.title = schema_json.value("title", "");
- schema.description = schema_json.value("description", "");
- schema.layout = schema_json.value("layout", nlohmann::json::object());
- if (schema_json.contains("fields") && schema_json["fields"].is_array()) {
- for (const auto& field_json : schema_json["fields"]) {
- SchemaField field;
- field.name = field_json.value("name", "");
- field.type = StringToFieldType(field_json.value("type", "text"));
- field.label = field_json.value("label", "");
- field.description = field_json.value("description", "");
- field.required = field_json.value("required", false);
- field.display_order = field_json.value("display_order", 0);
- field.widget = field_json.value("widget", "");
- field.group = field_json.value("group", "");
- field.default_value = field_json.value("default_value", nlohmann::json());
- field.options = field_json.value("options", nlohmann::json());
- field.reference_collection = field_json.value("reference_collection", "");
- field.computed_expression = field_json.value("computed_expression", "");
- schema.fields.push_back(field);
- }
- }
- request.schema = schema;
- }
- // Parse settings if provided
- if (body.contains("settings") && body["settings"].is_object()) {
- ViewSettings settings;
- const auto& settings_json = body["settings"];
- settings.is_default = settings_json.value("is_default", false);
- settings.show_in_sidebar = settings_json.value("show_in_sidebar", true);
- settings.icon = settings_json.value("icon", "");
- settings.filters = settings_json.value("filters", nlohmann::json::object());
- settings.sort = settings_json.value("sort", nlohmann::json::object());
- request.settings = settings;
- }
- auto result = viewService_->UpdateView(request);
- if (!result.success || !result.view) {
- res.status = 400;
- nlohmann::json error_response = {{"error", result.error}};
- res.set_content(error_response.dump(), "application/json");
- return;
- }
- // Build response with updated view info
- nlohmann::json view_json = {
- {"id", result.view->id},
- {"workspace_id", result.view->workspace_id},
- {"name", result.view->name},
- {"collection_name", result.view->collection_name},
- {"created_at", result.view->created_at},
- {"updated_at", result.view->updated_at}
- };
- // Add schema
- nlohmann::json schema_json = {
- {"title", result.view->schema.title},
- {"description", result.view->schema.description},
- {"layout", result.view->schema.layout}
- };
- nlohmann::json fields_json = nlohmann::json::array();
- for (const auto& field : result.view->schema.fields) {
- nlohmann::json field_json = {
- {"name", field.name},
- {"type", FieldTypeToString(field.type)},
- {"label", field.label},
- {"description", field.description},
- {"required", field.required},
- {"display_order", field.display_order},
- {"widget", field.widget},
- {"group", field.group}
- };
- if (!field.default_value.is_null()) {
- field_json["default_value"] = field.default_value;
- }
- if (!field.options.is_null()) {
- field_json["options"] = field.options;
- }
- if (!field.reference_collection.empty()) {
- field_json["reference_collection"] = field.reference_collection;
- }
- if (!field.computed_expression.empty()) {
- field_json["computed_expression"] = field.computed_expression;
- }
- fields_json.push_back(field_json);
- }
- schema_json["fields"] = fields_json;
- view_json["schema"] = schema_json;
- // Add settings
- view_json["settings"] = {
- {"is_default", result.view->settings.is_default},
- {"show_in_sidebar", result.view->settings.show_in_sidebar},
- {"icon", result.view->settings.icon},
- {"filters", result.view->settings.filters},
- {"sort", result.view->settings.sort}
- };
- res.set_content(view_json.dump(), "application/json");
- } catch (const std::exception& e) {
- spdlog::error("Update view error: {}", e.what());
- res.status = 500;
- res.set_content(R"({"error":"Internal server error"})", "application/json");
- }
- }
- void HttpServer::HandleDeleteView(const httplib::Request& req, httplib::Response& res) {
- // Authenticate the request
- auto auth_user = AuthenticateRequest(req);
- if (!auth_user) {
- res.status = 401;
- res.set_content(R"({"error":"Unauthorized"})", "application/json");
- return;
- }
- // Extract workspace ID and view ID from path
- std::string workspace_id = req.matches[1].str();
- std::string view_id = req.matches[2].str();
- // Verify workspace exists
- auto ws_result = workspaceService_->GetWorkspace(workspace_id);
- if (!ws_result.success) {
- res.status = 404;
- res.set_content(R"({"error":"Workspace not found"})", "application/json");
- return;
- }
- // 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");
- }
- }
- } // namespace smartbotic::webserver
|