http_server.cpp 207 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543
  1. #include "smartbotic/webserver/http_server.hpp"
  2. #include "smartbotic/webserver/permissions.hpp"
  3. #include <nlohmann/json.hpp>
  4. #include <spdlog/spdlog.h>
  5. #include <algorithm>
  6. #include <chrono>
  7. #include <cstring>
  8. #include <filesystem>
  9. #include <fstream>
  10. #include <random>
  11. #include <sstream>
  12. namespace smartbotic::webserver {
  13. // ============================================================================
  14. // WebSocketServer Implementation
  15. // ============================================================================
  16. // Static instance pointer for callback access
  17. WebSocketServer* WebSocketServer::instance_ = nullptr;
  18. namespace {
  19. auto GenerateSessionId() -> std::string {
  20. static std::random_device rd;
  21. static std::mt19937 gen(rd());
  22. static std::uniform_int_distribution<> dis(0, 15);
  23. static const char* hex = "0123456789abcdef";
  24. std::string uuid;
  25. uuid.reserve(36);
  26. for (int i = 0; i < 36; ++i) {
  27. if (i == 8 || i == 13 || i == 18 || i == 23) {
  28. uuid += '-';
  29. } else {
  30. uuid += hex[dis(gen)];
  31. }
  32. }
  33. return uuid;
  34. }
  35. } // namespace
  36. // LWS protocol definition - first protocol handles HTTP and is the default for WS
  37. static lws_protocols protocols[] = {
  38. {
  39. "http", // Default protocol name for HTTP/WS upgrade
  40. WebSocketServer::WsCallback,
  41. 0, // per_session_data_size - we manage our own
  42. 65536, // rx buffer size
  43. 0, // id
  44. nullptr, // user
  45. 65536 // tx_packet_size
  46. },
  47. LWS_PROTOCOL_LIST_TERM
  48. };
  49. WebSocketServer::WebSocketServer(uint16_t port, const std::string& path)
  50. : port_(port), path_(path) {
  51. instance_ = this;
  52. }
  53. WebSocketServer::~WebSocketServer() {
  54. Stop();
  55. instance_ = nullptr;
  56. }
  57. void WebSocketServer::Start() {
  58. if (running_.load()) {
  59. spdlog::warn("WebSocket server already running");
  60. return;
  61. }
  62. spdlog::info("Starting WebSocket server on port {}", port_);
  63. lws_context_creation_info info{};
  64. std::memset(&info, 0, sizeof(info));
  65. info.port = port_;
  66. info.protocols = protocols;
  67. info.gid = -1;
  68. info.uid = -1;
  69. info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
  70. info.vhost_name = "smartbotic-ws";
  71. // Suppress verbose lws logging
  72. lws_set_log_level(LLL_ERR | LLL_WARN, nullptr);
  73. context_ = lws_create_context(&info);
  74. if (context_ == nullptr) {
  75. spdlog::error("Failed to create libwebsocket context");
  76. return;
  77. }
  78. running_.store(true);
  79. eventLoopThread_ = std::thread(&WebSocketServer::RunEventLoop, this);
  80. spdlog::info("WebSocket server started on port {}", port_);
  81. }
  82. void WebSocketServer::Stop() {
  83. if (!running_.load()) {
  84. return;
  85. }
  86. spdlog::info("Stopping WebSocket server...");
  87. running_.store(false);
  88. // Cancel the event loop to wake up lws_service() immediately
  89. // This is async-signal-safe and can be called from signal handlers
  90. if (context_ != nullptr) {
  91. lws_cancel_service(context_);
  92. }
  93. if (eventLoopThread_.joinable()) {
  94. eventLoopThread_.join();
  95. }
  96. if (context_ != nullptr) {
  97. lws_context_destroy(context_);
  98. context_ = nullptr;
  99. }
  100. {
  101. std::lock_guard<std::mutex> lock(connectionsMutex_);
  102. connections_.clear();
  103. }
  104. spdlog::info("WebSocket server stopped");
  105. }
  106. void WebSocketServer::RunEventLoop() {
  107. while (running_.load()) {
  108. lws_service(context_, 50); // 50ms timeout
  109. }
  110. }
  111. void WebSocketServer::Broadcast(const std::string& message) {
  112. std::lock_guard<std::mutex> lock(connectionsMutex_);
  113. for (auto& [wsi, conn] : connections_) {
  114. QueueMessage(conn.get(), message);
  115. }
  116. }
  117. void WebSocketServer::SendToSession(const std::string& sessionId, const std::string& message) {
  118. std::lock_guard<std::mutex> lock(connectionsMutex_);
  119. for (auto& [wsi, conn] : connections_) {
  120. if (conn->sessionId == sessionId) {
  121. QueueMessage(conn.get(), message);
  122. break;
  123. }
  124. }
  125. }
  126. void WebSocketServer::SendToSubscribers(const std::string& subscription_key, const std::string& message) {
  127. std::lock_guard<std::mutex> lock(connectionsMutex_);
  128. for (auto& [wsi, conn] : connections_) {
  129. // Check if this connection is subscribed and authenticated
  130. if (!conn->authenticated) {
  131. continue;
  132. }
  133. const auto& subs = conn->subscriptions;
  134. if (std::find(subs.begin(), subs.end(), subscription_key) != subs.end()) {
  135. QueueMessage(conn.get(), message);
  136. }
  137. }
  138. }
  139. auto WebSocketServer::ConnectionCount() const -> size_t {
  140. std::lock_guard<std::mutex> lock(connectionsMutex_);
  141. return connections_.size();
  142. }
  143. void WebSocketServer::SetMessageHandler(WebSocketMessageHandler handler) {
  144. messageHandler_ = std::move(handler);
  145. }
  146. void WebSocketServer::HandleConnect(lws* wsi) {
  147. auto conn = std::make_unique<WsConnection>();
  148. conn->wsi = wsi;
  149. conn->sessionId = GenerateSessionId();
  150. spdlog::info("WebSocket client connected (session: {})", conn->sessionId);
  151. std::lock_guard<std::mutex> lock(connectionsMutex_);
  152. connections_[wsi] = std::move(conn);
  153. }
  154. void WebSocketServer::HandleDisconnect(lws* wsi) {
  155. std::lock_guard<std::mutex> lock(connectionsMutex_);
  156. auto it = connections_.find(wsi);
  157. if (it != connections_.end()) {
  158. spdlog::info("WebSocket client disconnected (session: {})", it->second->sessionId);
  159. connections_.erase(it);
  160. }
  161. }
  162. void WebSocketServer::HandleMessage(lws* wsi, const std::string& message) {
  163. WsConnection* conn = nullptr;
  164. {
  165. std::lock_guard<std::mutex> lock(connectionsMutex_);
  166. auto it = connections_.find(wsi);
  167. if (it != connections_.end()) {
  168. conn = it->second.get();
  169. }
  170. }
  171. // Call handler outside the lock to avoid deadlock with SendToSession
  172. if (conn && messageHandler_) {
  173. messageHandler_(conn, message);
  174. }
  175. }
  176. void WebSocketServer::QueueMessage(WsConnection* conn, const std::string& message) {
  177. std::lock_guard<std::mutex> lock(conn->sendMutex);
  178. // Prepend LWS_PRE bytes for libwebsockets
  179. conn->sendBuffer.resize(LWS_PRE + message.size());
  180. std::memcpy(conn->sendBuffer.data() + LWS_PRE, message.data(), message.size());
  181. lws_callback_on_writable(conn->wsi);
  182. }
  183. auto WebSocketServer::WsCallback(lws* wsi, lws_callback_reasons reason,
  184. void* user, void* in, size_t len) -> int {
  185. if (instance_ == nullptr) {
  186. return 0;
  187. }
  188. switch (reason) {
  189. case LWS_CALLBACK_PROTOCOL_INIT:
  190. spdlog::debug("WebSocket protocol initialized");
  191. break;
  192. case LWS_CALLBACK_ESTABLISHED:
  193. spdlog::debug("WebSocket connection established");
  194. instance_->HandleConnect(wsi);
  195. break;
  196. case LWS_CALLBACK_CLOSED:
  197. spdlog::debug("WebSocket connection closed");
  198. instance_->HandleDisconnect(wsi);
  199. break;
  200. case LWS_CALLBACK_RECEIVE: {
  201. std::string message(static_cast<char*>(in), len);
  202. spdlog::debug("WebSocket received {} bytes", len);
  203. instance_->HandleMessage(wsi, message);
  204. break;
  205. }
  206. case LWS_CALLBACK_SERVER_WRITEABLE: {
  207. std::lock_guard<std::mutex> lock(instance_->connectionsMutex_);
  208. auto it = instance_->connections_.find(wsi);
  209. if (it != instance_->connections_.end()) {
  210. auto& conn = it->second;
  211. std::lock_guard<std::mutex> sendLock(conn->sendMutex);
  212. if (conn->sendBuffer.size() > LWS_PRE) {
  213. size_t msgLen = conn->sendBuffer.size() - LWS_PRE;
  214. lws_write(wsi, conn->sendBuffer.data() + LWS_PRE, msgLen, LWS_WRITE_TEXT);
  215. conn->sendBuffer.clear();
  216. }
  217. }
  218. break;
  219. }
  220. case LWS_CALLBACK_HTTP:
  221. // Return non-zero to close the connection for non-WebSocket HTTP requests
  222. return -1;
  223. case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
  224. // Allow the connection
  225. return 0;
  226. default:
  227. break;
  228. }
  229. return 0;
  230. }
  231. // ============================================================================
  232. // HttpServer Implementation
  233. // ============================================================================
  234. HttpServer::HttpServer(HttpServerConfig config)
  235. : config_(std::move(config)),
  236. httpServer_(std::make_unique<httplib::Server>()),
  237. dbClient_(std::make_unique<DatabaseClient>(config_)),
  238. authService_(std::make_unique<AuthService>(config_.jwt)) {}
  239. HttpServer::~HttpServer() {
  240. if (running_.load()) {
  241. Stop();
  242. }
  243. }
  244. auto HttpServer::Start(bool require_database) -> bool {
  245. if (running_.load()) {
  246. spdlog::warn("Server already running");
  247. return true;
  248. }
  249. spdlog::info("Starting HTTP server on {}", config_.GetListenAddress());
  250. // Connect to database first (fail-fast if required)
  251. if (require_database) {
  252. spdlog::info("Connecting to database at {} (fail-fast mode enabled)", config_.database_address);
  253. if (!ConnectToDatabase()) {
  254. spdlog::error("Failed to connect to database - server startup aborted");
  255. return false;
  256. }
  257. } else {
  258. // Try to connect but don't fail if unavailable
  259. spdlog::info("Connecting to database at {} (optional mode)", config_.database_address);
  260. if (!ConnectToDatabase()) {
  261. spdlog::warn("Database connection failed - server will start without database connectivity");
  262. }
  263. }
  264. // Initialize services (only if database is connected)
  265. if (IsDatabaseConnected()) {
  266. if (!InitializeServices()) {
  267. spdlog::error("Failed to initialize services - server startup aborted");
  268. return false;
  269. }
  270. }
  271. // Setup routes
  272. SetupRoutes();
  273. // Start WebSocket server on separate port
  274. wsServer_ = std::make_unique<WebSocketServer>(config_.ws_port, config_.ws_path);
  275. // Setup WebSocket message handler using WsHandler if available
  276. if (wsHandler_) {
  277. wsServer_->SetMessageHandler([this](WsConnection* conn, const std::string& message) {
  278. wsHandler_->HandleMessage(conn, message,
  279. [this](WsConnection* c, const std::string& response) {
  280. wsServer_->SendToSession(c->sessionId, response);
  281. });
  282. });
  283. spdlog::info("WebSocket message handler configured with WsHandler");
  284. } else if (wsMessageHandler_) {
  285. wsServer_->SetMessageHandler(wsMessageHandler_);
  286. }
  287. wsServer_->Start();
  288. // Start HTTP server in a separate thread
  289. running_.store(true);
  290. httpThread_ = std::thread(&HttpServer::RunHttpServer, this);
  291. spdlog::info("HTTP server started successfully on {}", config_.GetListenAddress());
  292. spdlog::info("WebSocket server: ws://{}:{}{}", config_.address, config_.ws_port, config_.ws_path);
  293. spdlog::info("Static files: {}", config_.webui_path);
  294. spdlog::info("Database: {} ({})", config_.database_address,
  295. IsDatabaseConnected() ? "connected" : "not connected");
  296. return true;
  297. }
  298. void HttpServer::Stop() {
  299. if (!running_.load()) {
  300. return;
  301. }
  302. spdlog::info("Stopping HTTP server...");
  303. running_.store(false);
  304. // Stop WebSocket server first (this uses lws_cancel_service which is async-signal-safe)
  305. if (wsServer_) {
  306. wsServer_->Stop();
  307. }
  308. // Stop HTTP server - this makes listen() return
  309. if (httpServer_) {
  310. httpServer_->stop();
  311. }
  312. // Disconnect from database
  313. if (dbClient_) {
  314. dbClient_->Disconnect();
  315. }
  316. // NOTE: Don't join httpThread_ here - let Wait() handle it
  317. // This avoids race condition when Stop() is called from signal handler
  318. // while main thread is blocked in Wait()
  319. spdlog::info("HTTP server stop requested");
  320. }
  321. void HttpServer::Wait() {
  322. if (httpThread_.joinable()) {
  323. httpThread_.join();
  324. }
  325. spdlog::info("HTTP server stopped");
  326. }
  327. void HttpServer::BroadcastWebSocket(const std::string& message) {
  328. if (wsServer_) {
  329. wsServer_->Broadcast(message);
  330. }
  331. }
  332. void HttpServer::BroadcastDocumentEvent(const std::string& workspace_id,
  333. const std::string& collection,
  334. DocumentAction action,
  335. const std::string& document_id,
  336. const nlohmann::json& data) {
  337. if (!wsServer_ || !wsHandler_) {
  338. return;
  339. }
  340. std::string subscription_key = WsHandler::BuildSubscriptionKey(workspace_id, collection);
  341. // Build the event JSON
  342. nlohmann::json event = {
  343. {"type", "document"},
  344. {"action", DocumentActionToString(action)},
  345. {"workspace_id", workspace_id},
  346. {"collection", collection},
  347. {"document_id", document_id}
  348. };
  349. // Include data for create/update, exclude for delete
  350. if (action != DocumentAction::Delete && !data.is_null()) {
  351. event["data"] = data;
  352. }
  353. std::string event_str = event.dump();
  354. spdlog::debug("Broadcasting document event: {} {} in {} (key={})",
  355. DocumentActionToString(action), document_id, collection, subscription_key);
  356. wsServer_->SendToSubscribers(subscription_key, event_str);
  357. }
  358. void HttpServer::SetWebSocketMessageHandler(WebSocketMessageHandler handler) {
  359. wsMessageHandler_ = std::move(handler);
  360. if (wsServer_) {
  361. wsServer_->SetMessageHandler(wsMessageHandler_);
  362. }
  363. }
  364. auto HttpServer::GetWebSocketClientCount() const -> size_t {
  365. return wsServer_ ? wsServer_->ConnectionCount() : 0;
  366. }
  367. auto HttpServer::IsDatabaseConnected() const -> bool {
  368. return dbClient_ && dbClient_->IsConnected();
  369. }
  370. auto HttpServer::ConnectToDatabase() -> bool {
  371. if (!dbClient_) {
  372. dbClient_ = std::make_unique<DatabaseClient>(config_);
  373. }
  374. // Try to connect
  375. if (!dbClient_->Connect()) {
  376. return false;
  377. }
  378. // Perform a health check to verify the connection is working
  379. auto health = dbClient_->HealthCheck();
  380. if (!health.healthy) {
  381. spdlog::error("Database health check failed: {}", health.message);
  382. return false;
  383. }
  384. spdlog::info("Database health check passed (latency: {}ms)", health.latency.count());
  385. return true;
  386. }
  387. auto HttpServer::InitializeServices() -> bool {
  388. // Initialize UserService
  389. userService_ = std::make_unique<UserService>(*dbClient_);
  390. if (!userService_->Initialize()) {
  391. spdlog::error("Failed to initialize UserService");
  392. return false;
  393. }
  394. spdlog::info("UserService initialized successfully");
  395. // Initialize WorkspaceService
  396. workspaceService_ = std::make_unique<WorkspaceService>(*dbClient_);
  397. if (!workspaceService_->Initialize()) {
  398. spdlog::error("Failed to initialize WorkspaceService");
  399. return false;
  400. }
  401. spdlog::info("WorkspaceService initialized successfully");
  402. // Initialize GroupService
  403. groupService_ = std::make_unique<GroupService>(*dbClient_);
  404. if (!groupService_->Initialize()) {
  405. spdlog::error("Failed to initialize GroupService");
  406. return false;
  407. }
  408. spdlog::info("GroupService initialized successfully");
  409. // Initialize MembershipService
  410. membershipService_ = std::make_unique<MembershipService>(*dbClient_);
  411. if (!membershipService_->Initialize()) {
  412. spdlog::error("Failed to initialize MembershipService");
  413. return false;
  414. }
  415. spdlog::info("MembershipService initialized successfully");
  416. // Initialize ApiKeyService
  417. apiKeyService_ = std::make_unique<ApiKeyService>(*dbClient_);
  418. if (!apiKeyService_->Initialize()) {
  419. spdlog::error("Failed to initialize ApiKeyService");
  420. return false;
  421. }
  422. spdlog::info("ApiKeyService initialized successfully");
  423. // Initialize CollectionService
  424. collectionService_ = std::make_unique<CollectionService>(*dbClient_);
  425. spdlog::info("CollectionService initialized successfully");
  426. // Initialize DocumentService
  427. documentService_ = std::make_unique<DocumentService>(*dbClient_);
  428. spdlog::info("DocumentService initialized successfully");
  429. // Initialize ViewService
  430. viewService_ = std::make_unique<ViewService>(*dbClient_);
  431. if (!viewService_->Initialize()) {
  432. spdlog::error("Failed to initialize ViewService");
  433. return false;
  434. }
  435. spdlog::info("ViewService initialized successfully");
  436. // Initialize PageService
  437. pageService_ = std::make_unique<PageService>(*dbClient_);
  438. if (!pageService_->Initialize()) {
  439. spdlog::error("Failed to initialize PageService");
  440. return false;
  441. }
  442. spdlog::info("PageService initialized successfully");
  443. // Initialize AuthorizationService
  444. authorizationService_ = std::make_unique<AuthorizationService>(*groupService_, *collectionService_);
  445. if (!authorizationService_->Initialize()) {
  446. spdlog::error("Failed to initialize AuthorizationService");
  447. return false;
  448. }
  449. spdlog::info("AuthorizationService initialized successfully");
  450. // Initialize WsHandler for WebSocket real-time updates
  451. wsHandler_ = std::make_unique<WsHandler>(*authService_);
  452. spdlog::info("WsHandler initialized successfully");
  453. // Note: WebSocket message handler is set up in Start() after wsServer_ is created
  454. return true;
  455. }
  456. void HttpServer::SetupRoutes() {
  457. // Pre-routing handler for CORS
  458. httpServer_->set_pre_routing_handler([this](const httplib::Request& req, httplib::Response& res) {
  459. // Apply CORS headers if enabled
  460. if (config_.cors.enabled) {
  461. // Combine allowed origins
  462. std::string origins;
  463. for (size_t i = 0; i < config_.cors.allowed_origins.size(); ++i) {
  464. if (i > 0) origins += ", ";
  465. origins += config_.cors.allowed_origins[i];
  466. }
  467. res.set_header("Access-Control-Allow-Origin", origins);
  468. // Combine allowed methods
  469. std::string methods;
  470. for (size_t i = 0; i < config_.cors.allowed_methods.size(); ++i) {
  471. if (i > 0) methods += ", ";
  472. methods += config_.cors.allowed_methods[i];
  473. }
  474. res.set_header("Access-Control-Allow-Methods", methods);
  475. // Combine allowed headers
  476. std::string headers;
  477. for (size_t i = 0; i < config_.cors.allowed_headers.size(); ++i) {
  478. if (i > 0) headers += ", ";
  479. headers += config_.cors.allowed_headers[i];
  480. }
  481. res.set_header("Access-Control-Allow-Headers", headers);
  482. if (config_.cors.allow_credentials) {
  483. res.set_header("Access-Control-Allow-Credentials", "true");
  484. }
  485. res.set_header("Access-Control-Max-Age", std::to_string(config_.cors.max_age));
  486. }
  487. res.set_header("Server", "SmartBotic-Server/0.1.0");
  488. // Handle CORS preflight
  489. if (req.method == "OPTIONS") {
  490. res.status = 204;
  491. return httplib::Server::HandlerResponse::Handled;
  492. }
  493. return httplib::Server::HandlerResponse::Unhandled;
  494. });
  495. // Health check endpoint
  496. httpServer_->Get("/api/health", [this](const httplib::Request& req, httplib::Response& res) {
  497. HandleHealth(req, res);
  498. });
  499. // Version endpoint
  500. httpServer_->Get("/api/version", [this](const httplib::Request& req, httplib::Response& res) {
  501. HandleVersion(req, res);
  502. });
  503. // Bootstrap endpoint - creates first admin user if no users exist
  504. httpServer_->Post("/api/bootstrap", [this](const httplib::Request& req, httplib::Response& res) {
  505. HandleBootstrap(req, res);
  506. });
  507. // Setup authentication routes
  508. SetupAuthRoutes();
  509. // Setup user management routes
  510. SetupUserRoutes();
  511. // Setup API key routes (under /api/users/:id/api-keys)
  512. SetupApiKeyRoutes();
  513. // Setup membership routes (before workspace routes since they're more specific)
  514. SetupMembershipRoutes();
  515. // Setup document routes (most specific - before collections)
  516. SetupDocumentRoutes();
  517. // Setup view routes (schema definitions)
  518. SetupViewRoutes();
  519. // Setup page routes (page builder)
  520. SetupPageRoutes();
  521. // Setup collection routes (before workspace routes since they're more specific)
  522. SetupCollectionRoutes();
  523. // Setup group management routes (before workspace routes since they're more specific)
  524. SetupGroupRoutes();
  525. // Setup workspace management routes
  526. SetupWorkspaceRoutes();
  527. // Mount static file directory for WebUI
  528. if (std::filesystem::exists(config_.webui_path)) {
  529. httpServer_->set_mount_point("/", config_.webui_path);
  530. spdlog::info("Mounted static files from: {}", config_.webui_path);
  531. } else {
  532. spdlog::warn("WebUI path does not exist: {}", config_.webui_path);
  533. }
  534. // Custom error handler for 404 (only if response body is empty)
  535. httpServer_->set_error_handler([](const httplib::Request& /*req*/, httplib::Response& res) {
  536. // Only set default error message if body hasn't been set by a handler
  537. if (res.body.empty()) {
  538. res.set_content(R"({"error":"Not found"})", "application/json");
  539. }
  540. });
  541. // Logger for requests
  542. httpServer_->set_logger([](const httplib::Request& req, const httplib::Response& res) {
  543. auto status = res.status;
  544. if (status >= 500) {
  545. spdlog::error("{} {} {}", req.method, req.path, status);
  546. } else if (status >= 400) {
  547. spdlog::warn("{} {} {}", req.method, req.path, status);
  548. } else {
  549. spdlog::info("{} {} {}", req.method, req.path, status);
  550. }
  551. });
  552. }
  553. void HttpServer::RunHttpServer() {
  554. if (!httpServer_->listen(config_.address, config_.port)) {
  555. spdlog::error("Failed to start HTTP server on {}", config_.GetListenAddress());
  556. running_.store(false);
  557. }
  558. }
  559. void HttpServer::HandleHealth(const httplib::Request& req, httplib::Response& res) {
  560. res.set_content(R"({"status":"healthy","service":"smartbotic-server"})", "application/json");
  561. }
  562. void HttpServer::HandleVersion(const httplib::Request& req, httplib::Response& res) {
  563. res.set_content(R"({"version":"0.1.0","name":"SmartBotic Web Server"})", "application/json");
  564. }
  565. void HttpServer::HandleBootstrap(const httplib::Request& req, httplib::Response& res) {
  566. // Bootstrap endpoint - creates first admin user if no users exist
  567. if (!userService_) {
  568. res.status = 500;
  569. res.set_content(R"({"error":"User service not available"})", "application/json");
  570. return;
  571. }
  572. // Check if any users exist
  573. auto users = userService_->ListUsers();
  574. if (!users.users.empty()) {
  575. res.status = 400;
  576. res.set_content(R"({"error":"System already bootstrapped - users exist"})", "application/json");
  577. return;
  578. }
  579. // Parse request body
  580. nlohmann::json body;
  581. try {
  582. body = nlohmann::json::parse(req.body);
  583. } catch (...) {
  584. res.status = 400;
  585. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  586. return;
  587. }
  588. // Validate required fields
  589. if (!body.contains("email") || !body.contains("password")) {
  590. res.status = 400;
  591. res.set_content(R"({"error":"Email and password are required"})", "application/json");
  592. return;
  593. }
  594. CreateUserRequest user_request;
  595. user_request.email = body["email"].get<std::string>();
  596. user_request.password = body["password"].get<std::string>();
  597. user_request.name = body.value("name", "Admin");
  598. // Create the admin user
  599. auto result = userService_->CreateUser(user_request);
  600. if (!result.success) {
  601. res.status = 400;
  602. nlohmann::json error_response;
  603. error_response["error"] = result.error;
  604. res.set_content(error_response.dump(), "application/json");
  605. return;
  606. }
  607. // Get the superadmin group to assign to the first user
  608. std::string superadmin_group_id;
  609. if (groupService_) {
  610. auto sa_group = groupService_->GetSuperadminGroup();
  611. if (sa_group.success && sa_group.group) {
  612. superadmin_group_id = sa_group.group->id;
  613. spdlog::info("Found superadmin group: {}", superadmin_group_id);
  614. } else {
  615. spdlog::warn("Superadmin group not found - first user will not have admin privileges");
  616. }
  617. }
  618. // Create a default workspace
  619. if (workspaceService_) {
  620. CreateWorkspaceRequest ws_request;
  621. ws_request.name = "Default Workspace";
  622. ws_request.settings["default"] = true;
  623. auto ws_result = workspaceService_->CreateWorkspace(ws_request);
  624. // Add user to workspace with superadmin group
  625. if (ws_result.success && ws_result.workspace && membershipService_) {
  626. AddMemberRequest member_request;
  627. member_request.workspace_id = ws_result.workspace->id;
  628. member_request.user_id = result.user->id;
  629. // Assign the superadmin group to the first user
  630. if (!superadmin_group_id.empty()) {
  631. member_request.group_ids = {superadmin_group_id};
  632. spdlog::info("Assigning superadmin group to bootstrap user");
  633. } else {
  634. member_request.group_ids = {};
  635. }
  636. [[maybe_unused]] auto member_result = membershipService_->AddMember(member_request);
  637. }
  638. }
  639. // Return success with user info
  640. nlohmann::json response;
  641. response["success"] = true;
  642. response["message"] = "System bootstrapped successfully";
  643. response["user"]["id"] = result.user->id;
  644. response["user"]["email"] = result.user->email;
  645. response["user"]["name"] = result.user->name;
  646. spdlog::info("System bootstrapped with admin user: {}", user_request.email);
  647. res.set_content(response.dump(), "application/json");
  648. }
  649. auto HttpServer::GetMimeType(const std::string& path) -> std::string {
  650. auto ext_pos = path.rfind('.');
  651. if (ext_pos == std::string::npos) {
  652. return "application/octet-stream";
  653. }
  654. std::string ext = path.substr(ext_pos);
  655. static const std::unordered_map<std::string, std::string> mime_types = {
  656. {".html", "text/html"},
  657. {".htm", "text/html"},
  658. {".css", "text/css"},
  659. {".js", "application/javascript"},
  660. {".json", "application/json"},
  661. {".png", "image/png"},
  662. {".jpg", "image/jpeg"},
  663. {".jpeg", "image/jpeg"},
  664. {".gif", "image/gif"},
  665. {".svg", "image/svg+xml"},
  666. {".ico", "image/x-icon"},
  667. {".woff", "font/woff"},
  668. {".woff2", "font/woff2"},
  669. {".ttf", "font/ttf"},
  670. {".eot", "application/vnd.ms-fontobject"},
  671. {".txt", "text/plain"},
  672. {".xml", "application/xml"},
  673. {".pdf", "application/pdf"},
  674. };
  675. auto it = mime_types.find(ext);
  676. if (it != mime_types.end()) {
  677. return it->second;
  678. }
  679. return "application/octet-stream";
  680. }
  681. // ============================================================================
  682. // Authentication Routes Implementation
  683. // ============================================================================
  684. void HttpServer::SetupAuthRoutes() {
  685. // POST /api/auth/login - Authenticate user and return JWT
  686. httpServer_->Post("/api/auth/login", [this](const httplib::Request& req, httplib::Response& res) {
  687. HandleAuthLogin(req, res);
  688. });
  689. // POST /api/auth/refresh - Refresh access token
  690. httpServer_->Post("/api/auth/refresh", [this](const httplib::Request& req, httplib::Response& res) {
  691. HandleAuthRefresh(req, res);
  692. });
  693. // POST /api/auth/logout - Invalidate refresh token
  694. httpServer_->Post("/api/auth/logout", [this](const httplib::Request& req, httplib::Response& res) {
  695. HandleAuthLogout(req, res);
  696. });
  697. // GET /api/auth/me - Get current authenticated user info
  698. httpServer_->Get("/api/auth/me", [this](const httplib::Request& req, httplib::Response& res) {
  699. HandleAuthMe(req, res);
  700. });
  701. spdlog::info("Authentication routes registered: /api/auth/login, /api/auth/refresh, /api/auth/logout, /api/auth/me");
  702. }
  703. void HttpServer::HandleAuthLogin(const httplib::Request& req, httplib::Response& res) {
  704. try {
  705. // Parse request body
  706. auto body = nlohmann::json::parse(req.body);
  707. if (!body.contains("email") || !body.contains("password")) {
  708. res.status = 400;
  709. res.set_content(R"({"error":"Missing email or password"})", "application/json");
  710. return;
  711. }
  712. std::string email = body["email"].get<std::string>();
  713. std::string password = body["password"].get<std::string>();
  714. if (config_.jwt.secret.empty()) {
  715. res.status = 500;
  716. res.set_content(R"({"error":"JWT secret not configured"})", "application/json");
  717. return;
  718. }
  719. if (!userService_) {
  720. res.status = 500;
  721. res.set_content(R"({"error":"User service not available"})", "application/json");
  722. return;
  723. }
  724. // Verify user credentials
  725. auto user_result = userService_->VerifyCredentials(email, password);
  726. if (!user_result.success || !user_result.user) {
  727. res.status = 401;
  728. res.set_content(R"({"error":"Invalid email or password"})", "application/json");
  729. return;
  730. }
  731. const auto& user = *user_result.user;
  732. // Get user's workspace memberships
  733. std::vector<std::string> workspace_ids;
  734. if (membershipService_) {
  735. auto memberships = membershipService_->ListUserMemberships(user.id);
  736. for (const auto& m : memberships.members) {
  737. workspace_ids.push_back(m.workspace_id);
  738. }
  739. }
  740. // Collect user's groups from memberships
  741. // Note: Superadmin status is determined by group membership, not by special logic
  742. // The first user gets superadmin group assigned during bootstrap
  743. std::vector<std::string> groups;
  744. if (membershipService_) {
  745. auto memberships = membershipService_->ListUserMemberships(user.id);
  746. for (const auto& m : memberships.members) {
  747. for (const auto& gid : m.group_ids) {
  748. if (std::find(groups.begin(), groups.end(), gid) == groups.end()) {
  749. groups.push_back(gid);
  750. }
  751. }
  752. }
  753. }
  754. auto tokens = authService_->GenerateTokens(user.id, email, workspace_ids, groups);
  755. nlohmann::json response = {
  756. {"access_token", tokens.access_token},
  757. {"refresh_token", tokens.refresh_token},
  758. {"token_type", "Bearer"},
  759. {"expires_in", tokens.access_expires_in}
  760. };
  761. spdlog::info("User logged in: {}", email);
  762. res.set_content(response.dump(), "application/json");
  763. } catch (const nlohmann::json::parse_error& e) {
  764. res.status = 400;
  765. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  766. } catch (const std::exception& e) {
  767. spdlog::error("Login error: {}", e.what());
  768. res.status = 500;
  769. res.set_content(R"({"error":"Internal server error"})", "application/json");
  770. }
  771. }
  772. void HttpServer::HandleAuthRefresh(const httplib::Request& req, httplib::Response& res) {
  773. try {
  774. // Parse request body
  775. auto body = nlohmann::json::parse(req.body);
  776. if (!body.contains("refresh_token")) {
  777. res.status = 400;
  778. res.set_content(R"({"error":"Missing refresh_token"})", "application/json");
  779. return;
  780. }
  781. std::string refresh_token = body["refresh_token"].get<std::string>();
  782. auto new_tokens = authService_->RefreshAccessToken(refresh_token);
  783. if (!new_tokens) {
  784. res.status = 401;
  785. res.set_content(R"({"error":"Invalid or expired refresh token"})", "application/json");
  786. return;
  787. }
  788. nlohmann::json response = {
  789. {"access_token", new_tokens->access_token},
  790. {"refresh_token", new_tokens->refresh_token},
  791. {"token_type", "Bearer"},
  792. {"expires_in", new_tokens->access_expires_in}
  793. };
  794. spdlog::debug("Token refreshed successfully");
  795. res.set_content(response.dump(), "application/json");
  796. } catch (const nlohmann::json::parse_error& e) {
  797. res.status = 400;
  798. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  799. } catch (const std::exception& e) {
  800. spdlog::error("Token refresh error: {}", e.what());
  801. res.status = 500;
  802. res.set_content(R"({"error":"Internal server error"})", "application/json");
  803. }
  804. }
  805. void HttpServer::HandleAuthLogout(const httplib::Request& req, httplib::Response& res) {
  806. try {
  807. // Parse request body
  808. auto body = nlohmann::json::parse(req.body);
  809. if (!body.contains("refresh_token")) {
  810. res.status = 400;
  811. res.set_content(R"({"error":"Missing refresh_token"})", "application/json");
  812. return;
  813. }
  814. std::string refresh_token = body["refresh_token"].get<std::string>();
  815. if (authService_->InvalidateRefreshToken(refresh_token)) {
  816. spdlog::debug("User logged out successfully");
  817. res.set_content(R"({"message":"Logged out successfully"})", "application/json");
  818. } else {
  819. res.status = 400;
  820. res.set_content(R"({"error":"Invalid refresh token"})", "application/json");
  821. }
  822. } catch (const nlohmann::json::parse_error& e) {
  823. res.status = 400;
  824. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  825. } catch (const std::exception& e) {
  826. spdlog::error("Logout error: {}", e.what());
  827. res.status = 500;
  828. res.set_content(R"({"error":"Internal server error"})", "application/json");
  829. }
  830. }
  831. void HttpServer::HandleAuthMe(const httplib::Request& req, httplib::Response& res) {
  832. auto auth_user = AuthenticateRequest(req);
  833. if (!auth_user) {
  834. res.status = 401;
  835. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  836. return;
  837. }
  838. // Build response with user info from token
  839. nlohmann::json response = {
  840. {"id", auth_user->user_id},
  841. {"email", auth_user->email},
  842. {"groups", auth_user->groups},
  843. {"workspace_ids", auth_user->workspace_ids}
  844. };
  845. // Try to get additional user info from database
  846. if (userService_) {
  847. auto user_result = userService_->GetUser(auth_user->user_id);
  848. if (user_result.success && user_result.user) {
  849. response["name"] = user_result.user->name;
  850. response["created_at"] = user_result.user->created_at;
  851. }
  852. }
  853. // Check if user is superadmin based on groups
  854. response["is_superadmin"] = IsSuperadmin(*auth_user);
  855. res.set_content(response.dump(), "application/json");
  856. }
  857. auto HttpServer::AuthenticateRequest(const httplib::Request& req) -> std::optional<AuthUser> {
  858. std::string auth_token;
  859. // First check X-API-Key header (direct API key)
  860. auto api_key_header = req.get_header_value("X-API-Key");
  861. if (!api_key_header.empty()) {
  862. auth_token = api_key_header;
  863. } else {
  864. // Check Authorization header
  865. auto auth_header = req.get_header_value("Authorization");
  866. if (auth_header.empty()) {
  867. return std::nullopt;
  868. }
  869. auto token = AuthService::ExtractBearerToken(auth_header);
  870. if (!token) {
  871. return std::nullopt;
  872. }
  873. auth_token = *token;
  874. }
  875. // Check if this is an API key (starts with "sk_")
  876. if (auth_token.starts_with("sk_")) {
  877. if (!apiKeyService_) {
  878. spdlog::warn("API key authentication attempted but service not available");
  879. return std::nullopt;
  880. }
  881. auto validation = apiKeyService_->ValidateApiKey(auth_token);
  882. if (!validation.valid || !validation.api_key) {
  883. spdlog::debug("API key validation failed: {}", validation.error);
  884. return std::nullopt;
  885. }
  886. // Check if key is expired
  887. if (validation.api_key->IsExpired()) {
  888. spdlog::debug("API key expired");
  889. return std::nullopt;
  890. }
  891. // Update last used timestamp (fire and forget)
  892. apiKeyService_->UpdateLastUsed(validation.api_key->id);
  893. // Build AuthUser from API key
  894. AuthUser auth_user;
  895. auth_user.user_id = validation.api_key->user_id;
  896. // Use API key permissions as groups
  897. auth_user.groups = validation.api_key->permissions;
  898. // Try to fetch additional user info
  899. if (userService_) {
  900. auto user_result = userService_->GetUser(validation.api_key->user_id);
  901. if (user_result.success && user_result.user) {
  902. auth_user.email = user_result.user->email;
  903. // Fetch workspace memberships for the user
  904. if (membershipService_) {
  905. auto memberships = membershipService_->ListUserMemberships(user_result.user->id);
  906. for (const auto& m : memberships.members) {
  907. auth_user.workspace_ids.push_back(m.workspace_id);
  908. }
  909. }
  910. } else {
  911. spdlog::debug("API key owner user not found in database: {}", validation.api_key->user_id);
  912. }
  913. }
  914. return auth_user;
  915. }
  916. // Otherwise, treat as JWT token
  917. auto validation = authService_->ValidateAccessToken(auth_token);
  918. if (!validation.valid) {
  919. spdlog::debug("Token validation failed: {}", validation.error);
  920. return std::nullopt;
  921. }
  922. // Verify the user still exists in the database
  923. // This handles cases where the database was reset or user was deleted
  924. if (userService_ && validation.user) {
  925. auto user_result = userService_->GetUser(validation.user->user_id);
  926. if (!user_result.success || !user_result.user) {
  927. spdlog::debug("Token user no longer exists in database: {}", validation.user->user_id);
  928. return std::nullopt;
  929. }
  930. }
  931. return validation.user;
  932. }
  933. // ============================================================================
  934. // User Management Routes Implementation
  935. // ============================================================================
  936. void HttpServer::SetupUserRoutes() {
  937. // POST /api/users - Create a new user (superadmin only)
  938. httpServer_->Post("/api/users", [this](const httplib::Request& req, httplib::Response& res) {
  939. HandleCreateUser(req, res);
  940. });
  941. // GET /api/users - List all users (superadmin only)
  942. httpServer_->Get("/api/users", [this](const httplib::Request& req, httplib::Response& res) {
  943. HandleListUsers(req, res);
  944. });
  945. // GET /api/users/:id - Get a specific user
  946. httpServer_->Get(R"(/api/users/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  947. HandleGetUser(req, res);
  948. });
  949. // PATCH /api/users/:id - Update a user
  950. httpServer_->Patch(R"(/api/users/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  951. HandleUpdateUser(req, res);
  952. });
  953. // DELETE /api/users/:id - Soft delete a user (superadmin only)
  954. httpServer_->Delete(R"(/api/users/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  955. HandleDeleteUser(req, res);
  956. });
  957. spdlog::info("User management routes registered: /api/users");
  958. }
  959. auto HttpServer::IsSuperadmin(const AuthUser& user) -> bool {
  960. // Delegate to authorization service for proper permission checking
  961. // This maintains backward compatibility while using the new RBAC system
  962. if (authorizationService_) {
  963. return authorizationService_->IsSuperadmin(user);
  964. }
  965. // Fallback to old behavior if authorization service not available
  966. return std::find(user.groups.begin(), user.groups.end(), "superadmin") != user.groups.end();
  967. }
  968. auto HttpServer::ResolveUserName(const std::string& user_id) -> std::string {
  969. if (user_id.empty() || !userService_) {
  970. return "";
  971. }
  972. auto result = userService_->GetUser(user_id, true);
  973. if (result.success && result.user) {
  974. return result.user->name;
  975. }
  976. return "";
  977. }
  978. void HttpServer::HandleCreateUser(const httplib::Request& req, httplib::Response& res) {
  979. // Authenticate the request
  980. auto auth_user = AuthenticateRequest(req);
  981. if (!auth_user) {
  982. res.status = 401;
  983. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  984. return;
  985. }
  986. // Check permission to create users
  987. if (!authorizationService_ || !authorizationService_->HasPermission(*auth_user, smartbotic::permissions::kUsersCreate)) {
  988. res.status = 403;
  989. res.set_content(R"({"error":"Forbidden - requires system:users:create permission"})", "application/json");
  990. return;
  991. }
  992. // Check if UserService is available
  993. if (!userService_) {
  994. res.status = 503;
  995. res.set_content(R"({"error":"User service not available"})", "application/json");
  996. return;
  997. }
  998. try {
  999. auto body = nlohmann::json::parse(req.body);
  1000. CreateUserRequest create_req;
  1001. create_req.actor_id = auth_user->user_id;
  1002. if (body.contains("email")) {
  1003. create_req.email = body["email"].get<std::string>();
  1004. }
  1005. if (body.contains("password")) {
  1006. create_req.password = body["password"].get<std::string>();
  1007. }
  1008. if (body.contains("name")) {
  1009. create_req.name = body["name"].get<std::string>();
  1010. }
  1011. auto result = userService_->CreateUser(create_req);
  1012. if (!result.success) {
  1013. res.status = 400;
  1014. nlohmann::json error_response = {{"error", result.error}};
  1015. res.set_content(error_response.dump(), "application/json");
  1016. return;
  1017. }
  1018. // Return created user (without password_hash)
  1019. nlohmann::json user_json = {
  1020. {"id", result.user->id},
  1021. {"email", result.user->email},
  1022. {"name", result.user->name},
  1023. {"created_at", result.user->created_at},
  1024. {"updated_at", result.user->updated_at}
  1025. };
  1026. res.status = 201;
  1027. res.set_content(user_json.dump(), "application/json");
  1028. } catch (const nlohmann::json::parse_error& e) {
  1029. res.status = 400;
  1030. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  1031. } catch (const std::exception& e) {
  1032. spdlog::error("Create user error: {}", e.what());
  1033. res.status = 500;
  1034. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1035. }
  1036. }
  1037. void HttpServer::HandleListUsers(const httplib::Request& req, httplib::Response& res) {
  1038. // Authenticate the request
  1039. auto auth_user = AuthenticateRequest(req);
  1040. if (!auth_user) {
  1041. res.status = 401;
  1042. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1043. return;
  1044. }
  1045. // Requires system:users:read permission
  1046. if (!authorizationService_->HasPermission(*auth_user, permissions::kUsersRead)) {
  1047. res.status = 403;
  1048. res.set_content(R"({"error":"Forbidden - requires system:users:read permission"})", "application/json");
  1049. return;
  1050. }
  1051. // Check if UserService is available
  1052. if (!userService_) {
  1053. res.status = 503;
  1054. res.set_content(R"({"error":"User service not available"})", "application/json");
  1055. return;
  1056. }
  1057. try {
  1058. // Parse query parameters
  1059. int limit = 100;
  1060. int offset = 0;
  1061. bool include_deleted = false;
  1062. if (req.has_param("limit")) {
  1063. limit = std::stoi(req.get_param_value("limit"));
  1064. if (limit < 1 || limit > 1000) {
  1065. limit = 100;
  1066. }
  1067. }
  1068. if (req.has_param("offset")) {
  1069. offset = std::stoi(req.get_param_value("offset"));
  1070. if (offset < 0) {
  1071. offset = 0;
  1072. }
  1073. }
  1074. if (req.has_param("include_deleted")) {
  1075. include_deleted = req.get_param_value("include_deleted") == "true";
  1076. }
  1077. auto result = userService_->ListUsers(limit, offset, include_deleted);
  1078. if (!result.success) {
  1079. res.status = 500;
  1080. nlohmann::json error_response = {{"error", result.error}};
  1081. res.set_content(error_response.dump(), "application/json");
  1082. return;
  1083. }
  1084. // Build response
  1085. nlohmann::json users_array = nlohmann::json::array();
  1086. for (const auto& user : result.users) {
  1087. nlohmann::json user_obj = {
  1088. {"id", user.id},
  1089. {"email", user.email},
  1090. {"name", user.name},
  1091. {"created_at", user.created_at},
  1092. {"updated_at", user.updated_at},
  1093. {"deleted_at", user.deleted_at},
  1094. {"created_by", user.created_by},
  1095. {"updated_by", user.updated_by},
  1096. {"created_by_name", ResolveUserName(user.created_by)},
  1097. {"updated_by_name", ResolveUserName(user.updated_by)}
  1098. };
  1099. users_array.push_back(user_obj);
  1100. }
  1101. nlohmann::json response = {
  1102. {"users", users_array},
  1103. {"total_count", result.total_count},
  1104. {"limit", limit},
  1105. {"offset", offset}
  1106. };
  1107. res.set_content(response.dump(), "application/json");
  1108. } catch (const std::exception& e) {
  1109. spdlog::error("List users error: {}", e.what());
  1110. res.status = 500;
  1111. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1112. }
  1113. }
  1114. void HttpServer::HandleGetUser(const httplib::Request& req, httplib::Response& res) {
  1115. // Authenticate the request
  1116. auto auth_user = AuthenticateRequest(req);
  1117. if (!auth_user) {
  1118. res.status = 401;
  1119. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1120. return;
  1121. }
  1122. // Check if UserService is available
  1123. if (!userService_) {
  1124. res.status = 503;
  1125. res.set_content(R"({"error":"User service not available"})", "application/json");
  1126. return;
  1127. }
  1128. try {
  1129. // Extract user ID from path
  1130. std::string user_id = req.matches[1].str();
  1131. // Users can only get their own info, or require system:users:read permission
  1132. bool is_own = (auth_user->user_id == user_id);
  1133. if (!is_own && !authorizationService_->HasPermission(*auth_user, permissions::kUsersRead)) {
  1134. res.status = 403;
  1135. res.set_content(R"({"error":"Forbidden - can only access your own user data or requires system:users:read permission"})", "application/json");
  1136. return;
  1137. }
  1138. auto result = userService_->GetUser(user_id);
  1139. if (!result.success) {
  1140. res.status = 404;
  1141. nlohmann::json error_response = {{"error", result.error}};
  1142. res.set_content(error_response.dump(), "application/json");
  1143. return;
  1144. }
  1145. // Return user (without password_hash)
  1146. nlohmann::json user_json = {
  1147. {"id", result.user->id},
  1148. {"email", result.user->email},
  1149. {"name", result.user->name},
  1150. {"created_at", result.user->created_at},
  1151. {"updated_at", result.user->updated_at}
  1152. };
  1153. res.set_content(user_json.dump(), "application/json");
  1154. } catch (const std::exception& e) {
  1155. spdlog::error("Get user error: {}", e.what());
  1156. res.status = 500;
  1157. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1158. }
  1159. }
  1160. void HttpServer::HandleUpdateUser(const httplib::Request& req, httplib::Response& res) {
  1161. // Authenticate the request
  1162. auto auth_user = AuthenticateRequest(req);
  1163. if (!auth_user) {
  1164. res.status = 401;
  1165. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1166. return;
  1167. }
  1168. // Check if UserService is available
  1169. if (!userService_) {
  1170. res.status = 503;
  1171. res.set_content(R"({"error":"User service not available"})", "application/json");
  1172. return;
  1173. }
  1174. try {
  1175. // Extract user ID from path
  1176. std::string user_id = req.matches[1].str();
  1177. // Users can only update their own info, or require system:users:update permission
  1178. bool is_own = (auth_user->user_id == user_id);
  1179. if (!is_own && !authorizationService_->HasPermission(*auth_user, permissions::kUsersUpdate)) {
  1180. res.status = 403;
  1181. res.set_content(R"({"error":"Forbidden - can only update your own user data or requires system:users:update permission"})", "application/json");
  1182. return;
  1183. }
  1184. auto body = nlohmann::json::parse(req.body);
  1185. UpdateUserRequest update_req;
  1186. update_req.actor_id = auth_user->user_id;
  1187. if (body.contains("email")) {
  1188. update_req.email = body["email"].get<std::string>();
  1189. }
  1190. if (body.contains("password")) {
  1191. update_req.password = body["password"].get<std::string>();
  1192. }
  1193. if (body.contains("name")) {
  1194. update_req.name = body["name"].get<std::string>();
  1195. }
  1196. auto result = userService_->UpdateUser(user_id, update_req);
  1197. if (!result.success) {
  1198. res.status = 400;
  1199. nlohmann::json error_response = {{"error", result.error}};
  1200. res.set_content(error_response.dump(), "application/json");
  1201. return;
  1202. }
  1203. // Return updated user (without password_hash)
  1204. nlohmann::json user_json = {
  1205. {"id", result.user->id},
  1206. {"email", result.user->email},
  1207. {"name", result.user->name},
  1208. {"created_at", result.user->created_at},
  1209. {"updated_at", result.user->updated_at}
  1210. };
  1211. res.set_content(user_json.dump(), "application/json");
  1212. } catch (const nlohmann::json::parse_error& e) {
  1213. res.status = 400;
  1214. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  1215. } catch (const std::exception& e) {
  1216. spdlog::error("Update user error: {}", e.what());
  1217. res.status = 500;
  1218. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1219. }
  1220. }
  1221. void HttpServer::HandleDeleteUser(const httplib::Request& req, httplib::Response& res) {
  1222. // Authenticate the request
  1223. auto auth_user = AuthenticateRequest(req);
  1224. if (!auth_user) {
  1225. res.status = 401;
  1226. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1227. return;
  1228. }
  1229. // Requires system:users:delete permission
  1230. if (!authorizationService_->HasPermission(*auth_user, permissions::kUsersDelete)) {
  1231. res.status = 403;
  1232. res.set_content(R"({"error":"Forbidden - requires system:users:delete permission"})", "application/json");
  1233. return;
  1234. }
  1235. // Check if UserService is available
  1236. if (!userService_) {
  1237. res.status = 503;
  1238. res.set_content(R"({"error":"User service not available"})", "application/json");
  1239. return;
  1240. }
  1241. try {
  1242. // Extract user ID from path
  1243. std::string user_id = req.matches[1].str();
  1244. auto result = userService_->DeleteUser(user_id);
  1245. if (!result.success) {
  1246. res.status = 404;
  1247. nlohmann::json error_response = {{"error", result.error}};
  1248. res.set_content(error_response.dump(), "application/json");
  1249. return;
  1250. }
  1251. res.set_content(R"({"message":"User deleted successfully"})", "application/json");
  1252. } catch (const std::exception& e) {
  1253. spdlog::error("Delete user error: {}", e.what());
  1254. res.status = 500;
  1255. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1256. }
  1257. }
  1258. // ============================================================================
  1259. // Workspace Management Routes Implementation
  1260. // ============================================================================
  1261. void HttpServer::SetupWorkspaceRoutes() {
  1262. // POST /api/workspaces - Create a new workspace (superadmin only)
  1263. httpServer_->Post("/api/workspaces", [this](const httplib::Request& req, httplib::Response& res) {
  1264. HandleCreateWorkspace(req, res);
  1265. });
  1266. // GET /api/workspaces - List workspaces (filtered by user membership for non-superadmin)
  1267. httpServer_->Get("/api/workspaces", [this](const httplib::Request& req, httplib::Response& res) {
  1268. HandleListWorkspaces(req, res);
  1269. });
  1270. // GET /api/workspaces/:id - Get a specific workspace
  1271. httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  1272. HandleGetWorkspace(req, res);
  1273. });
  1274. // PATCH /api/workspaces/:id - Update a workspace
  1275. httpServer_->Patch(R"(/api/workspaces/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  1276. HandleUpdateWorkspace(req, res);
  1277. });
  1278. // DELETE /api/workspaces/:id - Soft delete a workspace (superadmin only)
  1279. httpServer_->Delete(R"(/api/workspaces/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  1280. HandleDeleteWorkspace(req, res);
  1281. });
  1282. spdlog::info("Workspace management routes registered: /api/workspaces");
  1283. }
  1284. void HttpServer::HandleCreateWorkspace(const httplib::Request& req, httplib::Response& res) {
  1285. // Authenticate the request
  1286. auto auth_user = AuthenticateRequest(req);
  1287. if (!auth_user) {
  1288. res.status = 401;
  1289. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1290. return;
  1291. }
  1292. // Requires system:workspaces:create permission
  1293. if (!authorizationService_->HasPermission(*auth_user, permissions::kWorkspacesCreate)) {
  1294. res.status = 403;
  1295. res.set_content(R"({"error":"Forbidden - requires system:workspaces:create permission"})", "application/json");
  1296. return;
  1297. }
  1298. // Check if WorkspaceService is available
  1299. if (!workspaceService_) {
  1300. res.status = 503;
  1301. res.set_content(R"({"error":"Workspace service not available"})", "application/json");
  1302. return;
  1303. }
  1304. try {
  1305. auto body = nlohmann::json::parse(req.body);
  1306. CreateWorkspaceRequest create_req;
  1307. create_req.actor_id = auth_user->user_id;
  1308. if (body.contains("name")) {
  1309. create_req.name = body["name"].get<std::string>();
  1310. }
  1311. if (body.contains("settings")) {
  1312. create_req.settings = body["settings"];
  1313. }
  1314. auto result = workspaceService_->CreateWorkspace(create_req);
  1315. if (!result.success) {
  1316. res.status = 400;
  1317. nlohmann::json error_response = {{"error", result.error}};
  1318. res.set_content(error_response.dump(), "application/json");
  1319. return;
  1320. }
  1321. // Return created workspace
  1322. nlohmann::json workspace_json = {
  1323. {"id", result.workspace->id},
  1324. {"name", result.workspace->name},
  1325. {"settings", result.workspace->settings},
  1326. {"created_at", result.workspace->created_at},
  1327. {"updated_at", result.workspace->updated_at}
  1328. };
  1329. res.status = 201;
  1330. res.set_content(workspace_json.dump(), "application/json");
  1331. } catch (const nlohmann::json::parse_error& e) {
  1332. res.status = 400;
  1333. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  1334. } catch (const std::exception& e) {
  1335. spdlog::error("Create workspace error: {}", e.what());
  1336. res.status = 500;
  1337. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1338. }
  1339. }
  1340. void HttpServer::HandleListWorkspaces(const httplib::Request& req, httplib::Response& res) {
  1341. // Authenticate the request
  1342. auto auth_user = AuthenticateRequest(req);
  1343. if (!auth_user) {
  1344. res.status = 401;
  1345. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1346. return;
  1347. }
  1348. // Check if WorkspaceService is available
  1349. if (!workspaceService_) {
  1350. res.status = 503;
  1351. res.set_content(R"({"error":"Workspace service not available"})", "application/json");
  1352. return;
  1353. }
  1354. try {
  1355. WorkspaceListResult result;
  1356. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kWorkspacesRead);
  1357. // Users with system:workspaces:read can see all workspaces, others only see their memberships
  1358. if (has_system_access) {
  1359. // Parse query parameters
  1360. int limit = 100;
  1361. int offset = 0;
  1362. bool include_deleted = false;
  1363. if (req.has_param("limit")) {
  1364. limit = std::stoi(req.get_param_value("limit"));
  1365. if (limit < 1 || limit > 1000) {
  1366. limit = 100;
  1367. }
  1368. }
  1369. if (req.has_param("offset")) {
  1370. offset = std::stoi(req.get_param_value("offset"));
  1371. if (offset < 0) {
  1372. offset = 0;
  1373. }
  1374. }
  1375. if (req.has_param("include_deleted")) {
  1376. include_deleted = req.get_param_value("include_deleted") == "true";
  1377. }
  1378. result = workspaceService_->ListWorkspaces(limit, offset, include_deleted);
  1379. } else {
  1380. // Non-privileged user: filter by user's workspace memberships
  1381. result = workspaceService_->ListWorkspacesByIds(auth_user->workspace_ids, false);
  1382. }
  1383. if (!result.success) {
  1384. res.status = 500;
  1385. nlohmann::json error_response = {{"error", result.error}};
  1386. res.set_content(error_response.dump(), "application/json");
  1387. return;
  1388. }
  1389. // Build response
  1390. nlohmann::json workspaces_array = nlohmann::json::array();
  1391. for (const auto& workspace : result.workspaces) {
  1392. nlohmann::json ws_json = {
  1393. {"id", workspace.id},
  1394. {"name", workspace.name},
  1395. {"settings", workspace.settings},
  1396. {"created_at", workspace.created_at},
  1397. {"updated_at", workspace.updated_at},
  1398. {"created_by", workspace.created_by},
  1399. {"updated_by", workspace.updated_by},
  1400. {"created_by_name", ResolveUserName(workspace.created_by)},
  1401. {"updated_by_name", ResolveUserName(workspace.updated_by)}
  1402. };
  1403. if (has_system_access) {
  1404. ws_json["deleted_at"] = workspace.deleted_at;
  1405. }
  1406. workspaces_array.push_back(ws_json);
  1407. }
  1408. nlohmann::json response = {
  1409. {"workspaces", workspaces_array},
  1410. {"total_count", result.total_count}
  1411. };
  1412. res.set_content(response.dump(), "application/json");
  1413. } catch (const std::exception& e) {
  1414. spdlog::error("List workspaces error: {}", e.what());
  1415. res.status = 500;
  1416. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1417. }
  1418. }
  1419. void HttpServer::HandleGetWorkspace(const httplib::Request& req, httplib::Response& res) {
  1420. // Authenticate the request
  1421. auto auth_user = AuthenticateRequest(req);
  1422. if (!auth_user) {
  1423. res.status = 401;
  1424. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1425. return;
  1426. }
  1427. // Check if WorkspaceService is available
  1428. if (!workspaceService_) {
  1429. res.status = 503;
  1430. res.set_content(R"({"error":"Workspace service not available"})", "application/json");
  1431. return;
  1432. }
  1433. try {
  1434. // Extract workspace ID from path
  1435. std::string workspace_id = req.matches[1].str();
  1436. // Check access: must be member OR have system:workspaces:read permission
  1437. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  1438. auth_user->workspace_ids.end(),
  1439. workspace_id) != auth_user->workspace_ids.end());
  1440. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kWorkspacesRead);
  1441. if (!is_member && !has_system_access) {
  1442. res.status = 403;
  1443. res.set_content(R"({"error":"Forbidden - not a member of this workspace"})", "application/json");
  1444. return;
  1445. }
  1446. auto result = workspaceService_->GetWorkspace(workspace_id);
  1447. if (!result.success) {
  1448. res.status = 404;
  1449. nlohmann::json error_response = {{"error", result.error}};
  1450. res.set_content(error_response.dump(), "application/json");
  1451. return;
  1452. }
  1453. // Return workspace
  1454. nlohmann::json workspace_json = {
  1455. {"id", result.workspace->id},
  1456. {"name", result.workspace->name},
  1457. {"settings", result.workspace->settings},
  1458. {"created_at", result.workspace->created_at},
  1459. {"updated_at", result.workspace->updated_at}
  1460. };
  1461. res.set_content(workspace_json.dump(), "application/json");
  1462. } catch (const std::exception& e) {
  1463. spdlog::error("Get workspace error: {}", e.what());
  1464. res.status = 500;
  1465. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1466. }
  1467. }
  1468. void HttpServer::HandleUpdateWorkspace(const httplib::Request& req, httplib::Response& res) {
  1469. // Authenticate the request
  1470. auto auth_user = AuthenticateRequest(req);
  1471. if (!auth_user) {
  1472. res.status = 401;
  1473. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1474. return;
  1475. }
  1476. // Extract workspace ID from path (needed for IsWorkspaceAdmin check)
  1477. std::string workspace_id = req.matches[1].str();
  1478. // Requires system:workspaces:update permission OR workspace admin
  1479. bool can_update = authorizationService_->HasPermission(*auth_user, permissions::kWorkspacesUpdate) ||
  1480. authorizationService_->IsWorkspaceAdmin(*auth_user, workspace_id);
  1481. if (!can_update) {
  1482. res.status = 403;
  1483. res.set_content(R"({"error":"Forbidden - requires system:workspaces:update permission or workspace admin"})", "application/json");
  1484. return;
  1485. }
  1486. // Check if WorkspaceService is available
  1487. if (!workspaceService_) {
  1488. res.status = 503;
  1489. res.set_content(R"({"error":"Workspace service not available"})", "application/json");
  1490. return;
  1491. }
  1492. try {
  1493. auto body = nlohmann::json::parse(req.body);
  1494. UpdateWorkspaceRequest update_req;
  1495. update_req.actor_id = auth_user->user_id;
  1496. if (body.contains("name")) {
  1497. update_req.name = body["name"].get<std::string>();
  1498. }
  1499. if (body.contains("settings")) {
  1500. update_req.settings = body["settings"];
  1501. }
  1502. auto result = workspaceService_->UpdateWorkspace(workspace_id, update_req);
  1503. if (!result.success) {
  1504. res.status = 400;
  1505. nlohmann::json error_response = {{"error", result.error}};
  1506. res.set_content(error_response.dump(), "application/json");
  1507. return;
  1508. }
  1509. // Return updated workspace
  1510. nlohmann::json workspace_json = {
  1511. {"id", result.workspace->id},
  1512. {"name", result.workspace->name},
  1513. {"settings", result.workspace->settings},
  1514. {"created_at", result.workspace->created_at},
  1515. {"updated_at", result.workspace->updated_at}
  1516. };
  1517. res.set_content(workspace_json.dump(), "application/json");
  1518. } catch (const nlohmann::json::parse_error& e) {
  1519. res.status = 400;
  1520. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  1521. } catch (const std::exception& e) {
  1522. spdlog::error("Update workspace error: {}", e.what());
  1523. res.status = 500;
  1524. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1525. }
  1526. }
  1527. void HttpServer::HandleDeleteWorkspace(const httplib::Request& req, httplib::Response& res) {
  1528. // Authenticate the request
  1529. auto auth_user = AuthenticateRequest(req);
  1530. if (!auth_user) {
  1531. res.status = 401;
  1532. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1533. return;
  1534. }
  1535. // Requires system:workspaces:delete permission
  1536. if (!authorizationService_->HasPermission(*auth_user, permissions::kWorkspacesDelete)) {
  1537. res.status = 403;
  1538. res.set_content(R"({"error":"Forbidden - requires system:workspaces:delete permission"})", "application/json");
  1539. return;
  1540. }
  1541. // Check if WorkspaceService is available
  1542. if (!workspaceService_) {
  1543. res.status = 503;
  1544. res.set_content(R"({"error":"Workspace service not available"})", "application/json");
  1545. return;
  1546. }
  1547. try {
  1548. // Extract workspace ID from path
  1549. std::string workspace_id = req.matches[1].str();
  1550. auto result = workspaceService_->DeleteWorkspace(workspace_id);
  1551. if (!result.success) {
  1552. res.status = 404;
  1553. nlohmann::json error_response = {{"error", result.error}};
  1554. res.set_content(error_response.dump(), "application/json");
  1555. return;
  1556. }
  1557. res.set_content(R"({"message":"Workspace deleted successfully"})", "application/json");
  1558. } catch (const std::exception& e) {
  1559. spdlog::error("Delete workspace error: {}", e.what());
  1560. res.status = 500;
  1561. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1562. }
  1563. }
  1564. // ============================================================================
  1565. // Group Management Routes Implementation
  1566. // ============================================================================
  1567. void HttpServer::SetupGroupRoutes() {
  1568. // POST /api/workspaces/:wid/groups - Create a new group in workspace
  1569. httpServer_->Post(R"(/api/workspaces/([a-zA-Z0-9\-]+)/groups)", [this](const httplib::Request& req, httplib::Response& res) {
  1570. HandleCreateGroup(req, res);
  1571. });
  1572. // GET /api/workspaces/:wid/groups - List groups in workspace
  1573. httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/groups)", [this](const httplib::Request& req, httplib::Response& res) {
  1574. HandleListGroups(req, res);
  1575. });
  1576. // GET /api/workspaces/:wid/groups/:id - Get a specific group
  1577. httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/groups/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  1578. HandleGetGroup(req, res);
  1579. });
  1580. // PATCH /api/workspaces/:wid/groups/:id - Update a group
  1581. httpServer_->Patch(R"(/api/workspaces/([a-zA-Z0-9\-]+)/groups/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  1582. HandleUpdateGroup(req, res);
  1583. });
  1584. // DELETE /api/workspaces/:wid/groups/:id - Delete a group
  1585. httpServer_->Delete(R"(/api/workspaces/([a-zA-Z0-9\-]+)/groups/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  1586. HandleDeleteGroup(req, res);
  1587. });
  1588. spdlog::info("Group management routes registered: /api/workspaces/:wid/groups");
  1589. }
  1590. void HttpServer::HandleCreateGroup(const httplib::Request& req, httplib::Response& res) {
  1591. // Authenticate the request
  1592. auto auth_user = AuthenticateRequest(req);
  1593. if (!auth_user) {
  1594. res.status = 401;
  1595. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1596. return;
  1597. }
  1598. // Extract workspace ID from path
  1599. std::string workspace_id = req.matches[1].str();
  1600. // Requires system:groups:create permission OR workspace group management permission
  1601. bool can_create = authorizationService_->HasPermission(*auth_user, permissions::kGroupsCreate) ||
  1602. authorizationService_->CanManageWorkspaceGroups(*auth_user, workspace_id);
  1603. if (!can_create) {
  1604. res.status = 403;
  1605. res.set_content(R"({"error":"Forbidden - requires system:groups:create permission or workspace group management"})", "application/json");
  1606. return;
  1607. }
  1608. // Check if GroupService is available
  1609. if (!groupService_) {
  1610. res.status = 503;
  1611. res.set_content(R"({"error":"Group service not available"})", "application/json");
  1612. return;
  1613. }
  1614. try {
  1615. auto body = nlohmann::json::parse(req.body);
  1616. CreateGroupRequest create_req;
  1617. create_req.workspace_id = workspace_id;
  1618. create_req.actor_id = auth_user->user_id;
  1619. if (body.contains("name")) {
  1620. create_req.name = body["name"].get<std::string>();
  1621. }
  1622. if (body.contains("permissions") && body["permissions"].is_array()) {
  1623. for (const auto& perm : body["permissions"]) {
  1624. if (perm.is_string()) {
  1625. create_req.permissions.push_back(perm.get<std::string>());
  1626. }
  1627. }
  1628. }
  1629. auto result = groupService_->CreateGroup(create_req);
  1630. if (!result.success) {
  1631. res.status = 400;
  1632. nlohmann::json error_response = {{"error", result.error}};
  1633. res.set_content(error_response.dump(), "application/json");
  1634. return;
  1635. }
  1636. // Return created group
  1637. nlohmann::json group_json = {
  1638. {"id", result.group->id},
  1639. {"workspace_id", result.group->workspace_id},
  1640. {"name", result.group->name},
  1641. {"permissions", result.group->permissions},
  1642. {"created_at", result.group->created_at},
  1643. {"updated_at", result.group->updated_at}
  1644. };
  1645. res.status = 201;
  1646. res.set_content(group_json.dump(), "application/json");
  1647. } catch (const nlohmann::json::parse_error& e) {
  1648. res.status = 400;
  1649. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  1650. } catch (const std::exception& e) {
  1651. spdlog::error("Create group error: {}", e.what());
  1652. res.status = 500;
  1653. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1654. }
  1655. }
  1656. void HttpServer::HandleListGroups(const httplib::Request& req, httplib::Response& res) {
  1657. // Authenticate the request
  1658. auto auth_user = AuthenticateRequest(req);
  1659. if (!auth_user) {
  1660. res.status = 401;
  1661. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1662. return;
  1663. }
  1664. // Extract workspace ID from path
  1665. std::string workspace_id = req.matches[1].str();
  1666. // Check access: must be member OR have system:groups:read permission
  1667. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  1668. auth_user->workspace_ids.end(),
  1669. workspace_id) != auth_user->workspace_ids.end());
  1670. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kGroupsRead);
  1671. if (!is_member && !has_system_access) {
  1672. res.status = 403;
  1673. res.set_content(R"({"error":"Forbidden - not a member of this workspace"})", "application/json");
  1674. return;
  1675. }
  1676. // Check if GroupService is available
  1677. if (!groupService_) {
  1678. res.status = 503;
  1679. res.set_content(R"({"error":"Group service not available"})", "application/json");
  1680. return;
  1681. }
  1682. try {
  1683. // Parse query parameters
  1684. int limit = 100;
  1685. int offset = 0;
  1686. bool include_deleted = false;
  1687. if (req.has_param("limit")) {
  1688. limit = std::stoi(req.get_param_value("limit"));
  1689. if (limit < 1 || limit > 1000) {
  1690. limit = 100;
  1691. }
  1692. }
  1693. if (req.has_param("offset")) {
  1694. offset = std::stoi(req.get_param_value("offset"));
  1695. if (offset < 0) {
  1696. offset = 0;
  1697. }
  1698. }
  1699. if (has_system_access && req.has_param("include_deleted")) {
  1700. include_deleted = req.get_param_value("include_deleted") == "true";
  1701. }
  1702. bool include_system = has_system_access &&
  1703. req.has_param("include_system") &&
  1704. req.get_param_value("include_system") == "true";
  1705. auto result = groupService_->ListGroups(workspace_id, limit, offset, include_deleted);
  1706. if (!result.success) {
  1707. res.status = 500;
  1708. nlohmann::json error_response = {{"error", result.error}};
  1709. res.set_content(error_response.dump(), "application/json");
  1710. return;
  1711. }
  1712. // Build response
  1713. nlohmann::json groups_array = nlohmann::json::array();
  1714. // Add global system groups first if requested
  1715. if (include_system) {
  1716. auto global_result = groupService_->ListSystemGroups();
  1717. if (global_result.success) {
  1718. for (const auto& group : global_result.groups) {
  1719. nlohmann::json grp_json = {
  1720. {"id", group.id},
  1721. {"workspace_id", group.workspace_id},
  1722. {"name", group.name},
  1723. {"permissions", group.permissions},
  1724. {"is_system", group.is_system},
  1725. {"parent_group_id", group.parent_group_id},
  1726. {"created_at", group.created_at},
  1727. {"updated_at", group.updated_at},
  1728. {"deleted_at", group.deleted_at},
  1729. {"created_by", group.created_by},
  1730. {"updated_by", group.updated_by},
  1731. {"created_by_name", ResolveUserName(group.created_by)},
  1732. {"updated_by_name", ResolveUserName(group.updated_by)}
  1733. };
  1734. groups_array.push_back(grp_json);
  1735. }
  1736. }
  1737. }
  1738. for (const auto& group : result.groups) {
  1739. nlohmann::json grp_json = {
  1740. {"id", group.id},
  1741. {"workspace_id", group.workspace_id},
  1742. {"name", group.name},
  1743. {"permissions", group.permissions},
  1744. {"is_system", group.is_system},
  1745. {"parent_group_id", group.parent_group_id},
  1746. {"created_at", group.created_at},
  1747. {"updated_at", group.updated_at},
  1748. {"created_by", group.created_by},
  1749. {"updated_by", group.updated_by},
  1750. {"created_by_name", ResolveUserName(group.created_by)},
  1751. {"updated_by_name", ResolveUserName(group.updated_by)}
  1752. };
  1753. if (has_system_access) {
  1754. grp_json["deleted_at"] = group.deleted_at;
  1755. }
  1756. groups_array.push_back(grp_json);
  1757. }
  1758. nlohmann::json response = {
  1759. {"groups", groups_array},
  1760. {"total_count", result.total_count}
  1761. };
  1762. res.set_content(response.dump(), "application/json");
  1763. } catch (const std::exception& e) {
  1764. spdlog::error("List groups error: {}", e.what());
  1765. res.status = 500;
  1766. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1767. }
  1768. }
  1769. void HttpServer::HandleGetGroup(const httplib::Request& req, httplib::Response& res) {
  1770. // Authenticate the request
  1771. auto auth_user = AuthenticateRequest(req);
  1772. if (!auth_user) {
  1773. res.status = 401;
  1774. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1775. return;
  1776. }
  1777. // Extract workspace ID and group ID from path
  1778. std::string workspace_id = req.matches[1].str();
  1779. std::string group_id = req.matches[2].str();
  1780. // Check access: must be member OR have system:groups:read permission
  1781. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  1782. auth_user->workspace_ids.end(),
  1783. workspace_id) != auth_user->workspace_ids.end());
  1784. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kGroupsRead);
  1785. if (!is_member && !has_system_access) {
  1786. res.status = 403;
  1787. res.set_content(R"({"error":"Forbidden - not a member of this workspace"})", "application/json");
  1788. return;
  1789. }
  1790. // Check if GroupService is available
  1791. if (!groupService_) {
  1792. res.status = 503;
  1793. res.set_content(R"({"error":"Group service not available"})", "application/json");
  1794. return;
  1795. }
  1796. try {
  1797. auto result = groupService_->GetGroup(group_id);
  1798. if (!result.success) {
  1799. res.status = 404;
  1800. nlohmann::json error_response = {{"error", result.error}};
  1801. res.set_content(error_response.dump(), "application/json");
  1802. return;
  1803. }
  1804. // Verify group belongs to the workspace
  1805. if (result.group->workspace_id != workspace_id) {
  1806. res.status = 404;
  1807. res.set_content(R"({"error":"Group not found in this workspace"})", "application/json");
  1808. return;
  1809. }
  1810. // Return group
  1811. nlohmann::json group_json = {
  1812. {"id", result.group->id},
  1813. {"workspace_id", result.group->workspace_id},
  1814. {"name", result.group->name},
  1815. {"permissions", result.group->permissions},
  1816. {"created_at", result.group->created_at},
  1817. {"updated_at", result.group->updated_at}
  1818. };
  1819. res.set_content(group_json.dump(), "application/json");
  1820. } catch (const std::exception& e) {
  1821. spdlog::error("Get group error: {}", e.what());
  1822. res.status = 500;
  1823. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1824. }
  1825. }
  1826. void HttpServer::HandleUpdateGroup(const httplib::Request& req, httplib::Response& res) {
  1827. // Authenticate the request
  1828. auto auth_user = AuthenticateRequest(req);
  1829. if (!auth_user) {
  1830. res.status = 401;
  1831. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1832. return;
  1833. }
  1834. // Extract workspace ID and group ID from path
  1835. std::string workspace_id = req.matches[1].str();
  1836. std::string group_id = req.matches[2].str();
  1837. // Requires system:groups:update permission OR workspace group management permission
  1838. bool can_update = authorizationService_->HasPermission(*auth_user, permissions::kGroupsUpdate) ||
  1839. authorizationService_->CanManageWorkspaceGroups(*auth_user, workspace_id);
  1840. if (!can_update) {
  1841. res.status = 403;
  1842. res.set_content(R"({"error":"Forbidden - requires system:groups:update permission or workspace group management"})", "application/json");
  1843. return;
  1844. }
  1845. // Check if GroupService is available
  1846. if (!groupService_) {
  1847. res.status = 503;
  1848. res.set_content(R"({"error":"Group service not available"})", "application/json");
  1849. return;
  1850. }
  1851. try {
  1852. // First verify group belongs to the workspace
  1853. auto existing = groupService_->GetGroup(group_id);
  1854. if (!existing.success) {
  1855. res.status = 404;
  1856. nlohmann::json error_response = {{"error", existing.error}};
  1857. res.set_content(error_response.dump(), "application/json");
  1858. return;
  1859. }
  1860. if (existing.group->workspace_id != workspace_id) {
  1861. res.status = 404;
  1862. res.set_content(R"({"error":"Group not found in this workspace"})", "application/json");
  1863. return;
  1864. }
  1865. auto body = nlohmann::json::parse(req.body);
  1866. UpdateGroupRequest update_req;
  1867. update_req.actor_id = auth_user->user_id;
  1868. if (body.contains("name")) {
  1869. update_req.name = body["name"].get<std::string>();
  1870. }
  1871. if (body.contains("permissions") && body["permissions"].is_array()) {
  1872. std::vector<std::string> perms;
  1873. for (const auto& perm : body["permissions"]) {
  1874. if (perm.is_string()) {
  1875. perms.push_back(perm.get<std::string>());
  1876. }
  1877. }
  1878. update_req.permissions = perms;
  1879. }
  1880. auto result = groupService_->UpdateGroup(group_id, update_req);
  1881. if (!result.success) {
  1882. res.status = 400;
  1883. nlohmann::json error_response = {{"error", result.error}};
  1884. res.set_content(error_response.dump(), "application/json");
  1885. return;
  1886. }
  1887. // Return updated group
  1888. nlohmann::json group_json = {
  1889. {"id", result.group->id},
  1890. {"workspace_id", result.group->workspace_id},
  1891. {"name", result.group->name},
  1892. {"permissions", result.group->permissions},
  1893. {"created_at", result.group->created_at},
  1894. {"updated_at", result.group->updated_at}
  1895. };
  1896. res.set_content(group_json.dump(), "application/json");
  1897. } catch (const nlohmann::json::parse_error& e) {
  1898. res.status = 400;
  1899. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  1900. } catch (const std::exception& e) {
  1901. spdlog::error("Update group error: {}", e.what());
  1902. res.status = 500;
  1903. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1904. }
  1905. }
  1906. void HttpServer::HandleDeleteGroup(const httplib::Request& req, httplib::Response& res) {
  1907. // Authenticate the request
  1908. auto auth_user = AuthenticateRequest(req);
  1909. if (!auth_user) {
  1910. res.status = 401;
  1911. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1912. return;
  1913. }
  1914. // Extract workspace ID and group ID from path
  1915. std::string workspace_id = req.matches[1].str();
  1916. std::string group_id = req.matches[2].str();
  1917. // Requires system:groups:delete permission OR workspace group management permission
  1918. bool can_delete = authorizationService_->HasPermission(*auth_user, permissions::kGroupsDelete) ||
  1919. authorizationService_->CanManageWorkspaceGroups(*auth_user, workspace_id);
  1920. if (!can_delete) {
  1921. res.status = 403;
  1922. res.set_content(R"({"error":"Forbidden - requires system:groups:delete permission or workspace group management"})", "application/json");
  1923. return;
  1924. }
  1925. // Check if GroupService is available
  1926. if (!groupService_) {
  1927. res.status = 503;
  1928. res.set_content(R"({"error":"Group service not available"})", "application/json");
  1929. return;
  1930. }
  1931. try {
  1932. // First verify group belongs to the workspace
  1933. auto existing = groupService_->GetGroup(group_id);
  1934. if (!existing.success) {
  1935. res.status = 404;
  1936. nlohmann::json error_response = {{"error", existing.error}};
  1937. res.set_content(error_response.dump(), "application/json");
  1938. return;
  1939. }
  1940. if (existing.group->workspace_id != workspace_id) {
  1941. res.status = 404;
  1942. res.set_content(R"({"error":"Group not found in this workspace"})", "application/json");
  1943. return;
  1944. }
  1945. auto result = groupService_->DeleteGroup(group_id);
  1946. if (!result.success) {
  1947. res.status = 400;
  1948. nlohmann::json error_response = {{"error", result.error}};
  1949. res.set_content(error_response.dump(), "application/json");
  1950. return;
  1951. }
  1952. res.set_content(R"({"message":"Group deleted successfully"})", "application/json");
  1953. } catch (const std::exception& e) {
  1954. spdlog::error("Delete group error: {}", e.what());
  1955. res.status = 500;
  1956. res.set_content(R"({"error":"Internal server error"})", "application/json");
  1957. }
  1958. }
  1959. // ============================================================================
  1960. // Membership Routes
  1961. // ============================================================================
  1962. void HttpServer::SetupMembershipRoutes() {
  1963. // POST /api/workspaces/:wid/members - Add a user to workspace
  1964. httpServer_->Post(R"(/api/workspaces/([a-zA-Z0-9\-]+)/members)", [this](const httplib::Request& req, httplib::Response& res) {
  1965. HandleAddMember(req, res);
  1966. });
  1967. // GET /api/workspaces/:wid/members - List members in workspace
  1968. httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/members)", [this](const httplib::Request& req, httplib::Response& res) {
  1969. HandleListMembers(req, res);
  1970. });
  1971. // GET /api/workspaces/:wid/members/:userId - Get a specific member
  1972. httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/members/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  1973. HandleGetMember(req, res);
  1974. });
  1975. // PUT /api/workspaces/:wid/members/:userId - Update user's groups in workspace
  1976. httpServer_->Put(R"(/api/workspaces/([a-zA-Z0-9\-]+)/members/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  1977. HandleUpdateMember(req, res);
  1978. });
  1979. // DELETE /api/workspaces/:wid/members/:userId - Remove user from workspace
  1980. httpServer_->Delete(R"(/api/workspaces/([a-zA-Z0-9\-]+)/members/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  1981. HandleRemoveMember(req, res);
  1982. });
  1983. spdlog::info("Membership routes registered: /api/workspaces/:wid/members");
  1984. }
  1985. void HttpServer::HandleAddMember(const httplib::Request& req, httplib::Response& res) {
  1986. // Authenticate the request
  1987. auto auth_user = AuthenticateRequest(req);
  1988. if (!auth_user) {
  1989. res.status = 401;
  1990. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  1991. return;
  1992. }
  1993. // Extract workspace ID from path
  1994. std::string workspace_id = req.matches[1].str();
  1995. // Requires system:memberships:create permission OR workspace member management permission
  1996. bool can_add = authorizationService_->HasPermission(*auth_user, permissions::kMembershipsCreate) ||
  1997. authorizationService_->CanManageWorkspaceMembers(*auth_user, workspace_id);
  1998. if (!can_add) {
  1999. res.status = 403;
  2000. res.set_content(R"({"error":"Forbidden - requires system:memberships:create permission or workspace member management"})", "application/json");
  2001. return;
  2002. }
  2003. // Check if MembershipService is available
  2004. if (!membershipService_) {
  2005. res.status = 503;
  2006. res.set_content(R"({"error":"Membership service not available"})", "application/json");
  2007. return;
  2008. }
  2009. try {
  2010. // Parse request body
  2011. auto json = nlohmann::json::parse(req.body);
  2012. AddMemberRequest add_req;
  2013. add_req.workspace_id = workspace_id;
  2014. if (json.contains("user_id") && json["user_id"].is_string()) {
  2015. add_req.user_id = json["user_id"].get<std::string>();
  2016. } else {
  2017. res.status = 400;
  2018. res.set_content(R"({"error":"user_id is required"})", "application/json");
  2019. return;
  2020. }
  2021. if (json.contains("group_ids") && json["group_ids"].is_array()) {
  2022. for (const auto& item : json["group_ids"]) {
  2023. if (item.is_string()) {
  2024. add_req.group_ids.push_back(item.get<std::string>());
  2025. }
  2026. }
  2027. }
  2028. auto result = membershipService_->AddMember(add_req);
  2029. if (!result.success) {
  2030. res.status = 400;
  2031. nlohmann::json error_response = {{"error", result.error}};
  2032. res.set_content(error_response.dump(), "application/json");
  2033. return;
  2034. }
  2035. // Return created membership
  2036. nlohmann::json member_json = {
  2037. {"id", result.member->id},
  2038. {"workspace_id", result.member->workspace_id},
  2039. {"user_id", result.member->user_id},
  2040. {"group_ids", result.member->group_ids},
  2041. {"created_at", result.member->created_at},
  2042. {"updated_at", result.member->updated_at}
  2043. };
  2044. res.status = 201;
  2045. res.set_content(member_json.dump(), "application/json");
  2046. } catch (const nlohmann::json::exception& e) {
  2047. res.status = 400;
  2048. nlohmann::json error_response = {{"error", "Invalid JSON: " + std::string(e.what())}};
  2049. res.set_content(error_response.dump(), "application/json");
  2050. } catch (const std::exception& e) {
  2051. spdlog::error("Add member error: {}", e.what());
  2052. res.status = 500;
  2053. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2054. }
  2055. }
  2056. void HttpServer::HandleListMembers(const httplib::Request& req, httplib::Response& res) {
  2057. // Authenticate the request
  2058. auto auth_user = AuthenticateRequest(req);
  2059. if (!auth_user) {
  2060. res.status = 401;
  2061. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2062. return;
  2063. }
  2064. // Extract workspace ID from path
  2065. std::string workspace_id = req.matches[1].str();
  2066. // Check access: must be member OR have system:memberships:read permission
  2067. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  2068. auth_user->workspace_ids.end(),
  2069. workspace_id) != auth_user->workspace_ids.end());
  2070. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kMembershipsRead);
  2071. if (!is_member && !has_system_access) {
  2072. res.status = 403;
  2073. res.set_content(R"({"error":"Forbidden - not a member of this workspace"})", "application/json");
  2074. return;
  2075. }
  2076. // Check if MembershipService is available
  2077. if (!membershipService_) {
  2078. res.status = 503;
  2079. res.set_content(R"({"error":"Membership service not available"})", "application/json");
  2080. return;
  2081. }
  2082. try {
  2083. // Get query parameters
  2084. int limit = 100;
  2085. int offset = 0;
  2086. bool include_deleted = false;
  2087. if (req.has_param("limit")) {
  2088. limit = std::stoi(req.get_param_value("limit"));
  2089. if (limit < 1 || limit > 1000) {
  2090. limit = 100;
  2091. }
  2092. }
  2093. if (req.has_param("offset")) {
  2094. offset = std::stoi(req.get_param_value("offset"));
  2095. if (offset < 0) {
  2096. offset = 0;
  2097. }
  2098. }
  2099. if (has_system_access && req.has_param("include_deleted")) {
  2100. include_deleted = req.get_param_value("include_deleted") == "true";
  2101. }
  2102. auto result = membershipService_->ListMembers(workspace_id, limit, offset, include_deleted);
  2103. if (!result.success) {
  2104. res.status = 500;
  2105. nlohmann::json error_response = {{"error", result.error}};
  2106. res.set_content(error_response.dump(), "application/json");
  2107. return;
  2108. }
  2109. // Build response with user details
  2110. nlohmann::json members_array = nlohmann::json::array();
  2111. for (const auto& member : result.members) {
  2112. nlohmann::json member_json = {
  2113. {"id", member.id},
  2114. {"workspace_id", member.workspace_id},
  2115. {"user_id", member.user_id},
  2116. {"group_ids", member.group_ids},
  2117. {"created_at", member.created_at},
  2118. {"updated_at", member.updated_at}
  2119. };
  2120. if (has_system_access) {
  2121. member_json["deleted_at"] = member.deleted_at;
  2122. }
  2123. // Fetch user details if UserService is available
  2124. if (userService_) {
  2125. auto user_result = userService_->GetUser(member.user_id);
  2126. if (user_result.success && user_result.user) {
  2127. member_json["user"] = {
  2128. {"id", user_result.user->id},
  2129. {"email", user_result.user->email},
  2130. {"name", user_result.user->name},
  2131. {"created_at", user_result.user->created_at}
  2132. };
  2133. }
  2134. }
  2135. members_array.push_back(member_json);
  2136. }
  2137. nlohmann::json response = {
  2138. {"members", members_array},
  2139. {"total_count", result.total_count}
  2140. };
  2141. res.set_content(response.dump(), "application/json");
  2142. } catch (const std::exception& e) {
  2143. spdlog::error("List members error: {}", e.what());
  2144. res.status = 500;
  2145. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2146. }
  2147. }
  2148. void HttpServer::HandleGetMember(const httplib::Request& req, httplib::Response& res) {
  2149. // Authenticate the request
  2150. auto auth_user = AuthenticateRequest(req);
  2151. if (!auth_user) {
  2152. res.status = 401;
  2153. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2154. return;
  2155. }
  2156. // Extract workspace ID and user ID from path
  2157. std::string workspace_id = req.matches[1].str();
  2158. std::string user_id = req.matches[2].str();
  2159. // Check access: must be member OR have system:memberships:read permission
  2160. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  2161. auth_user->workspace_ids.end(),
  2162. workspace_id) != auth_user->workspace_ids.end());
  2163. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kMembershipsRead);
  2164. if (!is_member && !has_system_access) {
  2165. res.status = 403;
  2166. res.set_content(R"({"error":"Forbidden - not a member of this workspace"})", "application/json");
  2167. return;
  2168. }
  2169. // Check if MembershipService is available
  2170. if (!membershipService_) {
  2171. res.status = 503;
  2172. res.set_content(R"({"error":"Membership service not available"})", "application/json");
  2173. return;
  2174. }
  2175. try {
  2176. auto result = membershipService_->GetMembershipByUser(workspace_id, user_id);
  2177. if (!result.success) {
  2178. res.status = 404;
  2179. nlohmann::json error_response = {{"error", result.error}};
  2180. res.set_content(error_response.dump(), "application/json");
  2181. return;
  2182. }
  2183. // Return membership
  2184. nlohmann::json member_json = {
  2185. {"id", result.member->id},
  2186. {"workspace_id", result.member->workspace_id},
  2187. {"user_id", result.member->user_id},
  2188. {"group_ids", result.member->group_ids},
  2189. {"created_at", result.member->created_at},
  2190. {"updated_at", result.member->updated_at}
  2191. };
  2192. res.set_content(member_json.dump(), "application/json");
  2193. } catch (const std::exception& e) {
  2194. spdlog::error("Get member error: {}", e.what());
  2195. res.status = 500;
  2196. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2197. }
  2198. }
  2199. void HttpServer::HandleUpdateMember(const httplib::Request& req, httplib::Response& res) {
  2200. // Authenticate the request
  2201. auto auth_user = AuthenticateRequest(req);
  2202. if (!auth_user) {
  2203. res.status = 401;
  2204. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2205. return;
  2206. }
  2207. // Extract workspace ID and user ID from path
  2208. std::string workspace_id = req.matches[1].str();
  2209. std::string user_id = req.matches[2].str();
  2210. // Requires system:memberships:update permission OR workspace member management permission
  2211. bool can_update = authorizationService_->HasPermission(*auth_user, permissions::kMembershipsUpdate) ||
  2212. authorizationService_->CanManageWorkspaceMembers(*auth_user, workspace_id);
  2213. if (!can_update) {
  2214. res.status = 403;
  2215. res.set_content(R"({"error":"Forbidden - requires system:memberships:update permission or workspace member management"})", "application/json");
  2216. return;
  2217. }
  2218. // Check if MembershipService is available
  2219. if (!membershipService_) {
  2220. res.status = 503;
  2221. res.set_content(R"({"error":"Membership service not available"})", "application/json");
  2222. return;
  2223. }
  2224. try {
  2225. // Parse request body
  2226. auto json = nlohmann::json::parse(req.body);
  2227. UpdateMemberRequest update_req;
  2228. if (json.contains("group_ids") && json["group_ids"].is_array()) {
  2229. std::vector<std::string> group_ids;
  2230. for (const auto& item : json["group_ids"]) {
  2231. if (item.is_string()) {
  2232. group_ids.push_back(item.get<std::string>());
  2233. }
  2234. }
  2235. update_req.group_ids = group_ids;
  2236. }
  2237. auto result = membershipService_->UpdateMembership(workspace_id, user_id, update_req);
  2238. if (!result.success) {
  2239. res.status = 400;
  2240. nlohmann::json error_response = {{"error", result.error}};
  2241. res.set_content(error_response.dump(), "application/json");
  2242. return;
  2243. }
  2244. // Return updated membership
  2245. nlohmann::json member_json = {
  2246. {"id", result.member->id},
  2247. {"workspace_id", result.member->workspace_id},
  2248. {"user_id", result.member->user_id},
  2249. {"group_ids", result.member->group_ids},
  2250. {"created_at", result.member->created_at},
  2251. {"updated_at", result.member->updated_at}
  2252. };
  2253. res.set_content(member_json.dump(), "application/json");
  2254. } catch (const nlohmann::json::exception& e) {
  2255. res.status = 400;
  2256. nlohmann::json error_response = {{"error", "Invalid JSON: " + std::string(e.what())}};
  2257. res.set_content(error_response.dump(), "application/json");
  2258. } catch (const std::exception& e) {
  2259. spdlog::error("Update member error: {}", e.what());
  2260. res.status = 500;
  2261. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2262. }
  2263. }
  2264. void HttpServer::HandleRemoveMember(const httplib::Request& req, httplib::Response& res) {
  2265. // Authenticate the request
  2266. auto auth_user = AuthenticateRequest(req);
  2267. if (!auth_user) {
  2268. res.status = 401;
  2269. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2270. return;
  2271. }
  2272. // Extract workspace ID and user ID from path
  2273. std::string workspace_id = req.matches[1].str();
  2274. std::string user_id = req.matches[2].str();
  2275. // Requires system:memberships:delete permission OR workspace member management permission
  2276. bool can_remove = authorizationService_->HasPermission(*auth_user, permissions::kMembershipsDelete) ||
  2277. authorizationService_->CanManageWorkspaceMembers(*auth_user, workspace_id);
  2278. if (!can_remove) {
  2279. res.status = 403;
  2280. res.set_content(R"({"error":"Forbidden - requires system:memberships:delete permission or workspace member management"})", "application/json");
  2281. return;
  2282. }
  2283. // Check if MembershipService is available
  2284. if (!membershipService_) {
  2285. res.status = 503;
  2286. res.set_content(R"({"error":"Membership service not available"})", "application/json");
  2287. return;
  2288. }
  2289. try {
  2290. auto result = membershipService_->RemoveMember(workspace_id, user_id);
  2291. if (!result.success) {
  2292. res.status = 400;
  2293. nlohmann::json error_response = {{"error", result.error}};
  2294. res.set_content(error_response.dump(), "application/json");
  2295. return;
  2296. }
  2297. res.set_content(R"({"message":"Member removed successfully"})", "application/json");
  2298. } catch (const std::exception& e) {
  2299. spdlog::error("Remove member error: {}", e.what());
  2300. res.status = 500;
  2301. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2302. }
  2303. }
  2304. // ============================================================================
  2305. // API Key Routes
  2306. // ============================================================================
  2307. void HttpServer::SetupApiKeyRoutes() {
  2308. // POST /api/users/:id/api-keys - Create an API key for a user
  2309. httpServer_->Post(R"(/api/users/([a-zA-Z0-9\-]+)/api-keys)", [this](const httplib::Request& req, httplib::Response& res) {
  2310. HandleCreateApiKey(req, res);
  2311. });
  2312. // GET /api/users/:id/api-keys - List user's API keys
  2313. httpServer_->Get(R"(/api/users/([a-zA-Z0-9\-]+)/api-keys)", [this](const httplib::Request& req, httplib::Response& res) {
  2314. HandleListApiKeys(req, res);
  2315. });
  2316. // DELETE /api/users/:id/api-keys/:keyId - Revoke an API key
  2317. httpServer_->Delete(R"(/api/users/([a-zA-Z0-9\-]+)/api-keys/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  2318. HandleRevokeApiKey(req, res);
  2319. });
  2320. spdlog::info("API key routes registered: /api/users/:id/api-keys");
  2321. }
  2322. void HttpServer::HandleCreateApiKey(const httplib::Request& req, httplib::Response& res) {
  2323. // Authenticate the request
  2324. auto auth_user = AuthenticateRequest(req);
  2325. if (!auth_user) {
  2326. res.status = 401;
  2327. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2328. return;
  2329. }
  2330. // Extract user ID from path
  2331. std::string target_user_id = req.matches[1].str();
  2332. // Check permissions: own OR system:api_keys:create permission
  2333. bool is_own = (auth_user->user_id == target_user_id);
  2334. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kApiKeysCreate);
  2335. if (!is_own && !has_system_access) {
  2336. res.status = 403;
  2337. res.set_content(R"({"error":"Forbidden - can only create API keys for yourself or requires system:api_keys:create permission"})", "application/json");
  2338. return;
  2339. }
  2340. // Check if ApiKeyService is available
  2341. if (!apiKeyService_) {
  2342. res.status = 503;
  2343. res.set_content(R"({"error":"API key service not available"})", "application/json");
  2344. return;
  2345. }
  2346. try {
  2347. // Parse request body
  2348. auto json = nlohmann::json::parse(req.body);
  2349. CreateApiKeyRequest create_req;
  2350. create_req.user_id = target_user_id;
  2351. if (json.contains("name") && json["name"].is_string()) {
  2352. create_req.name = json["name"].get<std::string>();
  2353. } else {
  2354. res.status = 400;
  2355. res.set_content(R"({"error":"name is required"})", "application/json");
  2356. return;
  2357. }
  2358. if (json.contains("permissions") && json["permissions"].is_array()) {
  2359. for (const auto& item : json["permissions"]) {
  2360. if (item.is_string()) {
  2361. std::string perm = item.get<std::string>();
  2362. // Users can only grant permissions they have (unless they have system access)
  2363. if (!has_system_access) {
  2364. // For now, allow all permissions - proper permission checking
  2365. // would require looking up user's actual permissions
  2366. }
  2367. create_req.permissions.push_back(perm);
  2368. }
  2369. }
  2370. }
  2371. if (json.contains("expires_at") && json["expires_at"].is_string()) {
  2372. create_req.expires_at = json["expires_at"].get<std::string>();
  2373. }
  2374. auto result = apiKeyService_->CreateApiKey(create_req);
  2375. if (!result.success) {
  2376. res.status = 400;
  2377. nlohmann::json error_response = {{"error", result.error}};
  2378. res.set_content(error_response.dump(), "application/json");
  2379. return;
  2380. }
  2381. // Return created key info INCLUDING the raw key (only time it's returned!)
  2382. nlohmann::json key_json = {
  2383. {"id", result.api_key->id},
  2384. {"user_id", result.api_key->user_id},
  2385. {"name", result.api_key->name},
  2386. {"key_prefix", result.api_key->key_prefix},
  2387. {"key", result.raw_key}, // THE RAW KEY - only returned once!
  2388. {"permissions", result.api_key->permissions},
  2389. {"created_at", result.api_key->created_at},
  2390. {"expires_at", result.api_key->expires_at}
  2391. };
  2392. res.status = 201;
  2393. res.set_content(key_json.dump(), "application/json");
  2394. } catch (const nlohmann::json::exception& e) {
  2395. res.status = 400;
  2396. nlohmann::json error_response = {{"error", "Invalid JSON: " + std::string(e.what())}};
  2397. res.set_content(error_response.dump(), "application/json");
  2398. } catch (const std::exception& e) {
  2399. spdlog::error("Create API key error: {}", e.what());
  2400. res.status = 500;
  2401. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2402. }
  2403. }
  2404. void HttpServer::HandleListApiKeys(const httplib::Request& req, httplib::Response& res) {
  2405. // Authenticate the request
  2406. auto auth_user = AuthenticateRequest(req);
  2407. if (!auth_user) {
  2408. res.status = 401;
  2409. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2410. return;
  2411. }
  2412. // Extract user ID from path
  2413. std::string target_user_id = req.matches[1].str();
  2414. // Check permissions: own OR system:api_keys:read permission
  2415. bool is_own = (auth_user->user_id == target_user_id);
  2416. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kApiKeysRead);
  2417. if (!is_own && !has_system_access) {
  2418. res.status = 403;
  2419. res.set_content(R"({"error":"Forbidden - can only list your own API keys or requires system:api_keys:read permission"})", "application/json");
  2420. return;
  2421. }
  2422. // Check if ApiKeyService is available
  2423. if (!apiKeyService_) {
  2424. res.status = 503;
  2425. res.set_content(R"({"error":"API key service not available"})", "application/json");
  2426. return;
  2427. }
  2428. try {
  2429. // Check for include_revoked parameter (requires system access)
  2430. bool include_revoked = false;
  2431. if (has_system_access && req.has_param("include_revoked")) {
  2432. include_revoked = req.get_param_value("include_revoked") == "true";
  2433. }
  2434. auto result = apiKeyService_->ListApiKeys(target_user_id, include_revoked);
  2435. if (!result.success) {
  2436. res.status = 500;
  2437. nlohmann::json error_response = {{"error", result.error}};
  2438. res.set_content(error_response.dump(), "application/json");
  2439. return;
  2440. }
  2441. // Build response (note: raw keys and hashes are NOT included)
  2442. nlohmann::json keys_array = nlohmann::json::array();
  2443. for (const auto& key : result.api_keys) {
  2444. nlohmann::json key_json = {
  2445. {"id", key.id},
  2446. {"user_id", key.user_id},
  2447. {"name", key.name},
  2448. {"key_prefix", key.key_prefix}, // Only prefix, not full key
  2449. {"permissions", key.permissions},
  2450. {"created_at", key.created_at},
  2451. {"updated_at", key.updated_at},
  2452. {"last_used_at", key.last_used_at},
  2453. {"expires_at", key.expires_at}
  2454. };
  2455. if (has_system_access) {
  2456. key_json["deleted_at"] = key.deleted_at;
  2457. }
  2458. keys_array.push_back(key_json);
  2459. }
  2460. nlohmann::json response = {
  2461. {"api_keys", keys_array},
  2462. {"total_count", result.total_count}
  2463. };
  2464. res.set_content(response.dump(), "application/json");
  2465. } catch (const std::exception& e) {
  2466. spdlog::error("List API keys error: {}", e.what());
  2467. res.status = 500;
  2468. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2469. }
  2470. }
  2471. void HttpServer::HandleRevokeApiKey(const httplib::Request& req, httplib::Response& res) {
  2472. // Authenticate the request
  2473. auto auth_user = AuthenticateRequest(req);
  2474. if (!auth_user) {
  2475. res.status = 401;
  2476. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2477. return;
  2478. }
  2479. // Extract user ID and key ID from path
  2480. std::string target_user_id = req.matches[1].str();
  2481. std::string key_id = req.matches[2].str();
  2482. // Check permissions: own OR system:api_keys:delete permission
  2483. bool is_own = (auth_user->user_id == target_user_id);
  2484. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kApiKeysDelete);
  2485. if (!is_own && !has_system_access) {
  2486. res.status = 403;
  2487. res.set_content(R"({"error":"Forbidden - can only revoke your own API keys or requires system:api_keys:delete permission"})", "application/json");
  2488. return;
  2489. }
  2490. // Check if ApiKeyService is available
  2491. if (!apiKeyService_) {
  2492. res.status = 503;
  2493. res.set_content(R"({"error":"API key service not available"})", "application/json");
  2494. return;
  2495. }
  2496. try {
  2497. auto result = apiKeyService_->RevokeApiKey(key_id, target_user_id);
  2498. if (!result.success) {
  2499. res.status = 400;
  2500. nlohmann::json error_response = {{"error", result.error}};
  2501. res.set_content(error_response.dump(), "application/json");
  2502. return;
  2503. }
  2504. res.set_content(R"({"message":"API key revoked successfully"})", "application/json");
  2505. } catch (const std::exception& e) {
  2506. spdlog::error("Revoke API key error: {}", e.what());
  2507. res.status = 500;
  2508. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2509. }
  2510. }
  2511. // ============================================================================
  2512. // Collection Routes
  2513. // ============================================================================
  2514. void HttpServer::SetupCollectionRoutes() {
  2515. // POST /api/workspaces/:wid/collections - Create a collection
  2516. httpServer_->Post(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections)", [this](const httplib::Request& req, httplib::Response& res) {
  2517. HandleCreateCollection(req, res);
  2518. });
  2519. // GET /api/workspaces/:wid/collections - List collections
  2520. httpServer_->Get(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections)", [this](const httplib::Request& req, httplib::Response& res) {
  2521. HandleListCollections(req, res);
  2522. });
  2523. // GET /api/workspaces/:wid/collections/:name - Get a collection
  2524. 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) {
  2525. HandleGetCollection(req, res);
  2526. });
  2527. // PUT /api/workspaces/:wid/collections/:name - Update a collection
  2528. 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) {
  2529. HandleUpdateCollection(req, res);
  2530. });
  2531. // DELETE /api/workspaces/:wid/collections/:name - Drop a collection
  2532. 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) {
  2533. HandleDropCollection(req, res);
  2534. });
  2535. spdlog::info("Collection routes registered: /api/workspaces/:wid/collections");
  2536. }
  2537. void HttpServer::HandleCreateCollection(const httplib::Request& req, httplib::Response& res) {
  2538. // Authenticate the request
  2539. auto auth_user = AuthenticateRequest(req);
  2540. if (!auth_user) {
  2541. res.status = 401;
  2542. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2543. return;
  2544. }
  2545. // Extract workspace ID from path
  2546. std::string workspace_id = req.matches[1].str();
  2547. // Verify workspace exists and user has access
  2548. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  2549. if (!ws_result.success) {
  2550. res.status = 404;
  2551. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  2552. return;
  2553. }
  2554. // Requires system:collections:create permission OR workspace membership with manage_collections
  2555. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  2556. auth_user->workspace_ids.end(),
  2557. workspace_id) != auth_user->workspace_ids.end());
  2558. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kCollectionsCreate);
  2559. if (!is_member && !has_system_access) {
  2560. res.status = 403;
  2561. res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
  2562. return;
  2563. }
  2564. // Check if CollectionService is available
  2565. if (!collectionService_) {
  2566. res.status = 503;
  2567. res.set_content(R"({"error":"Collection service not available"})", "application/json");
  2568. return;
  2569. }
  2570. try {
  2571. // Parse request body
  2572. auto json = nlohmann::json::parse(req.body);
  2573. CreateCollectionRequest create_req;
  2574. create_req.workspace_id = workspace_id;
  2575. if (json.contains("name") && json["name"].is_string()) {
  2576. create_req.name = json["name"].get<std::string>();
  2577. } else {
  2578. res.status = 400;
  2579. res.set_content(R"({"error":"name is required"})", "application/json");
  2580. return;
  2581. }
  2582. // Parse settings
  2583. if (json.contains("schema") && json["schema"].is_string()) {
  2584. create_req.settings.schema = json["schema"].get<std::string>();
  2585. }
  2586. if (json.contains("encrypted_fields") && json["encrypted_fields"].is_array()) {
  2587. for (const auto& item : json["encrypted_fields"]) {
  2588. if (item.is_string()) {
  2589. create_req.settings.encrypted_fields.push_back(item.get<std::string>());
  2590. }
  2591. }
  2592. }
  2593. if (json.contains("ttl_seconds") && json["ttl_seconds"].is_number_integer()) {
  2594. create_req.settings.ttl_seconds = json["ttl_seconds"].get<int64_t>();
  2595. }
  2596. auto result = collectionService_->CreateCollection(create_req);
  2597. if (!result.success) {
  2598. res.status = 400;
  2599. nlohmann::json error_response = {{"error", result.error}};
  2600. res.set_content(error_response.dump(), "application/json");
  2601. return;
  2602. }
  2603. // Return created collection info
  2604. nlohmann::json coll_json = {
  2605. {"name", result.collection->name},
  2606. {"workspace_id", result.collection->workspace_id},
  2607. {"document_count", result.collection->document_count},
  2608. {"size_bytes", result.collection->size_bytes},
  2609. {"created_at", result.collection->created_at},
  2610. {"settings", {
  2611. {"schema", result.collection->settings.schema},
  2612. {"encrypted_fields", result.collection->settings.encrypted_fields},
  2613. {"ttl_seconds", result.collection->settings.ttl_seconds}
  2614. }}
  2615. };
  2616. res.status = 201;
  2617. res.set_content(coll_json.dump(), "application/json");
  2618. } catch (const nlohmann::json::exception& e) {
  2619. spdlog::warn("Create collection JSON parse error: {}", e.what());
  2620. res.status = 400;
  2621. res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
  2622. } catch (const std::exception& e) {
  2623. spdlog::error("Create collection error: {}", e.what());
  2624. res.status = 500;
  2625. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2626. }
  2627. }
  2628. void HttpServer::HandleListCollections(const httplib::Request& req, httplib::Response& res) {
  2629. // Authenticate the request
  2630. auto auth_user = AuthenticateRequest(req);
  2631. if (!auth_user) {
  2632. res.status = 401;
  2633. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2634. return;
  2635. }
  2636. // Extract workspace ID from path
  2637. std::string workspace_id = req.matches[1].str();
  2638. // Verify workspace exists and user has access
  2639. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  2640. if (!ws_result.success) {
  2641. res.status = 404;
  2642. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  2643. return;
  2644. }
  2645. // Check access: must be member OR have system:collections:read permission
  2646. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  2647. auth_user->workspace_ids.end(),
  2648. workspace_id) != auth_user->workspace_ids.end());
  2649. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kCollectionsRead);
  2650. if (!is_member && !has_system_access) {
  2651. res.status = 403;
  2652. res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
  2653. return;
  2654. }
  2655. // Check if CollectionService is available
  2656. if (!collectionService_) {
  2657. res.status = 503;
  2658. res.set_content(R"({"error":"Collection service not available"})", "application/json");
  2659. return;
  2660. }
  2661. try {
  2662. // Check if user with system access wants to include system collections
  2663. bool include_system = has_system_access &&
  2664. req.has_param("include_system") &&
  2665. req.get_param_value("include_system") == "true";
  2666. auto result = collectionService_->ListCollections(workspace_id);
  2667. if (!result.success) {
  2668. res.status = 500;
  2669. nlohmann::json error_response = {{"error", result.error}};
  2670. res.set_content(error_response.dump(), "application/json");
  2671. return;
  2672. }
  2673. nlohmann::json collections_array = nlohmann::json::array();
  2674. for (const auto& coll : result.collections) {
  2675. // Check if collection name starts with underscore (system collection)
  2676. bool is_system = !coll.name.empty() && coll.name[0] == '_';
  2677. nlohmann::json coll_json = {
  2678. {"name", coll.name},
  2679. {"workspace_id", coll.workspace_id},
  2680. {"document_count", coll.document_count},
  2681. {"size_bytes", coll.size_bytes},
  2682. {"created_at", coll.created_at},
  2683. {"updated_at", coll.updated_at},
  2684. {"is_system", is_system},
  2685. {"created_by", coll.created_by},
  2686. {"updated_by", coll.updated_by},
  2687. {"created_by_name", ResolveUserName(coll.created_by)},
  2688. {"updated_by_name", ResolveUserName(coll.updated_by)},
  2689. {"settings", {
  2690. {"schema", coll.settings.schema},
  2691. {"encrypted_fields", coll.settings.encrypted_fields},
  2692. {"ttl_seconds", coll.settings.ttl_seconds}
  2693. }}
  2694. };
  2695. collections_array.push_back(coll_json);
  2696. }
  2697. // If superadmin requested system collections, add global system collections
  2698. if (include_system) {
  2699. auto system_result = collectionService_->ListSystemCollections();
  2700. if (system_result.success) {
  2701. for (const auto& coll : system_result.collections) {
  2702. nlohmann::json coll_json = {
  2703. {"name", coll.name},
  2704. {"workspace_id", ""},
  2705. {"document_count", coll.document_count},
  2706. {"size_bytes", coll.size_bytes},
  2707. {"created_at", coll.created_at},
  2708. {"updated_at", coll.updated_at},
  2709. {"is_system", true},
  2710. {"created_by", coll.created_by},
  2711. {"updated_by", coll.updated_by},
  2712. {"created_by_name", ResolveUserName(coll.created_by)},
  2713. {"updated_by_name", ResolveUserName(coll.updated_by)},
  2714. {"settings", nlohmann::json::object()}
  2715. };
  2716. collections_array.push_back(coll_json);
  2717. }
  2718. }
  2719. }
  2720. nlohmann::json response = {
  2721. {"collections", collections_array},
  2722. {"total_count", static_cast<int64_t>(collections_array.size())}
  2723. };
  2724. res.set_content(response.dump(), "application/json");
  2725. } catch (const std::exception& e) {
  2726. spdlog::error("List collections error: {}", e.what());
  2727. res.status = 500;
  2728. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2729. }
  2730. }
  2731. void HttpServer::HandleGetCollection(const httplib::Request& req, httplib::Response& res) {
  2732. // Authenticate the request
  2733. auto auth_user = AuthenticateRequest(req);
  2734. if (!auth_user) {
  2735. res.status = 401;
  2736. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2737. return;
  2738. }
  2739. // Extract workspace ID and collection name from path
  2740. std::string workspace_id = req.matches[1].str();
  2741. std::string collection_name = req.matches[2].str();
  2742. // Verify workspace exists and user has access
  2743. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  2744. if (!ws_result.success) {
  2745. res.status = 404;
  2746. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  2747. return;
  2748. }
  2749. // Check access: must be member OR have system:collections:read permission
  2750. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  2751. auth_user->workspace_ids.end(),
  2752. workspace_id) != auth_user->workspace_ids.end());
  2753. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kCollectionsRead);
  2754. if (!is_member && !has_system_access) {
  2755. res.status = 403;
  2756. res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
  2757. return;
  2758. }
  2759. // Check if CollectionService is available
  2760. if (!collectionService_) {
  2761. res.status = 503;
  2762. res.set_content(R"({"error":"Collection service not available"})", "application/json");
  2763. return;
  2764. }
  2765. try {
  2766. auto result = collectionService_->GetCollection(workspace_id, collection_name);
  2767. if (!result.success) {
  2768. res.status = 404;
  2769. nlohmann::json error_response = {{"error", result.error}};
  2770. res.set_content(error_response.dump(), "application/json");
  2771. return;
  2772. }
  2773. nlohmann::json coll_json = {
  2774. {"name", result.collection->name},
  2775. {"workspace_id", result.collection->workspace_id},
  2776. {"document_count", result.collection->document_count},
  2777. {"size_bytes", result.collection->size_bytes},
  2778. {"created_at", result.collection->created_at},
  2779. {"updated_at", result.collection->updated_at},
  2780. {"settings", {
  2781. {"schema", result.collection->settings.schema},
  2782. {"encrypted_fields", result.collection->settings.encrypted_fields},
  2783. {"ttl_seconds", result.collection->settings.ttl_seconds}
  2784. }}
  2785. };
  2786. res.set_content(coll_json.dump(), "application/json");
  2787. } catch (const std::exception& e) {
  2788. spdlog::error("Get collection error: {}", e.what());
  2789. res.status = 500;
  2790. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2791. }
  2792. }
  2793. void HttpServer::HandleUpdateCollection(const httplib::Request& req, httplib::Response& res) {
  2794. // Authenticate the request
  2795. auto auth_user = AuthenticateRequest(req);
  2796. if (!auth_user) {
  2797. res.status = 401;
  2798. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2799. return;
  2800. }
  2801. // Extract workspace ID and collection name from path
  2802. std::string workspace_id = req.matches[1].str();
  2803. std::string collection_name = req.matches[2].str();
  2804. // Verify workspace exists and user has access
  2805. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  2806. if (!ws_result.success) {
  2807. res.status = 404;
  2808. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  2809. return;
  2810. }
  2811. // Requires system:collections:update permission OR workspace membership
  2812. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  2813. auth_user->workspace_ids.end(),
  2814. workspace_id) != auth_user->workspace_ids.end());
  2815. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kCollectionsUpdate);
  2816. if (!is_member && !has_system_access) {
  2817. res.status = 403;
  2818. res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
  2819. return;
  2820. }
  2821. // Check if CollectionService is available
  2822. if (!collectionService_) {
  2823. res.status = 503;
  2824. res.set_content(R"({"error":"Collection service not available"})", "application/json");
  2825. return;
  2826. }
  2827. try {
  2828. // Parse request body
  2829. auto json = nlohmann::json::parse(req.body);
  2830. // Get existing settings first
  2831. auto get_result = collectionService_->GetCollection(workspace_id, collection_name);
  2832. if (!get_result.success) {
  2833. res.status = 404;
  2834. res.set_content(R"({"error":"Collection not found"})", "application/json");
  2835. return;
  2836. }
  2837. CollectionSettings settings = get_result.collection->settings;
  2838. // Update only provided fields
  2839. if (json.contains("schema") && json["schema"].is_string()) {
  2840. settings.schema = json["schema"].get<std::string>();
  2841. }
  2842. if (json.contains("encrypted_fields") && json["encrypted_fields"].is_array()) {
  2843. settings.encrypted_fields.clear();
  2844. for (const auto& item : json["encrypted_fields"]) {
  2845. if (item.is_string()) {
  2846. settings.encrypted_fields.push_back(item.get<std::string>());
  2847. }
  2848. }
  2849. }
  2850. if (json.contains("ttl_seconds") && json["ttl_seconds"].is_number_integer()) {
  2851. settings.ttl_seconds = json["ttl_seconds"].get<int64_t>();
  2852. }
  2853. auto result = collectionService_->UpdateCollection(workspace_id, collection_name, settings);
  2854. if (!result.success) {
  2855. res.status = 400;
  2856. nlohmann::json error_response = {{"error", result.error}};
  2857. res.set_content(error_response.dump(), "application/json");
  2858. return;
  2859. }
  2860. nlohmann::json coll_json = {
  2861. {"name", result.collection->name},
  2862. {"workspace_id", result.collection->workspace_id},
  2863. {"document_count", result.collection->document_count},
  2864. {"size_bytes", result.collection->size_bytes},
  2865. {"created_at", result.collection->created_at},
  2866. {"updated_at", result.collection->updated_at},
  2867. {"settings", {
  2868. {"schema", result.collection->settings.schema},
  2869. {"encrypted_fields", result.collection->settings.encrypted_fields},
  2870. {"ttl_seconds", result.collection->settings.ttl_seconds}
  2871. }}
  2872. };
  2873. res.set_content(coll_json.dump(), "application/json");
  2874. } catch (const nlohmann::json::exception& e) {
  2875. spdlog::warn("Update collection JSON parse error: {}", e.what());
  2876. res.status = 400;
  2877. res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
  2878. } catch (const std::exception& e) {
  2879. spdlog::error("Update collection error: {}", e.what());
  2880. res.status = 500;
  2881. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2882. }
  2883. }
  2884. // ============================================================================
  2885. // Document Routes
  2886. // ============================================================================
  2887. void HttpServer::SetupDocumentRoutes() {
  2888. // POST /api/workspaces/:wid/collections/:name/documents - Create a document
  2889. 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) {
  2890. HandleCreateDocument(req, res);
  2891. });
  2892. // GET /api/workspaces/:wid/collections/:name/documents - List documents
  2893. 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) {
  2894. HandleListDocuments(req, res);
  2895. });
  2896. // GET /api/workspaces/:wid/collections/:name/documents/:id - Get a document
  2897. 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) {
  2898. HandleGetDocument(req, res);
  2899. });
  2900. // PUT /api/workspaces/:wid/collections/:name/documents/:id - Update a document
  2901. 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) {
  2902. HandleUpdateDocument(req, res);
  2903. });
  2904. // PATCH /api/workspaces/:wid/collections/:name/documents/:id - Update a document (partial)
  2905. httpServer_->Patch(R"(/api/workspaces/([a-zA-Z0-9\-]+)/collections/([a-zA-Z_][a-zA-Z0-9_]*)/documents/([a-zA-Z0-9\-]+))", [this](const httplib::Request& req, httplib::Response& res) {
  2906. HandleUpdateDocument(req, res);
  2907. });
  2908. // DELETE /api/workspaces/:wid/collections/:name/documents/:id - Delete a document
  2909. 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) {
  2910. HandleDeleteDocument(req, res);
  2911. });
  2912. spdlog::info("Document routes registered: /api/workspaces/:wid/collections/:name/documents");
  2913. }
  2914. void HttpServer::HandleCreateDocument(const httplib::Request& req, httplib::Response& res) {
  2915. // Authenticate the request
  2916. auto auth_user = AuthenticateRequest(req);
  2917. if (!auth_user) {
  2918. res.status = 401;
  2919. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2920. return;
  2921. }
  2922. // Extract workspace ID and collection name from path
  2923. std::string workspace_id = req.matches[1].str();
  2924. std::string collection_name = req.matches[2].str();
  2925. // Verify workspace exists and user has access
  2926. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  2927. if (!ws_result.success) {
  2928. res.status = 404;
  2929. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  2930. return;
  2931. }
  2932. // Check document create permission using AuthorizationService
  2933. if (!authorizationService_->CanCreateDocument(*auth_user, workspace_id, collection_name)) {
  2934. res.status = 403;
  2935. res.set_content(R"({"error":"Forbidden - no permission to create documents in this collection"})", "application/json");
  2936. return;
  2937. }
  2938. // Check if DocumentService is available
  2939. if (!documentService_) {
  2940. res.status = 503;
  2941. res.set_content(R"({"error":"Document service not available"})", "application/json");
  2942. return;
  2943. }
  2944. try {
  2945. // Parse request body
  2946. auto json = nlohmann::json::parse(req.body);
  2947. CreateDocumentRequest create_req;
  2948. create_req.workspace_id = workspace_id;
  2949. create_req.collection = collection_name;
  2950. create_req.user_id = auth_user->user_id; // Set user for _created_by tracking
  2951. if (json.contains("id") && json["id"].is_string()) {
  2952. create_req.id = json["id"].get<std::string>();
  2953. }
  2954. if (json.contains("data") && json["data"].is_object()) {
  2955. create_req.data = json["data"];
  2956. } else {
  2957. // Treat entire body as data if no "data" field
  2958. create_req.data = json;
  2959. create_req.data.erase("id"); // Remove id from data
  2960. }
  2961. auto result = documentService_->CreateDocument(create_req);
  2962. if (!result.success) {
  2963. res.status = 400;
  2964. nlohmann::json error_response = {{"error", result.error}};
  2965. res.set_content(error_response.dump(), "application/json");
  2966. return;
  2967. }
  2968. // Return created document info
  2969. nlohmann::json doc_json = {
  2970. {"id", result.document->id},
  2971. {"collection", result.document->collection},
  2972. {"workspace_id", result.document->workspace_id},
  2973. {"data", result.document->data},
  2974. {"created_at", result.document->created_at},
  2975. {"updated_at", result.document->updated_at},
  2976. {"version", result.document->version}
  2977. };
  2978. // Broadcast document create event to WebSocket subscribers
  2979. BroadcastDocumentEvent(workspace_id, collection_name, DocumentAction::Create,
  2980. result.document->id, result.document->data);
  2981. res.status = 201;
  2982. res.set_content(doc_json.dump(), "application/json");
  2983. } catch (const nlohmann::json::exception& e) {
  2984. spdlog::warn("Create document JSON parse error: {}", e.what());
  2985. res.status = 400;
  2986. res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
  2987. } catch (const std::exception& e) {
  2988. spdlog::error("Create document error: {}", e.what());
  2989. res.status = 500;
  2990. res.set_content(R"({"error":"Internal server error"})", "application/json");
  2991. }
  2992. }
  2993. void HttpServer::HandleListDocuments(const httplib::Request& req, httplib::Response& res) {
  2994. // Authenticate the request
  2995. auto auth_user = AuthenticateRequest(req);
  2996. if (!auth_user) {
  2997. res.status = 401;
  2998. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  2999. return;
  3000. }
  3001. // Extract workspace ID and collection name from path
  3002. std::string workspace_id = req.matches[1].str();
  3003. std::string collection_name = req.matches[2].str();
  3004. // Verify workspace exists and user has access
  3005. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  3006. if (!ws_result.success) {
  3007. res.status = 404;
  3008. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  3009. return;
  3010. }
  3011. // Check collection read permission using AuthorizationService
  3012. if (!authorizationService_->CanReadCollection(*auth_user, workspace_id, collection_name)) {
  3013. res.status = 403;
  3014. res.set_content(R"({"error":"Forbidden - no permission to read documents in this collection"})", "application/json");
  3015. return;
  3016. }
  3017. // Check if DocumentService is available
  3018. if (!documentService_) {
  3019. res.status = 503;
  3020. res.set_content(R"({"error":"Document service not available"})", "application/json");
  3021. return;
  3022. }
  3023. try {
  3024. DocumentQuery query;
  3025. query.workspace_id = workspace_id;
  3026. query.collection = collection_name;
  3027. // Parse query parameters
  3028. if (req.has_param("limit")) {
  3029. query.limit = std::stoi(req.get_param_value("limit"));
  3030. if (query.limit < 1) query.limit = 1;
  3031. if (query.limit > 1000) query.limit = 1000;
  3032. }
  3033. if (req.has_param("offset")) {
  3034. query.offset = std::stoi(req.get_param_value("offset"));
  3035. if (query.offset < 0) query.offset = 0;
  3036. }
  3037. if (req.has_param("sort")) {
  3038. query.sort_field = req.get_param_value("sort");
  3039. }
  3040. if (req.has_param("order")) {
  3041. query.sort_ascending = (req.get_param_value("order") != "desc");
  3042. }
  3043. if (req.has_param("filter")) {
  3044. try {
  3045. query.filter = nlohmann::json::parse(req.get_param_value("filter"));
  3046. } catch (...) {
  3047. // Invalid filter JSON - ignore
  3048. }
  3049. }
  3050. auto result = documentService_->ListDocuments(query);
  3051. if (!result.success) {
  3052. res.status = 500;
  3053. nlohmann::json error_response = {{"error", result.error}};
  3054. res.set_content(error_response.dump(), "application/json");
  3055. return;
  3056. }
  3057. nlohmann::json docs_array = nlohmann::json::array();
  3058. for (const auto& doc : result.documents) {
  3059. // Resolve user names for _created_by and _updated_by
  3060. nlohmann::json enriched_data = doc.data;
  3061. if (doc.data.contains("_created_by") && doc.data["_created_by"].is_string()) {
  3062. std::string created_by_name = ResolveUserName(doc.data["_created_by"].get<std::string>());
  3063. if (!created_by_name.empty()) {
  3064. enriched_data["_created_by_name"] = created_by_name;
  3065. }
  3066. }
  3067. if (doc.data.contains("_updated_by") && doc.data["_updated_by"].is_string()) {
  3068. std::string updated_by_name = ResolveUserName(doc.data["_updated_by"].get<std::string>());
  3069. if (!updated_by_name.empty()) {
  3070. enriched_data["_updated_by_name"] = updated_by_name;
  3071. }
  3072. }
  3073. nlohmann::json doc_json = {
  3074. {"id", doc.id},
  3075. {"collection", doc.collection},
  3076. {"workspace_id", doc.workspace_id},
  3077. {"data", enriched_data},
  3078. {"created_at", doc.created_at},
  3079. {"updated_at", doc.updated_at},
  3080. {"version", doc.version}
  3081. };
  3082. docs_array.push_back(doc_json);
  3083. }
  3084. nlohmann::json response = {
  3085. {"documents", docs_array},
  3086. {"total_count", result.total_count}
  3087. };
  3088. res.set_content(response.dump(), "application/json");
  3089. } catch (const std::exception& e) {
  3090. spdlog::error("List documents error: {}", e.what());
  3091. res.status = 500;
  3092. res.set_content(R"({"error":"Internal server error"})", "application/json");
  3093. }
  3094. }
  3095. void HttpServer::HandleGetDocument(const httplib::Request& req, httplib::Response& res) {
  3096. // Authenticate the request
  3097. auto auth_user = AuthenticateRequest(req);
  3098. if (!auth_user) {
  3099. res.status = 401;
  3100. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  3101. return;
  3102. }
  3103. // Extract workspace ID, collection name, and document ID from path
  3104. std::string workspace_id = req.matches[1].str();
  3105. std::string collection_name = req.matches[2].str();
  3106. std::string document_id = req.matches[3].str();
  3107. // Verify workspace exists and user has access
  3108. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  3109. if (!ws_result.success) {
  3110. res.status = 404;
  3111. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  3112. return;
  3113. }
  3114. // Check if DocumentService is available
  3115. if (!documentService_) {
  3116. res.status = 503;
  3117. res.set_content(R"({"error":"Document service not available"})", "application/json");
  3118. return;
  3119. }
  3120. try {
  3121. auto result = documentService_->GetDocument(workspace_id, collection_name, document_id);
  3122. if (!result.success) {
  3123. res.status = 404;
  3124. nlohmann::json error_response = {{"error", result.error}};
  3125. res.set_content(error_response.dump(), "application/json");
  3126. return;
  3127. }
  3128. // Check document read permission with ownership
  3129. std::string doc_owner = result.document->data.value("_created_by", "");
  3130. if (!authorizationService_->CanReadDocument(*auth_user, workspace_id, collection_name, doc_owner)) {
  3131. res.status = 403;
  3132. res.set_content(R"({"error":"Forbidden - no permission to read this document"})", "application/json");
  3133. return;
  3134. }
  3135. nlohmann::json doc_json = {
  3136. {"id", result.document->id},
  3137. {"collection", result.document->collection},
  3138. {"workspace_id", result.document->workspace_id},
  3139. {"data", result.document->data},
  3140. {"created_at", result.document->created_at},
  3141. {"updated_at", result.document->updated_at},
  3142. {"version", result.document->version}
  3143. };
  3144. res.set_content(doc_json.dump(), "application/json");
  3145. } catch (const std::exception& e) {
  3146. spdlog::error("Get document error: {}", e.what());
  3147. res.status = 500;
  3148. res.set_content(R"({"error":"Internal server error"})", "application/json");
  3149. }
  3150. }
  3151. void HttpServer::HandleUpdateDocument(const httplib::Request& req, httplib::Response& res) {
  3152. // Authenticate the request
  3153. auto auth_user = AuthenticateRequest(req);
  3154. if (!auth_user) {
  3155. res.status = 401;
  3156. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  3157. return;
  3158. }
  3159. // Extract workspace ID, collection name, and document ID from path
  3160. std::string workspace_id = req.matches[1].str();
  3161. std::string collection_name = req.matches[2].str();
  3162. std::string document_id = req.matches[3].str();
  3163. // Verify workspace exists and user has access
  3164. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  3165. if (!ws_result.success) {
  3166. res.status = 404;
  3167. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  3168. return;
  3169. }
  3170. // Check if DocumentService is available
  3171. if (!documentService_) {
  3172. res.status = 503;
  3173. res.set_content(R"({"error":"Document service not available"})", "application/json");
  3174. return;
  3175. }
  3176. try {
  3177. // First get the document to check ownership
  3178. auto existing = documentService_->GetDocument(workspace_id, collection_name, document_id);
  3179. if (!existing.success) {
  3180. res.status = 404;
  3181. nlohmann::json error_response = {{"error", existing.error}};
  3182. res.set_content(error_response.dump(), "application/json");
  3183. return;
  3184. }
  3185. // Check document write permission with ownership
  3186. std::string doc_owner = existing.document->data.value("_created_by", "");
  3187. if (!authorizationService_->CanWriteDocument(*auth_user, workspace_id, collection_name, doc_owner)) {
  3188. res.status = 403;
  3189. res.set_content(R"({"error":"Forbidden - no permission to update this document"})", "application/json");
  3190. return;
  3191. }
  3192. // Parse request body
  3193. auto json = nlohmann::json::parse(req.body);
  3194. UpdateDocumentRequest update_req;
  3195. update_req.workspace_id = workspace_id;
  3196. update_req.collection = collection_name;
  3197. update_req.id = document_id;
  3198. update_req.user_id = auth_user->user_id; // Set user for _updated_by tracking
  3199. if (json.contains("data") && json["data"].is_object()) {
  3200. update_req.data = json["data"];
  3201. } else {
  3202. // Treat entire body as data if no "data" field
  3203. update_req.data = json;
  3204. }
  3205. if (json.contains("merge") && json["merge"].is_boolean()) {
  3206. update_req.merge = json["merge"].get<bool>();
  3207. }
  3208. if (json.contains("expected_version") && json["expected_version"].is_number_integer()) {
  3209. update_req.expected_version = json["expected_version"].get<int64_t>();
  3210. }
  3211. auto result = documentService_->UpdateDocument(update_req);
  3212. if (!result.success) {
  3213. res.status = 400;
  3214. nlohmann::json error_response = {{"error", result.error}};
  3215. res.set_content(error_response.dump(), "application/json");
  3216. return;
  3217. }
  3218. nlohmann::json doc_json = {
  3219. {"id", result.document->id},
  3220. {"collection", result.document->collection},
  3221. {"workspace_id", result.document->workspace_id},
  3222. {"data", result.document->data},
  3223. {"created_at", result.document->created_at},
  3224. {"updated_at", result.document->updated_at},
  3225. {"version", result.document->version}
  3226. };
  3227. // Broadcast document update event to WebSocket subscribers
  3228. BroadcastDocumentEvent(workspace_id, collection_name, DocumentAction::Update,
  3229. result.document->id, result.document->data);
  3230. res.set_content(doc_json.dump(), "application/json");
  3231. } catch (const nlohmann::json::exception& e) {
  3232. spdlog::warn("Update document JSON parse error: {}", e.what());
  3233. res.status = 400;
  3234. res.set_content(R"({"error":"Invalid JSON in request body"})", "application/json");
  3235. } catch (const std::exception& e) {
  3236. spdlog::error("Update document error: {}", e.what());
  3237. res.status = 500;
  3238. res.set_content(R"({"error":"Internal server error"})", "application/json");
  3239. }
  3240. }
  3241. void HttpServer::HandleDeleteDocument(const httplib::Request& req, httplib::Response& res) {
  3242. // Authenticate the request
  3243. auto auth_user = AuthenticateRequest(req);
  3244. if (!auth_user) {
  3245. res.status = 401;
  3246. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  3247. return;
  3248. }
  3249. // Extract workspace ID, collection name, and document ID from path
  3250. std::string workspace_id = req.matches[1].str();
  3251. std::string collection_name = req.matches[2].str();
  3252. std::string document_id = req.matches[3].str();
  3253. // Verify workspace exists and user has access
  3254. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  3255. if (!ws_result.success) {
  3256. res.status = 404;
  3257. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  3258. return;
  3259. }
  3260. // Check if DocumentService is available
  3261. if (!documentService_) {
  3262. res.status = 503;
  3263. res.set_content(R"({"error":"Document service not available"})", "application/json");
  3264. return;
  3265. }
  3266. try {
  3267. // First get the document to check ownership
  3268. auto existing = documentService_->GetDocument(workspace_id, collection_name, document_id);
  3269. if (!existing.success) {
  3270. res.status = 404;
  3271. nlohmann::json error_response = {{"error", existing.error}};
  3272. res.set_content(error_response.dump(), "application/json");
  3273. return;
  3274. }
  3275. // Check document delete permission with ownership
  3276. std::string doc_owner = existing.document->data.value("_created_by", "");
  3277. if (!authorizationService_->CanDeleteDocument(*auth_user, workspace_id, collection_name, doc_owner)) {
  3278. res.status = 403;
  3279. res.set_content(R"({"error":"Forbidden - no permission to delete this document"})", "application/json");
  3280. return;
  3281. }
  3282. auto result = documentService_->DeleteDocument(workspace_id, collection_name, document_id);
  3283. if (!result.success) {
  3284. res.status = 404;
  3285. nlohmann::json error_response = {{"error", result.error}};
  3286. res.set_content(error_response.dump(), "application/json");
  3287. return;
  3288. }
  3289. // Broadcast document delete event to WebSocket subscribers
  3290. BroadcastDocumentEvent(workspace_id, collection_name, DocumentAction::Delete,
  3291. document_id, nlohmann::json::object());
  3292. res.set_content(R"({"message":"Document deleted successfully"})", "application/json");
  3293. } catch (const std::exception& e) {
  3294. spdlog::error("Delete document error: {}", e.what());
  3295. res.status = 500;
  3296. res.set_content(R"({"error":"Internal server error"})", "application/json");
  3297. }
  3298. }
  3299. void HttpServer::HandleDropCollection(const httplib::Request& req, httplib::Response& res) {
  3300. // Authenticate the request
  3301. auto auth_user = AuthenticateRequest(req);
  3302. if (!auth_user) {
  3303. res.status = 401;
  3304. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  3305. return;
  3306. }
  3307. // Extract workspace ID and collection name from path
  3308. std::string workspace_id = req.matches[1].str();
  3309. std::string collection_name = req.matches[2].str();
  3310. // Verify workspace exists and user has access
  3311. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  3312. if (!ws_result.success) {
  3313. res.status = 404;
  3314. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  3315. return;
  3316. }
  3317. // Requires system:collections:delete permission OR workspace membership
  3318. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  3319. auth_user->workspace_ids.end(),
  3320. workspace_id) != auth_user->workspace_ids.end());
  3321. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kCollectionsDelete);
  3322. if (!is_member && !has_system_access) {
  3323. res.status = 403;
  3324. res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
  3325. return;
  3326. }
  3327. // Check if CollectionService is available
  3328. if (!collectionService_) {
  3329. res.status = 503;
  3330. res.set_content(R"({"error":"Collection service not available"})", "application/json");
  3331. return;
  3332. }
  3333. try {
  3334. // Check for force parameter
  3335. bool force = false;
  3336. if (req.has_param("force")) {
  3337. force = req.get_param_value("force") == "true";
  3338. }
  3339. auto result = collectionService_->DropCollection(workspace_id, collection_name, force);
  3340. if (!result.success) {
  3341. res.status = 400;
  3342. nlohmann::json error_response = {{"error", result.error}};
  3343. res.set_content(error_response.dump(), "application/json");
  3344. return;
  3345. }
  3346. res.set_content(R"({"message":"Collection dropped successfully"})", "application/json");
  3347. } catch (const std::exception& e) {
  3348. spdlog::error("Drop collection error: {}", e.what());
  3349. res.status = 500;
  3350. res.set_content(R"({"error":"Internal server error"})", "application/json");
  3351. }
  3352. }
  3353. // ============================================================================
  3354. // View Routes
  3355. // ============================================================================
  3356. void HttpServer::SetupViewRoutes() {
  3357. // POST /api/workspaces/:workspace_id/views - Create a new view
  3358. httpServer_->Post(R"(/api/workspaces/([^/]+)/views)",
  3359. [this](const httplib::Request& req, httplib::Response& res) {
  3360. HandleCreateView(req, res);
  3361. });
  3362. // GET /api/workspaces/:workspace_id/views - List all views
  3363. httpServer_->Get(R"(/api/workspaces/([^/]+)/views)",
  3364. [this](const httplib::Request& req, httplib::Response& res) {
  3365. HandleListViews(req, res);
  3366. });
  3367. // GET /api/workspaces/:workspace_id/views/:view_id - Get a specific view
  3368. httpServer_->Get(R"(/api/workspaces/([^/]+)/views/([^/]+))",
  3369. [this](const httplib::Request& req, httplib::Response& res) {
  3370. HandleGetView(req, res);
  3371. });
  3372. // PATCH /api/workspaces/:workspace_id/views/:view_id - Update a view
  3373. httpServer_->Patch(R"(/api/workspaces/([^/]+)/views/([^/]+))",
  3374. [this](const httplib::Request& req, httplib::Response& res) {
  3375. HandleUpdateView(req, res);
  3376. });
  3377. // DELETE /api/workspaces/:workspace_id/views/:view_id - Delete a view
  3378. httpServer_->Delete(R"(/api/workspaces/([^/]+)/views/([^/]+))",
  3379. [this](const httplib::Request& req, httplib::Response& res) {
  3380. HandleDeleteView(req, res);
  3381. });
  3382. spdlog::info("View routes configured");
  3383. }
  3384. void HttpServer::HandleCreateView(const httplib::Request& req, httplib::Response& res) {
  3385. // Authenticate the request
  3386. auto auth_user = AuthenticateRequest(req);
  3387. if (!auth_user) {
  3388. res.status = 401;
  3389. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  3390. return;
  3391. }
  3392. // Extract workspace ID from path
  3393. std::string workspace_id = req.matches[1].str();
  3394. // Verify workspace exists
  3395. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  3396. if (!ws_result.success) {
  3397. res.status = 404;
  3398. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  3399. return;
  3400. }
  3401. // Requires system:views:create permission OR workspace membership
  3402. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  3403. auth_user->workspace_ids.end(),
  3404. workspace_id) != auth_user->workspace_ids.end());
  3405. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kViewsCreate);
  3406. if (!is_member && !has_system_access) {
  3407. res.status = 403;
  3408. res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
  3409. return;
  3410. }
  3411. // Check if ViewService is available
  3412. if (!viewService_) {
  3413. res.status = 503;
  3414. res.set_content(R"({"error":"View service not available"})", "application/json");
  3415. return;
  3416. }
  3417. // Parse request body
  3418. nlohmann::json body;
  3419. try {
  3420. body = nlohmann::json::parse(req.body);
  3421. } catch (const nlohmann::json::parse_error& /*e*/) {
  3422. res.status = 400;
  3423. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  3424. return;
  3425. }
  3426. // Validate required fields
  3427. if (!body.contains("name") || !body["name"].is_string()) {
  3428. res.status = 400;
  3429. res.set_content(R"({"error":"name is required"})", "application/json");
  3430. return;
  3431. }
  3432. if (!body.contains("collection_name") || !body["collection_name"].is_string()) {
  3433. res.status = 400;
  3434. res.set_content(R"({"error":"collection_name is required"})", "application/json");
  3435. return;
  3436. }
  3437. try {
  3438. CreateViewRequest request;
  3439. request.workspace_id = workspace_id;
  3440. request.name = body["name"].get<std::string>();
  3441. request.collection_name = body["collection_name"].get<std::string>();
  3442. // Parse schema if provided
  3443. if (body.contains("schema") && body["schema"].is_object()) {
  3444. const auto& schema_json = body["schema"];
  3445. if (schema_json.contains("title")) {
  3446. request.schema.title = schema_json.value("title", "");
  3447. }
  3448. if (schema_json.contains("description")) {
  3449. request.schema.description = schema_json.value("description", "");
  3450. }
  3451. if (schema_json.contains("layout")) {
  3452. request.schema.layout = schema_json["layout"];
  3453. }
  3454. if (schema_json.contains("fields") && schema_json["fields"].is_array()) {
  3455. for (const auto& field_json : schema_json["fields"]) {
  3456. SchemaField field;
  3457. field.name = field_json.value("name", "");
  3458. field.type = StringToFieldType(field_json.value("type", "text"));
  3459. field.label = field_json.value("label", "");
  3460. field.description = field_json.value("description", "");
  3461. field.required = field_json.value("required", false);
  3462. field.display_order = field_json.value("display_order", 0);
  3463. field.widget = field_json.value("widget", "");
  3464. field.group = field_json.value("group", "");
  3465. field.default_value = field_json.value("default_value", nlohmann::json());
  3466. field.options = field_json.value("options", nlohmann::json());
  3467. field.reference_collection = field_json.value("reference_collection", "");
  3468. field.computed_expression = field_json.value("computed_expression", "");
  3469. request.schema.fields.push_back(field);
  3470. }
  3471. }
  3472. }
  3473. // Parse settings if provided
  3474. if (body.contains("settings") && body["settings"].is_object()) {
  3475. const auto& settings_json = body["settings"];
  3476. request.settings.is_default = settings_json.value("is_default", false);
  3477. request.settings.show_in_sidebar = settings_json.value("show_in_sidebar", true);
  3478. request.settings.show_create_button = settings_json.value("show_create_button", true);
  3479. request.settings.icon = settings_json.value("icon", "");
  3480. request.settings.filters = settings_json.value("filters", nlohmann::json::object());
  3481. request.settings.sort = settings_json.value("sort", nlohmann::json::object());
  3482. request.settings.quick_create_mode = settings_json.value("quick_create_mode", "modal");
  3483. request.settings.quick_edit_mode = settings_json.value("quick_edit_mode", "modal");
  3484. request.settings.show_edit_button = settings_json.value("show_edit_button", true);
  3485. if (settings_json.contains("quick_create_fields") && settings_json["quick_create_fields"].is_array()) {
  3486. for (const auto& field : settings_json["quick_create_fields"]) {
  3487. if (field.is_string()) {
  3488. request.settings.quick_create_fields.push_back(field.get<std::string>());
  3489. }
  3490. }
  3491. }
  3492. if (settings_json.contains("quick_edit_fields") && settings_json["quick_edit_fields"].is_array()) {
  3493. for (const auto& field : settings_json["quick_edit_fields"]) {
  3494. if (field.is_string()) {
  3495. request.settings.quick_edit_fields.push_back(field.get<std::string>());
  3496. }
  3497. }
  3498. }
  3499. }
  3500. auto result = viewService_->CreateView(request);
  3501. if (!result.success) {
  3502. res.status = 400;
  3503. nlohmann::json error_response = {{"error", result.error}};
  3504. res.set_content(error_response.dump(), "application/json");
  3505. return;
  3506. }
  3507. // Build response with view info
  3508. nlohmann::json view_json = {
  3509. {"id", result.view->id},
  3510. {"workspace_id", result.view->workspace_id},
  3511. {"name", result.view->name},
  3512. {"collection_name", result.view->collection_name},
  3513. {"created_at", result.view->created_at},
  3514. {"updated_at", result.view->updated_at}
  3515. };
  3516. // Add schema
  3517. nlohmann::json schema_json = {
  3518. {"title", result.view->schema.title},
  3519. {"description", result.view->schema.description},
  3520. {"layout", result.view->schema.layout}
  3521. };
  3522. nlohmann::json fields_json = nlohmann::json::array();
  3523. for (const auto& field : result.view->schema.fields) {
  3524. nlohmann::json field_json = {
  3525. {"name", field.name},
  3526. {"type", FieldTypeToString(field.type)},
  3527. {"label", field.label},
  3528. {"description", field.description},
  3529. {"required", field.required},
  3530. {"display_order", field.display_order},
  3531. {"widget", field.widget},
  3532. {"group", field.group}
  3533. };
  3534. if (!field.default_value.is_null()) {
  3535. field_json["default_value"] = field.default_value;
  3536. }
  3537. if (!field.options.is_null()) {
  3538. field_json["options"] = field.options;
  3539. }
  3540. if (!field.reference_collection.empty()) {
  3541. field_json["reference_collection"] = field.reference_collection;
  3542. }
  3543. if (!field.computed_expression.empty()) {
  3544. field_json["computed_expression"] = field.computed_expression;
  3545. }
  3546. fields_json.push_back(field_json);
  3547. }
  3548. schema_json["fields"] = fields_json;
  3549. view_json["schema"] = schema_json;
  3550. // Add settings
  3551. nlohmann::json create_settings_json = {
  3552. {"is_default", result.view->settings.is_default},
  3553. {"show_in_sidebar", result.view->settings.show_in_sidebar},
  3554. {"show_create_button", result.view->settings.show_create_button},
  3555. {"show_edit_button", result.view->settings.show_edit_button},
  3556. {"icon", result.view->settings.icon},
  3557. {"filters", result.view->settings.filters},
  3558. {"sort", result.view->settings.sort},
  3559. {"quick_create_mode", result.view->settings.quick_create_mode},
  3560. {"quick_edit_mode", result.view->settings.quick_edit_mode}
  3561. };
  3562. if (!result.view->settings.quick_create_fields.empty()) {
  3563. create_settings_json["quick_create_fields"] = result.view->settings.quick_create_fields;
  3564. }
  3565. if (!result.view->settings.quick_edit_fields.empty()) {
  3566. create_settings_json["quick_edit_fields"] = result.view->settings.quick_edit_fields;
  3567. }
  3568. view_json["settings"] = create_settings_json;
  3569. res.status = 201;
  3570. res.set_content(view_json.dump(), "application/json");
  3571. } catch (const std::exception& e) {
  3572. spdlog::error("Create view error: {}", e.what());
  3573. res.status = 500;
  3574. res.set_content(R"({"error":"Internal server error"})", "application/json");
  3575. }
  3576. }
  3577. void HttpServer::HandleListViews(const httplib::Request& req, httplib::Response& res) {
  3578. // Authenticate the request
  3579. auto auth_user = AuthenticateRequest(req);
  3580. if (!auth_user) {
  3581. res.status = 401;
  3582. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  3583. return;
  3584. }
  3585. // Extract workspace ID from path
  3586. std::string workspace_id = req.matches[1].str();
  3587. // Verify workspace exists
  3588. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  3589. if (!ws_result.success) {
  3590. res.status = 404;
  3591. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  3592. return;
  3593. }
  3594. // Check access: must be member OR have system:views:read permission
  3595. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  3596. auth_user->workspace_ids.end(),
  3597. workspace_id) != auth_user->workspace_ids.end());
  3598. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kViewsRead);
  3599. if (!is_member && !has_system_access) {
  3600. res.status = 403;
  3601. res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
  3602. return;
  3603. }
  3604. // Check if ViewService is available
  3605. if (!viewService_) {
  3606. res.status = 503;
  3607. res.set_content(R"({"error":"View service not available"})", "application/json");
  3608. return;
  3609. }
  3610. try {
  3611. ViewListResult result;
  3612. // Check if filtering by collection
  3613. if (req.has_param("collection")) {
  3614. std::string collection_name = req.get_param_value("collection");
  3615. result = viewService_->ListViewsForCollection(workspace_id, collection_name);
  3616. } else {
  3617. result = viewService_->ListViews(workspace_id);
  3618. }
  3619. if (!result.success) {
  3620. res.status = 500;
  3621. nlohmann::json error_response = {{"error", result.error}};
  3622. res.set_content(error_response.dump(), "application/json");
  3623. return;
  3624. }
  3625. nlohmann::json views_json = nlohmann::json::array();
  3626. for (const auto& view : result.views) {
  3627. nlohmann::json view_json = {
  3628. {"id", view.id},
  3629. {"workspace_id", view.workspace_id},
  3630. {"name", view.name},
  3631. {"collection_name", view.collection_name},
  3632. {"created_at", view.created_at},
  3633. {"updated_at", view.updated_at}
  3634. };
  3635. // Add schema summary (fields count and title)
  3636. view_json["schema"] = {
  3637. {"title", view.schema.title},
  3638. {"field_count", view.schema.fields.size()}
  3639. };
  3640. // Add settings
  3641. nlohmann::json settings_json = {
  3642. {"is_default", view.settings.is_default},
  3643. {"show_in_sidebar", view.settings.show_in_sidebar},
  3644. {"show_create_button", view.settings.show_create_button},
  3645. {"show_edit_button", view.settings.show_edit_button},
  3646. {"icon", view.settings.icon},
  3647. {"quick_create_mode", view.settings.quick_create_mode},
  3648. {"quick_edit_mode", view.settings.quick_edit_mode}
  3649. };
  3650. if (!view.settings.quick_create_fields.empty()) {
  3651. settings_json["quick_create_fields"] = view.settings.quick_create_fields;
  3652. }
  3653. if (!view.settings.quick_edit_fields.empty()) {
  3654. settings_json["quick_edit_fields"] = view.settings.quick_edit_fields;
  3655. }
  3656. view_json["settings"] = settings_json;
  3657. views_json.push_back(view_json);
  3658. }
  3659. nlohmann::json response = {{"views", views_json}};
  3660. res.set_content(response.dump(), "application/json");
  3661. } catch (const std::exception& e) {
  3662. spdlog::error("List views error: {}", e.what());
  3663. res.status = 500;
  3664. res.set_content(R"({"error":"Internal server error"})", "application/json");
  3665. }
  3666. }
  3667. void HttpServer::HandleGetView(const httplib::Request& req, httplib::Response& res) {
  3668. // Authenticate the request
  3669. auto auth_user = AuthenticateRequest(req);
  3670. if (!auth_user) {
  3671. res.status = 401;
  3672. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  3673. return;
  3674. }
  3675. // Extract workspace ID and view ID from path
  3676. std::string workspace_id = req.matches[1].str();
  3677. std::string view_id = req.matches[2].str();
  3678. // Verify workspace exists
  3679. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  3680. if (!ws_result.success) {
  3681. res.status = 404;
  3682. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  3683. return;
  3684. }
  3685. // Check access: must be member OR have system:views:read permission
  3686. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  3687. auth_user->workspace_ids.end(),
  3688. workspace_id) != auth_user->workspace_ids.end());
  3689. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kViewsRead);
  3690. if (!is_member && !has_system_access) {
  3691. res.status = 403;
  3692. res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
  3693. return;
  3694. }
  3695. // Check if ViewService is available
  3696. if (!viewService_) {
  3697. res.status = 503;
  3698. res.set_content(R"({"error":"View service not available"})", "application/json");
  3699. return;
  3700. }
  3701. try {
  3702. auto result = viewService_->GetView(workspace_id, view_id);
  3703. if (!result.success || !result.view) {
  3704. res.status = 404;
  3705. nlohmann::json error_response = {{"error", result.error}};
  3706. res.set_content(error_response.dump(), "application/json");
  3707. return;
  3708. }
  3709. // Build full response with view info
  3710. nlohmann::json view_json = {
  3711. {"id", result.view->id},
  3712. {"workspace_id", result.view->workspace_id},
  3713. {"name", result.view->name},
  3714. {"collection_name", result.view->collection_name},
  3715. {"created_at", result.view->created_at},
  3716. {"updated_at", result.view->updated_at}
  3717. };
  3718. // Add schema
  3719. nlohmann::json schema_json = {
  3720. {"title", result.view->schema.title},
  3721. {"description", result.view->schema.description},
  3722. {"layout", result.view->schema.layout}
  3723. };
  3724. nlohmann::json fields_json = nlohmann::json::array();
  3725. for (const auto& field : result.view->schema.fields) {
  3726. nlohmann::json field_json = {
  3727. {"name", field.name},
  3728. {"type", FieldTypeToString(field.type)},
  3729. {"label", field.label},
  3730. {"description", field.description},
  3731. {"required", field.required},
  3732. {"display_order", field.display_order},
  3733. {"widget", field.widget},
  3734. {"group", field.group}
  3735. };
  3736. if (!field.default_value.is_null()) {
  3737. field_json["default_value"] = field.default_value;
  3738. }
  3739. if (!field.options.is_null()) {
  3740. field_json["options"] = field.options;
  3741. }
  3742. if (!field.reference_collection.empty()) {
  3743. field_json["reference_collection"] = field.reference_collection;
  3744. }
  3745. if (!field.computed_expression.empty()) {
  3746. field_json["computed_expression"] = field.computed_expression;
  3747. }
  3748. fields_json.push_back(field_json);
  3749. }
  3750. schema_json["fields"] = fields_json;
  3751. view_json["schema"] = schema_json;
  3752. // Add settings
  3753. nlohmann::json settings_json = {
  3754. {"is_default", result.view->settings.is_default},
  3755. {"show_in_sidebar", result.view->settings.show_in_sidebar},
  3756. {"show_create_button", result.view->settings.show_create_button},
  3757. {"show_edit_button", result.view->settings.show_edit_button},
  3758. {"icon", result.view->settings.icon},
  3759. {"filters", result.view->settings.filters},
  3760. {"sort", result.view->settings.sort},
  3761. {"quick_create_mode", result.view->settings.quick_create_mode},
  3762. {"quick_edit_mode", result.view->settings.quick_edit_mode}
  3763. };
  3764. if (!result.view->settings.quick_create_fields.empty()) {
  3765. settings_json["quick_create_fields"] = result.view->settings.quick_create_fields;
  3766. }
  3767. if (!result.view->settings.quick_edit_fields.empty()) {
  3768. settings_json["quick_edit_fields"] = result.view->settings.quick_edit_fields;
  3769. }
  3770. view_json["settings"] = settings_json;
  3771. res.set_content(view_json.dump(), "application/json");
  3772. } catch (const std::exception& e) {
  3773. spdlog::error("Get view error: {}", e.what());
  3774. res.status = 500;
  3775. res.set_content(R"({"error":"Internal server error"})", "application/json");
  3776. }
  3777. }
  3778. void HttpServer::HandleUpdateView(const httplib::Request& req, httplib::Response& res) {
  3779. // Authenticate the request
  3780. auto auth_user = AuthenticateRequest(req);
  3781. if (!auth_user) {
  3782. res.status = 401;
  3783. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  3784. return;
  3785. }
  3786. // Extract workspace ID and view ID from path
  3787. std::string workspace_id = req.matches[1].str();
  3788. std::string view_id = req.matches[2].str();
  3789. // Verify workspace exists
  3790. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  3791. if (!ws_result.success) {
  3792. res.status = 404;
  3793. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  3794. return;
  3795. }
  3796. // Requires system:views:update permission OR workspace membership
  3797. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  3798. auth_user->workspace_ids.end(),
  3799. workspace_id) != auth_user->workspace_ids.end());
  3800. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kViewsUpdate);
  3801. if (!is_member && !has_system_access) {
  3802. res.status = 403;
  3803. res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
  3804. return;
  3805. }
  3806. // Check if ViewService is available
  3807. if (!viewService_) {
  3808. res.status = 503;
  3809. res.set_content(R"({"error":"View service not available"})", "application/json");
  3810. return;
  3811. }
  3812. // Parse request body
  3813. nlohmann::json body;
  3814. try {
  3815. body = nlohmann::json::parse(req.body);
  3816. } catch (const nlohmann::json::parse_error& /*e*/) {
  3817. res.status = 400;
  3818. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  3819. return;
  3820. }
  3821. try {
  3822. UpdateViewRequest request;
  3823. request.workspace_id = workspace_id;
  3824. request.id = view_id;
  3825. if (body.contains("name") && body["name"].is_string()) {
  3826. request.name = body["name"].get<std::string>();
  3827. }
  3828. if (body.contains("collection_name") && body["collection_name"].is_string()) {
  3829. request.collection_name = body["collection_name"].get<std::string>();
  3830. }
  3831. // Parse schema if provided
  3832. if (body.contains("schema") && body["schema"].is_object()) {
  3833. ViewSchema schema;
  3834. const auto& schema_json = body["schema"];
  3835. schema.title = schema_json.value("title", "");
  3836. schema.description = schema_json.value("description", "");
  3837. schema.layout = schema_json.value("layout", nlohmann::json::object());
  3838. if (schema_json.contains("fields") && schema_json["fields"].is_array()) {
  3839. for (const auto& field_json : schema_json["fields"]) {
  3840. SchemaField field;
  3841. field.name = field_json.value("name", "");
  3842. field.type = StringToFieldType(field_json.value("type", "text"));
  3843. field.label = field_json.value("label", "");
  3844. field.description = field_json.value("description", "");
  3845. field.required = field_json.value("required", false);
  3846. field.display_order = field_json.value("display_order", 0);
  3847. field.widget = field_json.value("widget", "");
  3848. field.group = field_json.value("group", "");
  3849. field.default_value = field_json.value("default_value", nlohmann::json());
  3850. field.options = field_json.value("options", nlohmann::json());
  3851. field.reference_collection = field_json.value("reference_collection", "");
  3852. field.computed_expression = field_json.value("computed_expression", "");
  3853. schema.fields.push_back(field);
  3854. }
  3855. }
  3856. request.schema = schema;
  3857. }
  3858. // Parse settings if provided
  3859. if (body.contains("settings") && body["settings"].is_object()) {
  3860. ViewSettings settings;
  3861. const auto& settings_json = body["settings"];
  3862. settings.is_default = settings_json.value("is_default", false);
  3863. settings.show_in_sidebar = settings_json.value("show_in_sidebar", true);
  3864. settings.show_create_button = settings_json.value("show_create_button", true);
  3865. settings.icon = settings_json.value("icon", "");
  3866. settings.filters = settings_json.value("filters", nlohmann::json::object());
  3867. settings.sort = settings_json.value("sort", nlohmann::json::object());
  3868. settings.quick_create_mode = settings_json.value("quick_create_mode", "modal");
  3869. settings.quick_edit_mode = settings_json.value("quick_edit_mode", "modal");
  3870. settings.show_edit_button = settings_json.value("show_edit_button", true);
  3871. if (settings_json.contains("quick_create_fields") && settings_json["quick_create_fields"].is_array()) {
  3872. for (const auto& field : settings_json["quick_create_fields"]) {
  3873. if (field.is_string()) {
  3874. settings.quick_create_fields.push_back(field.get<std::string>());
  3875. }
  3876. }
  3877. }
  3878. if (settings_json.contains("quick_edit_fields") && settings_json["quick_edit_fields"].is_array()) {
  3879. for (const auto& field : settings_json["quick_edit_fields"]) {
  3880. if (field.is_string()) {
  3881. settings.quick_edit_fields.push_back(field.get<std::string>());
  3882. }
  3883. }
  3884. }
  3885. request.settings = settings;
  3886. }
  3887. auto result = viewService_->UpdateView(request);
  3888. if (!result.success || !result.view) {
  3889. res.status = 400;
  3890. nlohmann::json error_response = {{"error", result.error}};
  3891. res.set_content(error_response.dump(), "application/json");
  3892. return;
  3893. }
  3894. // Build response with updated view info
  3895. nlohmann::json view_json = {
  3896. {"id", result.view->id},
  3897. {"workspace_id", result.view->workspace_id},
  3898. {"name", result.view->name},
  3899. {"collection_name", result.view->collection_name},
  3900. {"created_at", result.view->created_at},
  3901. {"updated_at", result.view->updated_at}
  3902. };
  3903. // Add schema
  3904. nlohmann::json schema_json = {
  3905. {"title", result.view->schema.title},
  3906. {"description", result.view->schema.description},
  3907. {"layout", result.view->schema.layout}
  3908. };
  3909. nlohmann::json fields_json = nlohmann::json::array();
  3910. for (const auto& field : result.view->schema.fields) {
  3911. nlohmann::json field_json = {
  3912. {"name", field.name},
  3913. {"type", FieldTypeToString(field.type)},
  3914. {"label", field.label},
  3915. {"description", field.description},
  3916. {"required", field.required},
  3917. {"display_order", field.display_order},
  3918. {"widget", field.widget},
  3919. {"group", field.group}
  3920. };
  3921. if (!field.default_value.is_null()) {
  3922. field_json["default_value"] = field.default_value;
  3923. }
  3924. if (!field.options.is_null()) {
  3925. field_json["options"] = field.options;
  3926. }
  3927. if (!field.reference_collection.empty()) {
  3928. field_json["reference_collection"] = field.reference_collection;
  3929. }
  3930. if (!field.computed_expression.empty()) {
  3931. field_json["computed_expression"] = field.computed_expression;
  3932. }
  3933. fields_json.push_back(field_json);
  3934. }
  3935. schema_json["fields"] = fields_json;
  3936. view_json["schema"] = schema_json;
  3937. // Add settings
  3938. nlohmann::json settings_json = {
  3939. {"is_default", result.view->settings.is_default},
  3940. {"show_in_sidebar", result.view->settings.show_in_sidebar},
  3941. {"show_create_button", result.view->settings.show_create_button},
  3942. {"show_edit_button", result.view->settings.show_edit_button},
  3943. {"icon", result.view->settings.icon},
  3944. {"filters", result.view->settings.filters},
  3945. {"sort", result.view->settings.sort},
  3946. {"quick_create_mode", result.view->settings.quick_create_mode},
  3947. {"quick_edit_mode", result.view->settings.quick_edit_mode}
  3948. };
  3949. if (!result.view->settings.quick_create_fields.empty()) {
  3950. settings_json["quick_create_fields"] = result.view->settings.quick_create_fields;
  3951. }
  3952. if (!result.view->settings.quick_edit_fields.empty()) {
  3953. settings_json["quick_edit_fields"] = result.view->settings.quick_edit_fields;
  3954. }
  3955. view_json["settings"] = settings_json;
  3956. res.set_content(view_json.dump(), "application/json");
  3957. } catch (const std::exception& e) {
  3958. spdlog::error("Update view error: {}", e.what());
  3959. res.status = 500;
  3960. res.set_content(R"({"error":"Internal server error"})", "application/json");
  3961. }
  3962. }
  3963. void HttpServer::HandleDeleteView(const httplib::Request& req, httplib::Response& res) {
  3964. // Authenticate the request
  3965. auto auth_user = AuthenticateRequest(req);
  3966. if (!auth_user) {
  3967. res.status = 401;
  3968. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  3969. return;
  3970. }
  3971. // Extract workspace ID and view ID from path
  3972. std::string workspace_id = req.matches[1].str();
  3973. std::string view_id = req.matches[2].str();
  3974. // Verify workspace exists
  3975. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  3976. if (!ws_result.success) {
  3977. res.status = 404;
  3978. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  3979. return;
  3980. }
  3981. // Requires system:views:delete permission OR workspace membership
  3982. bool is_member = (std::find(auth_user->workspace_ids.begin(),
  3983. auth_user->workspace_ids.end(),
  3984. workspace_id) != auth_user->workspace_ids.end());
  3985. bool has_system_access = authorizationService_->HasPermission(*auth_user, permissions::kViewsDelete);
  3986. if (!is_member && !has_system_access) {
  3987. res.status = 403;
  3988. res.set_content(R"({"error":"Forbidden - no access to this workspace"})", "application/json");
  3989. return;
  3990. }
  3991. // Check if ViewService is available
  3992. if (!viewService_) {
  3993. res.status = 503;
  3994. res.set_content(R"({"error":"View service not available"})", "application/json");
  3995. return;
  3996. }
  3997. try {
  3998. auto result = viewService_->DeleteView(workspace_id, view_id);
  3999. if (!result.success) {
  4000. res.status = 404;
  4001. nlohmann::json error_response = {{"error", result.error}};
  4002. res.set_content(error_response.dump(), "application/json");
  4003. return;
  4004. }
  4005. res.set_content(R"({"message":"View deleted successfully"})", "application/json");
  4006. } catch (const std::exception& e) {
  4007. spdlog::error("Delete view error: {}", e.what());
  4008. res.status = 500;
  4009. res.set_content(R"({"error":"Internal server error"})", "application/json");
  4010. }
  4011. }
  4012. // ============================================================================
  4013. // Page Routes
  4014. // ============================================================================
  4015. void HttpServer::SetupPageRoutes() {
  4016. // POST /api/workspaces/:workspace_id/pages - Create a new page
  4017. httpServer_->Post(R"(/api/workspaces/([^/]+)/pages$)",
  4018. [this](const httplib::Request& req, httplib::Response& res) {
  4019. HandleCreatePage(req, res);
  4020. });
  4021. // GET /api/workspaces/:workspace_id/pages - List all pages
  4022. httpServer_->Get(R"(/api/workspaces/([^/]+)/pages$)",
  4023. [this](const httplib::Request& req, httplib::Response& res) {
  4024. HandleListPages(req, res);
  4025. });
  4026. // GET /api/workspaces/:workspace_id/pages/sidebar - List sidebar pages
  4027. httpServer_->Get(R"(/api/workspaces/([^/]+)/pages/sidebar$)",
  4028. [this](const httplib::Request& req, httplib::Response& res) {
  4029. HandleListSidebarPages(req, res);
  4030. });
  4031. // GET /api/workspaces/:workspace_id/pages/slug/:slug - Get page by slug
  4032. httpServer_->Get(R"(/api/workspaces/([^/]+)/pages/slug/([^/]+)$)",
  4033. [this](const httplib::Request& req, httplib::Response& res) {
  4034. HandleGetPageBySlug(req, res);
  4035. });
  4036. // GET /api/workspaces/:workspace_id/pages/:page_id - Get a specific page
  4037. httpServer_->Get(R"(/api/workspaces/([^/]+)/pages/([^/]+)$)",
  4038. [this](const httplib::Request& req, httplib::Response& res) {
  4039. HandleGetPage(req, res);
  4040. });
  4041. // PATCH /api/workspaces/:workspace_id/pages/:page_id - Update a page
  4042. httpServer_->Patch(R"(/api/workspaces/([^/]+)/pages/([^/]+)$)",
  4043. [this](const httplib::Request& req, httplib::Response& res) {
  4044. HandleUpdatePage(req, res);
  4045. });
  4046. // PATCH /api/workspaces/:workspace_id/pages/:page_id/share - Update page sharing
  4047. httpServer_->Patch(R"(/api/workspaces/([^/]+)/pages/([^/]+)/share$)",
  4048. [this](const httplib::Request& req, httplib::Response& res) {
  4049. HandleUpdatePageSharing(req, res);
  4050. });
  4051. // DELETE /api/workspaces/:workspace_id/pages/:page_id - Delete a page
  4052. httpServer_->Delete(R"(/api/workspaces/([^/]+)/pages/([^/]+)$)",
  4053. [this](const httplib::Request& req, httplib::Response& res) {
  4054. HandleDeletePage(req, res);
  4055. });
  4056. spdlog::info("Page routes configured");
  4057. }
  4058. void HttpServer::HandleCreatePage(const httplib::Request& req, httplib::Response& res) {
  4059. // Authenticate the request
  4060. auto auth_user = AuthenticateRequest(req);
  4061. if (!auth_user) {
  4062. res.status = 401;
  4063. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  4064. return;
  4065. }
  4066. // Extract workspace ID from path
  4067. std::string workspace_id = req.matches[1].str();
  4068. // Verify workspace exists
  4069. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  4070. if (!ws_result.success) {
  4071. res.status = 404;
  4072. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  4073. return;
  4074. }
  4075. // Check permission to create pages
  4076. if (!authorizationService_->CanCreatePage(*auth_user, workspace_id)) {
  4077. res.status = 403;
  4078. res.set_content(R"({"error":"Forbidden - insufficient permissions to create pages"})", "application/json");
  4079. return;
  4080. }
  4081. // Check if PageService is available
  4082. if (!pageService_) {
  4083. res.status = 503;
  4084. res.set_content(R"({"error":"Page service not available"})", "application/json");
  4085. return;
  4086. }
  4087. // Parse request body
  4088. nlohmann::json body;
  4089. try {
  4090. body = nlohmann::json::parse(req.body);
  4091. } catch (const nlohmann::json::parse_error& /*e*/) {
  4092. res.status = 400;
  4093. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  4094. return;
  4095. }
  4096. // Validate required fields
  4097. if (!body.contains("name") || !body["name"].is_string()) {
  4098. res.status = 400;
  4099. res.set_content(R"({"error":"name is required"})", "application/json");
  4100. return;
  4101. }
  4102. try {
  4103. CreatePageRequest request;
  4104. request.workspace_id = workspace_id;
  4105. request.name = body["name"].get<std::string>();
  4106. request.slug = body.value("slug", "");
  4107. request.created_by = auth_user->user_id; // Set owner to current user
  4108. // Parse layout if provided
  4109. if (body.contains("layout") && body["layout"].is_object()) {
  4110. const auto& layout_json = body["layout"];
  4111. request.layout.version = layout_json.value("version", 1);
  4112. request.layout.grid_columns = layout_json.value("grid_columns", 12);
  4113. if (layout_json.contains("components") && layout_json["components"].is_array()) {
  4114. for (const auto& comp_json : layout_json["components"]) {
  4115. LayoutComponent comp;
  4116. comp.id = comp_json.value("id", "");
  4117. comp.type = comp_json.value("type", "");
  4118. comp.config = comp_json.value("config", nlohmann::json::object());
  4119. if (comp_json.contains("position") && comp_json["position"].is_object()) {
  4120. const auto& pos = comp_json["position"];
  4121. comp.position.x = pos.value("x", 0);
  4122. comp.position.y = pos.value("y", 0);
  4123. comp.position.width = pos.value("width", 12);
  4124. comp.position.height = pos.value("height", 1);
  4125. }
  4126. request.layout.components.push_back(comp);
  4127. }
  4128. }
  4129. }
  4130. // Parse settings if provided
  4131. if (body.contains("settings") && body["settings"].is_object()) {
  4132. const auto& settings_json = body["settings"];
  4133. request.settings.show_in_sidebar = settings_json.value("show_in_sidebar", true);
  4134. request.settings.icon = settings_json.value("icon", "");
  4135. request.settings.menu_order = settings_json.value("menu_order", 0);
  4136. }
  4137. auto result = pageService_->CreatePage(request);
  4138. if (!result.success) {
  4139. res.status = 400;
  4140. nlohmann::json error_response = {{"error", result.error}};
  4141. res.set_content(error_response.dump(), "application/json");
  4142. return;
  4143. }
  4144. // Build response with page info
  4145. nlohmann::json page_json = {
  4146. {"id", result.page->id},
  4147. {"workspace_id", result.page->workspace_id},
  4148. {"name", result.page->name},
  4149. {"slug", result.page->slug},
  4150. {"created_at", result.page->created_at},
  4151. {"updated_at", result.page->updated_at},
  4152. {"created_by", result.page->created_by},
  4153. {"shared_with_groups", result.page->shared_with_groups}
  4154. };
  4155. // Add layout
  4156. nlohmann::json layout_json = {
  4157. {"version", result.page->layout.version},
  4158. {"grid_columns", result.page->layout.grid_columns}
  4159. };
  4160. nlohmann::json components_json = nlohmann::json::array();
  4161. for (const auto& comp : result.page->layout.components) {
  4162. nlohmann::json comp_json = {
  4163. {"id", comp.id},
  4164. {"type", comp.type},
  4165. {"position", {
  4166. {"x", comp.position.x},
  4167. {"y", comp.position.y},
  4168. {"width", comp.position.width},
  4169. {"height", comp.position.height}
  4170. }},
  4171. {"config", comp.config}
  4172. };
  4173. components_json.push_back(comp_json);
  4174. }
  4175. layout_json["components"] = components_json;
  4176. page_json["layout"] = layout_json;
  4177. // Add settings
  4178. page_json["settings"] = {
  4179. {"show_in_sidebar", result.page->settings.show_in_sidebar},
  4180. {"icon", result.page->settings.icon},
  4181. {"menu_order", result.page->settings.menu_order}
  4182. };
  4183. res.status = 201;
  4184. res.set_content(page_json.dump(), "application/json");
  4185. } catch (const std::exception& e) {
  4186. spdlog::error("Create page error: {}", e.what());
  4187. res.status = 500;
  4188. res.set_content(R"({"error":"Internal server error"})", "application/json");
  4189. }
  4190. }
  4191. void HttpServer::HandleListPages(const httplib::Request& req, httplib::Response& res) {
  4192. // Authenticate the request
  4193. auto auth_user = AuthenticateRequest(req);
  4194. if (!auth_user) {
  4195. res.status = 401;
  4196. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  4197. return;
  4198. }
  4199. // Extract workspace ID from path
  4200. std::string workspace_id = req.matches[1].str();
  4201. // Verify workspace exists
  4202. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  4203. if (!ws_result.success) {
  4204. res.status = 404;
  4205. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  4206. return;
  4207. }
  4208. // Check if PageService is available
  4209. if (!pageService_) {
  4210. res.status = 503;
  4211. res.set_content(R"({"error":"Page service not available"})", "application/json");
  4212. return;
  4213. }
  4214. try {
  4215. auto result = pageService_->ListPages(workspace_id);
  4216. if (!result.success) {
  4217. res.status = 500;
  4218. nlohmann::json error_response = {{"error", result.error}};
  4219. res.set_content(error_response.dump(), "application/json");
  4220. return;
  4221. }
  4222. // Filter pages based on user permissions
  4223. nlohmann::json pages_json = nlohmann::json::array();
  4224. for (const auto& page : result.pages) {
  4225. // Check if user can view this page
  4226. if (!authorizationService_->CanViewPage(*auth_user, workspace_id,
  4227. page.created_by, page.shared_with_groups)) {
  4228. continue; // Skip pages user cannot view
  4229. }
  4230. nlohmann::json page_json = {
  4231. {"id", page.id},
  4232. {"workspace_id", page.workspace_id},
  4233. {"name", page.name},
  4234. {"slug", page.slug},
  4235. {"created_at", page.created_at},
  4236. {"updated_at", page.updated_at},
  4237. {"created_by", page.created_by},
  4238. {"shared_with_groups", page.shared_with_groups},
  4239. {"settings", {
  4240. {"show_in_sidebar", page.settings.show_in_sidebar},
  4241. {"icon", page.settings.icon},
  4242. {"menu_order", page.settings.menu_order}
  4243. }}
  4244. };
  4245. pages_json.push_back(page_json);
  4246. }
  4247. nlohmann::json response = {{"pages", pages_json}};
  4248. res.set_content(response.dump(), "application/json");
  4249. } catch (const std::exception& e) {
  4250. spdlog::error("List pages error: {}", e.what());
  4251. res.status = 500;
  4252. res.set_content(R"({"error":"Internal server error"})", "application/json");
  4253. }
  4254. }
  4255. void HttpServer::HandleListSidebarPages(const httplib::Request& req, httplib::Response& res) {
  4256. // Authenticate the request
  4257. auto auth_user = AuthenticateRequest(req);
  4258. if (!auth_user) {
  4259. res.status = 401;
  4260. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  4261. return;
  4262. }
  4263. // Extract workspace ID from path
  4264. std::string workspace_id = req.matches[1].str();
  4265. // Verify workspace exists
  4266. auto ws_result = workspaceService_->GetWorkspace(workspace_id);
  4267. if (!ws_result.success) {
  4268. res.status = 404;
  4269. res.set_content(R"({"error":"Workspace not found"})", "application/json");
  4270. return;
  4271. }
  4272. // Check if PageService is available
  4273. if (!pageService_) {
  4274. res.status = 503;
  4275. res.set_content(R"({"error":"Page service not available"})", "application/json");
  4276. return;
  4277. }
  4278. try {
  4279. auto result = pageService_->ListSidebarPages(workspace_id);
  4280. if (!result.success) {
  4281. res.status = 500;
  4282. nlohmann::json error_response = {{"error", result.error}};
  4283. res.set_content(error_response.dump(), "application/json");
  4284. return;
  4285. }
  4286. // Filter pages based on user permissions
  4287. nlohmann::json pages_json = nlohmann::json::array();
  4288. for (const auto& page : result.pages) {
  4289. // Check if user can view this page
  4290. if (!authorizationService_->CanViewPage(*auth_user, workspace_id,
  4291. page.created_by, page.shared_with_groups)) {
  4292. continue; // Skip pages user cannot view
  4293. }
  4294. nlohmann::json page_json = {
  4295. {"id", page.id},
  4296. {"workspace_id", page.workspace_id},
  4297. {"name", page.name},
  4298. {"slug", page.slug},
  4299. {"created_by", page.created_by},
  4300. {"settings", {
  4301. {"show_in_sidebar", page.settings.show_in_sidebar},
  4302. {"icon", page.settings.icon},
  4303. {"menu_order", page.settings.menu_order}
  4304. }}
  4305. };
  4306. pages_json.push_back(page_json);
  4307. }
  4308. nlohmann::json response = {{"pages", pages_json}};
  4309. res.set_content(response.dump(), "application/json");
  4310. } catch (const std::exception& e) {
  4311. spdlog::error("List sidebar pages error: {}", e.what());
  4312. res.status = 500;
  4313. res.set_content(R"({"error":"Internal server error"})", "application/json");
  4314. }
  4315. }
  4316. void HttpServer::HandleGetPageBySlug(const httplib::Request& req, httplib::Response& res) {
  4317. // Authenticate the request
  4318. auto auth_user = AuthenticateRequest(req);
  4319. if (!auth_user) {
  4320. res.status = 401;
  4321. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  4322. return;
  4323. }
  4324. // Extract workspace ID and slug from path
  4325. std::string workspace_id = req.matches[1].str();
  4326. std::string slug = req.matches[2].str();
  4327. // Check if PageService is available
  4328. if (!pageService_) {
  4329. res.status = 503;
  4330. res.set_content(R"({"error":"Page service not available"})", "application/json");
  4331. return;
  4332. }
  4333. try {
  4334. auto result = pageService_->GetPageBySlug(workspace_id, slug);
  4335. if (!result.success) {
  4336. res.status = 404;
  4337. nlohmann::json error_response = {{"error", result.error}};
  4338. res.set_content(error_response.dump(), "application/json");
  4339. return;
  4340. }
  4341. // Check if user can view this page
  4342. if (!authorizationService_->CanViewPage(*auth_user, workspace_id,
  4343. result.page->created_by, result.page->shared_with_groups)) {
  4344. res.status = 403;
  4345. res.set_content(R"({"error":"Forbidden - no access to this page"})", "application/json");
  4346. return;
  4347. }
  4348. // Build full response with layout
  4349. nlohmann::json page_json = {
  4350. {"id", result.page->id},
  4351. {"workspace_id", result.page->workspace_id},
  4352. {"name", result.page->name},
  4353. {"slug", result.page->slug},
  4354. {"created_at", result.page->created_at},
  4355. {"updated_at", result.page->updated_at},
  4356. {"created_by", result.page->created_by},
  4357. {"shared_with_groups", result.page->shared_with_groups}
  4358. };
  4359. // Add layout
  4360. nlohmann::json layout_json = {
  4361. {"version", result.page->layout.version},
  4362. {"grid_columns", result.page->layout.grid_columns}
  4363. };
  4364. nlohmann::json components_json = nlohmann::json::array();
  4365. for (const auto& comp : result.page->layout.components) {
  4366. nlohmann::json comp_json = {
  4367. {"id", comp.id},
  4368. {"type", comp.type},
  4369. {"position", {
  4370. {"x", comp.position.x},
  4371. {"y", comp.position.y},
  4372. {"width", comp.position.width},
  4373. {"height", comp.position.height}
  4374. }},
  4375. {"config", comp.config}
  4376. };
  4377. components_json.push_back(comp_json);
  4378. }
  4379. layout_json["components"] = components_json;
  4380. page_json["layout"] = layout_json;
  4381. // Add settings
  4382. page_json["settings"] = {
  4383. {"show_in_sidebar", result.page->settings.show_in_sidebar},
  4384. {"icon", result.page->settings.icon},
  4385. {"menu_order", result.page->settings.menu_order}
  4386. };
  4387. res.set_content(page_json.dump(), "application/json");
  4388. } catch (const std::exception& e) {
  4389. spdlog::error("Get page by slug error: {}", e.what());
  4390. res.status = 500;
  4391. res.set_content(R"({"error":"Internal server error"})", "application/json");
  4392. }
  4393. }
  4394. void HttpServer::HandleGetPage(const httplib::Request& req, httplib::Response& res) {
  4395. // Authenticate the request
  4396. auto auth_user = AuthenticateRequest(req);
  4397. if (!auth_user) {
  4398. res.status = 401;
  4399. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  4400. return;
  4401. }
  4402. // Extract workspace ID and page ID from path
  4403. std::string workspace_id = req.matches[1].str();
  4404. std::string page_id = req.matches[2].str();
  4405. // Check if PageService is available
  4406. if (!pageService_) {
  4407. res.status = 503;
  4408. res.set_content(R"({"error":"Page service not available"})", "application/json");
  4409. return;
  4410. }
  4411. try {
  4412. auto result = pageService_->GetPage(workspace_id, page_id);
  4413. if (!result.success) {
  4414. res.status = 404;
  4415. nlohmann::json error_response = {{"error", result.error}};
  4416. res.set_content(error_response.dump(), "application/json");
  4417. return;
  4418. }
  4419. // Check if user can view this page
  4420. if (!authorizationService_->CanViewPage(*auth_user, workspace_id,
  4421. result.page->created_by, result.page->shared_with_groups)) {
  4422. res.status = 403;
  4423. res.set_content(R"({"error":"Forbidden - no access to this page"})", "application/json");
  4424. return;
  4425. }
  4426. // Build full response with layout
  4427. nlohmann::json page_json = {
  4428. {"id", result.page->id},
  4429. {"workspace_id", result.page->workspace_id},
  4430. {"name", result.page->name},
  4431. {"slug", result.page->slug},
  4432. {"created_at", result.page->created_at},
  4433. {"updated_at", result.page->updated_at},
  4434. {"created_by", result.page->created_by},
  4435. {"shared_with_groups", result.page->shared_with_groups}
  4436. };
  4437. // Add layout
  4438. nlohmann::json layout_json = {
  4439. {"version", result.page->layout.version},
  4440. {"grid_columns", result.page->layout.grid_columns}
  4441. };
  4442. nlohmann::json components_json = nlohmann::json::array();
  4443. for (const auto& comp : result.page->layout.components) {
  4444. nlohmann::json comp_json = {
  4445. {"id", comp.id},
  4446. {"type", comp.type},
  4447. {"position", {
  4448. {"x", comp.position.x},
  4449. {"y", comp.position.y},
  4450. {"width", comp.position.width},
  4451. {"height", comp.position.height}
  4452. }},
  4453. {"config", comp.config}
  4454. };
  4455. components_json.push_back(comp_json);
  4456. }
  4457. layout_json["components"] = components_json;
  4458. page_json["layout"] = layout_json;
  4459. // Add settings
  4460. page_json["settings"] = {
  4461. {"show_in_sidebar", result.page->settings.show_in_sidebar},
  4462. {"icon", result.page->settings.icon},
  4463. {"menu_order", result.page->settings.menu_order}
  4464. };
  4465. res.set_content(page_json.dump(), "application/json");
  4466. } catch (const std::exception& e) {
  4467. spdlog::error("Get page error: {}", e.what());
  4468. res.status = 500;
  4469. res.set_content(R"({"error":"Internal server error"})", "application/json");
  4470. }
  4471. }
  4472. void HttpServer::HandleUpdatePage(const httplib::Request& req, httplib::Response& res) {
  4473. // Authenticate the request
  4474. auto auth_user = AuthenticateRequest(req);
  4475. if (!auth_user) {
  4476. res.status = 401;
  4477. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  4478. return;
  4479. }
  4480. // Extract workspace ID and page ID from path
  4481. std::string workspace_id = req.matches[1].str();
  4482. std::string page_id = req.matches[2].str();
  4483. // Check if PageService is available
  4484. if (!pageService_) {
  4485. res.status = 503;
  4486. res.set_content(R"({"error":"Page service not available"})", "application/json");
  4487. return;
  4488. }
  4489. // Get existing page to check ownership
  4490. auto existing = pageService_->GetPage(workspace_id, page_id);
  4491. if (!existing.success || !existing.page) {
  4492. res.status = 404;
  4493. res.set_content(R"({"error":"Page not found"})", "application/json");
  4494. return;
  4495. }
  4496. // Check permission to edit
  4497. if (!authorizationService_->CanEditPage(*auth_user, workspace_id, existing.page->created_by)) {
  4498. res.status = 403;
  4499. res.set_content(R"({"error":"Forbidden - insufficient permissions to edit this page"})", "application/json");
  4500. return;
  4501. }
  4502. // Parse request body
  4503. nlohmann::json body;
  4504. try {
  4505. body = nlohmann::json::parse(req.body);
  4506. } catch (const nlohmann::json::parse_error& /*e*/) {
  4507. res.status = 400;
  4508. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  4509. return;
  4510. }
  4511. try {
  4512. UpdatePageRequest request;
  4513. request.workspace_id = workspace_id;
  4514. request.id = page_id;
  4515. if (body.contains("name") && body["name"].is_string()) {
  4516. request.name = body["name"].get<std::string>();
  4517. }
  4518. if (body.contains("slug") && body["slug"].is_string()) {
  4519. request.slug = body["slug"].get<std::string>();
  4520. }
  4521. // Parse layout if provided
  4522. if (body.contains("layout") && body["layout"].is_object()) {
  4523. PageLayout layout;
  4524. const auto& layout_json = body["layout"];
  4525. layout.version = layout_json.value("version", 1);
  4526. layout.grid_columns = layout_json.value("grid_columns", 12);
  4527. if (layout_json.contains("components") && layout_json["components"].is_array()) {
  4528. for (const auto& comp_json : layout_json["components"]) {
  4529. LayoutComponent comp;
  4530. comp.id = comp_json.value("id", "");
  4531. comp.type = comp_json.value("type", "");
  4532. comp.config = comp_json.value("config", nlohmann::json::object());
  4533. if (comp_json.contains("position") && comp_json["position"].is_object()) {
  4534. const auto& pos = comp_json["position"];
  4535. comp.position.x = pos.value("x", 0);
  4536. comp.position.y = pos.value("y", 0);
  4537. comp.position.width = pos.value("width", 12);
  4538. comp.position.height = pos.value("height", 1);
  4539. }
  4540. layout.components.push_back(comp);
  4541. }
  4542. }
  4543. request.layout = layout;
  4544. }
  4545. // Parse settings if provided
  4546. if (body.contains("settings") && body["settings"].is_object()) {
  4547. PageSettings settings;
  4548. const auto& settings_json = body["settings"];
  4549. settings.show_in_sidebar = settings_json.value("show_in_sidebar", true);
  4550. settings.icon = settings_json.value("icon", "");
  4551. settings.menu_order = settings_json.value("menu_order", 0);
  4552. request.settings = settings;
  4553. }
  4554. auto result = pageService_->UpdatePage(request);
  4555. if (!result.success) {
  4556. res.status = 400;
  4557. nlohmann::json error_response = {{"error", result.error}};
  4558. res.set_content(error_response.dump(), "application/json");
  4559. return;
  4560. }
  4561. // Build response with page info
  4562. nlohmann::json page_json = {
  4563. {"id", result.page->id},
  4564. {"workspace_id", result.page->workspace_id},
  4565. {"name", result.page->name},
  4566. {"slug", result.page->slug},
  4567. {"created_at", result.page->created_at},
  4568. {"updated_at", result.page->updated_at},
  4569. {"created_by", result.page->created_by},
  4570. {"shared_with_groups", result.page->shared_with_groups}
  4571. };
  4572. // Add layout
  4573. nlohmann::json layout_json = {
  4574. {"version", result.page->layout.version},
  4575. {"grid_columns", result.page->layout.grid_columns}
  4576. };
  4577. nlohmann::json components_json = nlohmann::json::array();
  4578. for (const auto& comp : result.page->layout.components) {
  4579. nlohmann::json comp_json = {
  4580. {"id", comp.id},
  4581. {"type", comp.type},
  4582. {"position", {
  4583. {"x", comp.position.x},
  4584. {"y", comp.position.y},
  4585. {"width", comp.position.width},
  4586. {"height", comp.position.height}
  4587. }},
  4588. {"config", comp.config}
  4589. };
  4590. components_json.push_back(comp_json);
  4591. }
  4592. layout_json["components"] = components_json;
  4593. page_json["layout"] = layout_json;
  4594. // Add settings
  4595. page_json["settings"] = {
  4596. {"show_in_sidebar", result.page->settings.show_in_sidebar},
  4597. {"icon", result.page->settings.icon},
  4598. {"menu_order", result.page->settings.menu_order}
  4599. };
  4600. res.set_content(page_json.dump(), "application/json");
  4601. } catch (const std::exception& e) {
  4602. spdlog::error("Update page error: {}", e.what());
  4603. res.status = 500;
  4604. res.set_content(R"({"error":"Internal server error"})", "application/json");
  4605. }
  4606. }
  4607. void HttpServer::HandleUpdatePageSharing(const httplib::Request& req, httplib::Response& res) {
  4608. // Authenticate the request
  4609. auto auth_user = AuthenticateRequest(req);
  4610. if (!auth_user) {
  4611. res.status = 401;
  4612. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  4613. return;
  4614. }
  4615. // Extract workspace ID and page ID from path
  4616. std::string workspace_id = req.matches[1].str();
  4617. std::string page_id = req.matches[2].str();
  4618. // Check if PageService is available
  4619. if (!pageService_) {
  4620. res.status = 503;
  4621. res.set_content(R"({"error":"Page service not available"})", "application/json");
  4622. return;
  4623. }
  4624. // Get existing page to check ownership
  4625. auto existing = pageService_->GetPage(workspace_id, page_id);
  4626. if (!existing.success || !existing.page) {
  4627. res.status = 404;
  4628. res.set_content(R"({"error":"Page not found"})", "application/json");
  4629. return;
  4630. }
  4631. // Check permission to share
  4632. if (!authorizationService_->CanSharePage(*auth_user, workspace_id, existing.page->created_by)) {
  4633. res.status = 403;
  4634. res.set_content(R"({"error":"Forbidden - insufficient permissions to share this page"})", "application/json");
  4635. return;
  4636. }
  4637. // Parse request body
  4638. nlohmann::json body;
  4639. try {
  4640. body = nlohmann::json::parse(req.body);
  4641. } catch (const nlohmann::json::parse_error& /*e*/) {
  4642. res.status = 400;
  4643. res.set_content(R"({"error":"Invalid JSON body"})", "application/json");
  4644. return;
  4645. }
  4646. // Validate shared_with_groups field
  4647. if (!body.contains("shared_with_groups") || !body["shared_with_groups"].is_array()) {
  4648. res.status = 400;
  4649. res.set_content(R"({"error":"shared_with_groups array is required"})", "application/json");
  4650. return;
  4651. }
  4652. try {
  4653. std::vector<std::string> shared_with_groups;
  4654. for (const auto& group : body["shared_with_groups"]) {
  4655. if (group.is_string()) {
  4656. shared_with_groups.push_back(group.get<std::string>());
  4657. }
  4658. }
  4659. auto result = pageService_->UpdatePageSharing(workspace_id, page_id, shared_with_groups);
  4660. if (!result.success) {
  4661. res.status = 400;
  4662. nlohmann::json error_response = {{"error", result.error}};
  4663. res.set_content(error_response.dump(), "application/json");
  4664. return;
  4665. }
  4666. // Build response
  4667. nlohmann::json response = {
  4668. {"id", result.page->id},
  4669. {"shared_with_groups", result.page->shared_with_groups},
  4670. {"message", "Page sharing updated successfully"}
  4671. };
  4672. res.set_content(response.dump(), "application/json");
  4673. } catch (const std::exception& e) {
  4674. spdlog::error("Update page sharing error: {}", e.what());
  4675. res.status = 500;
  4676. res.set_content(R"({"error":"Internal server error"})", "application/json");
  4677. }
  4678. }
  4679. void HttpServer::HandleDeletePage(const httplib::Request& req, httplib::Response& res) {
  4680. // Authenticate the request
  4681. auto auth_user = AuthenticateRequest(req);
  4682. if (!auth_user) {
  4683. res.status = 401;
  4684. res.set_content(R"({"error":"Unauthorized"})", "application/json");
  4685. return;
  4686. }
  4687. // Extract workspace ID and page ID from path
  4688. std::string workspace_id = req.matches[1].str();
  4689. std::string page_id = req.matches[2].str();
  4690. // Check if PageService is available
  4691. if (!pageService_) {
  4692. res.status = 503;
  4693. res.set_content(R"({"error":"Page service not available"})", "application/json");
  4694. return;
  4695. }
  4696. // Get existing page to check ownership
  4697. auto existing = pageService_->GetPage(workspace_id, page_id);
  4698. if (!existing.success || !existing.page) {
  4699. res.status = 404;
  4700. res.set_content(R"({"error":"Page not found"})", "application/json");
  4701. return;
  4702. }
  4703. // Check permission to delete
  4704. if (!authorizationService_->CanDeletePage(*auth_user, workspace_id, existing.page->created_by)) {
  4705. res.status = 403;
  4706. res.set_content(R"({"error":"Forbidden - insufficient permissions to delete this page"})", "application/json");
  4707. return;
  4708. }
  4709. try {
  4710. auto result = pageService_->DeletePage(workspace_id, page_id);
  4711. if (!result.success) {
  4712. res.status = 404;
  4713. nlohmann::json error_response = {{"error", result.error}};
  4714. res.set_content(error_response.dump(), "application/json");
  4715. return;
  4716. }
  4717. res.set_content(R"({"message":"Page deleted successfully"})", "application/json");
  4718. } catch (const std::exception& e) {
  4719. spdlog::error("Delete page error: {}", e.what());
  4720. res.status = 500;
  4721. res.set_content(R"({"error":"Internal server error"})", "application/json");
  4722. }
  4723. }
  4724. } // namespace smartbotic::webserver