From bdea663178420309c2475ff848f841db3d4428c6 Mon Sep 17 00:00:00 2001 From: Yuri Tatishchev Date: Sun, 22 Dec 2024 00:55:33 -0800 Subject: [PATCH] opnsense: filter queried connections --- .env.example | 2 +- bun.lockb | Bin 164427 -> 161102 bytes package.json | 10 ++-- src/lib/opnsense/wg.ts | 8 +++ src/lib/server/auth.ts | 20 +++---- src/lib/server/db/index.ts | 11 ++-- src/lib/server/db/schema.ts | 52 ++++++++++++++++-- src/lib/server/db/seed.ts | 29 ++++++++++ src/lib/server/opnsense.ts | 16 ------ src/lib/server/opnsense/index.ts | 33 +++++++++++ src/routes/api/connections/+server.ts | 24 +++++--- src/routes/auth/authentik/callback/+server.ts | 4 +- 12 files changed, 155 insertions(+), 54 deletions(-) create mode 100644 src/lib/server/db/seed.ts delete mode 100644 src/lib/server/opnsense.ts create mode 100644 src/lib/server/opnsense/index.ts diff --git a/.env.example b/.env.example index d3c3a69..4a0393e 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -DATABASE_URL=local.db +DATABASE_URL=file:local.db AUTH_DOMAIN=auth.lab.cazzzer.com AUTH_CLIENT_ID= AUTH_CLIENT_SECRET= diff --git a/bun.lockb b/bun.lockb index ec7b8fcee49b6dd7f2f37a54054d3088319f4b6c..e22ff0d9e1688a4e7ae65403e74ddcb3ef57da4f 100755 GIT binary patch delta 32193 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„k{#?|{j$)6Dt;cqzQe75(BW$WXfwVRc_@C7T| zbl~20zBflc*nBT4X~yPHBYGtyM|~JUcluQ8Y*f=gO~OOpMqjp@z4lbS8odsFTNEAe z;9H|vlU{VUd)23l=TA-E^iNstHn!b$n|bqm^zF`qbcW|qyTKTOiLhaPLtyU(EIry4iROn&pzs;bWe z$Nsq_WyYPAs|LqTnmc4WAANSq(U>CE*%r0#?X$-V99BlBeu%hi(k$m-h2b0f)Oxru zxq0iX>ZWex9k+h$x#yC0qQ6bSnr6;E)!3OW`);`Zn6Tq4Z((Axt?Ai$3t7)))-3Cu zp-t7I)&9^d{6(9vAE)+wzO6)^#3|pjGxz5?mER?F>T-R`3C+hq{i|ugRpumJ_{HU1 zT%Y@kZBxeA?6kz0FKj#c(du75n+;;-KU*8wrvvfBf}+AUb#L^e+3S=31FAU;dFZ#Y z-}1`I$2=Y!pKf!sbj`YlA}+2OHP3pQ*OddIBea7Khr3kToHb^)quGMpMSQj|YTS_} zxqC*9HRh-PxTpTR2|H5eTpfM4*<|m*SNgu}J9|rBHDSV&StG?gHmQ@ZHwx=ppPimi zVbuc`I`^@@>PMsY(Ap(zJFNToxWV}JQk`Gdjqg6@m>nOO6qR~n-hyl5QGNgJx0+lq zJ>H?_@pZSaJ?^yTv1yy9ueTrkXnMH(iQBKU>>ox33s3J9vCgrmb<@v}%bmQj=?u$Y z8@tpdW7?FM96zx0fdLWaJ6DLASf*mITU_XZv+X9@9DZN*#M`llE^h62#w&oQoHf?D zcjAD>)`6c`(l6FVK5E5h@5I65zwFvEYJ1NeZEx3kG_duQ_m{pN9@?vwW9t^@Uax3p z=+QZU{?lhKEdqY@*s#0t&CO21uY0u26t_PpKMcOOt%Vu+BUemy<_)gn_9f;U&`k5iqGZGZ3tU7qr?J@)0ahe+cr*4x82qA z<|9^hxwX+^pr^z0_BX74nE8J9g%$qJ8r|i!W7iG{x;Q0PXVq)TWY4$jeLa?WMGQS0 z*1p@#wnqzw`cyp^eRB1ltFgnc1#GKW#JXBVt;;%^H!9%5S+|BW2J{G9xWeUTl805r z%#r4go?pKk;B$P!^xqb3tFcmRbHJrl>8FSLtlI8$?s&a{$?rVhuZ_6hMfWg{9fNh6 zylzn)?!CNLzRS8XrR&eV_viQ#uLteeR^sK`XR9_(sXaF!XW^M?{w3D9EtwE{wzm6v z=5uV&%jYMm{64`s?pC)2@^1z5GFqpobsL&q*yP{BYj?9NUu^0xJh$%4iF*Zs3)%%u zxeRc+2 zjNY_ws$=!ESesExtmZXoKXLBQF28Z>uAPsZXt#P#*o+lI7<0|DHYRWT^vjHe_O0#) z&w10={d!%mY1uIg-``1JnliM+*OWfd59{Qmjazi&NWqKsU3Zt*TBd)SUWw)U46B;# z?wHW<&WqoRSm#pIx-`cm&8H4Obba>dwApE^b1~&6<+STJ=>5*sr(^fds8RJ!de~Nz zi3-);M3-)Gt%?)Xt^W-@u|)!Ol%7k-*>>FLZ7L%)t)-MhQb$94m68oj%1 ze1DKN-`?=%@tJEz8uxrDam?hFug5W)A{Dz8t@y}*10xnMxvJ^8Z{v|^j5fdaE9>pVtDULhenZxO^uB$2!qnRJ*Pk75`|*Qqn=CJ#E+}}?d4bE2 z0*Aik)*d|VHfZ9k3t`uj%eWrD>FrdcVmw#z{bO>uUT(|7!>3%mxV7@Qge<47Zk?}g z{IGb}%UvgPTDlw0O>b19>Q+0=+-rHu-)uWEt+W2t)V}Wze=21*BX31Q?*8pwbW27( zjCy>p76#Yq{c@{zz_tck#_sDlZi;8wvsFLlBy}8g&FyNH)C;Na3e4QLJ#F4&_$xzV zv!9xoq@;sSQ$pw#c@sTRwjX)oWBvZaj-tT<_)!hr5#_Pu;0y+obXHLtWunAG88$aMb@k9pggOf`4AJoaqwgGDO#X2PBDHVulJ?RRNv$Hm)@>-R+q<9d#HTKCAU8&5tMKep|$ZtnckmOE?e zzdg+zb>rsyKW9!pyZzR6C%f!#Mu#I__V-;+J#W%H+I(1>h|mRTS4@AJlkT&li0^(y zD^4l1-aTf^o&3$`y5%2qZkvC0ewD?~d%O%9HpYFpk7#w^YDb6QeVYSKdmK1(#OqSt zx@kGpd*|QgElyfoyz#R7?juF&^_QMvR4O<5C1K~L4~ur(TJojkgc-fIJX$pJYw6{C zy1gEi(`5R@N^g^oMvN(-~{ z4qKQ0smqt{6ST80pU-Vkt84vgf4ax->)1Q(`oPPj-oKk@-(cITjjmhAzqi?7>OHZ{ zvZOk1YwVtMro*+4Q~x|E|C}x#I0BfkB%H;qHD@tP3_>8wUlK0pnQm#g5X2(!9n7}i zyD_s|7A`bl`K!Z)rcAdcTxiB3@!gzl!FLO0xi(yA$>QsSBZ{c_@FB{)&*(rRtV@~VBh4ySXzB{l>`0mI&)`tt7*f@N5W;gNOh52s?$BI7{ z-{I^5zPqx<8^eVNHhW_@+q~7Bec5Oby0ef?;X)6#2;V)~2YmNp9X5vxz1a$U_hG`8 za3PXK;5&-t<2#z^wuTFRStPz=*cN=pGRtk@LO&Ld?>M#_-|@`m_i!PBrQy3jE5LUm zbKD*-B(ZFKC$m%dPGL?v!i7{e9N%f|628-!$IfsegN@r6&X(^rXHRw-ge>O2E1Y%R zXU^vDG6)0N18}DM%~_k>1|gfx-VHy%eE~Owh3vst9583A_85er>;pLKgXXN~UV~s{ zEB1!7L*UH!8HC|1VqZ8LbjX}-2RD-G_J^~&1?DVizd;zywt%|`uId4UFowk+2xnst zo3mr!#xa|N;mq%dIU9P=AWUEd;O>KSJ!B9jvg||QY}Qe8b`{)Y=2Q^QS{*ZI6AKK& zRCWp6dvJk=4FY514#U6W@DJP!=6?kKoq&Hw3<75lz?q(ee@6|%Y&QES`~&v|+*}rN z4E~*hf5!~MeD(pH^=bHb+#vkKRvd?a;LJ}Lghedk1pGS#|G+I_x|8tlEc`oZ5SFnm z;4Xrzddh%D3gb_~zjN>p+zMuM8vdP!f2R$2X0!m@eQ>U448kgweFpwrfPdiDFsHNd z?;`v=YY_6;C2;S-1)ehq>)5z+@b41*1GjHuu-O;j zAGj~zwy}_l@b3!zyJ!%$vk%~`ufo4e24N>#aS8r`GylUN>}C;vz`two58Pg+yA1!X z!@tW0VL#gf?jpFVR}8{I7Jmi)-GG1K3Yg7R_;(ZjT{Q?tSOK{E;9Rd6gkvoG8vOec z{((EeoUX&aTk!9?K{&-OfqM@w@P<91< z+*NQ-nA1b}_XHk2GzibwC2*~t!lOq9;RPG_2>yY40`3*_e+>Vg!K23p;SGBLuIqDn z^u!>%W3!*YzZdWb+y@r&6#ju*_0%AIVjsZ8y@W^440ytO#WVQ#3M2O1AbevH&*2}q z?avLOATYBRVQkQA4A~2VSVCZ1!Pb3)F?(qcwE|0c8OAPxJqEUvz^c3oV`JaK$5#eH z#|mD-$9M4YwLvJ&vR}hTa96>ZGp9H3@jZNeV-U)*OW<04fRAqtf+ZXG7CwS|0403X4v`d|=j*#~fOU*O|MgHV;N z_y`}r!pBbr!Jb8Yf{);~gL7oM<mRaPhN2sKK^;4xdpM7FPXY5NfgbFX1yTf;$GT z4zu|h&c_O1hki8(POJc|p9t3Vn?Z14+26wXePXYIb)DfPg!5S?1oIga1%pt3#wD<= zOayZtD1i6i;{*uali&#nytuyz!E!AG^F;_6@CPL5S`vabB_QzSvr9l=S_*$Apr?$N8W`*)mt>d)3~#ZmeL%OZ;$-UImxEn3lYp(IEUk027v z^NBR(x>6ubcqEagd<&6g+|m@JIgcmOg6}5MlH2G&TJba@A-sS{D0egiG4O06t@$Y; zZMaiukhXj{k#_tNk@no93`hq)jz~v-lSn7-Zw}I#PbJcYKOhpu8ii1g<9MEY=Dd5}mRNhFGIArj3kD}eOn@kC=eu_vUcd7)E#D^0}=9h@1aF5C$seBxfG=7sv zI`_8*$>38#_;Oo}`x|SFdlqkOLy=(z!7>{N2J#mqFs%wfmnsls^F>u4*h~VgEd)b& z2U`f@szI=U1Vg!C2Z6Oc1Tl6H7m90|s8M+XS}YCtgB0fGtq6bbG_z+8VZh!X`i_?Iv~ zt0oj0I6`4EAMOZ+R<$6wLxQQ?qdEldNid^21dQJ#!SdP=G_L`{3_i661YPSu@P-7O zH?9eRDP}QWRuh8R`~?X%lb}m22x^+%;oozX*2`y(3$xr{Np-SQ^1zYwrJ#+~qZKOIRN$$Bg2|>v{=y&u`t2o_+XV1lw_L=?AnrfN@1M(7gaGvjHIde1F`g=n!_Pma7{k{Ni< zRAy#;O!|*~NcL8$>2qVjRJ_`kmuw=87cW)Xw7Qw_QL4zS(0dYY6}8#;zYcSs#^{EpfI9)@<2u zxmVT<-_$EaXw@F0xSsmJ$L!=1d8M`bnXjEKG*a#beElYW$UFHau{2b~e&?xG^@ck= zpGrSj5UK$5iIN93BBO`1=-F@j(6g-glYT0_rrwK4RD|@j*jviQCkd1q zpkL+Fk)}FR6^by`WGH%+M@fW%W~d^gJWA5T=SC!`EWPWFDE*VbM1blx4uFunqNgIq z0Q8v%8U83(HI|AelN1>}1*UYNX0jr~gps@lk)Eujvh)<@c@p5GJXK3?UD5O*0Y1uu zwMKe*iuAN3HO5d96GD0}XARQS*b1c)`YEOb($rXDy{9~KLBc)v`c2;151yJ`CJ9U$E=& z6b3&7o&zs{m%uCFHSh*_3%mn903U%*z-QnIe0>T$2VMfNfH%Nf{sDCxPat^`I0aC= zQ+QK2Q~1(NbqY@k$1H#%a1byUp!-uf0EO`|-~gIB2pj_F|63*l=K5IxA*7H9{w2ST~cPq5t&Nq?Xr5CAj+ z0s%b`1Ox+(fhIsxpc&8{umEVb1fkgmfDb^o#p(lY0R7*AGvET$0BQo&0DHgzr~=s1 z1I(3?ums8g=71Sc3ZQ=$`36w52|x*;BpUnz>J70Ea1rT=08Q7)z+!-=>r!ADKr#9Y zFazMg9H1T09_R>kq5nOgX*eFBRW1&omF+w#KMWiJP5|qH4ZucV6R;WB0&E4g0lx#= zfgQk3U>C3(*aPeZ_5u3=dZ4i@5CL=py3>l-1Lz6#0(t{|fJh(;h$afJ=_%VT06m^t z52ye%grRgd&L5};)COt+)d5GKJWv5B3(zfA9pH!hrhu*jRs+Vh_@cQ_bA35L3lJ?h zVZeN#8_*q?24th~V1SIy1%?3lUqI=<+u}g`0#-1rB0#73bpZOenR38XfF7aQ3~U6} z16ah2ns6j~0X+a5?WF%mqvLLWAQ4CcERaXf+iV540kq<62k4ezSD-hL3B45H2+~J^ z{XhXQ5VAqQYoyZwIzHqAbWE5x)zE2xu5%vQ60Qy&h1HdkT)>h(G zc`%I*KC?FQkBf!cMhf9M0EO{%fX0GCegZ&|I~<_3B@>{2AAqtH7#@H-;0DwOTme`l zjf)yAx4|C+sOy^mL4Y0z0O%lOg_-ly65*23A50~HBFGo;0o;MQKm))Fr~|kG&VUV2 z8L$SN0MeuUDnJdu0k8+E0k#UIpOfieLuDL+>Of5ZIDQXh{ir_e&zf7<<{)uE3Kmkgt`7PimK(mPQXw|<7(0X$N zpwT1!g#fKTs=NSc$~y>9nSH=sU@Nc%*bHn0HUR4ZGIt%VBCC;D4lD*10zU!sfw{mO zU>3lEnZQ(F0x%vRW5)tN0Hc9XKrTRr4+3ar(TQXZ&;MDdY|H^mD+1Bsz(`;OFb1G1 zXu|ynOadkYQ-Eo}bbtXffZ0G-o`QF;MDa>63u7hoB%1Xv3E46G!pRv?iF{0giB z@&WQ`EwBdo4WN$M1PlhKUlsz?2YUeWcL(r0kPB=BwgbC>oxpBjA&?8~2Mz%=APWI+ z>J{?xByb#{5mrZ!6e)cSH~~=4Tn8=z7l8}FDc}rn8aNA_1I`0f=M~@&;4(nvt^(Hp zwf==jA4U68I8m5T28GEDfOxX9rXU}P>F7X}HUpjj0{G9MWcVlGBS6Nq0Nw)>3GaZn zz+>PM@DR8M+yU+ac7X8#zU~9c#l<(uq(Y*D>_TD1u6nn04rc6^hr*#NN9~Ax%9=2X=0`R0uZGDo!8`8eA>Vka9u&L8C!Qe@+e&vm#8>BhV4MI}iv& z0D7Pa5Ck*^f&n@@(-EFdC%OQgflfe2fDW$hdEiRH*SQ;#3sGe&AOq=iAPq2hluu<$X{}iY=0`9Sfl)|L1)Ty+25y7D3(%}!3Qw}xNG}AP14&9eS>Tsi$X6CH+MG= zbQzCcD|oe@hH4N!x$3JwNx{R-!_8A_u{3ff*Itw}Zb65w$Z>b`@^W*RE?&)%v#R4@ z=VymvmWoQdJoU0*wdu-QVP%ODUT&Uz{CdI1wkGYGGG0R`OLX_;5|4^ZDqDw*Kv zhC=Gj1nT=vk>l>xfO6EW1k@LlN@b*hRCgUv-(`v%FE<({ypLSo??8QvD&-huyVcu+ z>WfmP>O9?iv5f2Fn^Ed3R*~Z^mr?g?P~QTJ9Hkf3ogLKo#UjVu&4)}-_j*v@AS)V$ zUg&HJKy~*8^_{Mg2{h#Y)C-=)dn6mDZX%(+npQH-!wu_QRH7Vj^2=t8PGgWo9-&j! zog~!v(@G|IlL_ix6Y3jmr5tI*)!iu6cic)1Nwul_SEz5rMUIyoKSj#8NH4}MER>5W zM${a@9dRKfa9o6*?&WQtFP*tSMeQ> zg{c^a5HKDHXH=(d%b~vP7u6xgiY@fLjJll70Qo>o?@k4!&uqX^8<51`y<}`BDm-(yjB1RdSHgHJ2A1}XsxaKXiLn_%su?(lK# z+RphaJ-7Rtz-&~3PElX=9lxc~a_^E!cMEfZD)VbpR^2sZ_noQBD^zazu24hWSH!3F z(T$6ytsh*NlU145`yJh-?nJV9#ju2@rnOENYRs+7`#{4--MhrpQtz@^n0~ZSL*3ov zP^tF3#r>o`g*oRc^VL+AHbkKV+#b!-mbD*Fvf)r)!MW)Bvu9&oTwL){E!WsCTp=|Oeo73OTV=0{OhJZ{bJZWmhHshgi{v#M0S z=j7QVOayN?Pj6abH{0{JJJ5={T}p>JPoD-aJoU9uL)}p2yyv!W6JLf4g*oLM_&k)g zb9IpJG~}+UWzy6A{^$yY8m%4pNvgd+G^)eP36sCf+f>8yVWGxE?~F?G}4z46sO#um0V*@>6g zjXG94@tl)_HP`PJ%G(`xl72nO{kxGC!gVK}xLa@#gPi#!D)zVHVz@JRqiR;46v~^a z)ogd>7JJZd#oI>`}Ac`qva2`8Ae@Wl*!FfcvhCKf(* zWmorEsicjl+P3P*0#WdH^N^PBiZ1+Dl(nnlBK^3O+ii^FVE+wo_sg~WVEOTO;WwZm zc68w%_n^;XUAWs`p|v>3h3D)AKhlM--HRI3%_rv0uDPQP`{gxiz;MAl>}$hs?-gRj zjV`?PKGd||g~#qg$#X7z4$&Jf{5Ytcx?xXPl~$+hU#x?b7)9xTVNs9UVHEAuZCLEZ zKYMj}`ZQJX5D}q_vt6xv@;d76s4}Y4G_3QCb`_Wo{J-(UfwtDkpz1<@kTZgX<4sEOvSF9}mKp4qp6c@OJ9XLrG56qVlFkwG#ylHW`dPhj`gTs8iiz zXx8-LeP=g!wMLyd-KM#P(duzXa5gi6=QM+R`S2-+V5hoUQUAc{W7E!DAuE(~G1R&5 z5UiN!%gYyl_VMF=3Q$7bxuk9VJ&SD)@3le+%r%OLn*Mxx0jy9r8j4D*85mr;oF6ol zQ-pwq{7=$Q_fE1oG*x$X^jaDf<=k>@L+*Z<#;+j{J&f^0d=4YNOdy|iSXeKX(et)P zP{TjRjGqOc2Mw`$5I=bY_Nu!JJ$@f`wRZ(mZ^>TioN0RyH#v$J`RBSe2g}DB!Mcm=WcaTtxHg*4?34N#M|Nck8b*0BK?#az~nd8vJtgL+; zy)vvB??!ZAGd>A&A9Y8gz*oES4_h>dLrLW-QQhOnq^oD=VaFVKKDP7e{3%JQz3kk)7MxD(%VQaJCE zjT;@fK+N?BT3Rb-q-7NpQ!X~u{fY+J#h<)6w%1GKU~QlZZ-nsUrw~`_CQ*-)_RZ~j zufq|kNnGCI4oW2+d>MWIFofHkhNikJ)Pf(jHC_It?9b4onGWw?hw%2$5Wj`+dgr03 zZqMYNa$@$>%0nAK)1!gZtZgV?08JltLnk}0@_w~W*2H6P>U>qV1#0(U?}pz^qUI>} zV0maA${(M`c*&Pwp5lcN-uVp14f4fjFqhQ5oR*DSSi`yPV!culzD{h-pOfaL)?9y9 zu(nmVi5j^!=7h6LmP>U{sfQh+hqRwicNprzYDwYPrW5~6-L38-)p6YU)z!L&nkkN>%VIil_X}7q zS9g*x0hAIv=YmkhN7*zgcUP5KCAanNa|6X_^@3ObJmF2c@S_*dq`EPcP5JVPookdk ztCU4w+jp6H5en+IZ|#RA{(dEY-LFvaMHfTiR2S}jQE=8rgvm>|x|h}SeM3{{T{wCM znmFa5>u7Zos|9(}9#yJ%dNguyMnMMpg!6f5rNN$X*{?5&!rIj8&HjLfGB9_LV}U90 z{^O9iJzb|S6NTfDREFdYPx(Wzv9szbU*h+T*t=&zxrJI$_yz?!E3e*_SG$Bk41xyT zQ1dW*@yNM_m5C_0KtkvI#U48(=k|p2M3l8t_uHyk{br3_y6O~R%29DoSH6&H{|5~W zn!2~vHkn?|yf) z!dr?ay}88|xNO~9zGT?GXpiTL&Uvp3HGGj%3szlCig^6P$VQQcIX!ywUR2fy4e63; zYL`{fNte17YD`BC-CXo|=5e&%?%zff=B(~Nt*`988&hh|rI-1q8c_rjb8eYo3I)ZPUebjfh9U(TIR$A7s} zsFBu(M_m;LnbnPyeyGL%e*E@T%^UpmO8W&O@q z%M}IsfL_lPI!IgVU!Q~t;PH( zzTmpxEY^wQhp!6_|NBXo!woF&%E^|9Q>xB4u$*E6UVKAXZ>R247GE{gXWhk5O<*_f zm(W}vAH#pVi3L~P*{ovOkZ#d&7aBst+YRdhJ?yjdrr>3#Zss=ABDb+u&(+PLK(|;u zJv19)dD%ZPMCvwgw;ykXycZmL0d2m)cW{EUNCS`=DK6j+o4c-4>deEl!pXgSM6VQPgEu++8FO zTn2h#3}@a!W5o`kq)^;VB)KSVBDJNm#SWoV_WMqKCcnn@wrn^^GE~Z0U762AS-WC~ zP}0BPvGMhxdWJ%zaFMXJ}YrQCiWi>Zy6tPH%&Ta`#xO z*UL`b=kHRfiTA?COk6HaZcjHq{E9n}PeK*qpn-h(eXM}N+58H4J9W!HuW8YPch6rq zR1{ig%ZKZjY;N%Y8tNv2Lp|N>_f#lRU8++$y^)V#6}33%Ww++N9tfVc|5@FH!E$x8 z_{s-@jjg(c;3n(+S5BV%dKJJ#VgHIDTD=o#g%gV=T`U;7Z6 z>K=s)Pi;*-RW^AN45Z&vaGi{U$L)t$=+lRCi$}QiBcDH8iwB4C){h{+IE)X1+{!9f z-n}qn>W0hLPK|tmGa@7Zl}a?s<;NchgT$t}{P1JJfyX@-%8Qe8dCp^u$duuH?PH9P zk}19(&L2O<4HaPo@BBo_6|F|_^G~2xYXmoYDmdG@jgS|Ii~%u;v_bZxnK%12oH*0k zLckeC`=@BmID)4?6+Ek``y}Q}-QBI!E(csBxZoiVy3NiPKEXxn#izK2L^H3UY}Wph zAoZNZXYT$?2o~{+Zt63^-Pi+9H9KQok<~b z^zyG>O%m4{%C|)|rJ*1Cz93QiY)R+8=VxR^r$=VQXg2OoEj_nQ#q_crW&O)thAa(V z>0o06p4uB;xl7q^R^GPPYC&KD!G8bVoq`55yb%0ZHgw(aO%0z5a|M3ug;2F@6rO#L zNq5aiOT@0>UK?KuElc9pT6Z4$UI?(o=9tK+k7w7hk9crudVEGwq{8uq?*&hO=e@9% zS4|WhZ16KNUEXH)8;r;4rF~FcluiFS312w+dqd{^lg9p^u8pOKj!6Pbh``^Bm)RZdZr zv!G5B8|pD|I3-po5u4!}okAUnjoeW~-^eJqqf}=3RA77<@MKSCz z-@9)NZA+cbFZzoe`M_$Tt>a%a$h*I2x_WS-rdXq*`hy@8)TFB?Ut2<~VO!KpWEW+* z@JXmu<1bD@<1f4`?g_bh^WmQb`*QRfCi01pm`|-II+l}vzJyp#@T6*Dtx{1GMNx@- zcyqCq{r7>96Rr(#nkRLB6ef+2H@|5jR6(H3UO+WF{ZpNVKz+V_TLKRwVKKSu?SAa52?DuNbMTbdgBG zmxSHiqt!-v<5z;6<6lHHHh&eMQ_?;PHSE%(k(C~ee!~Hjs*?7oj~SGSq2`vZVg>%T zp4c3VhNHb$!$$puRH`l+jb>!ZCHW$I(V=W|JTmb6Dr8=6eteNZbf`jG-pkb?uSn&t z?p%lyYt&UAeL$|7MC|+09Tr%FCq3%RKe6&OgIJ9x`-`ALD)~NNjwF}CBlVHlbOL4-_lz>IQRT3Rs)dy0bttMfCvb4s=WAfAf z@|h`E{xajZWew4eKX(*Osuhu<=zl2E9NhHN2VtpTN*yr(i@n?pb|#`z>EO)l)R+vZ z*ETgb5f{|uj-jIcri!kjhZ&z~iZ)Jqh~@c@)}r~QJ-%Wkag%dHaZ43zJW?c$wDLbu v2%CNwM=ULkyNH!H)$SsmHRY3fiKR9*>xD7p-e2$^eQT3ZNlK!4-Sqze4eopy diff --git a/package.json b/package.json index 8bc45b9..d64ca26 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,10 @@ "format": "prettier --write .", "lint": "prettier --check . && eslint .", "db:push": "drizzle-kit push", + "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio" + "db:studio": "drizzle-kit studio", + "db:seed": "bun run ./src/lib/server/db/seed.ts" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.0.0", @@ -26,7 +28,7 @@ "autoprefixer": "^10.4.20", "bits-ui": "^0.21.16", "clsx": "^2.1.1", - "drizzle-kit": "^0.22.0", + "drizzle-kit": "^0.30.1", "eslint": "^9.7.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", @@ -44,11 +46,11 @@ "vite": "^5.0.3" }, "dependencies": { + "@libsql/client": "^0.14.0", "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", "arctic": "^2.2.1", - "better-sqlite3": "^11.1.2", - "drizzle-orm": "^0.33.0", + "drizzle-orm": "^0.38.2", "lucide-svelte": "^0.454.0" } } diff --git a/src/lib/opnsense/wg.ts b/src/lib/opnsense/wg.ts index b463f58..3bef846 100644 --- a/src/lib/opnsense/wg.ts +++ b/src/lib/opnsense/wg.ts @@ -59,3 +59,11 @@ export interface OpnsenseWgPeers { * }; * ``` */ + +export interface OpnsenseWgServers { + status: "ok" | string | number; + rows: { + name: string; + uuid: string; + }[]; +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 667c442..1973590 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -23,7 +23,7 @@ export async function createSession(userId: string): Promise { userId, expiresAt: new Date(Date.now() + DAY_IN_MS * 30) }; - await db.insert(table.session).values(session); + await db.insert(table.sessions).values(session); return session; } @@ -38,7 +38,7 @@ export function setSessionTokenCookie(event: RequestEvent, sessionId: string, ex } export async function invalidateSession(sessionId: string): Promise { - await db.delete(table.session).where(eq(table.session.id, sessionId)); + await db.delete(table.sessions).where(eq(table.sessions.id, sessionId)); } export function deleteSessionTokenCookie(event: RequestEvent) { @@ -49,12 +49,12 @@ export async function validateSession(sessionId: string) { const [result] = await db .select({ // Adjust user table here to tweak returned data - user: { id: table.user.id, username: table.user.username, name: table.user.name }, - session: table.session + user: { id: table.users.id, username: table.users.username, name: table.users.name }, + session: table.sessions }) - .from(table.session) - .innerJoin(table.user, eq(table.session.userId, table.user.id)) - .where(eq(table.session.id, sessionId)); + .from(table.sessions) + .innerJoin(table.users, eq(table.sessions.userId, table.users.id)) + .where(eq(table.sessions.id, sessionId)); if (!result) { return { session: null, user: null }; @@ -63,7 +63,7 @@ export async function validateSession(sessionId: string) { const sessionExpired = Date.now() >= session.expiresAt.getTime(); if (sessionExpired) { - await db.delete(table.session).where(eq(table.session.id, session.id)); + await db.delete(table.sessions).where(eq(table.sessions.id, session.id)); return { session: null, user: null }; } @@ -71,9 +71,9 @@ export async function validateSession(sessionId: string) { if (renewSession) { session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30); await db - .update(table.session) + .update(table.sessions) .set({ expiresAt: session.expiresAt }) - .where(eq(table.session.id, session.id)); + .where(eq(table.sessions.id, session.id)); } return { session, user }; diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index d8c8b85..62c6f8e 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -1,8 +1,7 @@ -import { drizzle } from 'drizzle-orm/better-sqlite3'; -import Database from 'better-sqlite3'; -import { env } from '$env/dynamic/private'; +import { drizzle } from 'drizzle-orm/libsql'; import assert from 'node:assert'; +import * as schema from './schema'; +import { DATABASE_URL } from '$env/static/private'; -assert(env.DATABASE_URL, 'DATABASE_URL is not set'); -const client = new Database(env.DATABASE_URL); -export const db = drizzle(client); +assert(DATABASE_URL, 'DATABASE_URL is not set'); +export const db= drizzle(DATABASE_URL, { schema }); diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index ddcbc75..6b51959 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -1,19 +1,59 @@ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; +import { relations } from 'drizzle-orm'; -export const user = sqliteTable('user', { +export const users = sqliteTable('users', { id: text('id').primaryKey(), username: text('username').notNull(), name: text('name').notNull(), }); -export const session = sqliteTable('session', { +export const usersRelations = relations(users, ({ many }) => ({ + wgClients: many(wgClients), +})); + +export const sessions = sqliteTable('sessions', { id: text('id').primaryKey(), userId: text('user_id') .notNull() - .references(() => user.id), - expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull() + .references(() => users.id), + expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(), }); -export type Session = typeof session.$inferSelect; +export const ipAllocations = sqliteTable('ip_allocations', { + // for now, id will be the same as the ipIndex + id: integer('id').primaryKey({ autoIncrement: true }), + // clientId is nullable because allocations can remain after the client is deleted + // unique for now, only allowing one allocation per client + clientId: integer('client_id') + .unique() + .references(() => wgClients.id), +}); -export type User = typeof user.$inferSelect; +export const wgClients = sqliteTable('wg_clients', { + id: integer().primaryKey({ autoIncrement: true }), + userId: text('user_id') + .notNull() + .references(() => users.id), + name: text('name').notNull(), + // questioning whether this should be nullable + opnsenseId: text('opnsense_id'), + publicKey: text('public_key').notNull().unique(), + // nullable for the possibility of a client supplying their own private key + privateKey: text('private_key'), + // nullable for the possibility of no psk + preSharedKey: text('pre_shared_key'), + // discarded ideas: + // (mostly because they make finding the next available ipIndex difficult) + // ipIndex: integer('ip_index').notNull().unique(), + // allowedIps: text('allowed_ips').notNull(), +}); + +export const wgClientsRelations = relations(wgClients, ({ one }) => ({ + ipAllocation: one(ipAllocations), +})); + +export type WgClient = typeof wgClients.$inferSelect; + +export type Session = typeof sessions.$inferSelect; + +export type User = typeof users.$inferSelect; diff --git a/src/lib/server/db/seed.ts b/src/lib/server/db/seed.ts new file mode 100644 index 0000000..92f1418 --- /dev/null +++ b/src/lib/server/db/seed.ts @@ -0,0 +1,29 @@ +import { users, wgClients } from './schema'; +import { eq } from 'drizzle-orm'; +import assert from 'node:assert'; +import { drizzle } from 'drizzle-orm/libsql'; +import * as schema from '$lib/server/db/schema'; + +assert(process.env.DATABASE_URL, 'DATABASE_URL is not set'); +const db = drizzle(process.env.DATABASE_URL, { schema }); + +export async function seed() { + const user = await db.query.users.findFirst({ where: eq(users.username, 'CaZzzer') }); + assert(user, 'User not found'); + + const clients = [ + { + userId: user.id, + name: 'Client1', + publicKey: 'BJ5faPVJsDP4CCxNYilmKnwlQXOtXEOJjqIwb4U/CgM=', + privateKey: 'KKqsHDu30WCSrVsyzMkOKbE3saQ+wlx0sBwGs61UGXk=', + preSharedKey: '0LWopbrISXBNHUxr+WOhCSAg+0hD8j3TLmpyzHkBHCQ=', + ipIndex: 1, + // allowedIps: '10.18.11.101/32,fd00::1/112', + } + ] + + await db.insert(wgClients).values(clients); +} + +seed(); diff --git a/src/lib/server/opnsense.ts b/src/lib/server/opnsense.ts deleted file mode 100644 index 58655b0..0000000 --- a/src/lib/server/opnsense.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { env } from '$env/dynamic/private'; -import assert from 'node:assert'; -import { encodeBasicCredentials } from 'arctic/dist/request'; -import { dev } from '$app/environment'; - -assert(env.OPNSENSE_API_URL, 'OPNSENSE_API_URL is not set'); -assert(env.OPNSENSE_API_KEY, 'OPNSENSE_API_KEY is not set'); -assert(env.OPNSENSE_API_SECRET, 'OPNSENSE_API_SECRET is not set'); -assert(env.OPNSENSE_WG_IFNAME, 'OPNSENSE_WG_IFNAME is not set'); - -export const opnsenseUrl = env.OPNSENSE_API_URL; -export const opnsenseAuth = "Basic " + encodeBasicCredentials(env.OPNSENSE_API_KEY, env.OPNSENSE_API_SECRET); -export const opnsenseIfname = env.OPNSENSE_WG_IFNAME; - -// unset secret for security -if (!dev) env.OPNSENSE_API_SECRET = ""; diff --git a/src/lib/server/opnsense/index.ts b/src/lib/server/opnsense/index.ts new file mode 100644 index 0000000..435365b --- /dev/null +++ b/src/lib/server/opnsense/index.ts @@ -0,0 +1,33 @@ +import { env } from '$env/dynamic/private'; +import assert from 'node:assert'; +import { encodeBasicCredentials } from 'arctic/dist/request'; +import { dev } from '$app/environment'; +import type { OpnsenseWgServers } from '$lib/opnsense/wg'; + +assert(env.OPNSENSE_API_URL, 'OPNSENSE_API_URL is not set'); +assert(env.OPNSENSE_API_KEY, 'OPNSENSE_API_KEY is not set'); +assert(env.OPNSENSE_API_SECRET, 'OPNSENSE_API_SECRET is not set'); +assert(env.OPNSENSE_WG_IFNAME, 'OPNSENSE_WG_IFNAME is not set'); + +export const opnsenseUrl = env.OPNSENSE_API_URL; +export const opnsenseAuth = "Basic " + encodeBasicCredentials(env.OPNSENSE_API_KEY, env.OPNSENSE_API_SECRET); +export const opnsenseIfname = env.OPNSENSE_WG_IFNAME; + +// unset secret for security +if (!dev) env.OPNSENSE_API_SECRET = ""; + +// this might be pretty bad if the server is down and in a bunch of other cases +// TODO: write a retry loop later +const resServers = await fetch(`${opnsenseUrl}/api/wireguard/client/list_servers`, { + method: 'GET', + headers: { + Authorization: opnsenseAuth, + Accept: 'application/json', + } +}); +assert(resServers.ok, 'Failed to fetch OPNsense WireGuard servers'); +const servers = await resServers.json() as OpnsenseWgServers; +assert.equal(servers.status, 'ok', 'Failed to fetch OPNsense WireGuard servers'); +export const serverUuid = servers.rows.find(server => server.name === opnsenseIfname)?.uuid; +assert(serverUuid, 'Failed to find server UUID for OPNsense WireGuard server'); +console.log('OPNsense WireGuard server UUID:', serverUuid); diff --git a/src/routes/api/connections/+server.ts b/src/routes/api/connections/+server.ts index 4b2e343..1167743 100644 --- a/src/routes/api/connections/+server.ts +++ b/src/routes/api/connections/+server.ts @@ -1,9 +1,12 @@ import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { opnsenseAuth, opnsenseIfname, opnsenseUrl } from '$lib/server/opnsense'; +import { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense'; import type { OpnsenseWgPeers } from '$lib/opnsense/wg'; -export const GET: RequestHandler = async () => { +export const GET: RequestHandler = async (event) => { + if (!event.locals.user) { + return error(401, 'Unauthorized'); + } const apiUrl = `${opnsenseUrl}/api/wireguard/service/show`; const options: RequestInit = { method: 'POST', @@ -13,21 +16,24 @@ export const GET: RequestHandler = async () => { 'Content-Type': 'application/json', }, body: JSON.stringify({ - "current": 1, + 'current': 1, // "rowCount": 7, - "sort": {}, - "searchPhrase": "", - "type": ["peer"], + 'sort': {}, + // TODO: use a more unique search phrase + // unfortunately 64 character limit, + // but it should be fine if users can't change their own username + 'searchPhrase': `vpgen-${event.locals.user.username}`, + 'type': ['peer'], }), }; - console.log("Fetching peers from OPNsense WireGuard API: ", apiUrl, options) + console.log('Fetching peers from OPNsense WireGuard API: ', apiUrl, options) const res = await fetch(apiUrl, options); const peers = await res.json() as OpnsenseWgPeers; - peers.rows = peers.rows.filter(peer => peer['latest-handshake'] && peer.ifname === opnsenseIfname) + peers.rows = peers.rows.filter(peer => peer['latest-handshake']) if (!peers) { - error(500, "Error getting info from OPNsense API"); + return error(500, 'Error getting info from OPNsense API'); } return new Response(JSON.stringify(peers), { headers: { diff --git a/src/routes/auth/authentik/callback/+server.ts b/src/routes/auth/authentik/callback/+server.ts index 791e8fa..a150155 100644 --- a/src/routes/auth/authentik/callback/+server.ts +++ b/src/routes/auth/authentik/callback/+server.ts @@ -39,7 +39,7 @@ export async function GET(event: RequestEvent): Promise { const userId: string = claims.sub; const username: string = claims.preferred_username; - const [existingUser] = await db.select().from(table.user).where(eq(table.user.id, userId)); + const existingUser = await db.query.users.findFirst({where: eq(table.users.id, userId)}); if (existingUser) { const session = await createSession(existingUser.id); @@ -59,7 +59,7 @@ export async function GET(event: RequestEvent): Promise { }; try { - await db.insert(table.user).values(user); + await db.insert(table.users).values(user); const session = await createSession(user.id); setSessionTokenCookie(event, session.id, session.expiresAt); } catch (e) {