From defe6054b314e131254752e26db83e0c2ef460c4 Mon Sep 17 00:00:00 2001 From: Timothy Carambat <rambat1010@gmail.com> Date: Wed, 23 Aug 2023 19:15:07 -0700 Subject: [PATCH] Full developer api (#221) * Autodocument Swagger API with JSDocs on /v1/ endpoints for API access implement single-player API keys WIP Admin API Keys * Create new api keys as both single and multi-user * Add boot and telem * Complete Admin API * Complete endpoints dark mode swagger * update docs * undo debug * update docs and readme --- README.md | 1 + frontend/public/anything-llm-dark.png | Bin 0 -> 8413 bytes frontend/public/anything-llm-light.png | Bin 0 -> 6324 bytes frontend/src/App.jsx | 5 + .../src/components/AdminSidebar/index.jsx | 11 + .../Modals/Settings/ApiKey/index.jsx | 198 ++ .../src/components/Modals/Settings/index.jsx | 2 + .../Sidebar/SettingsOverlay/index.jsx | 7 + frontend/src/models/admin.js | 45 + frontend/src/models/system.js | 43 + .../pages/Admin/ApiKeys/ApiKeyRow/index.jsx | 69 + .../Admin/ApiKeys/NewApiKeyModal/index.jsx | 118 ++ frontend/src/pages/Admin/ApiKeys/index.jsx | 109 + frontend/src/utils/paths.js | 6 + server/endpoints/admin.js | 69 +- server/endpoints/api/admin/index.js | 642 ++++++ server/endpoints/api/auth/index.js | 33 + server/endpoints/api/document/index.js | 194 ++ server/endpoints/api/index.js | 21 + server/endpoints/api/system/index.js | 153 ++ server/endpoints/api/workspace/index.js | 430 ++++ server/endpoints/system.js | 112 +- server/index.js | 2 + server/models/apiKeys.js | 133 ++ server/models/systemSettings.js | 57 + server/nodemon.json | 6 + server/package.json | 9 +- server/swagger/dark-swagger.css | 1722 ++++++++++++++++ server/swagger/index.css | 3 + server/swagger/index.js | 28 + server/swagger/init.js | 37 + server/swagger/openapi.json | 1767 +++++++++++++++++ server/swagger/utils.js | 52 + server/utils/database/index.js | 2 + server/utils/middleware/validApiKey.js | 30 + server/yarn.lock | 39 +- 36 files changed, 6098 insertions(+), 57 deletions(-) create mode 100644 frontend/public/anything-llm-dark.png create mode 100644 frontend/public/anything-llm-light.png create mode 100644 frontend/src/components/Modals/Settings/ApiKey/index.jsx create mode 100644 frontend/src/pages/Admin/ApiKeys/ApiKeyRow/index.jsx create mode 100644 frontend/src/pages/Admin/ApiKeys/NewApiKeyModal/index.jsx create mode 100644 frontend/src/pages/Admin/ApiKeys/index.jsx create mode 100644 server/endpoints/api/admin/index.js create mode 100644 server/endpoints/api/auth/index.js create mode 100644 server/endpoints/api/document/index.js create mode 100644 server/endpoints/api/index.js create mode 100644 server/endpoints/api/system/index.js create mode 100644 server/endpoints/api/workspace/index.js create mode 100644 server/models/apiKeys.js create mode 100644 server/nodemon.json create mode 100644 server/swagger/dark-swagger.css create mode 100644 server/swagger/index.css create mode 100644 server/swagger/index.js create mode 100644 server/swagger/init.js create mode 100644 server/swagger/openapi.json create mode 100644 server/swagger/utils.js create mode 100644 server/utils/middleware/validApiKey.js diff --git a/README.md b/README.md index cf49a77ea..f427ef50d 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Some cool features of AnythingLLM - 100% Cloud deployment ready. - "Bring your own LLM" model. _still in progress - openai support only currently_ - Extremely efficient cost-saving measures for managing very large documents. You'll never pay to embed a massive document or transcript more than once. 90% more cost effective than other document chatbot solutions. +- Full Developer API for custom integrations! ### Technical Overview This monorepo consists of three main sections: diff --git a/frontend/public/anything-llm-dark.png b/frontend/public/anything-llm-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a294843869eface3065ca61c413528b3bfca668d GIT binary patch literal 8413 zcmcI}g<F(e(Dzb;0wUcYwMZ<D#1hgi($d|+(p^fYlynOU(%m54EF}mF3oKoWbS&}m zyx;pTeD`(dxn|Cp-^^Td&&-Mb@LmD`1=R}x0D!NgD60top!+?&TjD%>y5k*IdOba! zyC~|r0|2<h|6OQ+tQ?A`K{R(w1!+LlB+cH_2d1r*niK#~`xW=j918$=?WQCvrR{@u zlso1{v)~zVC7~9grj7}mG#DAi3TgQeij&Mrx%gVVS<%7yi(g3M97a?K-MeCwkg#(= zQL2*eGhRbVYYW`bcfE3$qm)+mX0iWH4Tvynpd)adKW4G$Q#&3DmF<t}<#VrHZgrc_ z@&j4WpQQNzdwjrLc6`^+LZM+oLvuLf+MzDfD;(_o-<o)mtS>q<!^fHz+Z+?@Gf!E8 z3Lt-3N7oKYt@x-SyV0C(m-?Dx<Z{H}X}oCA|FXo<NfP-@FPE!~U)J976}f#}S%DjU zr*g4(b!~Opn-=~*Dq#F1!Svyspyfw7xTK~pr|DgLaE9OIQT}5nyOmea$q)DQ=zuFW z_ODy9!H@2Km+6wh^8c#^tVA5~csc%055@tp)-Q5Negv0k{@eAob!E{Ke46EE_3>iS z^x<?B^FQiDe)-1*yV>u`vT$78?<%ol+I-Q&_2*%~`lmbJjPmYb{73Y9BWLi3zM<lb z+^fN<?{8vExcT|N!H~TC3fgR9*1%=joAsCYlvy3p6E@fAXq3aRZEd#6^|sbcs$Jh# zT=|2u^-6(ZMlvTrq89nwL}db<Jc2$C_$_i))Wr03F$KH2$9BAY=M&7B38`;gg2$L= zCy_R!>D<f5yIWi%>lv~K#4nuU-{G!E^t@twB{cm>LurUIMAW@%(WvghB-~)RRLZr~ zd!im8!wD168IV28s@;En81HjX)o%)EDrMYsN8ZF`XSN3fd+(HcmjbOAAqEnURSU$x z?5yQhznOeI;%<3Csq^jA@m}fmFA8HrQ*7X3Dj$b8E6wxpVhJ-Zm64cNPl_nTI7Dg_ zN_$a#vi{X@?mvoV+1lX77$n?9KQTdSW3MvaQuUq@BCK@D<eoni{gbMWk@<<MfUbrY zxgknn+`mz}jq>;X<`3QhPn8S=p%TgwA?W=P)L&zx7W|U}*7JQ3Gw4v>i|4Es!|^#6 zqYW#$QbrGHMBW#oXipP0hH#%^il@?7Y0AmE$6C>@5P3U7Gb~N-!r_Fb^u^hI=XYB} zs*?*b#F^1Q27^|LXw1j1w)kby)T37zj43z$p_3p9w$eVu65(wj?B^7k>(xj@X6yfe z;pV=oTdAzX*5=D?5#v?tR1rad&Yum?1_j3gvsK_f6n~H|E}CvO*3WL@lCN=X2Kw(- z73eIgYb^ND0j=F1?qbAJgLpq9P9{fXp1lPWm_&(3t$yVKIj82zG7i3J%<rJLOWcnx zOrFYat^Z8cmg6uU&E^p<ox=UMi2jI;N61Yn!!>P{nJ24Q-`BshiF1|?0NBT*mLV8Z zrBdBET^4`Ir+A~0HKj%5`Dw-Y(e__zW-)8%;bFr8c4Qhrzr6PoSoSW<tze^z<v*31 zsGe=!L~5h&*EIs!(o+nLOoU2ah+AJBRAO^gY<;u>a-Cmn>BwTTnhH19ws*EFugNz2 z_A<$Qr@Y<u6vY4zSj46rZJXJ<B>3j78G3BrQr;NvvQ<en(_mTEUFxq!ux1mW%PCbT zv0}b2ak3A0!$cZBu?e!oFL2asW@idR>pHW4qHS3#s|@bS8^%Yuv=0n+Lo4isSEfO$ zC;B_F1z)=6GY?$vtX0=I-CSW_iYv{rga`%&T&?EY9KkK$_D<-W9U-8gs%Cv~$imm3 z=_(mwpd4-|hfEJQz~><Jiy1ta7zYKs=D&B!KS`r9aZ*jS^D+sg)MLZ+^4>V&7Q)F< zo+lSMrLGhtzc<>p;j1O8BBG$AX<eY8ZTBNOKHL?@J_4YX3eDOezNCqbRLU7>lxV*% zB3f{YT*<8j=biZR-v3*%rQ4V(8pbD13s_&qR_xkUi-PGMuE04+M6lf&iM}`}RY_TR zNT31YULGPhDXWK^hUx;1ipQE{N~G1NeL78Og*4M0ASw2&H;e*Y>R7C3^kP_<u;8}N zn?lHp8lnrt^9(&TmJG-W08oJXgQSkPHBLcnCA|S(QO-<M^43+bm--!WPj|l#xIxFp zj;0{-jvyB|W3YukPIELtJ>sQz7i#{5_4ap!B&qvK9aIZXXbxRlWucYpH4q47REgh+ zO{OMsZ}03*X@dhw(mLd`>7rp?nGvZQmJ>8d{Pw$Ou)w!Ak2gI%gDoW9bU*H0wr&3q zRb@v#V)+(=W*X?7JsMXXU$d1IRhb#8K6yf^(Jq@(2mB5})oCOnZ~O~vST0eW#A1r6 zzg<ycsEGcr@9ntYY7~c~D&zSf!D@B($d77I1!7xWHU*oVood?EYxTk-D@NhMTVJBD zf4vs07EAPdG*Bho;8snYBNh(+MkBt;W`e!?sWT<kvO(Ed=mcBJ!tBwJ`WxG#R=sp8 zt4`gt6_m<V^~NCgrgRpW-jQRSdYZA>$Gng3-xnJSZWfPKZ2G)=$Yba*+#h<?nvhvd z9AXmlL#qSKLxz*q;_xL@n>E$>`a*W7-UPJ&qz?M`Cph~+-1K+`;nv4QxLv%UeA#Dp z)|3Ms6Tj+P{&Fd?YWD#ZT1O7P6@dz7t5m58)n6u0m*s!+aBk^<S{a@Wsn5;2ARJ%T ztp4_4&}~_0h5B?>wRH3<m*vQU!t#n54~kj`K60sNx?GbU?DkzBRHi}QJ&TC+Yn9C_ zzOBHmGLoY2)bMMM<6&Q$yY94uXGo#v%NDWse)sZz^%G|qANj6YhbFRTRgv>2k&1HA zKz&S3VpLh$r#k(&DSTz)peh*CDe_klN)cFu3X10F;7&2(fH=kknXU$n^HEmOJoaV6 zis!*L)%+VIjK0STozE!i+Fp(8eDDa1KU@IUmFAq1lqEIUrk+{K$>ob#UkIG6g)WB- z5dA@+Y%8v)en;r$m^OaCm?Rhp`T)UkulsPhL{K(nU#1pMO1NKnsc{m!joh%VG~$NG zz7T1$AKw)eu_<Sztq;1Srm$?-gdN@m52*K%!5ROZS9I#hKZT@kL%W{DV-89iTVCF8 zAvf=@mDYq{HE)#@<~2wb^Bcl*Y^V3eonQ>EtA30~l=ZK~|1u4H7&K508Xbi*<;-O+ z)sDpIb`|^P{n@@V2)zp;7FAkC%)aYecO$~0YxVeLpSwFQD%6wk>g#C~tPH);D0>9g zpEhZM!0%yLq+5vJ`zA;u*E!iyOLE6-NJZ)E;vE8BL&yJtgr+XUW_B%-VJ9)C&jG(< z;<NSYX@RbEHGRtwT_n)-@AuG<x1T~5eaQupElyt_K}f^XLEmpnY+w2>j+Ze;BNc7B zQKBa|cuiGDo$TZ7zZ+c97HiFEog89g#QKm*&gJ{6%LLgj8M&q%_84I7+~|)Y{fJB7 zJd_kUqXln3vPm|VIw2(sqG7EH+bqiEvadk?rBVl-Z=<3L;`FcDn79BB7q?<S-slJ1 z&dk<u)MtGTrPF~HOzj=1<dH*T_Kk2@Qz+8$G0j`-k2-d$)CdBs#|9CgHLVcujIaka z7T|5sTqU_bAPkIW1U5@KBrgD)>{O|C8T}>}BcMV2OjwX2X6sfD=V9j*mcHM8sB5JD zHoH61A)ALB+4=Dm4J#K+fQx&#i`a0-tqdmAAg!);Q;2*9Yw}JCAdG?(g`6hCnq(4D zX}OQ6t#@R7&B}5N^K|s(pX=vm1GDpE!Nri;7t@5TUPb5|mV8NCaVS3Z8M?J_!swDd zA>_YJ@{HW>AG!<Zm^VrlM1^Ur9|#ms?yj|A(dBd?gOsq&e=TA^{kDvl`g?suW!TO= z1eYSSoj?LEM0N#T!D2mQ&ajQu$QDTJ`E__yjC{p2@dL~z0>cNJ+W@0w2CN!<CY+U& zVO68j&hXM*GnYoOs{=AMPT%;!@5!<z(u+MO1$l=-oh;3cL2JFe+8@bQSqBeBEDXrE z4SGT6dH>+8&r`BZC`Aj?nN2aP<Y`Y>6}xa)IX^u9bcVg`Tk8T*{TR$V@v|5H9IPq- z9yPoW!^@{cQ8Hy2F8Nltjoe`+AjxRQ0YZxPJsK?PQn!K=yYPK4ed9ua>$vsKglx{l zHDKuYcvlJTXT2L{A~`~ioMN~sqF+Me>eX6qqTN620%7F;6?MGVOFH$#(2o~LSwe|Y zB{9eGQJ!DNJ*(LI@TJ19F3L3A6JI-;vn_g3FkeIk5-E0U&EZ9sPQG8a;;vwRe9-sd zxj23nzDn2Hy}%8+tM7ecB<%9~_%jmrYa@rIRs9ppB>l|RKv%CK+jHCVKGz_sjKELr z!9a6ZlC2MfU-$J(!V#MiMIC+D6x9E8`|U6;T=HJf<kS2Xak(4fO7VrsSuQ>8L0@Pr zV@LJyF9{xEZWh8znOEftYSs7e0@s~U$jDD8(|WsuC$<ZY+7f56Q1#hR8U>!b;m(iv zFF{UAul@;;g$|+G3XbbjV^Z8Nju_9Con8R17?rwQoCP;U>K2aQZC)rKni@mfoxowF z>f3Dk8f66#jgiuKt_lI-3C)P~$23oysUza}R!e70{d6$mu%ZI;*yK7Scp+`66Pj?O zuSC%fHqCXY904BXq207~UwOUdaF7{B)g79^VDQ@!iSYytm1bt3aH9ZLbYjp~^_$@@ zf7=dN|8ND`OIA40Teem|`?!2xy%khZuqeGYhqzj7EjQ%AMd>qqX(T<j@wOcs$???F zZK$pd-u5P{wgq)t|Bbl33#u*!zLYVaR!QOh!q2quN>2bF()NU+1wy^WYRz1mgWSdD z#&yvo8emTQl4oh^BDR;S>~M3gGP>LT!{m1@?Fus<p-@8gp|jHkiNz9!C&b@fE(njE zo2h75@Rww*xJNZgm1i&Ldf%_$TOS7r)0@;aEXOv*f4MXlXO4#{2KR4%-ddS8xZ;bz zsP~n(Y|+%S$&#*yLQcZ_w>-7{3$GY&dL@PR^n4Y&DB{Fcnua#emz&=MBa@9yYr%I9 zx{C2~xxz-t8e5X~84+NuU6$kHiUstN)hs`>w3H;ZUd4+0&z<fUc|?dN?uG}$_vge* zMJut=bwt+r7bG3=F6l=G-B&*1NcK~P%Xi$Hbm|^fSD)H*{g;i%|4)MSwioCr+zGp6 zpSwmlV3*d?^^m<gsB*be$vroUt+>(xDrq<m9ktBu&cLZGBi95&v{QJ_X%6*%i++zb zhNCkSiEh|+u1=A8rr#zU{eg(q;G1%RIEj-WM^oeg>;a~7<5nn;K~O5~llqI;v4bD; z(%ed21vPl8%%*tnKN?ol^oPB=0vY-%RQPG5W9U~Mu7CEXNwfB@UC2l*j${6NKf`!K z^t3FAsCiBp|BvIA*8UikrY&&SWG)|PBPy?@cXGNu++<_;8S`Iz+_8Ykv6bp(>4Opx zbjxV*+n$joAnR|st;jreN4bF~ovRf7ys#}=yg49QstutPHN}fU`s?p)Bb&tO=-0ab zY-0Jofd_&Mw%CU?_pV9v%^$vGcs_EX;^MZ6q;{=YXbay{NcJqC2U1O*yn|LhuN};s z0YsG``A5-geHMdxf|GZADJ?QFH&is;N)`jQ@$P}#>A_?cx}!IG;KFo$mmg~13AL>f z1#J45YN*bc8IOtHK0JlALL^LH5w|Ib7l}(n@<UQ1A&lADZanwr)+~4}Ff4`V8Icg% z5sk@r1EZH)4lPdZiv+3<N*tGOOt17UGG!M@*FFy(WbQa~v!(~HyZWxoO&59dHr!OZ zBi)%zH$&~)!SGPy{EN*e?@CL~^3!Jigl$^rI+-H(QBxi=_S3+@I>$Dx3I?I4i~I6t zpLMv?;@=;i@4V+ycfiQAZ>*{kY52$CED#cNYnvVUQzPt~lm<oq?a&~{GMiMQ^aZub zFq4@(?>fq?0EW>F$n{IzX&w%)ip3k>_<V+cU+kRUL)zdhVCg3ZhvK@e0-Z<r>LHGy z!J31vYMu2_{cZ?bTD4`%lc`ur9?omfKJB7W<#tR2|6iku4l^T{$W`zn963v5Wbl;J zG!ptP-Ra?QDLdr0u9wHz?xb8znfXkN{-OB2cj*1vdc@Q!^1n#c3i*l)O`v5!J2ucd z9xsjyS@I3V+{1V!-LM<{xP{*KJ;#yR5Q$C{$7Kwf(N@4@*-fQ<LIM)Cv?6cU9U`dw z*tKnl%b~_sK+5mC-ld|O7%^Vl=6YTA@v>nY5Dy-Yf8AX9(Jb>eJP`cOiV+w?$4ve= zV;#mO1Il+Jo_4KFfBcbV@VMlAcT*sB3&4Ek`_*c$6XW8YVey2KfnD?upGZ8E*Y7ui zT-Dj5Aivzr*rt-2P23&2>%lE+X}2X7P-fc-rWi|+I>C&GlC_wFijBrawc$`SwM+By zL@Vq8A~=YIg%ro}i@x$PbjYNs@GTIS5lsz>k~=2He5a;}xX6(Fcixst_k@bps97s3 zfRHM^xxL;~$n4G|Yt5BNY=9VPNu|XwJ>d42R}s@)U~PNiZyN+7nNPN4MQkLH+M<m1 z4>KOm-Sym4HscFkWJE6@5CzTn^&jZ`XY{86EQNc#|Hi+N5lT0sv)bcO=FqUWu7BJ! zu404e?M&>g|1Ph{u)_XSdT#gzNGJ2{mSp+W*Pq?4*_Nq^vb=tdks1s6(4J;K%@ZcZ zYmt&P!GAy=LjUAj{#;!8{(eGLzYwQwWvaV!?$!H!ze+hDTX<#+x_Y*u*43AyIjN+r zd<hnef1M1`GEr}fc$t~mHo~&}76QmM=acfcUWFLBcJUS_eQJ%a(=!chJlm+o36Yrg z5${9RYZ(;lkUc?Qk>-xg^v}Ij>cSBSj>y=GS2Aju6$rv?b1LsqormpzWbSIjZr`B1 z5Ut0*oUm9epJ@aPl6yVqS4U7UoEpRhdwa;H9VQ2V_5r+q`U|PdBRupRZ@uo@W?DL& zf7R`(@x#7`=B@8)n93!EYMu(3e5IS@<eU=L@NRa(ZcPpnuR>lF6vvUD#K9ZSWOw!U z4FDUl`0O3Xhi`5lQ-2A5ZS9fz_6#75v0%muGjtOQloyP8ilH8bLj29Ym%Zq*hqh>G zJ7fYATRZcv6QG=o-e(C|WgJqxMx-gU+16At$xESDh7y)v9rv;3CLQUwXPZev%<||z zU0)Gw#nw>-3-Bx6&u&@YB3r-di`YNE2BW{m7CrHsZ=;@{uKcxe+Qz1{JHcr*A9*D* ztUU=8xUool?K%-NqTRGZd>wf6cwnjNwzb65;smDP&y!XsMBCB1EoO?s_f4x&O6K|H z$~|lwj}s>-mAT~+5VZQ#BO5QgvZIwen-CH`Q=@*5JOgY-3?7uZvcVTCbUNJapX@Q# zPMi}k?M!gEF>jzY-}*YXV14jos!=We#sXQn#4?TEm6Jp6C9aYB3ub?=(qC%5E?RbS zIG=o04|Nu`OO4NuoQoNus(H4V=T#JA!8m(AU;gGHo}=8Du?QBjidX#Pm>lZfMOqB5 z7*KD<wx<h-dvoaNO5IKNT7U`E%2!|GqNf`)$6ck?D2Ec`vxgW$xwtHDl30DP-}`SY zb1)2r?{2<)wE=KXPOY8u&uQMOM=;|d=Pcu%$AR3sf_`Lfq3t+nvh}FG^(<^-uUh_k z{&tWvS!4VGTF-!==}#lnV1xnt-p5Ait{&tGu5l*?$i#O~fgx6Iwf8o~4jP-U`CFNS zI-2kQux~|)-<rp>W<%;AHckd^Tfx=`?JU|$&XvK1A9*+!$_@OPd)r@=ub$r(HRFmL z0H%w0`|0S!5=TG+hWFYrD!q1Ng0ugq$`<D=N{n@yu_@GPD0NJZc@`x#DU!rob2V{I zRhoiR)cOJthsSMiZo|3k2C|w}0dWj)X{a#j?`0<rU!kTgl(wxanR`;&QtGKY02%(J z4gm(ykDQ>q5(nrPv^LAALl-;uI;n>PsfG-BNy7qOc+%fK&uGBt&vhkv=J!$^3X0H| zm@KGaj{E*S19%@sS2A?ZUpp#B$e^<Ech+Y)+PiB`wiCZ~I^eYs@c1&yco;ukSC*$r z7{v%2*;BV*_h>YByn-GY)iMw`r4Qz8Nv-E!;9SMm<G8J)g#t}m%H{MEHY4hI-R&8Q z2~SG-whaAb2+JNaUdS%9flgX{41Ohm-!<6bCHXpn$P4w%h$3FhSRpRsh*K@AK`;Ka zU4K@6f#SO8V^QI}ksOmf@@h6eq1Nn38#Rdi<mbFzm9y4uvAys-M+(8#CO1&0AhK+! zMSw?coUgmX>Tw|w?ryK+mQc!76IiV{mQ;tjn8&D%xzh!XacC1OL;Z5>p0SvdtYGMD z^!Eo+_XWbJSM>PL(yy@bf!t!x`}p%}V*R5HMUT)=u{v8^9r^v^@r_w{X6!(GzCN*> z_dmU(CO-CR)m{54V)gFbH_C-Fk}V;a7_$we@uiPR@aoI7?kgJqx3`Mk!=m@1zqwP# z#O_&x@SgxM(8?Kx@Gr1I8m{(OW(Q^?I+QDOq}G)v97;+g6b;CJAi<6Q;O_g$?tK~u zgn06rv8poOc=v-PWGsN+AsLA=D7hlJc+`HtG^^6cH?Q_nI**uj=8P6qM)I{=j-{v0 zfhq+}J7kDRZL|RG&39sXyIMCuYVtrHhkqgdyTyz-P5v5Bvk&a{ciE}Vu2m$s2+Bk8 z6|UA63$W^I(xaxMM4ZqNKI;80+LWSsNrt^t-rtxyx~y@c`jb*YRwYd@R9qGN;XOTh zU@@P*lC%fLj$oziU-F@FCV+efvxpMNo?CDOl=OZl)`9)Zapc>tZtyon<CP{_>6K=I zXL6P5GAVqe^vZ`}g=y}*e=HcWdhE!on+o)66X25-{=PU86X!ScO-G)VQyY|V5<|JX z%hLpU{6Z?x-<aigg{xFI*+$}vdxY3u`TLUwUKu=<4w&r_sG!W3dxaQ*K`ybwqQmzT zS$DEw&q>b}I_fh?I?`|KfrJ5joo)HPqt^3Vg8tX{6+RxyC)$7yf<w~kSWx)nwoWD% ze8hFjgXp%6iu|OXu1%<6`MFM<0qvz%6FiZAVe}e#y2`}C6|LOmulE1I;H!;NXk9re zyZn_il4FhIe9#>*iN;mpQKrF_aOg14RXSdXc4Z`lAR9ofKM%wgra^>0{7av@RJpML zI%g@OevA7kDFJZs1jR$6k!<ycJc@_jhky5xo*h$lBX*wJ-TsIsr&+b*%(t#5-yZd4 zVp!W7#z&2*X6J4tM6FY)36{c5Ox<2qxo6Yacmfn^;y`bxKvk;t%|%v)IiT|(hZWm; zb%oV|SDt3eB8_Hl^OkSZCF=VIae}=)VGfxuxGQbr%dF_F22xfps3)ogX0k#KSnq1< ze{tic&r(37!PbBKX81dTWr^w)JOs1+J);*Lo_{mV{{Ys&S{p;LRR?A-JV4Kcx9<(C zk0SY2547tNaM-sENTR@Xw>)zA8~kl}Js&T{4>*@Yf~#@6^6L{;SvLcOJ!y330C>C( zCDsRYC=Ze8{;bZU$^z*{>(^wNb5wmK(Jc3*u1&_w+qUy<-7)*CKbr{0CVC_|`8Q$> zj>d0r>oghQD>v-T&wXuzG<JULEQ*n(-Q6HB^&qbRGV=I_1!w^Dr~t!75ofU?s^sS_ zWN1_2G41XXz(!cw%5;*=!fNUf`E<!Zd+WKGu?g_D8`3ZUuMY5A7p)6s1Vp^T5eG!1 z&SmYakZ~q7cgdzPl!`}RIM<aSf!|EW+7j1CwvYQ~z%1R<R!!>Gs*Y2@%W0w2L_rI= zCKGdl1j<-Ko}72PXgnQJg3x*(<FCh1sx$TOJC^%HNH8&I=+oDifBb&lYd}O*l3Gs6 zIPRh{S5T+im6A)%9Fl}7h-YMuaG4#--&U2CTJ0YI9VgN6umE#$nK!Q;=@yyhYB4V4 z<nmmME4;gfQg*sa1P&xb?JM4NE=<UtJ&fO>D`@z1AB<a+o+3@&sZ9&R7t1<GC;<0> zLhRp(yx@FQ=W?|9jtlnKWQ-|XR;{E@)xNmgKS*o$134wtY!_tIwb$$qni7VYphWL) zPcrVvbpUhtRx9Wqh>I3|MTN|FZ+g&jNrH=0v<;WlB9aZF;L)V>GvC-_m@(E=ba9W1 zGCOKG+SrRL%_|tk34{Bs<dP_DuZNUct8%S!Vi^)I5c+As-PAL*Q4O&g;ZCAx6eLgT zcKAZJPgZ!PN9ae*w|>Oxs${Sr8LeT#HwcQIY9BH0jG;evf~>p-I83VAlmUZP49PpV zrK?%~JCB3g9xY(|=w2#rwD)Q6U9s~&+bMc`dpmM8VBiB9(WD7NvUtS1TOOqQ?FkH< zxuqedz|ykfYDh*2lN2Z6Y>jM)d4c7hjgf8oP0@@Wel_jwogykv?in?WQIN)5^q&7^ zdh(EjY+lW?B<zTAW+QWiEi{K8Vtwz(5s^P4y(fhEI}zhayLNTy7tYVj>=aRdm!OsI z#~&I&WjRjG5c5Jzd9){t@?V*KasU+4h}`0Xfi}<cw>nIhG~hN;$rbs}t~^Bld5mUM z`$#7Be-Z&n{pI`fd}UJMbl;u@JA#Y@+v;x^(0CaCBNIK;AJQAR5AUrq+f`a_!Eay3 zGbXM5*;}r7kmsI|_gCghC6y8V`?D}QK;PGKjXUA83#=q;`6wTEt>(Y2moc74LPkrY z-0D3)s~c@$m$&dqpu^VZB+O1{v{C{tSil%v8EFv<%>QJ;v=JwSkOjUt9T~qngCG@I zO<$aPkupD^Qa3F#7RDm2QBB*|@ZjNp#)NJbCPn*XNa?2ybIKuz=mC7QxMFmKMJLA# z!v(43Iveyth?qi8_I$6e&fHs#bLlZ)UhumoTW)=AR3AQHhk5Yz!*}6?tLGuSfd5QJ zBsWcx>&Qj>h!7c&#YSZm0E9E&DEFJ5Z>%D+&^9I>G48$kuOo&Kvrijv042HivQ^S% GU;YOq98zci literal 0 HcmV?d00001 diff --git a/frontend/public/anything-llm-light.png b/frontend/public/anything-llm-light.png new file mode 100644 index 0000000000000000000000000000000000000000..341d21b6cea9a1b7b8ed431920931c55f4d1c0c2 GIT binary patch literal 6324 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 literal 0 HcmV?d00001 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index df74f73eb..b6cfc7eb1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -14,6 +14,7 @@ const AdminWorkspaces = lazy(() => import("./pages/Admin/Workspaces")); const AdminChats = lazy(() => import("./pages/Admin/Chats")); const AdminSystem = lazy(() => import("./pages/Admin/System")); const AdminAppearance = lazy(() => import("./pages/Admin/Appearance")); +const AdminApiKeys = lazy(() => import("./pages/Admin/ApiKeys")); export default function App() { return ( @@ -52,6 +53,10 @@ export default function App() { path="/admin/appearance" element={<AdminRoute Component={AdminAppearance} />} /> + <Route + path="/admin/api-keys" + element={<AdminRoute Component={AdminApiKeys} />} + /> </Routes> <ToastContainer /> </ContextWrapper> diff --git a/frontend/src/components/AdminSidebar/index.jsx b/frontend/src/components/AdminSidebar/index.jsx index b185d4f9b..a583cb326 100644 --- a/frontend/src/components/AdminSidebar/index.jsx +++ b/frontend/src/components/AdminSidebar/index.jsx @@ -3,6 +3,7 @@ import { BookOpen, Eye, GitHub, + Key, Mail, Menu, MessageSquare, @@ -82,6 +83,11 @@ export default function AdminSidebar() { btnText="Appearance" icon={<Eye className="h-4 w-4 flex-shrink-0" />} /> + <Option + href={paths.admin.apiKeys()} + btnText="API Keys" + icon={<Key className="h-4 w-4 flex-shrink-0" />} + /> </div> </div> <div> @@ -242,6 +248,11 @@ export function SidebarMobileHeader() { btnText="Appearance" icon={<Eye className="h-4 w-4 flex-shrink-0" />} /> + <Option + href={paths.admin.apiKeys()} + btnText="API Keys" + icon={<Key className="h-4 w-4 flex-shrink-0" />} + /> </div> </div> <div> diff --git a/frontend/src/components/Modals/Settings/ApiKey/index.jsx b/frontend/src/components/Modals/Settings/ApiKey/index.jsx new file mode 100644 index 000000000..7f023180a --- /dev/null +++ b/frontend/src/components/Modals/Settings/ApiKey/index.jsx @@ -0,0 +1,198 @@ +import { useEffect, useState } from "react"; +import System from "../../../../models/system"; +import PreLoader from "../../../Preloader"; +import paths from "../../../../utils/paths"; +import showToast from "../../../../utils/toast"; +import { CheckCircle, Copy, RefreshCcw, Trash } from "react-feather"; + +export default function ApiKey() { + const [loading, setLoading] = useState(true); + const [generating, setGenerating] = useState(false); + const [copied, setCopied] = useState(false); + const [deleting, setDeleting] = useState(false); + const [apiKey, setApiKey] = useState(null); + + useEffect(() => { + async function fetchExistingApiKey() { + const { apiKey: _apiKey } = await System.getApiKey(); + setApiKey(_apiKey); + setLoading(false); + } + fetchExistingApiKey(); + }, []); + + const generateApiKey = async () => { + setGenerating(true); + const isRefresh = !!apiKey; + const { apiKey: newApiKey, error } = await System.generateApiKey(); + if (!!error) { + showToast(error, "error"); + } else { + showToast( + isRefresh ? "API key regenerated!" : "API key generated!", + "info" + ); + setApiKey(newApiKey); + } + setGenerating(false); + }; + + const removeApiKey = async () => { + setDeleting(true); + const ok = await System.deleteApiKey(); + if (ok) { + showToast("API key deleted from instance.", "info"); + setApiKey(null); + } else { + showToast("API key could not be deleted.", "error"); + } + setDeleting(false); + }; + + const copyToClipboard = async () => { + window.navigator.clipboard.writeText(apiKey.secret); + showToast("API key copied to clipboard!", "info"); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 1200); + }; + + if (loading) { + return ( + <div className="relative w-full w-full max-h-full"> + <div className="relative bg-white rounded-lg shadow dark:bg-stone-700"> + <div className="flex items-start justify-between px-6 py-4"> + <p className="text-gray-800 dark:text-stone-200 text-base "> + Generate an API Key for your AnythingLLM instance. + </p> + </div> + <div className="px-1 md:px-8 pb-10 "> + <PreLoader /> + </div> + </div> + </div> + ); + } + + if (!apiKey) { + return ( + <div className="relative w-full w-full max-h-full"> + <div className="relative bg-white rounded-lg shadow dark:bg-stone-700"> + <div className="flex items-start justify-between px-6 py-4"> + <p className="text-gray-800 dark:text-stone-200 text-base "> + Generate an API Key for your AnythingLLM instance. + </p> + </div> + <div className="md:px-8 pb-10 "> + <div className="flex flex-col gap-y-1 text-gray-800 dark:text-stone-200 mb-2"> + <p> + No api key for this instance exists. Create one by clicking the + button below. + </p> + <a + href={paths.apiDocs()} + target="_blank" + className="dark:text-blue-300 text-blue-600 hover:underline" + > + View endpoint documentation → + </a> + </div> + <button + disabled={generating} + type="button" + onClick={generateApiKey} + className="w-full text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600" + > + {generating ? "Generating..." : "Generate new API key"} + </button> + </div> + </div> + </div> + ); + } + + return ( + <div className="relative w-full w-full max-h-full"> + <div className="relative bg-white rounded-lg shadow dark:bg-stone-700"> + <div className="flex flex-col items-start justify-between px-6 py-4"> + <p className="text-gray-800 dark:text-stone-200 text-base "> + Use this API key for interacting with your AnythingLLM instance + programmatically. + </p> + <a + href={paths.apiDocs()} + target="_blank" + className="dark:text-blue-300 text-blue-600 hover:underline" + > + View endpoint documentation → + </a> + </div> + + <div className="md:px-8 pb-10"> + <div className="mb-6"> + <div className="flex flex-col md:flex-row items-center"> + <div className="flex md:flex-row flex-col gap-y-2 w-full gap-x-2 items-center px-4 md:px-0"> + <input + key={apiKey.secret} + type="text" + disabled={true} + className="w-full md:w-1/2 bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200" + defaultValue={apiKey.secret} + autoComplete="off" + spellCheck={false} + /> + <button + onClick={copyToClipboard} + disabled={copied} + className="w-full flex justify-center items-center gap-x-2 md:w-fit disabled:bg-green-300 dark:disabled:bg-green-600 bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200 group hover:bg-gray-100 dark:hover:bg-stone-600" + > + {copied ? ( + <CheckCircle className="stroke-green-800 dark:stroke-green-300" /> + ) : ( + <Copy /> + )} + <p className="block md:hidden text-base">Copy API Key</p> + </button> + <button + onClick={() => { + if ( + !confirm( + "Are you sure you want to refresh the API key? The old key will no longer work!" + ) + ) + return false; + generateApiKey(); + }} + disabled={generating} + className="w-full flex justify-center items-center gap-x-2 md:w-fit disabled:bg-green-300 dark:disabled:bg-green-600 bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200 group hover:bg-gray-100 dark:hover:bg-stone-600" + > + <RefreshCcw /> + <p className="block md:hidden text-base"> + Regenerate API Key + </p> + </button> + <button + onClick={() => { + if ( + !confirm( + "Are you sure you want to delete the API key? All API keys will be deleted." + ) + ) + return false; + removeApiKey(); + }} + disabled={deleting} + className="w-full flex justify-center items-center gap-x-2 md:w-fit disabled:bg-red-300 dark:disabled:bg-red-600 border border-red-500 text-red-900 placeholder-red-500 text-sm rounded-lg dark:bg-transparent focus:border-red-500 block p-2.5 dark:text-red-200 dark:placeholder-red-500 dark:border-red-200 group hover:bg-red-100 dark:hover:bg-red-600" + > + <Trash /> + <p className="block md:hidden text-base">Delete API Key</p> + </button> + </div> + </div> + </div> + </div> + </div> + </div> + ); +} diff --git a/frontend/src/components/Modals/Settings/index.jsx b/frontend/src/components/Modals/Settings/index.jsx index 591b06a62..a531c5078 100644 --- a/frontend/src/components/Modals/Settings/index.jsx +++ b/frontend/src/components/Modals/Settings/index.jsx @@ -8,6 +8,7 @@ import useUser from "../../../hooks/useUser"; import VectorDBSelection from "./VectorDbs"; import LLMSelection from "./LLMSelection"; import Appearance from "./Appearance"; +import ApiKey from "./ApiKey"; export const TABS = { llm: LLMSelection, @@ -16,6 +17,7 @@ export const TABS = { multiuser: MultiUserMode, vectordb: VectorDBSelection, appearance: Appearance, + apikey: ApiKey, }; const noop = () => false; diff --git a/frontend/src/components/Sidebar/SettingsOverlay/index.jsx b/frontend/src/components/Sidebar/SettingsOverlay/index.jsx index 794968f38..f71f8921d 100644 --- a/frontend/src/components/Sidebar/SettingsOverlay/index.jsx +++ b/frontend/src/components/Sidebar/SettingsOverlay/index.jsx @@ -7,6 +7,7 @@ import { Database, MessageSquare, Eye, + Key, } from "react-feather"; import SystemSettingsModal, { useSystemSettingsModal, @@ -127,6 +128,12 @@ export default function SettingsOverlay() { isActive={tab === "multiuser"} onClick={() => selectTab("multiuser")} /> + <Option + btnText="API Key" + icon={<Key className="h-4 w-4 flex-shrink-0" />} + isActive={tab === "apikey"} + onClick={() => selectTab("apikey")} + /> </> )} </div> diff --git a/frontend/src/models/admin.js b/frontend/src/models/admin.js index e98a18707..1c78f07a6 100644 --- a/frontend/src/models/admin.js +++ b/frontend/src/models/admin.js @@ -232,6 +232,51 @@ const Admin = { return { success: false, error: e.message }; }); }, + + // API Keys + getApiKeys: async function () { + return fetch(`${API_BASE}/admin/api-keys`, { + method: "GET", + headers: baseHeaders(), + }) + .then((res) => { + if (!res.ok) { + throw new Error(res.statusText || "Error fetching api keys."); + } + return res.json(); + }) + .catch((e) => { + console.error(e); + return { apiKeys: [], error: e.message }; + }); + }, + generateApiKey: async function () { + return fetch(`${API_BASE}/admin/generate-api-key`, { + method: "POST", + headers: baseHeaders(), + }) + .then((res) => { + if (!res.ok) { + throw new Error(res.statusText || "Error generating api key."); + } + return res.json(); + }) + .catch((e) => { + console.error(e); + return { apiKey: null, error: e.message }; + }); + }, + deleteApiKey: async function (apiKeyId = "") { + return fetch(`${API_BASE}/admin/delete-api-key/${apiKeyId}`, { + method: "DELETE", + headers: baseHeaders(), + }) + .then((res) => res.ok) + .catch((e) => { + console.error(e); + return false; + }); + }, }; export default Admin; diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index 2405d283e..bcab69f3e 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -220,6 +220,49 @@ const System = { return { success: false, error: e.message }; }); }, + getApiKey: async function () { + return fetch(`${API_BASE}/system/api-key`, { + method: "GET", + headers: baseHeaders(), + }) + .then((res) => { + if (!res.ok) { + throw new Error(res.statusText || "Error fetching api key."); + } + return res.json(); + }) + .catch((e) => { + console.error(e); + return { apiKey: null, error: e.message }; + }); + }, + generateApiKey: async function () { + return fetch(`${API_BASE}/system/generate-api-key`, { + method: "POST", + headers: baseHeaders(), + }) + .then((res) => { + if (!res.ok) { + throw new Error(res.statusText || "Error generating api key."); + } + return res.json(); + }) + .catch((e) => { + console.error(e); + return { apiKey: null, error: e.message }; + }); + }, + deleteApiKey: async function () { + return fetch(`${API_BASE}/system/api-key`, { + method: "DELETE", + headers: baseHeaders(), + }) + .then((res) => res.ok) + .catch((e) => { + console.error(e); + return false; + }); + }, }; export default System; diff --git a/frontend/src/pages/Admin/ApiKeys/ApiKeyRow/index.jsx b/frontend/src/pages/Admin/ApiKeys/ApiKeyRow/index.jsx new file mode 100644 index 000000000..b6645f3bd --- /dev/null +++ b/frontend/src/pages/Admin/ApiKeys/ApiKeyRow/index.jsx @@ -0,0 +1,69 @@ +import { useEffect, useRef, useState } from "react"; +import Admin from "../../../../models/admin"; +import showToast from "../../../../utils/toast"; + +export default function ApiKeyRow({ apiKey }) { + const rowRef = useRef(null); + const [copied, setCopied] = useState(false); + const handleDelete = async () => { + if ( + !window.confirm( + `Are you sure you want to deactivate this api key?\nAfter you do this it will not longer be useable.\n\nThis action is irreversible.` + ) + ) + return false; + if (rowRef?.current) { + rowRef.current.remove(); + } + await Admin.deleteApiKey(apiKey.id); + showToast("API Key permanently deleted", "info"); + }; + const copyApiKey = () => { + if (!apiKey) return false; + window.navigator.clipboard.writeText(apiKey.secret); + showToast("API Key copied to clipboard", "success"); + setCopied(true); + }; + + useEffect(() => { + function resetStatus() { + if (!copied) return false; + setTimeout(() => { + setCopied(false); + }, 3000); + } + resetStatus(); + }, [copied]); + + return ( + <> + <tr ref={rowRef} className="bg-transparent"> + <td + scope="row" + className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white font-mono" + > + {apiKey.secret} + </td> + <td className="px-6 py-4"> + {apiKey.createdBy?.username || "unknown user"} + </td> + <td className="px-6 py-4">{apiKey.createdAt}</td> + <td className="px-6 py-4 flex items-center gap-x-6"> + <button + onClick={copyApiKey} + disabled={copied} + className="font-medium text-blue-600 dark:text-blue-300 px-2 py-1 rounded-lg hover:bg-blue-50 hover:dark:bg-blue-800 hover:dark:bg-opacity-20" + > + {copied ? "Copied" : "Copy API Key"} + </button> + <button + onClick={handleDelete} + className="font-medium text-red-600 dark:text-red-300 px-2 py-1 rounded-lg hover:bg-red-50 hover:dark:bg-red-800 hover:dark:bg-opacity-20" + > + Deactivate API Key + </button> + </td> + </tr> + </> + ); +} diff --git a/frontend/src/pages/Admin/ApiKeys/NewApiKeyModal/index.jsx b/frontend/src/pages/Admin/ApiKeys/NewApiKeyModal/index.jsx new file mode 100644 index 000000000..ae59eff71 --- /dev/null +++ b/frontend/src/pages/Admin/ApiKeys/NewApiKeyModal/index.jsx @@ -0,0 +1,118 @@ +import React, { useEffect, useState } from "react"; +import { X } from "react-feather"; +import Admin from "../../../../models/admin"; +import paths from "../../../../utils/paths"; +const DIALOG_ID = `new-api-key-modal`; + +function hideModal() { + document.getElementById(DIALOG_ID)?.close(); +} + +export const NewApiKeyModalId = DIALOG_ID; +export default function NewApiKeyModal() { + const [apiKey, setApiKey] = useState(null); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); + + const handleCreate = async (e) => { + setError(null); + e.preventDefault(); + const { apiKey: newApiKey, error } = await Admin.generateApiKey(); + if (!!newApiKey) setApiKey(newApiKey); + setError(error); + }; + const copyApiKey = () => { + if (!apiKey) return false; + window.navigator.clipboard.writeText(apiKey.secret); + setCopied(true); + }; + useEffect(() => { + function resetStatus() { + if (!copied) return false; + setTimeout(() => { + setCopied(false); + }, 3000); + } + resetStatus(); + }, [copied]); + + return ( + <dialog id={DIALOG_ID} className="bg-transparent outline-none"> + <div className="relative w-full max-w-2xl max-h-full"> + <div className="relative bg-white rounded-lg shadow dark:bg-stone-700"> + <div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"> + <h3 className="text-xl font-semibold text-gray-900 dark:text-white"> + Create new API key + </h3> + <button + onClick={hideModal} + type="button" + className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" + data-modal-hide="staticModal" + > + <X className="text-gray-300 text-lg" /> + </button> + </div> + <form onSubmit={handleCreate}> + <div className="p-6 space-y-6 flex h-full w-full"> + <div className="w-full flex flex-col gap-y-4"> + {error && ( + <p className="text-red-600 dark:text-red-400 text-sm"> + Error: {error} + </p> + )} + {apiKey && ( + <input + type="text" + defaultValue={`${apiKey.secret}`} + disabled={true} + className="rounded-lg px-4 py-2 text-gray-800 bg-gray-100 dark:text-slate-200 dark:bg-stone-800" + /> + )} + <p className="text-gray-800 dark:text-slate-200 text-xs md:text-sm"> + Once created the API key can be used to programmatically + access and configure this AnythingLLM instance. + </p> + <a + href={paths.apiDocs()} + target="_blank" + className="text-blue-600 dark:text-blue-300 hover:underline" + > + Read the API documentation → + </a> + </div> + </div> + <div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"> + {!apiKey ? ( + <> + <button + onClick={hideModal} + type="button" + className="text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:hover:bg-stone-900" + > + Cancel + </button> + <button + type="submit" + className="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800" + > + Create API key + </button> + </> + ) : ( + <button + onClick={copyApiKey} + type="button" + disabled={copied} + className="w-full disabled:bg-green-200 disabled:text-green-600 text-gray-800 bg-gray-100 px-4 py-2 rounded-lg dark:text-slate-200 dark:bg-stone-900" + > + {copied ? "Copied API key" : "Copy API key"} + </button> + )} + </div> + </form> + </div> + </div> + </dialog> + ); +} diff --git a/frontend/src/pages/Admin/ApiKeys/index.jsx b/frontend/src/pages/Admin/ApiKeys/index.jsx new file mode 100644 index 000000000..f685b6c68 --- /dev/null +++ b/frontend/src/pages/Admin/ApiKeys/index.jsx @@ -0,0 +1,109 @@ +import { useEffect, useState } from "react"; +import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar"; +import { isMobile } from "react-device-detect"; +import * as Skeleton from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; +import { PlusCircle } from "react-feather"; +import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode"; +import Admin from "../../../models/admin"; +import ApiKeyRow from "./ApiKeyRow"; +import NewApiKeyModal, { NewApiKeyModalId } from "./NewApiKeyModal"; +import paths from "../../../utils/paths"; + +export default function AdminApiKeys() { + return ( + <div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex"> + {!isMobile && <Sidebar />} + <div + style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} + className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll" + > + {isMobile && <SidebarMobileHeader />} + <div className="flex flex-col w-full px-1 md:px-8"> + <div className="w-full flex flex-col gap-y-1"> + <div className="items-center flex gap-x-4"> + <p className="text-3xl font-semibold text-slate-600 dark:text-slate-200"> + API Keys + </p> + <button + onClick={() => + document?.getElementById(NewApiKeyModalId)?.showModal() + } + className="border border-slate-800 dark:border-slate-200 px-4 py-1 rounded-lg text-slate-800 dark:text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-800 hover:text-slate-100 dark:hover:bg-slate-200 dark:hover:text-slate-800" + > + <PlusCircle className="h-4 w-4" /> Generate New API Key + </button> + </div> + <p className="text-sm font-base text-slate-600 dark:text-slate-200"> + API keys allow the holder to programmatically access and manage + this AnythingLLM instance. + </p> + <a + href={paths.apiDocs()} + target="_blank" + className="text-blue-600 dark:text-blue-300 hover:underline" + > + Read the API documentation → + </a> + </div> + <ApiKeysContainer /> + </div> + <NewApiKeyModal /> + </div> + </div> + ); +} + +function ApiKeysContainer() { + const darkMode = usePrefersDarkMode(); + const [loading, setLoading] = useState(true); + const [apiKeys, setApiKeys] = useState([]); + useEffect(() => { + async function fetchExistingKeys() { + const { apiKeys: foundKeys } = await Admin.getApiKeys(); + setApiKeys(foundKeys); + setLoading(false); + } + fetchExistingKeys(); + }, []); + + if (loading) { + return ( + <Skeleton.default + height="80vh" + width="100%" + baseColor={darkMode ? "#2a3a53" : null} + highlightColor={darkMode ? "#395073" : null} + count={1} + className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6" + containerClassName="flex w-full" + /> + ); + } + + return ( + <table className="md:w-3/4 w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5"> + <thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400"> + <tr> + <th scope="col" className="px-6 py-3"> + API Key + </th> + <th scope="col" className="px-6 py-3"> + Created By + </th> + <th scope="col" className="px-6 py-3"> + Created + </th> + <th scope="col" className="px-6 py-3 rounded-tr-lg"> + Actions + </th> + </tr> + </thead> + <tbody> + {apiKeys.map((apiKey) => ( + <ApiKeyRow key={apiKey.id} apiKey={apiKey} /> + ))} + </tbody> + </table> + ); +} diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index 707d49763..4e1ab138d 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -30,6 +30,9 @@ export default { exports: () => { return `${API_BASE.replace("/api", "")}/system/data-exports`; }, + apiDocs: () => { + return `${API_BASE}/docs`; + }, admin: { system: () => { return `/admin/system-preferences`; @@ -49,5 +52,8 @@ export default { appearance: () => { return "/admin/appearance"; }, + apiKeys: () => { + return "/admin/api-keys"; + }, }, }; diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index d27ccf230..e72011004 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -1,3 +1,4 @@ +const { ApiKey } = require("../models/apiKeys"); const { Document } = require("../models/documents"); const { Invite } = require("../models/invite"); const { SystemSettings } = require("../models/systemSettings"); @@ -8,8 +9,6 @@ const { WorkspaceChats } = require("../models/workspaceChats"); const { getVectorDbClass } = require("../utils/helpers"); const { userFromSession, reqBody } = require("../utils/http"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); -const { setupLogoUploads } = require("../utils/files/multer"); -const { handleLogoUploads } = setupLogoUploads(); function adminEndpoints(app) { if (!app) return; @@ -345,6 +344,72 @@ function adminEndpoints(app) { } } ); + + app.get("/admin/api-keys", [validatedRequest], async (request, response) => { + try { + const user = await userFromSession(request, response); + if (!user || user?.role !== "admin") { + response.sendStatus(401).end(); + return; + } + + const apiKeys = await ApiKey.whereWithUser("id IS NOT NULL"); + return response.status(200).json({ + apiKeys, + error: null, + }); + } catch (error) { + console.error(error); + response.status(500).json({ + apiKey: null, + error: "Could not find an API Keys.", + }); + } + }); + + app.post( + "/admin/generate-api-key", + [validatedRequest], + async (request, response) => { + try { + const user = await userFromSession(request, response); + if (!user || user?.role !== "admin") { + response.sendStatus(401).end(); + return; + } + + const { apiKey, error } = await ApiKey.create(user.id); + return response.status(200).json({ + apiKey, + error, + }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + } + ); + + app.delete( + "/admin/delete-api-key/:id", + [validatedRequest], + async (request, response) => { + try { + const { id } = request.params; + const user = await userFromSession(request, response); + if (!user || user?.role !== "admin") { + response.sendStatus(401).end(); + return; + } + + await ApiKey.delete(`id = ${id}`); + return response.status(200).end(); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + } + ); } module.exports = { adminEndpoints }; diff --git a/server/endpoints/api/admin/index.js b/server/endpoints/api/admin/index.js new file mode 100644 index 000000000..bc39e24e2 --- /dev/null +++ b/server/endpoints/api/admin/index.js @@ -0,0 +1,642 @@ +const { Invite } = require("../../../models/invite"); +const { SystemSettings } = require("../../../models/systemSettings"); +const { User } = require("../../../models/user"); +const { Workspace } = require("../../../models/workspace"); +const { WorkspaceChats } = require("../../../models/workspaceChats"); +const { multiUserMode, reqBody } = require("../../../utils/http"); +const { validApiKey } = require("../../../utils/middleware/validApiKey"); + +function apiAdminEndpoints(app) { + if (!app) return; + + app.get("/v1/admin/is-multi-user-mode", [validApiKey], (_, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.description = 'Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + "isMultiUser": true + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + const isMultiUser = multiUserMode(response); + response.status(200).json({ isMultiUser }); + }); + + app.get("/v1/admin/users", [validApiKey], async (request, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.description = 'Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + "users": [ + { + username: "sample-sam", + role: 'default', + } + ] + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[401] = { + description: "Instance is not in Multi-User mode. Method denied", + } + */ + try { + if (!multiUserMode(response)) { + response.sendStatus(401).end(); + return; + } + + const users = (await User.where()).map((user) => { + const { password, ...rest } = user; + return rest; + }); + response.status(200).json({ users }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + }); + + app.post("/v1/admin/users/new", [validApiKey], async (request, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.description = 'Create a new user with username and password. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.requestBody = { + description: 'Key pair object that will define the new user to add to the system.', + required: true, + type: 'object', + content: { + "application/json": { + example: { + username: "sample-sam", + password: 'hunter2', + role: 'default | admin' + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + user: { + id: 1, + username: 'sample-sam', + role: 'default', + }, + error: null, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[401] = { + description: "Instance is not in Multi-User mode. Method denied", + } + */ + try { + if (!multiUserMode(response)) { + response.sendStatus(401).end(); + return; + } + + const newUserParams = reqBody(request); + const { user: newUser, error } = await User.create(newUserParams); + response.status(200).json({ user: newUser, error }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + }); + + app.post("/v1/admin/users/:id", [validApiKey], async (request, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.path = '/v1/admin/users/{id}' + #swagger.parameters['id'] = { + in: 'path', + description: 'id of the user in the database.', + required: true, + type: 'string' + } + #swagger.description = 'Update existing user settings. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.requestBody = { + description: 'Key pair object that will update the found user. All fields are optional and will not update unless specified.', + required: true, + type: 'object', + content: { + "application/json": { + example: { + username: "sample-sam", + password: 'hunter2', + role: 'default | admin', + suspended: 0, + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + success: true, + error: null, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[401] = { + description: "Instance is not in Multi-User mode. Method denied", + } + */ + try { + if (!multiUserMode(response)) { + response.sendStatus(401).end(); + return; + } + + const { id } = request.params; + const updates = reqBody(request); + const { success, error } = await User.update(id, updates); + response.status(200).json({ success, error }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + }); + + app.delete( + "/v1/admin/users/:id", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.description = 'Delete existing user by id. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.path = '/v1/admin/users/{id}' + #swagger.parameters['id'] = { + in: 'path', + description: 'id of the user in the database.', + required: true, + type: 'string' + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + success: true, + error: null, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[401] = { + description: "Instance is not in Multi-User mode. Method denied", + } + */ + try { + if (!multiUserMode(response)) { + response.sendStatus(401).end(); + return; + } + + const { id } = request.params; + await User.delete(`id = ${id}`); + response.status(200).json({ success: true, error: null }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + } + ); + + app.get("/v1/admin/invites", [validApiKey], async (request, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.description = 'List all existing invitations to instance regardless of status. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + "invites": [ + { + id: 1, + status: "pending", + code: 'abc-123', + claimedBy: null + } + ] + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[401] = { + description: "Instance is not in Multi-User mode. Method denied", + } + */ + try { + if (!multiUserMode(response)) { + response.sendStatus(401).end(); + return; + } + + const invites = await Invite.whereWithUsers(); + response.status(200).json({ invites }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + }); + + app.post("/v1/admin/invite/new", [validApiKey], async (request, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.description = 'Create a new invite code for someone to use to register with instance. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + invite: { + id: 1, + status: "pending", + code: 'abc-123', + }, + error: null, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[401] = { + description: "Instance is not in Multi-User mode. Method denied", + } + */ + try { + if (!multiUserMode(response)) { + response.sendStatus(401).end(); + return; + } + + const { invite, error } = await Invite.create(); + response.status(200).json({ invite, error }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + }); + + app.delete( + "/v1/admin/invite/:id", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.description = 'Deactivates (soft-delete) invite by id. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.path = '/v1/admin/invite/{id}' + #swagger.parameters['id'] = { + in: 'path', + description: 'id of the invite in the database.', + required: true, + type: 'string' + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + success: true, + error: null, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[401] = { + description: "Instance is not in Multi-User mode. Method denied", + } + */ + try { + if (!multiUserMode(response)) { + response.sendStatus(401).end(); + return; + } + + const { id } = request.params; + const { success, error } = await Invite.deactivate(id); + response.status(200).json({ success, error }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + } + ); + + app.post( + "/v1/admin/workspaces/:workspaceId/update-users", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.path = '/v1/admin/workspaces/{workspaceId}/update-users' + #swagger.parameters['workspaceId'] = { + in: 'path', + description: 'id of the workspace in the database.', + required: true, + type: 'string' + } + #swagger.description = 'Overwrite workspace permissions to only be accessible by the given user ids and admins. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.requestBody = { + description: 'Entire array of user ids who can access the workspace. All fields are optional and will not update unless specified.', + required: true, + type: 'object', + content: { + "application/json": { + example: { + userIds: [1,2,4,12], + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + success: true, + error: null, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[401] = { + description: "Instance is not in Multi-User mode. Method denied", + } + */ + try { + if (!multiUserMode(response)) { + response.sendStatus(401).end(); + return; + } + + const { workspaceId } = request.params; + const { userIds } = reqBody(request); + const { success, error } = await Workspace.updateUsers( + workspaceId, + userIds + ); + response.status(200).json({ success, error }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + } + ); + + app.post( + "/v1/admin/workspace-chats", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.description = 'All chats in the system ordered by most recent. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.requestBody = { + description: 'Page offset to show of workspace chats. All fields are optional and will not update unless specified.', + required: false, + type: 'integer', + content: { + "application/json": { + example: { + offset: 2, + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + success: true, + error: null, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[401] = { + description: "Instance is not in Multi-User mode. Method denied", + } + */ + try { + if (!multiUserMode(response)) { + response.sendStatus(401).end(); + return; + } + + const { offset = 0 } = reqBody(request); + const chats = await WorkspaceChats.whereWithData(`id >= ${offset}`, 20); + const hasPages = (await WorkspaceChats.count()) > 20; + response.status(200).json({ chats: chats.reverse(), hasPages }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + } + ); + + app.get("/v1/admin/preferences", [validApiKey], async (request, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.description = 'Show all multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + settings: { + users_can_delete_workspaces: true, + limit_user_messages: false, + message_limit: 10, + } + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[401] = { + description: "Instance is not in Multi-User mode. Method denied", + } + */ + try { + if (!multiUserMode(response)) { + response.sendStatus(401).end(); + return; + } + + const settings = { + users_can_delete_workspaces: + (await SystemSettings.get(`label = 'users_can_delete_workspaces'`)) + ?.value === "true", + limit_user_messages: + (await SystemSettings.get(`label = 'limit_user_messages'`))?.value === + "true", + message_limit: + Number( + (await SystemSettings.get(`label = 'message_limit'`))?.value + ) || 10, + }; + response.status(200).json({ settings }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + }); + + app.post( + "/v1/admin/preferences", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Admin'] + #swagger.description = 'Update multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.' + #swagger.requestBody = { + description: 'Object with setting key and new value to set. All keys are optional and will not update unless specified.', + required: true, + type: 'object', + content: { + "application/json": { + example: { + users_can_delete_workspaces: false, + limit_user_messages: true, + message_limit: 5, + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + success: true, + error: null, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + #swagger.responses[401] = { + description: "Instance is not in Multi-User mode. Method denied", + } + */ + try { + if (!multiUserMode(response)) { + response.sendStatus(401).end(); + return; + } + + const updates = reqBody(request); + await SystemSettings.updateSettings(updates); + response.status(200).json({ success: true, error: null }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + } + ); +} + +module.exports = { apiAdminEndpoints }; diff --git a/server/endpoints/api/auth/index.js b/server/endpoints/api/auth/index.js new file mode 100644 index 000000000..e58420b00 --- /dev/null +++ b/server/endpoints/api/auth/index.js @@ -0,0 +1,33 @@ +const { validApiKey } = require("../../../utils/middleware/validApiKey"); + +function apiAuthEndpoints(app) { + if (!app) return; + + app.get("/v1/auth", [validApiKey], (_, response) => { + /* + #swagger.tags = ['Authentication'] + #swagger.description = 'Verify the attached Authentication header contains a valid API token.' + #swagger.responses[200] = { + description: 'Valid auth token was found.', + content: { + "application/json": { + schema: { + type: 'object', + example: { + authenticated: true, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + response.status(200).json({ authenticated: true }); + }); +} + +module.exports = { apiAuthEndpoints }; diff --git a/server/endpoints/api/document/index.js b/server/endpoints/api/document/index.js new file mode 100644 index 000000000..28de98a78 --- /dev/null +++ b/server/endpoints/api/document/index.js @@ -0,0 +1,194 @@ +const { Telemetry } = require("../../../models/telemetry"); +const { validApiKey } = require("../../../utils/middleware/validApiKey"); +const { setupMulter } = require("../../../utils/files/multer"); +const { + checkPythonAppAlive, + acceptedFileTypes, + processDocument, +} = require("../../../utils/files/documentProcessor"); +const { viewLocalFiles } = require("../../../utils/files"); +const { handleUploads } = setupMulter(); + +function apiDocumentEndpoints(app) { + if (!app) return; + + app.post( + "/v1/document/upload", + [validApiKey], + handleUploads.single("file"), + async (request, response) => { + /* + #swagger.tags = ['Documents'] + #swagger.description = 'Upload a new file to AnythingLLM to be parsed and prepared for embedding.' + + #swagger.requestBody = { + description: 'File to be uploaded.', + required: true, + type: 'file', + content: { + "multipart/form-data": { + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + } + } + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + success: true, + error: null, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { originalname } = request.file; + const processingOnline = await checkPythonAppAlive(); + + if (!processingOnline) { + response + .status(500) + .json({ + success: false, + error: `Python processing API is not online. Document ${originalname} will not be processed automatically.`, + }) + .end(); + } + + const { success, reason } = await processDocument(originalname); + if (!success) { + response.status(500).json({ success: false, error: reason }).end(); + } + + console.log( + `Document ${originalname} uploaded processed and successfully. It is now available in documents.` + ); + await Telemetry.sendTelemetry("document_uploaded"); + response.status(200).json({ success: true, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.get("/v1/documents", [validApiKey], async (_, response) => { + /* + #swagger.tags = ['Documents'] + #swagger.description = 'List of all locally-stored documents in instance' + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + "localFiles": { + "name": "documents", + "type": "folder", + items: [ + { + "name": "my-stored-document.json", + "type": "file", + "id": "bb07c334-4dab-4419-9462-9d00065a49a1", + "url": "file://my-stored-document.txt", + "title": "my-stored-document.txt", + "cached": false + }, + ] + } + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const localFiles = await viewLocalFiles(); + response.status(200).json({ localFiles }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get( + "/v1/document/accepted-file-types", + [validApiKey], + async (_, response) => { + /* + #swagger.tags = ['Documents'] + #swagger.description = 'Check available filetypes and MIMEs that can be uploaded.' + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + "types": { + "application/mbox": [ + ".mbox" + ], + "application/pdf": [ + ".pdf" + ], + "application/vnd.oasis.opendocument.text": [ + ".odt" + ], + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [ + ".docx" + ], + "text/plain": [ + ".txt", + ".md" + ] + } + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const types = await acceptedFileTypes(); + if (!types) { + response.sendStatus(404).end(); + return; + } + + response.status(200).json({ types }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); +} + +module.exports = { apiDocumentEndpoints }; diff --git a/server/endpoints/api/index.js b/server/endpoints/api/index.js new file mode 100644 index 000000000..e5149ad70 --- /dev/null +++ b/server/endpoints/api/index.js @@ -0,0 +1,21 @@ +const { useSwagger } = require("../../swagger/utils"); +const { apiAdminEndpoints } = require("./admin"); +const { apiAuthEndpoints } = require("./auth"); +const { apiDocumentEndpoints } = require("./document"); +const { apiSystemEndpoints } = require("./system"); +const { apiWorkspaceEndpoints } = require("./workspace"); + +// All endpoints must be documented and pass through the validApiKey Middleware. +// How to JSDoc an endpoint +// https://www.npmjs.com/package/swagger-autogen#openapi-3x +function developerEndpoints(app, router) { + if (!router) return; + useSwagger(app); + apiAuthEndpoints(router); + apiAdminEndpoints(router); + apiSystemEndpoints(router); + apiWorkspaceEndpoints(router); + apiDocumentEndpoints(router); +} + +module.exports = { developerEndpoints }; diff --git a/server/endpoints/api/system/index.js b/server/endpoints/api/system/index.js new file mode 100644 index 000000000..dd5f59b75 --- /dev/null +++ b/server/endpoints/api/system/index.js @@ -0,0 +1,153 @@ +const { SystemSettings } = require("../../../models/systemSettings"); +const { getVectorDbClass } = require("../../../utils/helpers"); +const { dumpENV, updateENV } = require("../../../utils/helpers/updateENV"); +const { reqBody } = require("../../../utils/http"); +const { validApiKey } = require("../../../utils/middleware/validApiKey"); + +function apiSystemEndpoints(app) { + if (!app) return; + + app.get("/v1/system/env-dump", async (_, response) => { + /* + #swagger.tags = ['System Settings'] + #swagger.description = 'Dump all settings to file storage' + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + if (process.env.NODE_ENV !== "production") + return response.sendStatus(200).end(); + await dumpENV(); + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/system", [validApiKey], async (_, response) => { + /* + #swagger.tags = ['System Settings'] + #swagger.description = 'Get all current system settings that are defined.' + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + "settings": { + "VectorDB": "pinecone", + "PineConeEnvironment": "us-west4-gcp-free", + "PineConeKey": true, + "PineConeIndex": "my-pinecone-index", + "LLMProvider": "azure", + "[KEY_NAME]": "KEY_VALUE", + } + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const settings = await SystemSettings.currentSettings(); + response.status(200).json({ settings }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/system/vector-count", [validApiKey], async (_, response) => { + /* + #swagger.tags = ['System Settings'] + #swagger.description = 'Number of all vectors in connected vector database' + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + "vectorCount": 5450 + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const VectorDb = getVectorDbClass(); + const vectorCount = await VectorDb.totalIndicies(); + response.status(200).json({ vectorCount }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.post( + "/v1/system/update-env", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['System Settings'] + #swagger.description = 'Update a system setting or preference.' + #swagger.requestBody = { + description: 'Key pair object that matches a valid setting and value. Get keys from GET /v1/system or refer to codebase.', + required: true, + type: 'object', + content: { + "application/json": { + example: { + VectorDB: "lancedb", + AnotherKey: "updatedValue" + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + newValues: {"[ENV_KEY]": 'Value'}, + error: 'error goes here, otherwise null' + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const body = reqBody(request); + const { newValues, error } = updateENV(body); + if (process.env.NODE_ENV === "production") await dumpENV(); + response.status(200).json({ newValues, error }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); +} + +module.exports = { apiSystemEndpoints }; diff --git a/server/endpoints/api/workspace/index.js b/server/endpoints/api/workspace/index.js new file mode 100644 index 000000000..35a16edce --- /dev/null +++ b/server/endpoints/api/workspace/index.js @@ -0,0 +1,430 @@ +const { Document } = require("../../../models/documents"); +const { Telemetry } = require("../../../models/telemetry"); +const { DocumentVectors } = require("../../../models/vectors"); +const { Workspace } = require("../../../models/workspace"); +const { WorkspaceChats } = require("../../../models/workspaceChats"); +const { convertToChatHistory } = require("../../../utils/chats"); +const { getVectorDbClass } = require("../../../utils/helpers"); +const { multiUserMode, reqBody } = require("../../../utils/http"); +const { validApiKey } = require("../../../utils/middleware/validApiKey"); + +function apiWorkspaceEndpoints(app) { + if (!app) return; + + app.post("/v1/workspace/new", [validApiKey], async (request, response) => { + /* + #swagger.tags = ['Workspaces'] + #swagger.description = 'Create a new workspace' + #swagger.requestBody = { + description: 'JSON object containing new display name of workspace.', + required: true, + type: 'object', + content: { + "application/json": { + example: { + name: "My New Workspace", + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + workspace: { + "id": 79, + "name": "Sample workspace", + "slug": "sample-workspace", + "createdAt": "2023-08-17 00:45:03", + "openAiTemp": null, + "lastUpdatedAt": "2023-08-17 00:45:03", + "openAiHistory": 20, + "openAiPrompt": null + }, + message: 'Workspace created' + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { name = null } = reqBody(request); + const { workspace, message } = await Workspace.new(name); + await Telemetry.sendTelemetry("workspace_created", { + multiUserMode: multiUserMode(response), + LLMSelection: process.env.LLM_PROVIDER || "openai", + VectorDbSelection: process.env.VECTOR_DB || "pinecone", + }); + response.status(200).json({ workspace, message }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/workspaces", [validApiKey], async (request, response) => { + /* + #swagger.tags = ['Workspaces'] + #swagger.description = 'List all current workspaces' + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + workspaces: [ + { + "id": 79, + "name": "Sample workspace", + "slug": "sample-workspace", + "createdAt": "2023-08-17 00:45:03", + "openAiTemp": null, + "lastUpdatedAt": "2023-08-17 00:45:03", + "openAiHistory": 20, + "openAiPrompt": null + } + ], + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const workspaces = await Workspace.where(); + response.status(200).json({ workspaces }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/workspace/:slug", [validApiKey], async (request, response) => { + /* + #swagger.tags = ['Workspaces'] + #swagger.description = 'Get a workspace by its unique slug.' + #swagger.path = '/v1/workspace/{slug}' + #swagger.parameters['slug'] = { + in: 'path', + description: 'Unique slug of workspace to find', + required: true, + type: 'string' + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + workspace: { + "id": 79, + "name": "My workspace", + "slug": "my-workspace-123", + "createdAt": "2023-08-17 00:45:03", + "openAiTemp": null, + "lastUpdatedAt": "2023-08-17 00:45:03", + "openAiHistory": 20, + "openAiPrompt": null, + "documents": [] + } + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { slug } = request.params; + const workspace = await Workspace.get(`slug = '${slug}'`); + response.status(200).json({ workspace }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.delete( + "/v1/workspace/:slug", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Workspaces'] + #swagger.description = 'Deletes a workspace by its slug.' + #swagger.path = '/v1/workspace/{slug}' + #swagger.parameters['slug'] = { + in: 'path', + description: 'Unique slug of workspace to delete', + required: true, + type: 'string' + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { slug = "" } = request.params; + const VectorDb = getVectorDbClass(); + const workspace = await Workspace.get(`slug = '${slug}'`); + + if (!workspace) { + response.sendStatus(400).end(); + return; + } + + await Workspace.delete(`slug = '${slug.toLowerCase()}'`); + await DocumentVectors.deleteForWorkspace(workspace.id); + await Document.delete(`workspaceId = ${Number(workspace.id)}`); + await WorkspaceChats.delete(`workspaceId = ${Number(workspace.id)}`); + try { + await VectorDb["delete-namespace"]({ namespace: slug }); + } catch (e) { + console.error(e.message); + } + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.post( + "/v1/workspace/:slug/update", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Workspaces'] + #swagger.description = 'Update workspace settings by its unique slug.' + #swagger.path = '/v1/workspace/{slug}/update' + #swagger.parameters['slug'] = { + in: 'path', + description: 'Unique slug of workspace to find', + required: true, + type: 'string' + } + #swagger.requestBody = { + description: 'JSON object containing new settings to update a workspace. All keys are optional and will not update unless provided', + required: true, + type: 'object', + content: { + "application/json": { + example: { + "name": 'Updated Workspace Name', + "openAiTemp": 0.2, + "openAiHistory": 20, + "openAiPrompt": "Respond to all inquires and questions in binary - do not respond in any other format." + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + workspace: { + "id": 79, + "name": "My workspace", + "slug": "my-workspace-123", + "createdAt": "2023-08-17 00:45:03", + "openAiTemp": null, + "lastUpdatedAt": "2023-08-17 00:45:03", + "openAiHistory": 20, + "openAiPrompt": null, + "documents": [] + }, + message: null, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { slug = null } = request.params; + const data = reqBody(request); + const currWorkspace = await Workspace.get(`slug = '${slug}'`); + + if (!currWorkspace) { + response.sendStatus(400).end(); + return; + } + + const { workspace, message } = await Workspace.update( + currWorkspace.id, + data + ); + response.status(200).json({ workspace, message }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.get( + "/v1/workspace/:slug/chats", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Workspaces'] + #swagger.description = 'Get a workspaces chats regardless of user by its unique slug.' + #swagger.path = '/v1/workspace/{slug}/chats' + #swagger.parameters['slug'] = { + in: 'path', + description: 'Unique slug of workspace to find', + required: true, + type: 'string' + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + history: [ + { + "role": "user", + "content": "What is AnythingLLM?", + "sentAt": 1692851630 + }, + { + "role": "assistant", + "content": "AnythingLLM is a platform that allows you to convert notes, PDFs, and other source materials into a chatbot. It ensures privacy, cites its answers, and allows multiple people to interact with the same documents simultaneously. It is particularly useful for businesses to enhance the visibility and readability of various written communications such as SOPs, contracts, and sales calls. You can try it out with a free trial to see if it meets your business needs.", + "sources": [{"source": "object about source document and snippets used"}] + } + ] + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { slug } = request.params; + const workspace = await Workspace.get(`slug = '${slug}'`); + + if (!workspace) { + response.sendStatus(400).end(); + return; + } + + const history = await WorkspaceChats.forWorkspace(workspace.id); + response.status(200).json({ history: convertToChatHistory(history) }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.post( + "/v1/workspace/:slug/update-embeddings", + [validApiKey], + async (request, response) => { + /* + #swagger.tags = ['Workspaces'] + #swagger.description = 'Add or remove documents from a workspace by its unique slug.' + #swagger.path = '/v1/workspace/{slug}/update-embeddings' + #swagger.parameters['slug'] = { + in: 'path', + description: 'Unique slug of workspace to find', + required: true, + type: 'string' + } + #swagger.requestBody = { + description: 'JSON object of additions and removals of documents to add to update a workspace. The value should be the folder + filename with the exclusions of the top-level documents path.', + required: true, + type: 'object', + content: { + "application/json": { + example: { + adds: [], + deletes: ["custom-documents/anythingllm-hash.json"] + } + } + } + } + #swagger.responses[200] = { + content: { + "application/json": { + schema: { + type: 'object', + example: { + workspace: { + "id": 79, + "name": "My workspace", + "slug": "my-workspace-123", + "createdAt": "2023-08-17 00:45:03", + "openAiTemp": null, + "lastUpdatedAt": "2023-08-17 00:45:03", + "openAiHistory": 20, + "openAiPrompt": null, + "documents": [] + }, + message: null, + } + } + } + } + } + #swagger.responses[403] = { + schema: { + "$ref": "#/definitions/InvalidAPIKey" + } + } + */ + try { + const { slug = null } = request.params; + const { adds = [], deletes = [] } = reqBody(request); + const currWorkspace = await Workspace.get(`slug = '${slug}'`); + + if (!currWorkspace) { + response.sendStatus(400).end(); + return; + } + + await Document.removeDocuments(currWorkspace, deletes); + await Document.addDocuments(currWorkspace, adds); + const updatedWorkspace = await Workspace.get(`slug = '${slug}'`); + response.status(200).json({ workspace: updatedWorkspace }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); +} + +module.exports = { apiWorkspaceEndpoints }; diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 72b525e0a..85416f4cd 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -36,6 +36,7 @@ const { } = require("../utils/files/logo"); const { Telemetry } = require("../models/telemetry"); const { WelcomeMessages } = require("../models/welcomeMessages"); +const { ApiKey } = require("../models/apiKeys"); function systemEndpoints(app) { if (!app) return; @@ -58,57 +59,7 @@ function systemEndpoints(app) { app.get("/setup-complete", async (_, response) => { try { - const llmProvider = process.env.LLM_PROVIDER || "openai"; - const vectorDB = process.env.VECTOR_DB || "pinecone"; - const results = { - CanDebug: !!!process.env.NO_DEBUG, - RequiresAuth: !!process.env.AUTH_TOKEN, - AuthToken: !!process.env.AUTH_TOKEN, - JWTSecret: !!process.env.JWT_SECRET, - StorageDir: process.env.STORAGE_DIR, - MultiUserMode: await SystemSettings.isMultiUserMode(), - VectorDB: vectorDB, - ...(vectorDB === "pinecone" - ? { - PineConeEnvironment: process.env.PINECONE_ENVIRONMENT, - PineConeKey: !!process.env.PINECONE_API_KEY, - PineConeIndex: process.env.PINECONE_INDEX, - } - : {}), - ...(vectorDB === "chroma" - ? { - ChromaEndpoint: process.env.CHROMA_ENDPOINT, - } - : {}), - ...(vectorDB === "weaviate" - ? { - WeaviateEndpoint: process.env.WEAVIATE_ENDPOINT, - WeaviateApiKey: process.env.WEAVIATE_API_KEY, - } - : {}), - ...(vectorDB === "qdrant" - ? { - QdrantEndpoint: process.env.QDRANT_ENDPOINT, - QdrantApiKey: process.env.QDRANT_API_KEY, - } - : {}), - LLMProvider: llmProvider, - ...(llmProvider === "openai" - ? { - OpenAiKey: !!process.env.OPEN_AI_KEY, - OpenAiModelPref: process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo", - } - : {}), - - ...(llmProvider === "azure" - ? { - AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT, - AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY, - AzureOpenAiModelPref: process.env.OPEN_MODEL_PREF, - AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF, - } - : {}), - }; + const results = await SystemSettings.currentSettings(); response.status(200).json({ results }); } catch (e) { console.log(e.message, e); @@ -526,6 +477,65 @@ function systemEndpoints(app) { } } ); + + app.get("/system/api-key", [validatedRequest], async (_, response) => { + try { + if (response.locals.multiUserMode) { + return response.sendStatus(401).end(); + } + + const apiKey = await ApiKey.get("id IS NOT NULL"); + return response.status(200).json({ + apiKey, + error: null, + }); + } catch (error) { + console.error(error); + response.status(500).json({ + apiKey: null, + error: "Could not find an API Key.", + }); + } + }); + + app.post( + "/system/generate-api-key", + [validatedRequest], + async (_, response) => { + try { + if (response.locals.multiUserMode) { + return response.sendStatus(401).end(); + } + + await ApiKey.delete(); + const { apiKey, error } = await ApiKey.create(); + return response.status(200).json({ + apiKey, + error, + }); + } catch (error) { + console.error(error); + response.status(500).json({ + apiKey: null, + error: "Error generating api key.", + }); + } + } + ); + + app.delete("/system/api-key", [validatedRequest], async (_, response) => { + try { + if (response.locals.multiUserMode) { + return response.sendStatus(401).end(); + } + + await ApiKey.delete(); + return response.status(200).end(); + } catch (error) { + console.error(error); + response.status(500).end(); + } + }); } module.exports = { systemEndpoints }; diff --git a/server/index.js b/server/index.js index 590cc133e..e2f54a3a8 100644 --- a/server/index.js +++ b/server/index.js @@ -17,6 +17,7 @@ const { adminEndpoints } = require("./endpoints/admin"); const { inviteEndpoints } = require("./endpoints/invite"); const { utilEndpoints } = require("./endpoints/utils"); const { Telemetry } = require("./models/telemetry"); +const { developerEndpoints } = require("./endpoints/api"); const app = express(); const apiRouter = express.Router(); const FILE_LIMIT = "3GB"; @@ -38,6 +39,7 @@ chatEndpoints(apiRouter); adminEndpoints(apiRouter); inviteEndpoints(apiRouter); utilEndpoints(apiRouter); +developerEndpoints(app, apiRouter); apiRouter.post("/v/:command", async (request, response) => { try { diff --git a/server/models/apiKeys.js b/server/models/apiKeys.js new file mode 100644 index 000000000..01f66576e --- /dev/null +++ b/server/models/apiKeys.js @@ -0,0 +1,133 @@ +const { Telemetry } = require("./telemetry"); + +const ApiKey = { + tablename: "api_keys", + writable: [], + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + secret TEXT UNIQUE, + createdBy INTEGER DEFAULT NULL, + createdAt TEXT DEFAULT CURRENT_TIMESTAMP, + lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP + `, + migrateTable: async function () { + const { checkForMigrations } = require("../utils/database"); + console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for ApiKey migrations`); + const db = await this.db(false); + await checkForMigrations(this, db); + }, + migrations: function () { + return []; + }, + makeSecret: () => { + const uuidAPIKey = require("uuid-apikey"); + return uuidAPIKey.create().apiKey; + }, + db: async function (tracing = true) { + const sqlite3 = require("sqlite3").verbose(); + const { open } = require("sqlite"); + + const db = await open({ + filename: `${ + !!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/" + }anythingllm.db`, + driver: sqlite3.Database, + }); + + await db.exec( + `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})` + ); + + if (tracing) db.on("trace", (sql) => console.log(sql)); + return db; + }, + create: async function (createdByUserId = null) { + const db = await this.db(); + const { id, success, message } = await db + .run(`INSERT INTO ${this.tablename} (secret, createdBy) VALUES(?, ?)`, [ + this.makeSecret(), + createdByUserId, + ]) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + return { id: null, success: false, message: error.message }; + }); + + if (!success) { + db.close(); + console.error("FAILED TO CREATE API KEY.", message); + return { apiKey: null, error: message }; + } + + const apiKey = await db.get( + `SELECT * FROM ${this.tablename} WHERE id = ${id} ` + ); + db.close(); + await Telemetry.sendTelemetry("api_key_created"); + return { apiKey, error: null }; + }, + get: async function (clause = "") { + const db = await this.db(); + const result = await db + .get( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : clause}` + ) + .then((res) => res || null); + if (!result) return null; + db.close(); + return { ...result }; + }, + count: async function (clause = null) { + const db = await this.db(); + const { count } = await db.get( + `SELECT COUNT(*) as count FROM ${this.tablename} ${ + clause ? `WHERE ${clause}` : "" + } ` + ); + db.close(); + + return count; + }, + delete: async function (clause = "") { + const db = await this.db(); + await db.get( + `DELETE FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""}` + ); + db.close(); + + return true; + }, + where: async function (clause = "", limit = null) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + }` + ); + db.close(); + + return results; + }, + whereWithUser: async function (clause = "", limit = null) { + const { User } = require("./user"); + const apiKeys = await this.where(clause, limit); + + for (const apiKey of apiKeys) { + if (!apiKey.createdBy) continue; + const user = await User.get(`id = ${apiKey.createdBy}`); + if (!user) continue; + + apiKey.createdBy = { + id: user.id, + username: user.username, + role: user.role, + }; + } + + return apiKeys; + }, +}; + +module.exports = { ApiKey }; diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index bc46dfe1f..7e2356301 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -1,3 +1,7 @@ +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); + const SystemSettings = { supportedFields: [ "multi_user_mode", @@ -45,6 +49,59 @@ const SystemSettings = { if (tracing) db.on("trace", (sql) => console.log(sql)); return db; }, + currentSettings: async function () { + const llmProvider = process.env.LLM_PROVIDER || "openai"; + const vectorDB = process.env.VECTOR_DB || "pinecone"; + return { + CanDebug: !!!process.env.NO_DEBUG, + RequiresAuth: !!process.env.AUTH_TOKEN, + AuthToken: !!process.env.AUTH_TOKEN, + JWTSecret: !!process.env.JWT_SECRET, + StorageDir: process.env.STORAGE_DIR, + MultiUserMode: await this.isMultiUserMode(), + VectorDB: vectorDB, + ...(vectorDB === "pinecone" + ? { + PineConeEnvironment: process.env.PINECONE_ENVIRONMENT, + PineConeKey: !!process.env.PINECONE_API_KEY, + PineConeIndex: process.env.PINECONE_INDEX, + } + : {}), + ...(vectorDB === "chroma" + ? { + ChromaEndpoint: process.env.CHROMA_ENDPOINT, + } + : {}), + ...(vectorDB === "weaviate" + ? { + WeaviateEndpoint: process.env.WEAVIATE_ENDPOINT, + WeaviateApiKey: process.env.WEAVIATE_API_KEY, + } + : {}), + ...(vectorDB === "qdrant" + ? { + QdrantEndpoint: process.env.QDRANT_ENDPOINT, + QdrantApiKey: process.env.QDRANT_API_KEY, + } + : {}), + LLMProvider: llmProvider, + ...(llmProvider === "openai" + ? { + OpenAiKey: !!process.env.OPEN_AI_KEY, + OpenAiModelPref: process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo", + } + : {}), + + ...(llmProvider === "azure" + ? { + AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT, + AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY, + AzureOpenAiModelPref: process.env.OPEN_MODEL_PREF, + AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF, + } + : {}), + }; + }, get: async function (clause = "") { const db = await this.db(); const result = await db diff --git a/server/nodemon.json b/server/nodemon.json new file mode 100644 index 000000000..d778fe53b --- /dev/null +++ b/server/nodemon.json @@ -0,0 +1,6 @@ +{ + "events": { + "start": "yarn swagger", + "restart": "yarn swagger" + } +} \ No newline at end of file diff --git a/server/package.json b/server/package.json index 15bbff6f6..c17633805 100644 --- a/server/package.json +++ b/server/package.json @@ -10,9 +10,10 @@ "node": ">=18.12.1" }, "scripts": { - "dev": "NODE_ENV=development nodemon --ignore documents --ignore vector-cache --ignore storage --trace-warnings index.js", + "dev": "NODE_ENV=development nodemon --ignore documents --ignore vector-cache --ignore storage --ignore swagger --trace-warnings index.js", "start": "NODE_ENV=production node index.js", - "lint": "yarn prettier --write ./endpoints ./models ./utils index.js" + "lint": "yarn prettier --write ./endpoints ./models ./utils index.js", + "swagger": "node ./swagger/init.js" }, "dependencies": { "@azure/openai": "^1.0.0-beta.3", @@ -41,6 +42,8 @@ "slugify": "^1.6.6", "sqlite": "^4.2.1", "sqlite3": "^5.1.6", + "swagger-autogen": "^2.23.5", + "swagger-ui-express": "^5.0.0", "uuid": "^9.0.0", "uuid-apikey": "^1.5.3", "vectordb": "0.1.12", @@ -50,4 +53,4 @@ "nodemon": "^2.0.22", "prettier": "^2.4.1" } -} +} \ No newline at end of file diff --git a/server/swagger/dark-swagger.css b/server/swagger/dark-swagger.css new file mode 100644 index 000000000..574e1d953 --- /dev/null +++ b/server/swagger/dark-swagger.css @@ -0,0 +1,1722 @@ +@media only screen and (prefers-color-scheme: dark) { + + a { + color: #8c8cfa; + } + + ::-webkit-scrollbar-track-piece { + background-color: rgba(255, 255, 255, .2) !important; + } + + ::-webkit-scrollbar-track { + background-color: rgba(255, 255, 255, .3) !important; + } + + ::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, .5) !important; + } + + embed[type="application/pdf"] { + filter: invert(90%); + } + + html { + background: #1f1f1f !important; + box-sizing: border-box; + filter: contrast(100%) brightness(100%) saturate(100%); + overflow-y: scroll; + } + + body { + background: #1f1f1f; + background-color: #1f1f1f; + background-image: none !important; + } + + button, + input, + select, + textarea { + background-color: #1f1f1f; + color: #bfbfbf; + } + + font, + html { + color: #bfbfbf; + } + + .swagger-ui, + .swagger-ui section h3 { + color: #b5bac9; + } + + .swagger-ui a { + background-color: transparent; + } + + .swagger-ui mark { + background-color: #664b00; + color: #bfbfbf; + } + + .swagger-ui legend { + color: inherit; + } + + .swagger-ui .debug * { + outline: #e6da99 solid 1px; + } + + .swagger-ui .debug-white * { + outline: #fff solid 1px; + } + + .swagger-ui .debug-black * { + outline: #bfbfbf solid 1px; + } + + .swagger-ui .debug-grid { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) 0 0; + } + + .swagger-ui .debug-grid-16 { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODYyRjhERDU2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODYyRjhERDQ2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QTY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3QjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvCS01IAAABMSURBVHjaYmR4/5+BFPBfAMFm/MBgx8RAGWCn1AAmSg34Q6kBDKMGMDCwICeMIemF/5QawEipAWwUhwEjMDvbAWlWkvVBwu8vQIABAEwBCph8U6c0AAAAAElFTkSuQmCC) 0 0; + } + + .swagger-ui .debug-grid-8-solid { + background: url(data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAAAAD/4QMxaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzExMSA3OS4xNTgzMjUsIDIwMTUvMDkvMTAtMDE6MTA6MjAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE1IChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkIxMjI0OTczNjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkIxMjI0OTc0NjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QjEyMjQ5NzE2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QjEyMjQ5NzI2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAbGhopHSlBJiZBQi8vL0JHPz4+P0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHAR0pKTQmND8oKD9HPzU/R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0f/wAARCAAIAAgDASIAAhEBAxEB/8QAWQABAQAAAAAAAAAAAAAAAAAAAAYBAQEAAAAAAAAAAAAAAAAAAAIEEAEBAAMBAAAAAAAAAAAAAAABADECA0ERAAEDBQAAAAAAAAAAAAAAAAARITFBUWESIv/aAAwDAQACEQMRAD8AoOnTV1QTD7JJshP3vSM3P//Z) 0 0 #1c1c21; + } + + .swagger-ui .debug-grid-16-solid { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzY3MkJEN0U2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzY3MkJEN0Y2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3RDY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pve6J3kAAAAzSURBVHjaYvz//z8D0UDsMwMjSRoYP5Gq4SPNbRjVMEQ1fCRDg+in/6+J1AJUxsgAEGAA31BAJMS0GYEAAAAASUVORK5CYII=) 0 0 #1c1c21; + } + + .swagger-ui .b--black { + border-color: #000; + } + + .swagger-ui .b--near-black { + border-color: #121212; + } + + .swagger-ui .b--dark-gray { + border-color: #333; + } + + .swagger-ui .b--mid-gray { + border-color: #545454; + } + + .swagger-ui .b--gray { + border-color: #787878; + } + + .swagger-ui .b--silver { + border-color: #999; + } + + .swagger-ui .b--light-silver { + border-color: #6e6e6e; + } + + .swagger-ui .b--moon-gray { + border-color: #4d4d4d; + } + + .swagger-ui .b--light-gray { + border-color: #2b2b2b; + } + + .swagger-ui .b--near-white { + border-color: #242424; + } + + .swagger-ui .b--white { + border-color: #1c1c21; + } + + .swagger-ui .b--white-90 { + border-color: rgba(28, 28, 33, .9); + } + + .swagger-ui .b--white-80 { + border-color: rgba(28, 28, 33, .8); + } + + .swagger-ui .b--white-70 { + border-color: rgba(28, 28, 33, .7); + } + + .swagger-ui .b--white-60 { + border-color: rgba(28, 28, 33, .6); + } + + .swagger-ui .b--white-50 { + border-color: rgba(28, 28, 33, .5); + } + + .swagger-ui .b--white-40 { + border-color: rgba(28, 28, 33, .4); + } + + .swagger-ui .b--white-30 { + border-color: rgba(28, 28, 33, .3); + } + + .swagger-ui .b--white-20 { + border-color: rgba(28, 28, 33, .2); + } + + .swagger-ui .b--white-10 { + border-color: rgba(28, 28, 33, .1); + } + + .swagger-ui .b--white-05 { + border-color: rgba(28, 28, 33, .05); + } + + .swagger-ui .b--white-025 { + border-color: rgba(28, 28, 33, .024); + } + + .swagger-ui .b--white-0125 { + border-color: rgba(28, 28, 33, .01); + } + + .swagger-ui .b--black-90 { + border-color: rgba(0, 0, 0, .9); + } + + .swagger-ui .b--black-80 { + border-color: rgba(0, 0, 0, .8); + } + + .swagger-ui .b--black-70 { + border-color: rgba(0, 0, 0, .7); + } + + .swagger-ui .b--black-60 { + border-color: rgba(0, 0, 0, .6); + } + + .swagger-ui .b--black-50 { + border-color: rgba(0, 0, 0, .5); + } + + .swagger-ui .b--black-40 { + border-color: rgba(0, 0, 0, .4); + } + + .swagger-ui .b--black-30 { + border-color: rgba(0, 0, 0, .3); + } + + .swagger-ui .b--black-20 { + border-color: rgba(0, 0, 0, .2); + } + + .swagger-ui .b--black-10 { + border-color: rgba(0, 0, 0, .1); + } + + .swagger-ui .b--black-05 { + border-color: rgba(0, 0, 0, .05); + } + + .swagger-ui .b--black-025 { + border-color: rgba(0, 0, 0, .024); + } + + .swagger-ui .b--black-0125 { + border-color: rgba(0, 0, 0, .01); + } + + .swagger-ui .b--dark-red { + border-color: #bc2f36; + } + + .swagger-ui .b--red { + border-color: #c83932; + } + + .swagger-ui .b--light-red { + border-color: #ab3c2b; + } + + .swagger-ui .b--orange { + border-color: #cc6e33; + } + + .swagger-ui .b--purple { + border-color: #5e2ca5; + } + + .swagger-ui .b--light-purple { + border-color: #672caf; + } + + .swagger-ui .b--dark-pink { + border-color: #ab2b81; + } + + .swagger-ui .b--hot-pink { + border-color: #c03086; + } + + .swagger-ui .b--pink { + border-color: #8f2464; + } + + .swagger-ui .b--light-pink { + border-color: #721d4d; + } + + .swagger-ui .b--dark-green { + border-color: #1c6e50; + } + + .swagger-ui .b--green { + border-color: #279b70; + } + + .swagger-ui .b--light-green { + border-color: #228762; + } + + .swagger-ui .b--navy { + border-color: #0d1d35; + } + + .swagger-ui .b--dark-blue { + border-color: #20497e; + } + + .swagger-ui .b--blue { + border-color: #4380d0; + } + + .swagger-ui .b--light-blue { + border-color: #20517e; + } + + .swagger-ui .b--lightest-blue { + border-color: #143a52; + } + + .swagger-ui .b--washed-blue { + border-color: #0c312d; + } + + .swagger-ui .b--washed-green { + border-color: #0f3d2c; + } + + .swagger-ui .b--washed-red { + border-color: #411010; + } + + .swagger-ui .b--transparent { + border-color: transparent; + } + + .swagger-ui .b--gold, + .swagger-ui .b--light-yellow, + .swagger-ui .b--washed-yellow, + .swagger-ui .b--yellow { + border-color: #664b00; + } + + .swagger-ui .shadow-1 { + box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2 { + box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3 { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4 { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5 { + box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; + } + + @media screen and (min-width: 30em) { + .swagger-ui .shadow-1-ns { + box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2-ns { + box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3-ns { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4-ns { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5-ns { + box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; + } + } + + @media screen and (max-width: 60em) and (min-width: 30em) { + .swagger-ui .shadow-1-m { + box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2-m { + box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3-m { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4-m { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5-m { + box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; + } + } + + @media screen and (min-width: 60em) { + .swagger-ui .shadow-1-l { + box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2-l { + box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3-l { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4-l { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5-l { + box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; + } + } + + .swagger-ui .black-05 { + color: rgba(191, 191, 191, .05); + } + + .swagger-ui .bg-black-05 { + background-color: rgba(0, 0, 0, .05); + } + + .swagger-ui .black-90, + .swagger-ui .hover-black-90:focus, + .swagger-ui .hover-black-90:hover { + color: rgba(191, 191, 191, .9); + } + + .swagger-ui .black-80, + .swagger-ui .hover-black-80:focus, + .swagger-ui .hover-black-80:hover { + color: rgba(191, 191, 191, .8); + } + + .swagger-ui .black-70, + .swagger-ui .hover-black-70:focus, + .swagger-ui .hover-black-70:hover { + color: rgba(191, 191, 191, .7); + } + + .swagger-ui .black-60, + .swagger-ui .hover-black-60:focus, + .swagger-ui .hover-black-60:hover { + color: rgba(191, 191, 191, .6); + } + + .swagger-ui .black-50, + .swagger-ui .hover-black-50:focus, + .swagger-ui .hover-black-50:hover { + color: rgba(191, 191, 191, .5); + } + + .swagger-ui .black-40, + .swagger-ui .hover-black-40:focus, + .swagger-ui .hover-black-40:hover { + color: rgba(191, 191, 191, .4); + } + + .swagger-ui .black-30, + .swagger-ui .hover-black-30:focus, + .swagger-ui .hover-black-30:hover { + color: rgba(191, 191, 191, .3); + } + + .swagger-ui .black-20, + .swagger-ui .hover-black-20:focus, + .swagger-ui .hover-black-20:hover { + color: rgba(191, 191, 191, .2); + } + + .swagger-ui .black-10, + .swagger-ui .hover-black-10:focus, + .swagger-ui .hover-black-10:hover { + color: rgba(191, 191, 191, .1); + } + + .swagger-ui .hover-white-90:focus, + .swagger-ui .hover-white-90:hover, + .swagger-ui .white-90 { + color: rgba(255, 255, 255, .9); + } + + .swagger-ui .hover-white-80:focus, + .swagger-ui .hover-white-80:hover, + .swagger-ui .white-80 { + color: rgba(255, 255, 255, .8); + } + + .swagger-ui .hover-white-70:focus, + .swagger-ui .hover-white-70:hover, + .swagger-ui .white-70 { + color: rgba(255, 255, 255, .7); + } + + .swagger-ui .hover-white-60:focus, + .swagger-ui .hover-white-60:hover, + .swagger-ui .white-60 { + color: rgba(255, 255, 255, .6); + } + + .swagger-ui .hover-white-50:focus, + .swagger-ui .hover-white-50:hover, + .swagger-ui .white-50 { + color: rgba(255, 255, 255, .5); + } + + .swagger-ui .hover-white-40:focus, + .swagger-ui .hover-white-40:hover, + .swagger-ui .white-40 { + color: rgba(255, 255, 255, .4); + } + + .swagger-ui .hover-white-30:focus, + .swagger-ui .hover-white-30:hover, + .swagger-ui .white-30 { + color: rgba(255, 255, 255, .3); + } + + .swagger-ui .hover-white-20:focus, + .swagger-ui .hover-white-20:hover, + .swagger-ui .white-20 { + color: rgba(255, 255, 255, .2); + } + + .swagger-ui .hover-white-10:focus, + .swagger-ui .hover-white-10:hover, + .swagger-ui .white-10 { + color: rgba(255, 255, 255, .1); + } + + .swagger-ui .hover-moon-gray:focus, + .swagger-ui .hover-moon-gray:hover, + .swagger-ui .moon-gray { + color: #ccc; + } + + .swagger-ui .hover-light-gray:focus, + .swagger-ui .hover-light-gray:hover, + .swagger-ui .light-gray { + color: #ededed; + } + + .swagger-ui .hover-near-white:focus, + .swagger-ui .hover-near-white:hover, + .swagger-ui .near-white { + color: #f5f5f5; + } + + .swagger-ui .dark-red, + .swagger-ui .hover-dark-red:focus, + .swagger-ui .hover-dark-red:hover { + color: #e6999d; + } + + .swagger-ui .hover-red:focus, + .swagger-ui .hover-red:hover, + .swagger-ui .red { + color: #e69d99; + } + + .swagger-ui .hover-light-red:focus, + .swagger-ui .hover-light-red:hover, + .swagger-ui .light-red { + color: #e6a399; + } + + .swagger-ui .hover-orange:focus, + .swagger-ui .hover-orange:hover, + .swagger-ui .orange { + color: #e6b699; + } + + .swagger-ui .gold, + .swagger-ui .hover-gold:focus, + .swagger-ui .hover-gold:hover { + color: #e6d099; + } + + .swagger-ui .hover-yellow:focus, + .swagger-ui .hover-yellow:hover, + .swagger-ui .yellow { + color: #e6da99; + } + + .swagger-ui .hover-light-yellow:focus, + .swagger-ui .hover-light-yellow:hover, + .swagger-ui .light-yellow { + color: #ede6b6; + } + + .swagger-ui .hover-purple:focus, + .swagger-ui .hover-purple:hover, + .swagger-ui .purple { + color: #b99ae4; + } + + .swagger-ui .hover-light-purple:focus, + .swagger-ui .hover-light-purple:hover, + .swagger-ui .light-purple { + color: #bb99e6; + } + + .swagger-ui .dark-pink, + .swagger-ui .hover-dark-pink:focus, + .swagger-ui .hover-dark-pink:hover { + color: #e699cc; + } + + .swagger-ui .hot-pink, + .swagger-ui .hover-hot-pink:focus, + .swagger-ui .hover-hot-pink:hover, + .swagger-ui .hover-pink:focus, + .swagger-ui .hover-pink:hover, + .swagger-ui .pink { + color: #e699c7; + } + + .swagger-ui .hover-light-pink:focus, + .swagger-ui .hover-light-pink:hover, + .swagger-ui .light-pink { + color: #edb6d5; + } + + .swagger-ui .dark-green, + .swagger-ui .green, + .swagger-ui .hover-dark-green:focus, + .swagger-ui .hover-dark-green:hover, + .swagger-ui .hover-green:focus, + .swagger-ui .hover-green:hover { + color: #99e6c9; + } + + .swagger-ui .hover-light-green:focus, + .swagger-ui .hover-light-green:hover, + .swagger-ui .light-green { + color: #a1e8ce; + } + + .swagger-ui .hover-navy:focus, + .swagger-ui .hover-navy:hover, + .swagger-ui .navy { + color: #99b8e6; + } + + .swagger-ui .blue, + .swagger-ui .dark-blue, + .swagger-ui .hover-blue:focus, + .swagger-ui .hover-blue:hover, + .swagger-ui .hover-dark-blue:focus, + .swagger-ui .hover-dark-blue:hover { + color: #99bae6; + } + + .swagger-ui .hover-light-blue:focus, + .swagger-ui .hover-light-blue:hover, + .swagger-ui .light-blue { + color: #a9cbea; + } + + .swagger-ui .hover-lightest-blue:focus, + .swagger-ui .hover-lightest-blue:hover, + .swagger-ui .lightest-blue { + color: #d6e9f5; + } + + .swagger-ui .hover-washed-blue:focus, + .swagger-ui .hover-washed-blue:hover, + .swagger-ui .washed-blue { + color: #f7fdfc; + } + + .swagger-ui .hover-washed-green:focus, + .swagger-ui .hover-washed-green:hover, + .swagger-ui .washed-green { + color: #ebfaf4; + } + + .swagger-ui .hover-washed-yellow:focus, + .swagger-ui .hover-washed-yellow:hover, + .swagger-ui .washed-yellow { + color: #fbf9ef; + } + + .swagger-ui .hover-washed-red:focus, + .swagger-ui .hover-washed-red:hover, + .swagger-ui .washed-red { + color: #f9e7e7; + } + + .swagger-ui .color-inherit, + .swagger-ui .hover-inherit:focus, + .swagger-ui .hover-inherit:hover { + color: inherit; + } + + .swagger-ui .bg-black-90, + .swagger-ui .hover-bg-black-90:focus, + .swagger-ui .hover-bg-black-90:hover { + background-color: rgba(0, 0, 0, .9); + } + + .swagger-ui .bg-black-80, + .swagger-ui .hover-bg-black-80:focus, + .swagger-ui .hover-bg-black-80:hover { + background-color: rgba(0, 0, 0, .8); + } + + .swagger-ui .bg-black-70, + .swagger-ui .hover-bg-black-70:focus, + .swagger-ui .hover-bg-black-70:hover { + background-color: rgba(0, 0, 0, .7); + } + + .swagger-ui .bg-black-60, + .swagger-ui .hover-bg-black-60:focus, + .swagger-ui .hover-bg-black-60:hover { + background-color: rgba(0, 0, 0, .6); + } + + .swagger-ui .bg-black-50, + .swagger-ui .hover-bg-black-50:focus, + .swagger-ui .hover-bg-black-50:hover { + background-color: rgba(0, 0, 0, .5); + } + + .swagger-ui .bg-black-40, + .swagger-ui .hover-bg-black-40:focus, + .swagger-ui .hover-bg-black-40:hover { + background-color: rgba(0, 0, 0, .4); + } + + .swagger-ui .bg-black-30, + .swagger-ui .hover-bg-black-30:focus, + .swagger-ui .hover-bg-black-30:hover { + background-color: rgba(0, 0, 0, .3); + } + + .swagger-ui .bg-black-20, + .swagger-ui .hover-bg-black-20:focus, + .swagger-ui .hover-bg-black-20:hover { + background-color: rgba(0, 0, 0, .2); + } + + .swagger-ui .bg-white-90, + .swagger-ui .hover-bg-white-90:focus, + .swagger-ui .hover-bg-white-90:hover { + background-color: rgba(28, 28, 33, .9); + } + + .swagger-ui .bg-white-80, + .swagger-ui .hover-bg-white-80:focus, + .swagger-ui .hover-bg-white-80:hover { + background-color: rgba(28, 28, 33, .8); + } + + .swagger-ui .bg-white-70, + .swagger-ui .hover-bg-white-70:focus, + .swagger-ui .hover-bg-white-70:hover { + background-color: rgba(28, 28, 33, .7); + } + + .swagger-ui .bg-white-60, + .swagger-ui .hover-bg-white-60:focus, + .swagger-ui .hover-bg-white-60:hover { + background-color: rgba(28, 28, 33, .6); + } + + .swagger-ui .bg-white-50, + .swagger-ui .hover-bg-white-50:focus, + .swagger-ui .hover-bg-white-50:hover { + background-color: rgba(28, 28, 33, .5); + } + + .swagger-ui .bg-white-40, + .swagger-ui .hover-bg-white-40:focus, + .swagger-ui .hover-bg-white-40:hover { + background-color: rgba(28, 28, 33, .4); + } + + .swagger-ui .bg-white-30, + .swagger-ui .hover-bg-white-30:focus, + .swagger-ui .hover-bg-white-30:hover { + background-color: rgba(28, 28, 33, .3); + } + + .swagger-ui .bg-white-20, + .swagger-ui .hover-bg-white-20:focus, + .swagger-ui .hover-bg-white-20:hover { + background-color: rgba(28, 28, 33, .2); + } + + .swagger-ui .bg-black, + .swagger-ui .hover-bg-black:focus, + .swagger-ui .hover-bg-black:hover { + background-color: #000; + } + + .swagger-ui .bg-near-black, + .swagger-ui .hover-bg-near-black:focus, + .swagger-ui .hover-bg-near-black:hover { + background-color: #121212; + } + + .swagger-ui .bg-dark-gray, + .swagger-ui .hover-bg-dark-gray:focus, + .swagger-ui .hover-bg-dark-gray:hover { + background-color: #333; + } + + .swagger-ui .bg-mid-gray, + .swagger-ui .hover-bg-mid-gray:focus, + .swagger-ui .hover-bg-mid-gray:hover { + background-color: #545454; + } + + .swagger-ui .bg-gray, + .swagger-ui .hover-bg-gray:focus, + .swagger-ui .hover-bg-gray:hover { + background-color: #787878; + } + + .swagger-ui .bg-silver, + .swagger-ui .hover-bg-silver:focus, + .swagger-ui .hover-bg-silver:hover { + background-color: #999; + } + + .swagger-ui .bg-white, + .swagger-ui .hover-bg-white:focus, + .swagger-ui .hover-bg-white:hover { + background-color: #1c1c21; + } + + .swagger-ui .bg-transparent, + .swagger-ui .hover-bg-transparent:focus, + .swagger-ui .hover-bg-transparent:hover { + background-color: transparent; + } + + .swagger-ui .bg-dark-red, + .swagger-ui .hover-bg-dark-red:focus, + .swagger-ui .hover-bg-dark-red:hover { + background-color: #bc2f36; + } + + .swagger-ui .bg-red, + .swagger-ui .hover-bg-red:focus, + .swagger-ui .hover-bg-red:hover { + background-color: #c83932; + } + + .swagger-ui .bg-light-red, + .swagger-ui .hover-bg-light-red:focus, + .swagger-ui .hover-bg-light-red:hover { + background-color: #ab3c2b; + } + + .swagger-ui .bg-orange, + .swagger-ui .hover-bg-orange:focus, + .swagger-ui .hover-bg-orange:hover { + background-color: #cc6e33; + } + + .swagger-ui .bg-gold, + .swagger-ui .bg-light-yellow, + .swagger-ui .bg-washed-yellow, + .swagger-ui .bg-yellow, + .swagger-ui .hover-bg-gold:focus, + .swagger-ui .hover-bg-gold:hover, + .swagger-ui .hover-bg-light-yellow:focus, + .swagger-ui .hover-bg-light-yellow:hover, + .swagger-ui .hover-bg-washed-yellow:focus, + .swagger-ui .hover-bg-washed-yellow:hover, + .swagger-ui .hover-bg-yellow:focus, + .swagger-ui .hover-bg-yellow:hover { + background-color: #664b00; + } + + .swagger-ui .bg-purple, + .swagger-ui .hover-bg-purple:focus, + .swagger-ui .hover-bg-purple:hover { + background-color: #5e2ca5; + } + + .swagger-ui .bg-light-purple, + .swagger-ui .hover-bg-light-purple:focus, + .swagger-ui .hover-bg-light-purple:hover { + background-color: #672caf; + } + + .swagger-ui .bg-dark-pink, + .swagger-ui .hover-bg-dark-pink:focus, + .swagger-ui .hover-bg-dark-pink:hover { + background-color: #ab2b81; + } + + .swagger-ui .bg-hot-pink, + .swagger-ui .hover-bg-hot-pink:focus, + .swagger-ui .hover-bg-hot-pink:hover { + background-color: #c03086; + } + + .swagger-ui .bg-pink, + .swagger-ui .hover-bg-pink:focus, + .swagger-ui .hover-bg-pink:hover { + background-color: #8f2464; + } + + .swagger-ui .bg-light-pink, + .swagger-ui .hover-bg-light-pink:focus, + .swagger-ui .hover-bg-light-pink:hover { + background-color: #721d4d; + } + + .swagger-ui .bg-dark-green, + .swagger-ui .hover-bg-dark-green:focus, + .swagger-ui .hover-bg-dark-green:hover { + background-color: #1c6e50; + } + + .swagger-ui .bg-green, + .swagger-ui .hover-bg-green:focus, + .swagger-ui .hover-bg-green:hover { + background-color: #279b70; + } + + .swagger-ui .bg-light-green, + .swagger-ui .hover-bg-light-green:focus, + .swagger-ui .hover-bg-light-green:hover { + background-color: #228762; + } + + .swagger-ui .bg-navy, + .swagger-ui .hover-bg-navy:focus, + .swagger-ui .hover-bg-navy:hover { + background-color: #0d1d35; + } + + .swagger-ui .bg-dark-blue, + .swagger-ui .hover-bg-dark-blue:focus, + .swagger-ui .hover-bg-dark-blue:hover { + background-color: #20497e; + } + + .swagger-ui .bg-blue, + .swagger-ui .hover-bg-blue:focus, + .swagger-ui .hover-bg-blue:hover { + background-color: #4380d0; + } + + .swagger-ui .bg-light-blue, + .swagger-ui .hover-bg-light-blue:focus, + .swagger-ui .hover-bg-light-blue:hover { + background-color: #20517e; + } + + .swagger-ui .bg-lightest-blue, + .swagger-ui .hover-bg-lightest-blue:focus, + .swagger-ui .hover-bg-lightest-blue:hover { + background-color: #143a52; + } + + .swagger-ui .bg-washed-blue, + .swagger-ui .hover-bg-washed-blue:focus, + .swagger-ui .hover-bg-washed-blue:hover { + background-color: #0c312d; + } + + .swagger-ui .bg-washed-green, + .swagger-ui .hover-bg-washed-green:focus, + .swagger-ui .hover-bg-washed-green:hover { + background-color: #0f3d2c; + } + + .swagger-ui .bg-washed-red, + .swagger-ui .hover-bg-washed-red:focus, + .swagger-ui .hover-bg-washed-red:hover { + background-color: #411010; + } + + .swagger-ui .bg-inherit, + .swagger-ui .hover-bg-inherit:focus, + .swagger-ui .hover-bg-inherit:hover { + background-color: inherit; + } + + .swagger-ui .shadow-hover { + transition: all .5s cubic-bezier(.165, .84, .44, 1) 0s; + } + + .swagger-ui .shadow-hover::after { + border-radius: inherit; + box-shadow: rgba(0, 0, 0, .2) 0 0 16px 2px; + content: ""; + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + transition: opacity .5s cubic-bezier(.165, .84, .44, 1) 0s; + width: 100%; + z-index: -1; + } + + .swagger-ui .bg-animate, + .swagger-ui .bg-animate:focus, + .swagger-ui .bg-animate:hover { + transition: background-color .15s ease-in-out 0s; + } + + .swagger-ui .nested-links a { + color: #99bae6; + transition: color .15s ease-in 0s; + } + + .swagger-ui .nested-links a:focus, + .swagger-ui .nested-links a:hover { + color: #a9cbea; + transition: color .15s ease-in 0s; + } + + .swagger-ui .opblock-tag { + border-bottom: 1px solid rgba(58, 64, 80, .3); + color: #b5bac9; + transition: all .2s ease 0s; + } + + .swagger-ui .opblock-tag svg, + .swagger-ui section.models h4 svg { + transition: all .4s ease 0s; + } + + .swagger-ui .opblock { + border: 1px solid #000; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, .19) 0 0 3px; + margin: 0 0 15px; + } + + .swagger-ui .opblock .tab-header .tab-item.active h4 span::after { + background: gray; + } + + .swagger-ui .opblock.is-open .opblock-summary { + border-bottom: 1px solid #000; + } + + .swagger-ui .opblock .opblock-section-header { + background: rgba(28, 28, 33, .8); + box-shadow: rgba(0, 0, 0, .1) 0 1px 2px; + } + + .swagger-ui .opblock .opblock-section-header>label>span { + padding: 0 10px 0 0; + } + + .swagger-ui .opblock .opblock-summary-method { + background: #000; + color: #fff; + text-shadow: rgba(0, 0, 0, .1) 0 1px 0; + } + + .swagger-ui .opblock.opblock-post { + background: rgba(72, 203, 144, .1); + border-color: #48cb90; + } + + .swagger-ui .opblock.opblock-post .opblock-summary-method, + .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after { + background: #48cb90; + } + + .swagger-ui .opblock.opblock-post .opblock-summary { + border-color: #48cb90; + } + + .swagger-ui .opblock.opblock-put { + background: rgba(213, 157, 88, .1); + border-color: #d59d58; + } + + .swagger-ui .opblock.opblock-put .opblock-summary-method, + .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after { + background: #d59d58; + } + + .swagger-ui .opblock.opblock-put .opblock-summary { + border-color: #d59d58; + } + + .swagger-ui .opblock.opblock-delete { + background: rgba(200, 50, 50, .1); + border-color: #c83232; + } + + .swagger-ui .opblock.opblock-delete .opblock-summary-method, + .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after { + background: #c83232; + } + + .swagger-ui .opblock.opblock-delete .opblock-summary { + border-color: #c83232; + } + + .swagger-ui .opblock.opblock-get { + background: rgba(42, 105, 167, .1); + border-color: #2a69a7; + } + + .swagger-ui .opblock.opblock-get .opblock-summary-method, + .swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after { + background: #2a69a7; + } + + .swagger-ui .opblock.opblock-get .opblock-summary { + border-color: #2a69a7; + } + + .swagger-ui .opblock.opblock-patch { + background: rgba(92, 214, 188, .1); + border-color: #5cd6bc; + } + + .swagger-ui .opblock.opblock-patch .opblock-summary-method, + .swagger-ui .opblock.opblock-patch .tab-header .tab-item.active h4 span::after { + background: #5cd6bc; + } + + .swagger-ui .opblock.opblock-patch .opblock-summary { + border-color: #5cd6bc; + } + + .swagger-ui .opblock.opblock-head { + background: rgba(140, 63, 207, .1); + border-color: #8c3fcf; + } + + .swagger-ui .opblock.opblock-head .opblock-summary-method, + .swagger-ui .opblock.opblock-head .tab-header .tab-item.active h4 span::after { + background: #8c3fcf; + } + + .swagger-ui .opblock.opblock-head .opblock-summary { + border-color: #8c3fcf; + } + + .swagger-ui .opblock.opblock-options { + background: rgba(36, 89, 143, .1); + border-color: #24598f; + } + + .swagger-ui .opblock.opblock-options .opblock-summary-method, + .swagger-ui .opblock.opblock-options .tab-header .tab-item.active h4 span::after { + background: #24598f; + } + + .swagger-ui .opblock.opblock-options .opblock-summary { + border-color: #24598f; + } + + .swagger-ui .opblock.opblock-deprecated { + background: rgba(46, 46, 46, .1); + border-color: #2e2e2e; + opacity: .6; + } + + .swagger-ui .opblock.opblock-deprecated .opblock-summary-method, + .swagger-ui .opblock.opblock-deprecated .tab-header .tab-item.active h4 span::after { + background: #2e2e2e; + } + + .swagger-ui .opblock.opblock-deprecated .opblock-summary { + border-color: #2e2e2e; + } + + .swagger-ui .filter .operation-filter-input { + border: 2px solid #2b3446; + } + + .swagger-ui .tab li:first-of-type::after { + background: rgba(0, 0, 0, .2); + } + + .swagger-ui .download-contents { + background: #7c8192; + color: #fff; + } + + .swagger-ui .scheme-container { + background: #1c1c21; + box-shadow: rgba(0, 0, 0, .15) 0 1px 2px 0; + } + + .swagger-ui .loading-container .loading::before { + animation: 1s linear 0s infinite normal none running rotation, .5s ease 0s 1 normal none running opacity; + border-color: rgba(0, 0, 0, .6) rgba(84, 84, 84, .1) rgba(84, 84, 84, .1); + } + + .swagger-ui .response-control-media-type--accept-controller select { + border-color: #196619; + } + + .swagger-ui .response-control-media-type__accept-message { + color: #99e699; + } + + .swagger-ui .version-pragma__message code { + background-color: #3b3b3b; + } + + .swagger-ui .btn { + background: 0 0; + border: 2px solid gray; + box-shadow: rgba(0, 0, 0, .1) 0 1px 2px; + color: #b5bac9; + } + + .swagger-ui .btn:hover { + box-shadow: rgba(0, 0, 0, .3) 0 0 5px; + } + + .swagger-ui .btn.authorize, + .swagger-ui .btn.cancel { + background-color: transparent; + border-color: #a72a2a; + color: #e69999; + } + + .swagger-ui .btn.cancel:hover { + background-color: #a72a2a; + color: #fff; + } + + .swagger-ui .btn.authorize { + border-color: #48cb90; + color: #9ce3c3; + } + + .swagger-ui .btn.authorize svg { + fill: #9ce3c3; + } + + .btn.authorize.unlocked:hover { + background-color: #48cb90; + color: #fff; + } + + .btn.authorize.unlocked:hover svg { + fill: #fbfbfb; + } + + .swagger-ui .btn.execute { + background-color: #5892d5; + border-color: #5892d5; + color: #fff; + } + + .swagger-ui .copy-to-clipboard { + background: #7c8192; + } + + .swagger-ui .copy-to-clipboard button { + background: url("data:image/svg+xml;charset=utf-8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path fill=\"%23fff\" fill-rule=\"evenodd\" d=\"M2 13h4v1H2v-1zm5-6H2v1h5V7zm2 3V8l-3 3 3 3v-2h5v-2H9zM4.5 9H2v1h2.5V9zM2 12h2.5v-1H2v1zm9 1h1v2c-.02.28-.11.52-.3.7-.19.18-.42.28-.7.3H1c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h3c0-1.11.89-2 2-2 1.11 0 2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V6H1v9h10v-2zM2 5h8c0-.55-.45-1-1-1H8c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H3c-.55 0-1 .45-1 1z\"/></svg>") 50% center no-repeat; + } + + .swagger-ui select { + background: url("data:image/svg+xml;charset=utf-8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><path d=\"M13.418 7.859a.695.695 0 01.978 0 .68.68 0 010 .969l-3.908 3.83a.697.697 0 01-.979 0l-3.908-3.83a.68.68 0 010-.969.695.695 0 01.978 0L10 11l3.418-3.141z\"/></svg>") right 10px center/20px no-repeat #212121; + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wICg0MDM1YTRmYjQ5LCAyMDIwLTA1LTAxKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZG93bmxvYWQuc3ZnIgogICBpZD0ic3ZnNCIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMjAgMjAiPgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTEwIj4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZGVmcwogICAgIGlkPSJkZWZzOCIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnNCIKICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIgogICAgIGlua3NjYXBlOndpbmRvdy15PSItOSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iLTkiCiAgICAgaW5rc2NhcGU6Y3k9IjEwIgogICAgIGlua3NjYXBlOmN4PSIxMCIKICAgICBpbmtzY2FwZTp6b29tPSI0MS41IgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDAxIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGd1aWRldG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIKICAgICBpZD0icGF0aDIiCiAgICAgZD0iTTEzLjQxOCA3Ljg1OWEuNjk1LjY5NSAwIDAxLjk3OCAwIC42OC42OCAwIDAxMCAuOTY5bC0zLjkwOCAzLjgzYS42OTcuNjk3IDAgMDEtLjk3OSAwbC0zLjkwOC0zLjgzYS42OC42OCAwIDAxMC0uOTY5LjY5NS42OTUgMCAwMS45NzggMEwxMCAxMWwzLjQxOC0zLjE0MXoiIC8+Cjwvc3ZnPgo=) right 10px center/20px no-repeat #1c1c21; + border: 2px solid #41444e; + } + + .swagger-ui select[multiple] { + background: #212121; + } + + .swagger-ui button.invalid, + .swagger-ui input[type=email].invalid, + .swagger-ui input[type=file].invalid, + .swagger-ui input[type=password].invalid, + .swagger-ui input[type=search].invalid, + .swagger-ui input[type=text].invalid, + .swagger-ui select.invalid, + .swagger-ui textarea.invalid { + background: #390e0e; + border-color: #c83232; + } + + .swagger-ui input[type=email], + .swagger-ui input[type=file], + .swagger-ui input[type=password], + .swagger-ui input[type=search], + .swagger-ui input[type=text], + .swagger-ui textarea { + background: #1c1c21; + border: 1px solid #404040; + } + + .swagger-ui textarea { + background: rgba(28, 28, 33, .8); + color: #b5bac9; + } + + .swagger-ui input[disabled], + .swagger-ui select[disabled] { + background-color: #1f1f1f; + color: #bfbfbf; + } + + .swagger-ui textarea[disabled] { + background-color: #41444e; + color: #fff; + } + + .swagger-ui select[disabled] { + border-color: #878787; + } + + .swagger-ui textarea:focus { + border: 2px solid #2a69a7; + } + + .swagger-ui .checkbox input[type=checkbox]+label>.item { + background: #303030; + box-shadow: #303030 0 0 0 2px; + } + + .swagger-ui .checkbox input[type=checkbox]:checked+label>.item { + background: url("data:image/svg+xml;charset=utf-8,<svg width=\"10\" height=\"8\" viewBox=\"3 7 10 8\" xmlns=\"http://www.w3.org/2000/svg\"><path fill=\"%2341474E\" fill-rule=\"evenodd\" d=\"M6.333 15L3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z\"/></svg>") 50% center no-repeat #303030; + } + + .swagger-ui .dialog-ux .backdrop-ux { + background: rgba(0, 0, 0, .8); + } + + .swagger-ui .dialog-ux .modal-ux { + background: #1c1c21; + border: 1px solid #2e2e2e; + box-shadow: rgba(0, 0, 0, .2) 0 10px 30px 0; + } + + .swagger-ui .dialog-ux .modal-ux-header .close-modal { + background: 0 0; + } + + .swagger-ui .model .deprecated span, + .swagger-ui .model .deprecated td { + color: #bfbfbf !important; + } + + .swagger-ui .model-toggle::after { + background: url("data:image/svg+xml;charset=utf-8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\"><path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\"/></svg>") 50% center/100% no-repeat; + } + + .swagger-ui .model-hint { + background: rgba(0, 0, 0, .7); + color: #ebebeb; + } + + .swagger-ui section.models { + border: 1px solid rgba(58, 64, 80, .3); + } + + .swagger-ui section.models.is-open h4 { + border-bottom: 1px solid rgba(58, 64, 80, .3); + } + + .swagger-ui section.models .model-container { + background: rgba(0, 0, 0, .05); + } + + .swagger-ui section.models .model-container:hover { + background: rgba(0, 0, 0, .07); + } + + .swagger-ui .model-box { + background: rgba(0, 0, 0, .1); + } + + .swagger-ui .prop-type { + color: #aaaad4; + } + + .swagger-ui table thead tr td, + .swagger-ui table thead tr th { + border-bottom: 1px solid rgba(58, 64, 80, .2); + color: #b5bac9; + } + + .swagger-ui .parameter__name.required::after { + color: rgba(230, 153, 153, .6); + } + + .swagger-ui .topbar .download-url-wrapper .select-label { + color: #f0f0f0; + } + + .swagger-ui .topbar .download-url-wrapper .download-url-button { + background: #63a040; + color: #fff; + } + + .swagger-ui .info .title small { + background: #7c8492; + } + + .swagger-ui .info .title small.version-stamp { + background-color: #7a9b27; + } + + .swagger-ui .auth-container .errors { + background-color: #350d0d; + color: #b5bac9; + } + + .swagger-ui .errors-wrapper { + background: rgba(200, 50, 50, .1); + border: 2px solid #c83232; + } + + .swagger-ui .markdown code, + .swagger-ui .renderedmarkdown code { + background: rgba(0, 0, 0, .05); + color: #c299e6; + } + + .swagger-ui .model-toggle:after { + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wICg0MDM1YTRmYjQ5LCAyMDIwLTA1LTAxKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZG93bmxvYWQyLnN2ZyIKICAgaWQ9InN2ZzQiCiAgIHZlcnNpb249IjEuMSIKICAgaGVpZ2h0PSIyNCIKICAgd2lkdGg9IjI0Ij4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGExMCI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGRlZnMKICAgICBpZD0iZGVmczgiIC8+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9InN2ZzQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTkiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9Ii05IgogICAgIGlua3NjYXBlOmN5PSIxMiIKICAgICBpbmtzY2FwZTpjeD0iMTIiCiAgICAgaW5rc2NhcGU6em9vbT0iMzQuNTgzMzMzIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDAxIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGd1aWRldG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIKICAgICBpZD0icGF0aDIiCiAgICAgZD0iTTEwIDZMOC41OSA3LjQxIDEzLjE3IDEybC00LjU4IDQuNTlMMTAgMThsNi02eiIgLz4KPC9zdmc+Cg==) 50% no-repeat; + } + + /* arrows for each operation and request are now white */ + .arrow, + #large-arrow-up { + fill: #fff; + } + + #unlocked { + fill: #fff; + } + + ::-webkit-scrollbar-track { + background-color: #646464 !important; + } + + ::-webkit-scrollbar-thumb { + background-color: #242424 !important; + border: 2px solid #3e4346 !important; + } + + ::-webkit-scrollbar-button:vertical:start:decrement { + background: linear-gradient(130deg, #696969 40%, rgba(255, 0, 0, 0) 41%), linear-gradient(230deg, #696969 40%, transparent 41%), linear-gradient(0deg, #696969 40%, transparent 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:vertical:end:increment { + background: linear-gradient(310deg, #696969 40%, transparent 41%), linear-gradient(50deg, #696969 40%, transparent 41%), linear-gradient(180deg, #696969 40%, transparent 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:horizontal:end:increment { + background: linear-gradient(210deg, #696969 40%, transparent 41%), linear-gradient(330deg, #696969 40%, transparent 41%), linear-gradient(90deg, #696969 30%, transparent 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:horizontal:start:decrement { + background: linear-gradient(30deg, #696969 40%, transparent 41%), linear-gradient(150deg, #696969 40%, transparent 41%), linear-gradient(270deg, #696969 30%, transparent 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button, + ::-webkit-scrollbar-track-piece { + background-color: #3e4346 !important; + } + + .swagger-ui .black, + .swagger-ui .checkbox, + .swagger-ui .dark-gray, + .swagger-ui .download-url-wrapper .loading, + .swagger-ui .errors-wrapper .errors small, + .swagger-ui .fallback, + .swagger-ui .filter .loading, + .swagger-ui .gray, + .swagger-ui .hover-black:focus, + .swagger-ui .hover-black:hover, + .swagger-ui .hover-dark-gray:focus, + .swagger-ui .hover-dark-gray:hover, + .swagger-ui .hover-gray:focus, + .swagger-ui .hover-gray:hover, + .swagger-ui .hover-light-silver:focus, + .swagger-ui .hover-light-silver:hover, + .swagger-ui .hover-mid-gray:focus, + .swagger-ui .hover-mid-gray:hover, + .swagger-ui .hover-near-black:focus, + .swagger-ui .hover-near-black:hover, + .swagger-ui .hover-silver:focus, + .swagger-ui .hover-silver:hover, + .swagger-ui .light-silver, + .swagger-ui .markdown pre, + .swagger-ui .mid-gray, + .swagger-ui .model .property, + .swagger-ui .model .property.primitive, + .swagger-ui .model-title, + .swagger-ui .near-black, + .swagger-ui .parameter__extension, + .swagger-ui .parameter__in, + .swagger-ui .prop-format, + .swagger-ui .renderedmarkdown pre, + .swagger-ui .response-col_links .response-undocumented, + .swagger-ui .response-col_status .response-undocumented, + .swagger-ui .silver, + .swagger-ui section.models h4, + .swagger-ui section.models h5, + .swagger-ui span.token-not-formatted, + .swagger-ui span.token-string, + .swagger-ui table.headers .header-example, + .swagger-ui table.model tr.description, + .swagger-ui table.model tr.extension { + color: #bfbfbf; + } + + .swagger-ui .hover-white:focus, + .swagger-ui .hover-white:hover, + .swagger-ui .info .title small pre, + .swagger-ui .topbar a, + .swagger-ui .white { + color: #fff; + } + + .swagger-ui .bg-black-10, + .swagger-ui .hover-bg-black-10:focus, + .swagger-ui .hover-bg-black-10:hover, + .swagger-ui .stripe-dark:nth-child(2n + 1) { + background-color: rgba(0, 0, 0, .1); + } + + .swagger-ui .bg-white-10, + .swagger-ui .hover-bg-white-10:focus, + .swagger-ui .hover-bg-white-10:hover, + .swagger-ui .stripe-light:nth-child(2n + 1) { + background-color: rgba(28, 28, 33, .1); + } + + .swagger-ui .bg-light-silver, + .swagger-ui .hover-bg-light-silver:focus, + .swagger-ui .hover-bg-light-silver:hover, + .swagger-ui .striped--light-silver:nth-child(2n + 1) { + background-color: #6e6e6e; + } + + .swagger-ui .bg-moon-gray, + .swagger-ui .hover-bg-moon-gray:focus, + .swagger-ui .hover-bg-moon-gray:hover, + .swagger-ui .striped--moon-gray:nth-child(2n + 1) { + background-color: #4d4d4d; + } + + .swagger-ui .bg-light-gray, + .swagger-ui .hover-bg-light-gray:focus, + .swagger-ui .hover-bg-light-gray:hover, + .swagger-ui .striped--light-gray:nth-child(2n + 1) { + background-color: #2b2b2b; + } + + .swagger-ui .bg-near-white, + .swagger-ui .hover-bg-near-white:focus, + .swagger-ui .hover-bg-near-white:hover, + .swagger-ui .striped--near-white:nth-child(2n + 1) { + background-color: #242424; + } + + .swagger-ui .opblock-tag:hover, + .swagger-ui section.models h4:hover { + background: rgba(0, 0, 0, .02); + } + + .swagger-ui .checkbox p, + .swagger-ui .dialog-ux .modal-ux-content h4, + .swagger-ui .dialog-ux .modal-ux-content p, + .swagger-ui .dialog-ux .modal-ux-header h3, + .swagger-ui .errors-wrapper .errors h4, + .swagger-ui .errors-wrapper hgroup h4, + .swagger-ui .info .base-url, + .swagger-ui .info .title, + .swagger-ui .info h1, + .swagger-ui .info h2, + .swagger-ui .info h3, + .swagger-ui .info h4, + .swagger-ui .info h5, + .swagger-ui .info li, + .swagger-ui .info p, + .swagger-ui .info table, + .swagger-ui .loading-container .loading::after, + .swagger-ui .model, + .swagger-ui .opblock .opblock-section-header h4, + .swagger-ui .opblock .opblock-section-header>label, + .swagger-ui .opblock .opblock-summary-description, + .swagger-ui .opblock .opblock-summary-operation-id, + .swagger-ui .opblock .opblock-summary-path, + .swagger-ui .opblock .opblock-summary-path__deprecated, + .swagger-ui .opblock-description-wrapper, + .swagger-ui .opblock-description-wrapper h4, + .swagger-ui .opblock-description-wrapper p, + .swagger-ui .opblock-external-docs-wrapper, + .swagger-ui .opblock-external-docs-wrapper h4, + .swagger-ui .opblock-external-docs-wrapper p, + .swagger-ui .opblock-tag small, + .swagger-ui .opblock-title_normal, + .swagger-ui .opblock-title_normal h4, + .swagger-ui .opblock-title_normal p, + .swagger-ui .parameter__name, + .swagger-ui .parameter__type, + .swagger-ui .response-col_links, + .swagger-ui .response-col_status, + .swagger-ui .responses-inner h4, + .swagger-ui .responses-inner h5, + .swagger-ui .scheme-container .schemes>label, + .swagger-ui .scopes h2, + .swagger-ui .servers>label, + .swagger-ui .tab li, + .swagger-ui label, + .swagger-ui select, + .swagger-ui table.headers td { + color: #b5bac9; + } + + .swagger-ui .download-url-wrapper .failed, + .swagger-ui .filter .failed, + .swagger-ui .model-deprecated-warning, + .swagger-ui .parameter__deprecated, + .swagger-ui .parameter__name.required span, + .swagger-ui table.model tr.property-row .star { + color: #e69999; + } + + .swagger-ui .opblock-body pre.microlight, + .swagger-ui textarea.curl { + background: #41444e; + border-radius: 4px; + color: #fff; + } + + .swagger-ui .expand-methods svg, + .swagger-ui .expand-methods:hover svg { + fill: #bfbfbf; + } + + .swagger-ui .auth-container, + .swagger-ui .dialog-ux .modal-ux-header { + border-bottom: 1px solid #2e2e2e; + } + + .swagger-ui .topbar .download-url-wrapper .select-label select, + .swagger-ui .topbar .download-url-wrapper input[type=text] { + border: 2px solid #63a040; + } + + .swagger-ui .info a, + .swagger-ui .info a:hover, + .swagger-ui .scopes h2 a { + color: #99bde6; + } + + /* Dark Scrollbar */ + ::-webkit-scrollbar { + width: 14px; + height: 14px; + } + + ::-webkit-scrollbar-button { + background-color: #3e4346 !important; + } + + ::-webkit-scrollbar-track { + background-color: #646464 !important; + } + + ::-webkit-scrollbar-track-piece { + background-color: #3e4346 !important; + } + + ::-webkit-scrollbar-thumb { + height: 50px; + background-color: #242424 !important; + border: 2px solid #3e4346 !important; + } + + ::-webkit-scrollbar-corner {} + + ::-webkit-resizer {} + + ::-webkit-scrollbar-button:vertical:start:decrement { + background: + linear-gradient(130deg, #696969 40%, rgba(255, 0, 0, 0) 41%), + linear-gradient(230deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(0deg, #696969 40%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:vertical:end:increment { + background: + linear-gradient(310deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(50deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(180deg, #696969 40%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:horizontal:end:increment { + background: + linear-gradient(210deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(330deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(90deg, #696969 30%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:horizontal:start:decrement { + background: + linear-gradient(30deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(150deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(270deg, #696969 30%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; + } +} \ No newline at end of file diff --git a/server/swagger/index.css b/server/swagger/index.css new file mode 100644 index 000000000..1d346cbc9 --- /dev/null +++ b/server/swagger/index.css @@ -0,0 +1,3 @@ +.schemes.wrapper>div:first-of-type { + display: none; +} \ No newline at end of file diff --git a/server/swagger/index.js b/server/swagger/index.js new file mode 100644 index 000000000..3f2529f8f --- /dev/null +++ b/server/swagger/index.js @@ -0,0 +1,28 @@ +function waitForElm(selector) { + return new Promise(resolve => { + if (document.querySelector(selector)) { + return resolve(document.querySelector(selector)); + } + + const observer = new MutationObserver(mutations => { + if (document.querySelector(selector)) { + resolve(document.querySelector(selector)); + observer.disconnect(); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + }); +} + +// Force change the Swagger logo in the header +waitForElm('img[alt="Swagger UI"]').then((elm) => { + if (window.SWAGGER_DOCS_ENV === 'development') { + elm.src = 'http://localhost:3000/public/anything-llm-light.png' + } else { + elm.src = `${window.location.origin}/anything-llm-light.png` + } +}); \ No newline at end of file diff --git a/server/swagger/init.js b/server/swagger/init.js new file mode 100644 index 000000000..c84daf323 --- /dev/null +++ b/server/swagger/init.js @@ -0,0 +1,37 @@ +const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' }); + +const doc = { + info: { + version: '1.0.0', + title: 'AnythingLLM Developer API', + description: 'API endpoints that enable programmatic reading, writing, and updating of your AnythingLLM instance. UI supplied by Swagger.io.', + }, + host: '/api', + schemes: ['http'], + securityDefinitions: { + BearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT' + } + }, + security: [ + { BearerAuth: [] } + ], + definitions: { + InvalidAPIKey: { + message: 'Invalid API Key', + }, + } +}; + +const outputFile = './openapi.json'; +const endpointsFiles = [ + '../endpoints/api/auth/index.js', + '../endpoints/api/admin/index.js', + '../endpoints/api/document/index.js', + '../endpoints/api/workspace/index.js', + '../endpoints/api/system/index.js', +]; + +swaggerAutogen(outputFile, endpointsFiles, doc) \ No newline at end of file diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json new file mode 100644 index 000000000..6ff36279b --- /dev/null +++ b/server/swagger/openapi.json @@ -0,0 +1,1767 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "AnythingLLM Developer API", + "description": "API endpoints that enable programmatic reading, writing, and updating of your AnythingLLM instance. UI supplied by Swagger.io." + }, + "servers": [ + { + "url": "http:///api/" + } + ], + "paths": { + "/v1/auth": { + "get": { + "tags": [ + "Authentication" + ], + "description": "Verify the attached Authentication header contains a valid API token.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Valid auth token was found.", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "authenticated": true + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + } + } + } + }, + "/v1/admin/is-multi-user-mode": { + "get": { + "tags": [ + "Admin" + ], + "description": "Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "isMultiUser": true + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + } + } + } + }, + "/v1/admin/users": { + "get": { + "tags": [ + "Admin" + ], + "description": "Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "users": [ + { + "username": "sample-sam", + "role": "default" + } + ] + } + } + } + } + }, + "401": { + "description": "Instance is not in Multi-User mode. Method denied" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/admin/users/new": { + "post": { + "tags": [ + "Admin" + ], + "description": "Create a new user with username and password. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "user": { + "id": 1, + "username": "sample-sam", + "role": "default" + }, + "error": null + } + } + } + } + }, + "401": { + "description": "Instance is not in Multi-User mode. Method denied" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "Key pair object that will define the new user to add to the system.", + "required": true, + "type": "object", + "content": { + "application/json": { + "example": { + "username": "sample-sam", + "password": "hunter2", + "role": "default | admin" + } + } + } + } + } + }, + "/v1/admin/users/{id}": { + "post": { + "tags": [ + "Admin" + ], + "description": "Update existing user settings. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "id of the user in the database." + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "success": true, + "error": null + } + } + } + } + }, + "401": { + "description": "Instance is not in Multi-User mode. Method denied" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "Key pair object that will update the found user. All fields are optional and will not update unless specified.", + "required": true, + "type": "object", + "content": { + "application/json": { + "example": { + "username": "sample-sam", + "password": "hunter2", + "role": "default | admin", + "suspended": 0 + } + } + } + } + }, + "delete": { + "tags": [ + "Admin" + ], + "description": "Delete existing user by id. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "id of the user in the database." + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "success": true, + "error": null + } + } + } + } + }, + "401": { + "description": "Instance is not in Multi-User mode. Method denied" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/admin/invites": { + "get": { + "tags": [ + "Admin" + ], + "description": "List all existing invitations to instance regardless of status. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "invites": [ + { + "id": 1, + "status": "pending", + "code": "abc-123", + "claimedBy": null + } + ] + } + } + } + } + }, + "401": { + "description": "Instance is not in Multi-User mode. Method denied" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/admin/invite/new": { + "post": { + "tags": [ + "Admin" + ], + "description": "Create a new invite code for someone to use to register with instance. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "invite": { + "id": 1, + "status": "pending", + "code": "abc-123" + }, + "error": null + } + } + } + } + }, + "401": { + "description": "Instance is not in Multi-User mode. Method denied" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/admin/invite/{id}": { + "delete": { + "tags": [ + "Admin" + ], + "description": "Deactivates (soft-delete) invite by id. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "id of the invite in the database." + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "success": true, + "error": null + } + } + } + } + }, + "401": { + "description": "Instance is not in Multi-User mode. Method denied" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/admin/workspaces/{workspaceId}/update-users": { + "post": { + "tags": [ + "Admin" + ], + "description": "Overwrite workspace permissions to only be accessible by the given user ids and admins. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "id of the workspace in the database." + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "success": true, + "error": null + } + } + } + } + }, + "401": { + "description": "Instance is not in Multi-User mode. Method denied" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "Entire array of user ids who can access the workspace. All fields are optional and will not update unless specified.", + "required": true, + "type": "object", + "content": { + "application/json": { + "example": { + "userIds": [ + 1, + 2, + 4, + 12 + ] + } + } + } + } + } + }, + "/v1/admin/workspace-chats": { + "post": { + "tags": [ + "Admin" + ], + "description": "All chats in the system ordered by most recent. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "success": true, + "error": null + } + } + } + } + }, + "401": { + "description": "Instance is not in Multi-User mode. Method denied" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "Page offset to show of workspace chats. All fields are optional and will not update unless specified.", + "required": false, + "type": "integer", + "content": { + "application/json": { + "example": { + "offset": 2 + } + } + } + } + } + }, + "/v1/admin/preferences": { + "get": { + "tags": [ + "Admin" + ], + "description": "Show all multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "settings": { + "users_can_delete_workspaces": true, + "limit_user_messages": false, + "message_limit": 10 + } + } + } + } + } + }, + "401": { + "description": "Instance is not in Multi-User mode. Method denied" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + }, + "post": { + "tags": [ + "Admin" + ], + "description": "Update multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "success": true, + "error": null + } + } + } + } + }, + "401": { + "description": "Instance is not in Multi-User mode. Method denied" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "Object with setting key and new value to set. All keys are optional and will not update unless specified.", + "required": true, + "type": "object", + "content": { + "application/json": { + "example": { + "users_can_delete_workspaces": false, + "limit_user_messages": true, + "message_limit": 5 + } + } + } + } + } + }, + "/v1/document/upload": { + "post": { + "tags": [ + "Documents" + ], + "description": "Upload a new file to AnythingLLM to be parsed and prepared for embedding.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "success": true, + "error": null + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "File to be uploaded.", + "required": true, + "type": "file", + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + } + }, + "/v1/documents": { + "get": { + "tags": [ + "Documents" + ], + "description": "List of all locally-stored documents in instance", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "localFiles": { + "name": "documents", + "type": "folder", + "items": [ + { + "name": "my-stored-document.json", + "type": "file", + "id": "bb07c334-4dab-4419-9462-9d00065a49a1", + "url": "file://my-stored-document.txt", + "title": "my-stored-document.txt", + "cached": false + } + ] + } + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/document/accepted-file-types": { + "get": { + "tags": [ + "Documents" + ], + "description": "Check available filetypes and MIMEs that can be uploaded.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "types": { + "application/mbox": [ + ".mbox" + ], + "application/pdf": [ + ".pdf" + ], + "application/vnd.oasis.opendocument.text": [ + ".odt" + ], + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [ + ".docx" + ], + "text/plain": [ + ".txt", + ".md" + ] + } + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "404": { + "description": "Not Found" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/workspace/new": { + "post": { + "tags": [ + "Workspaces" + ], + "description": "Create a new workspace", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "workspace": { + "id": 79, + "name": "Sample workspace", + "slug": "sample-workspace", + "createdAt": "2023-08-17 00:45:03", + "openAiTemp": null, + "lastUpdatedAt": "2023-08-17 00:45:03", + "openAiHistory": 20, + "openAiPrompt": null + }, + "message": "Workspace created" + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "JSON object containing new display name of workspace.", + "required": true, + "type": "object", + "content": { + "application/json": { + "example": { + "name": "My New Workspace" + } + } + } + } + } + }, + "/v1/workspaces": { + "get": { + "tags": [ + "Workspaces" + ], + "description": "List all current workspaces", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "workspaces": [ + { + "id": 79, + "name": "Sample workspace", + "slug": "sample-workspace", + "createdAt": "2023-08-17 00:45:03", + "openAiTemp": null, + "lastUpdatedAt": "2023-08-17 00:45:03", + "openAiHistory": 20, + "openAiPrompt": null + } + ] + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/workspace/{slug}": { + "get": { + "tags": [ + "Workspaces" + ], + "description": "Get a workspace by its unique slug.", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique slug of workspace to find" + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "workspace": { + "id": 79, + "name": "My workspace", + "slug": "my-workspace-123", + "createdAt": "2023-08-17 00:45:03", + "openAiTemp": null, + "lastUpdatedAt": "2023-08-17 00:45:03", + "openAiHistory": 20, + "openAiPrompt": null, + "documents": [] + } + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + }, + "delete": { + "tags": [ + "Workspaces" + ], + "description": "Deletes a workspace by its slug.", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique slug of workspace to delete" + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/workspace/{slug}/update": { + "post": { + "tags": [ + "Workspaces" + ], + "description": "Update workspace settings by its unique slug.", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique slug of workspace to find" + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "workspace": { + "id": 79, + "name": "My workspace", + "slug": "my-workspace-123", + "createdAt": "2023-08-17 00:45:03", + "openAiTemp": null, + "lastUpdatedAt": "2023-08-17 00:45:03", + "openAiHistory": 20, + "openAiPrompt": null, + "documents": [] + }, + "message": null + } + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "JSON object containing new settings to update a workspace. All keys are optional and will not update unless provided", + "required": true, + "type": "object", + "content": { + "application/json": { + "example": { + "name": "Updated Workspace Name", + "openAiTemp": 0.2, + "openAiHistory": 20, + "openAiPrompt": "Respond to all inquires and questions in binary - do not respond in any other format." + } + } + } + } + } + }, + "/v1/workspace/{slug}/chats": { + "get": { + "tags": [ + "Workspaces" + ], + "description": "Get a workspaces chats regardless of user by its unique slug.", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique slug of workspace to find" + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "history": [ + { + "role": "user", + "content": "What is AnythingLLM?", + "sentAt": 1692851630 + }, + { + "role": "assistant", + "content": "AnythingLLM is a platform that allows you to convert notes, PDFs, and other source materials into a chatbot. It ensures privacy, cites its answers, and allows multiple people to interact with the same documents simultaneously. It is particularly useful for businesses to enhance the visibility and readability of various written communications such as SOPs, contracts, and sales calls. You can try it out with a free trial to see if it meets your business needs.", + "sources": [ + { + "source": "object about source document and snippets used" + } + ] + } + ] + } + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/workspace/{slug}/update-embeddings": { + "post": { + "tags": [ + "Workspaces" + ], + "description": "Add or remove documents from a workspace by its unique slug.", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique slug of workspace to find" + }, + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "workspace": { + "id": 79, + "name": "My workspace", + "slug": "my-workspace-123", + "createdAt": "2023-08-17 00:45:03", + "openAiTemp": null, + "lastUpdatedAt": "2023-08-17 00:45:03", + "openAiHistory": 20, + "openAiPrompt": null, + "documents": [] + }, + "message": null + } + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "JSON object of additions and removals of documents to add to update a workspace. The value should be the folder + filename with the exclusions of the top-level documents path.", + "required": true, + "type": "object", + "content": { + "application/json": { + "example": { + "adds": [], + "deletes": [ + "custom-documents/anythingllm-hash.json" + ] + } + } + } + } + } + }, + "/v1/system/env-dump": { + "get": { + "tags": [ + "System Settings" + ], + "description": "Dump all settings to file storage", + "responses": { + "200": { + "description": "OK" + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/system": { + "get": { + "tags": [ + "System Settings" + ], + "description": "Get all current system settings that are defined.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "settings": { + "VectorDB": "pinecone", + "PineConeEnvironment": "us-west4-gcp-free", + "PineConeKey": true, + "PineConeIndex": "my-pinecone-index", + "LLMProvider": "azure", + "[KEY_NAME]": "KEY_VALUE" + } + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/system/vector-count": { + "get": { + "tags": [ + "System Settings" + ], + "description": "Number of all vectors in connected vector database", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "vectorCount": 5450 + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/v1/system/update-env": { + "post": { + "tags": [ + "System Settings" + ], + "description": "Update a system setting or preference.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "example": { + "newValues": { + "[ENV_KEY]": "Value" + }, + "error": "error goes here, otherwise null" + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/InvalidAPIKey" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "description": "Key pair object that matches a valid setting and value. Get keys from GET /v1/system or refer to codebase.", + "required": true, + "type": "object", + "content": { + "application/json": { + "example": { + "VectorDB": "lancedb", + "AnotherKey": "updatedValue" + } + } + } + } + } + } + }, + "components": { + "schemas": { + "InvalidAPIKey": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Invalid API Key" + } + }, + "xml": { + "name": "InvalidAPIKey" + } + } + }, + "securitySchemes": { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ] +} \ No newline at end of file diff --git a/server/swagger/utils.js b/server/swagger/utils.js new file mode 100644 index 000000000..bd8e3e81c --- /dev/null +++ b/server/swagger/utils.js @@ -0,0 +1,52 @@ +const fs = require('fs'); +const path = require('path'); +const swaggerUi = require('swagger-ui-express'); + +function faviconUrl() { + return process.env.NODE_ENV === "production" ? + '/public/favicon.png' : + 'http://localhost:3000/public/favicon.png' +} + +function useSwagger(app) { + app.use('/api/docs', swaggerUi.serve); + const options = { + customCss: [ + fs.readFileSync(path.resolve(__dirname, 'index.css')), + fs.readFileSync(path.resolve(__dirname, 'dark-swagger.css')) + ].join('\n\n\n'), + customSiteTitle: 'AnythingLLM Developer API Documentation', + customfavIcon: faviconUrl(), + } + + if (process.env.NODE_ENV === "production") { + const swaggerDocument = require('./openapi.json'); + app.get('/api/docs', swaggerUi.setup( + swaggerDocument, + { + ...options, + customJsStr: 'window.SWAGGER_DOCS_ENV = "production";\n\n' + fs.readFileSync(path.resolve(__dirname, 'index.js'), 'utf8'), + }, + )); + } else { + // we regenerate the html page only in development mode to ensure it is up-to-date when the code is hot-reloaded. + app.get( + "/api/docs", + async (_, response) => { + // #swagger.ignore = true + const swaggerDocument = require('./openapi.json'); + return response.send( + swaggerUi.generateHTML( + swaggerDocument, + { + ...options, + customJsStr: 'window.SWAGGER_DOCS_ENV = "development";\n\n' + fs.readFileSync(path.resolve(__dirname, 'index.js'), 'utf8'), + } + ) + ); + } + ); + } +} + +module.exports = { faviconUrl, useSwagger } \ No newline at end of file diff --git a/server/utils/database/index.js b/server/utils/database/index.js index 0cdc7ba10..75f5f7116 100644 --- a/server/utils/database/index.js +++ b/server/utils/database/index.js @@ -62,6 +62,7 @@ async function validateTablePragmas(force = false) { const { WorkspaceChats } = require("../../models/workspaceChats"); const { Invite } = require("../../models/invite"); const { WelcomeMessages } = require("../../models/welcomeMessages"); + const { ApiKey } = require("../../models/apiKeys"); await SystemSettings.migrateTable(); await User.migrateTable(); @@ -72,6 +73,7 @@ async function validateTablePragmas(force = false) { await WorkspaceChats.migrateTable(); await Invite.migrateTable(); await WelcomeMessages.migrateTable(); + await ApiKey.migrateTable(); } catch (e) { console.error(`validateTablePragmas: Migrations failed`, e); } diff --git a/server/utils/middleware/validApiKey.js b/server/utils/middleware/validApiKey.js new file mode 100644 index 000000000..6c7431066 --- /dev/null +++ b/server/utils/middleware/validApiKey.js @@ -0,0 +1,30 @@ +const { ApiKey } = require("../../models/apiKeys"); +const { SystemSettings } = require("../../models/systemSettings"); + +async function validApiKey(request, response, next) { + const multiUserMode = await SystemSettings.isMultiUserMode(); + response.locals.multiUserMode = multiUserMode; + + const auth = request.header("Authorization"); + const bearerKey = auth ? auth.split(" ")[1] : null; + if (!bearerKey) { + response.status(403).json({ + error: "No valid api key found.", + }); + return; + } + + const apiKey = await ApiKey.get(`secret = '${bearerKey}'`); + if (!apiKey) { + response.status(403).json({ + error: "No valid api key found.", + }); + return; + } + + next(); +} + +module.exports = { + validApiKey, +}; diff --git a/server/yarn.lock b/server/yarn.lock index 6a9e1669e..1d47eb610 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -252,6 +252,11 @@ accepts@~1.3.4, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" +acorn@^7.4.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -800,6 +805,11 @@ deep-extend@~0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1149,7 +1159,7 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@^7.1.3, glob@^7.1.4: +glob@^7.1.3, glob@^7.1.4, glob@^7.1.7: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -1458,6 +1468,11 @@ json-bignum@^0.0.3: resolved "https://registry.yarnpkg.com/json-bignum/-/json-bignum-0.0.3.tgz#41163b50436c773d82424dbc20ed70db7604b8d7" integrity sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg== +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + jsonpointer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" @@ -2429,6 +2444,28 @@ supports-color@^5.3.0, supports-color@^5.5.0: dependencies: has-flag "^3.0.0" +swagger-autogen@^2.23.5: + version "2.23.5" + resolved "https://registry.yarnpkg.com/swagger-autogen/-/swagger-autogen-2.23.5.tgz#fe86bde66daf991a2e9064ec83f2136319d19258" + integrity sha512-4Tl2+XhZMyHoBYkABnScHtQE0lKPKUD3NBt09mClrI6UKOUYljKlYw1xiFVwsHCTGR2hAXmhT4PpgjruCtt1ZA== + dependencies: + acorn "^7.4.1" + deepmerge "^4.2.2" + glob "^7.1.7" + json5 "^2.2.3" + +swagger-ui-dist@>=5.0.0: + version "5.4.2" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.4.2.tgz#ff7b936bdfc84673a1823a0f05f3a933ba7ccd4c" + integrity sha512-vT5QxP/NOr9m4gLZl+SpavWI3M9Fdh30+Sdw9rEtZbkqNmNNEPhjXas2xTD9rsJYYdLzAiMfwXvtooWH3xbLJA== + +swagger-ui-express@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz#7a00a18dd909574cb0d628574a299b9ba53d4d49" + integrity sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA== + dependencies: + swagger-ui-dist ">=5.0.0" + table-layout@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04"