From 07e7d1d6e9390607dc7bdf0d9f157c38508f356f Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Tue, 10 Nov 2020 09:31:02 +0100 Subject: [PATCH] wip: cleanup --- .gitignore | 1 + Makefile | 40 ++ assets/img/PROLICHT.png | Bin 1979 -> 0 bytes assets/img/PROLICHT_FULL.png | Bin 2112 -> 0 bytes assets/img/avatar.png | Bin 20686 -> 0 bytes assets/img/close.png | Bin 220 -> 0 bytes assets/img/header-logo.png | Bin 0 -> 10556 bytes assets/img/loading.gif | Bin 8476 -> 0 bytes assets/img/login.jpg | Bin 32796 -> 0 bytes assets/img/next.png | Bin 986 -> 0 bytes assets/img/prev.png | Bin 1028 -> 0 bytes assets/img/prolicht_fallback.jpg | Bin 1672 -> 0 bytes assets/tpl/admin_create_clients.html | 3 +- assets/tpl/admin_edit_client.html | 3 +- assets/tpl/admin_edit_interface.html | 3 +- assets/tpl/admin_index.html | 5 +- assets/tpl/error.html | 3 +- assets/tpl/index.html | 5 +- assets/tpl/login.html | 3 +- assets/tpl/profile.html | 120 ------ assets/tpl/prt_footer.html | 2 +- assets/tpl/prt_nav.html | 6 +- assets/tpl/user_index.html | 3 +- internal/common/configuration.go | 28 +- internal/ldap/usercache.go | 130 ------ internal/server/core.go | 34 +- internal/server/handlers.go | 588 -------------------------- internal/server/handlers_auth.go | 14 +- internal/server/handlers_common.go | 188 ++++++++ internal/server/handlers_interface.go | 94 ++++ internal/server/handlers_peer.go | 318 ++++++++++++++ internal/server/routes.go | 16 +- 32 files changed, 710 insertions(+), 897 deletions(-) create mode 100644 Makefile delete mode 100644 assets/img/PROLICHT.png delete mode 100644 assets/img/PROLICHT_FULL.png delete mode 100644 assets/img/avatar.png delete mode 100644 assets/img/close.png create mode 100644 assets/img/header-logo.png delete mode 100644 assets/img/loading.gif delete mode 100644 assets/img/login.jpg delete mode 100644 assets/img/next.png delete mode 100644 assets/img/prev.png delete mode 100644 assets/img/prolicht_fallback.jpg delete mode 100644 assets/tpl/profile.html delete mode 100644 internal/server/handlers.go create mode 100644 internal/server/handlers_common.go create mode 100644 internal/server/handlers_interface.go create mode 100644 internal/server/handlers_peer.go diff --git a/.gitignore b/.gitignore index eef70a1..2a1c43e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ .idea/ *.iws out/ +dist/ ssh.key .testCoverage.txt wg_portal.db diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0cc3161 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +# Go parameters +GOCMD=go +MODULENAME=github.com/h44z/wg-portal +GOFILES:=$(shell go list ./... | grep -v /vendor/) +BUILDDIR=dist +BINARIES=$(subst cmd/,,$(wildcard cmd/*)) + +.PHONY: all test clean phony + +all: dep test build + +build: dep $(addprefix $(BUILDDIR)/,$(BINARIES)) + cp -r assets $(BUILDDIR) + +dep: + $(GOCMD) mod download + +validate: + $(GOCMD) fmt $(GOFILES) + $(GOCMD) vet $(GOFILES) + $(GOCMD) test -race $(GOFILES) + +coverage: + $(GOCMD) fmt $(GOFILES) + $(GOCMD) test $(GOFILES) -v -coverprofile .testCoverage.txt + $(GOCMD) tool cover -func=.testCoverage.txt # use total:\s+\(statements\)\s+(\d+.\d+\%) as Gitlab CI regextotal:\s+\(statements\)\s+(\d+.\d+\%) + +coverage-html: coverage + $(GOCMD) tool cover -html=.testCoverage.txt + +test: + $(GOCMD) test $(MODULENAME)/... -v -count=1 + +clean: + $(GOCMD) clean $(GOFILES) + rm -rf .testCoverage.txt + rm -rf $(BUILDDIR) + +$(BUILDDIR)/%: cmd/%/main.go dep phony + $(GOCMD) build -o $@ $< \ No newline at end of file diff --git a/assets/img/PROLICHT.png b/assets/img/PROLICHT.png deleted file mode 100644 index f781789ba7403e26ad0102d823e52181143e2436..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1979 zcmV;s2SoUZP)W3F;B#393v`W`f!iM4rI*1hqG?CUEu6 z;8n2!h@=1lB);#CcR49`7y0S%?~h+;(xQ0CS%`X7ZV=%# zJC>2k{lw1u%p?WUV4>tHMEx@TP4i4XEmjkf;+Yxxa{(@x1u}$4ERA5!ks*)}hoOn$ zhv)rUGmL(u>9UYWWiDV#&)<78Qp|Nu@!ZI8Ns~fOLOe5~TROf}wduH!A8UDC_;N|* zn|U7@0z&MFPGohHZHBzqfQGw5q~ z$PgU;HhJ02S$iTDE@`95;!6BgVi{7-1>y@|sfDQ0v)vx`U8Z-J1N={uSwcjHaIAkw zY9XFEQsB;$8etmfyzhhMbsegnC+O_R5L{^ZddxwH8?n#jY=pw=IdFaKYCO^i1Q~*h z-P=%>N{B0YU3Ijb;D&aZwGgWjhwXpS3~(fr{Zz%0st$20_8mno#6AB6(;Pb6Mgg&l zv>V3-7vL~_t=Mcdc5*~9XL zl!y!=csd+Qh?)>6!e-d)>KjA)6EcJ$>F{Xy8bgSU<8^Bt5X_RTql5OnAnNqWJu-xz zyt_p&l@KeT2pe7#;_i(M>j5&{3VRtse>Ppi*J6Ydu~E}T#im4L2xF(i&LfduBA!Ss z6k)^9De)N@!pN+p$JAV`vKw6m)VUs8-Qg#p?hv|B8IdAG7=ljCt@`{xtRXW+*brib z@dFvciRrKxF%lw0*bqX7Z0riB@E`+rXOEE(DZ&P12&oygKSJn)sN;3lZCd&sb%%fq zAvU^jTMhZa&3Kb&hRn54gbm0L(x=16Q+`2+gy<@>Q7FO&WC&vrWZ^bMLcDaykRohA zhA}|vl~Xr&@D*VrLwGC0t+1CNgeO>8-zf=E zjZ6_XAVWw=(6YW$<`A3MwgzMf&>zWYN$hIQvC-Y^X?hqOFmEoZjxs?yP5Pfget_iV8VQa{qdVk!VAVX3j z^dWj#Uzdbf2}Rgom!WUV|1L6Y6XqQsCmbPo5%y3avJ*DMl3~lU@cd=)GfG+CqC>~0 z2phV}aA!Jh)AM&;^Ra@5(aQRK3DH-C9i=D|CNINkV8=a{IH6od-95{f5Pe11$PgKZ zk|7BZe5_@}g`GO0LN>$F;}#6VCbJeR;j#Kfhn^G

=H5FYI0Du}qB%E5;`|B`O zR$S-mYnrL+vLS+aK59MN{a+yBO6_CTL#1fQ)lo@`R9CV#&qFcRJEb9r;NTfoI90&t zx)-G1-a51qMcFMj`GZT?f)K7nc9)T!>@&eEJ^`L-uzz!HHgWoE3!1cmzEHw^gYBL9OCX+UBqx4N z4ADy?c%(ekBCasARm?n$p8%~?_f_{+7x47$rHkF7+?PKrVYl)~dmf=E6f4oWN~af1NvIg`tD}V2TJUp zbz8@{)?pu=sMLBy57k~=F=jHkg|9m&Fx-lYJ|qtf&mX@sNr9S=UWkW_ugZ~F?2T_r zb^Zm@vpKO8^j7;;OsKOOa}XwX&;@2=wGWegcVsjc=IDeqf^hV~&=O7=MWcMH{b0J7tF{N-~hoUtWdS(Zy3HnB@an`hMo;qa+gJ0-~(euE}9l zA2_koOQc1-Urv!^J3L&{Fc_EsO^gfDryMcOL^3pIKa(nsDX*(uMz9ia%D=w3cX7cO zoWn_XUpnL*ZTx8yg<5rOTZkeoHyZZn;_x;=g$W1xGZ4>rPjkP&qR9#6z+>X~%a*?l zcQ`~eO*unr_1frs1~rjR-M>cD$9*2Ze1JvbeXiUT;7`4?EG)^SOgy%Fq1LVYyptB5 zpu%b(#nTRlo+ zH0jk_pJKsdNNbnKAhlIqmPLV7!$xO!`blnTL4jp@bvmZ|){Ih?Eq1Bpa7vBsqt*=R zIIxliplcXfqAwVYmm`rQYWNu|N0UyUoc^iwqnC0;StfZ(pvF#HcyV8cbK6?T$4_L# zCAs4RK6EYNR&QdZKgLA9rmr-ye-)E4bE&0ceWR~U`lp@7gBRo?I%L-|M{b~syT{dd ztGig@Ht{OLR{s9}l$rw-#JSiXQ;s1K*m}lZ79M!>|8c09(z;>zJ@B#&JqdO;@_W`i zdWI{>r-@DYo`VHe6w@|WSk`~cGV#xz;IpNs@LFg>2!CLo)cdK%IeaS#?cQVbFvC)N zK9=~s>e;KV3H9t~vx%}It2>;Twx=2vk>NVcZ|eLpd<`KR(G|YuL2zcBs<0Qh>|89f zZfQLI zVEx4lGiB$W=LX7y2E;jiT%14)ff-Egq2B#^14Zt!=7z; z7i~&?u4h*o`{4KS3@I74@~9Ebh#A2g!W0q zEQa;PAmyx23_%c{V8c2_VtD%7oFLKW4N_{zJ?#jWObaWU>{OJ%aNHmCf;_jmD37|0 zLQTm+n@+`6UhJ#9cEq<@7bh&3yxaT3n{1RaTf;GOr|AZ(i%5P-xZJ5+W-In diff --git a/assets/img/avatar.png b/assets/img/avatar.png deleted file mode 100644 index d7ac03abffa2625fce66e25360bbea21fab88463..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20686 zcmbqZQ+F;ru&!;pZ*AMQZQHhO+qP}DyS8n+yVgG6xj4VzWUVBV$s~E!WO9>vq7>yN z;9+oJfPjGDr6fg_fq;M$|F=Sc{-^0M`t$o=2WBB8Cj@ z?-B?I6i884RZP~(2_7C68xsu=4+kF)2L>AIKLHLF1{xX?4h{wt1qlfW5e*Fm2@(Fk zSril`Y%FwKTx?_{1XNTMJX~xPWJG*C?EjWvprc@6Vf-f$5#nNEqT^s=Vq;;*OYr}v zloaMtmJw8v7F3iH_^%}a;K_*cP>>Sxve5CcGjOxfvocWq`}gni@@eqo7#|;(ln7s0 zMrdberz69xVr-O;kFUFXc&4xRzs<3+F^LKBE-oI^Vghh*u>L!Uhya%mADfAZ2^}3( zOGB-EbVOB7gn$57MOGLU75V!5FButW)%c%*&%2nq3PrY8UX{TMzw-QM2r>gsA)S$=%{U}j|z;9{btAb$G#M?nf;rl%Ypo?>8R zproYW;N+mEChOQ(mlWX^=4Gv)of*BnprxfIBLT>Y^YtGdC@Co|EUdFL(KIeDOiU~Y z2nwvM?34}-*4MY593QKyss^SeA0J=;{{D7tZyg*QczgS>v9mb@1)QDTi3zax?(K$U zXATZdc*nZ(tVkNUROet!OnI$Dy-ib>U#o0|uA_I7Cvby`|l);?a+($YISCs74?9qnzg zrNxDXrJ5?z^73*;eZ3nS8`-VRy}cuPdU~y`J+H4XuHj)Ee7sIhj@j8c)zuAmcX!|4 zUlX@C7Z>NEax#(cB~>fS%b_8`1o*h-?rwQqow>P% zadGi2%`G9mE?t@CCeDuIeEwG;SRC_5@Hq6x_!O9R;Iej z;w*MHChh5_4JE0bPL{Ke4?}|kqobn*`MGfsfj!w4kB@I9CB?4BqIp><`ik81gZ0C$ zr7fw(>B({J>1IhK1@(!B78-mR@gbGvr4>1G{dty`mw&tK^4ii(4-YRl{;<3J_b}*O zmE~1{aJBv(0AIs^p%~kFv91b6b&v*5Dhz(p*S|}rRTVtM`y36`EZyfgN4$0%#NTF_ z009Ajq(p^OJ-2RhmKL-M8)g$CEu$u`G7y*lyu}b>wVrb?5)(yZoM?y%q%g zhwuMU-dSxbVr-DYxRy*PaQGq^XAsYo()c1rdQT)grBF?=iJ1s#FPsAb%d$a)eVt9V z7s$I2f^reNEvI>PQQBc36(iD;HIl#qVr#^Zp=FRwP(#_`ZQLBgeg|(yi$boV z0I0>!A22z-Vgp}&I69u^Kn!O1_F4!hPW^;<5G70lfbKxfDua7#2A?=hXQ$ObI{VfH zH`omasy>cgz&x72yYYbV+LqG~VT#Vg#>mMkJD#PC$^i_MZ8W9X}qKUGzoM*A$ekcGKOXkU(4?ka?M5qZa8TC9^Ce>p>=lQ zIpdeUy|G8%EeC4d?YPi6d@gZYK~(_(@7ir4j*9--Ri~_p?R?3@N-w+b&=#4S@OBS` z-Dr9t%nx7hn{6XmzOfrHtqm7;bmHZ6p8&sxeU9~oVJZD}Y zi|y9X!evt-dLzxN76kp8#o0CiM&Pkds)(8k!-&{_AA^m{5w7IH^%)YWYFoNVTrk+T z-3^cD1VBb05!cD_$4U8$*}HDaNE4y&4|#M_XDY!yV>hl#Cdk20?G5*hZ8BafS13z{ z(|r6ugIlY7JY}`=fmM8W+*cH`g5G4Ev{G7#9WVAi$ov;mFwyu;-pn&>cMuB89Hm9Q z&8mcWMu}FP)Do*{Zpafr8Pj@c0##tZ^{JfJi>sXL^L*1-~a3LnR*>^gq?)4mOfnCtJKo)A2Ax& zrwoZFn$`ek4-^A^?KffG@r+r#vI10xpqy}xKvjfMO2xAS3F#SQ5%J5tXwaW$CNMsJ zkAi}~a2kKW(%VZJJ;`^VT{{Usu9?l)@Em=oMnWbUHmehTx+B}Lc7Tgv!syGQgIqJF zEnKuSt<=%L_zuLB(7KGd&eM%V*Tzf1Uw_Hv*we>#E`BX=&$ib3X;hnPP)tf8hr5WK zsX{!PRvnr)=8?oeXO ztq$Yt*80tir8+1N5#jH9wt*e2B4^YS#Ktod7bQ9Q4FS);!fhTm@~txiZQ*2)DQ%xZ zNk&-uGcd0G&J^oq-oWceGt|Bn`T=U>uJE7r`_^dV`lT+zI`vvOI~KaxVDx)>c9^f< z(IcC*^Cs?fFe#UaGGXE^N|${Tp}TB|3HxoGb&cnmypN2O59Zf{$mfw|gG?Xx72FHW zBGi`k(yJvp$}EKjPHdX;U zONOo81l=-&6K_`~Vq(5~!5q3*A`M>3sEcXYDEn{V4oYaJlc~dXuicu@EQ?mInpZ^X z3ti$6t|=7-Zr)jG3X5Fx9Jvo)3gjqiiUv=N`D#eD80SW#l0871b!qGjRz8FZbf5~l zR7fyt5PMQbB%Sj2b2Gl2`_wLYd9*>c>sXuX$rx{i^wb*@0F<|w78(DfvTgA==S(C0 z?KW@_e#xY!`)}vVyCUNL={n>YkN(n)6qQDP~2hRW{`Is!22v_ppP}-T9&26&&wyNnCNK4 z{1(JFWEI$XeT%y~{%mLkA3v@u7fm0_^Ck9p`BK);u+T3QcvH}@OOIOG@4V$5bj(O` z{%RlW{zIg5v^(;f{mbE4#F2=@Vnxz|E`zLacYoIjo3?Y{#6daVv=LjFCT^QbE_01q z=U+bgbtBfDmAOcE&-N1=QVUpAfTPPF>(3g%tp7R1D^Jd{aoa0Qlt65Xokh@|P%p5Z zcWXZ0yvF_Wfc#J2F!6p{=W!FQj-(Ir*(;aOK($L1rIp|KbAM#KEcku;+lS#D}XQV+<<2p!&Gmd;;Q-mk!)AoL{AP`ES~4yx89D6 z{wx_$IqE5$+U@S3u?wJ+TwhyeUw^-KZYSOq$H#^oPi~vr{9BY!{!?hClTaObyrS9X z9bY4T$u4o3(<7^+x8PJlx2WM_B8$KjGm9wgq1u@~t5UeiB9ic1lD+;aZwjvAPys(L za7%;sX9FE~z0Qssy(JTK;}4S>Hk!=(w^d@dTYuxNoo&BAe=Y)`a@a8~x=VERzow8x zcR%nd+U?6-tp088$5>nlM$*jn5m+l(^U&`jPx#uKXVyNYoU!r!G$iK=X{_sE7}zTLiL)Rd5MB37Halr!~v>V$9zpw3T8ebi3bFK3>0E zrnPgrM&Wj}3HM~9EE5zvS|wJM5-qM{p4Zo2Yrk^rimYG~fbyJUnqFsG991hU=dn8P zE#-AZ&$kf3$f_p^nmdtc9-T_Lnz>*3XrW*0P{!vBh#vbDbiCGfthRWT zSDh72$%yr5fmT5SX7MfteDS$u%4<;gm{$_wrdi*U3t2)sSh2-nkM?Gg78Avrhz5q_!M2942op z5w8+0lt#ymb)vPZtL?TW+X9SLs}--R8L#FE+PJQ8uf0dKir4&MWhckiODQ*)A%EHK zf4d8L3tYcKeYnhZ;79U*`@Q>cndf^`6f0qSwIt8U*&aVS<@!+5c2uM)DP=(8tn^a3 zYOZp`Lv@zM*KzG`+L7ZMRgzF&3J@Ei)8{vBOm&pf0QvP_*q<_*-w=~>e)J%k2$B>| zxE`|6KmcJR(wcWBt>!b{eJ+`nl*+~9oAzR<)E&IXyqp2UsK6)Kf-gM@_x~+~h8B*8 z%Sy5Lap)m<#e2pZgtS?@D<>?I$U@9SaV=vCtGft-reV9a>giXjY1>3v*BmJx4HFDv zXp}Pdh%rqus-o%hAy)bpSy@bfF6hC*FXAnw9Bz8$Vlkk?;AvSE2wXW@M_U07x{5J> zvo*@yK)zkl;ZxS|D30$f)ya)Qlh+x*LYp?MAQ7lgR)<0h_ib6YbLv!rb)|5d3!yPv zaS*yeFfntX?{v1HZbnCwP*j$OQ5C&c9~&VQ{fU?hNjOQ~2P3-Nyb#Z1q4;Cbj?^3& z>?;FKXbBxIdGCD=CwJehy(se6KgM2ue`kmHm9>fqUSH?IZicB(f4`cA@afqYeqgz{ z$*Sk3N4Z!`|D8M*MR+0PWfR<2%${NaQ66(tqPe_F zQuM2~v`q`4#DJ=eb|Q=yq?fm-R&LqA8C1@$2?L1a6yQ$#!Iri}j%=Ygi$A(b-CGng z9P2zQoMVg0K1iKHcY(0Vq~@L=SMaKJGWJQ2;TTz~=AyZ=aB1cv5^o;PseQnOY+^27M!XoKz~m3v*tQ)sa?sKAU)!4bRCR>Q}42BVrlAS$u*qB+MX%3AO5hS>eBuhjT?_=k< z>87iDsS@1IP+NM}LlXm6Fc&--D9xDilLzR}FQs%GX9Ue#^yIW>7Mo&}32{raaaGx&`$=S*nP)4pi=p>cYj<~NSJ&pYl4!vx5i{&$W*M(m zE$wW-f5o2wuZidE>&?wFV}IR%fn9#hoRJYz_x1~?pPIeB+P1h^wdH4^-q6|(61`HQ z*B{YdsCgKLdLf##s{0uC;gT*0z2{=gLt>Y$*3M0y7sv9;y;X|iM`t%BeOBzr6v`); zlSgcm*-!Ag`u3Ir26Df)t9WHydye(3GwtoI{nS-8GHl$*Aj)(}csJ~0j(fIQUf;HC zXa*X=m`kOS3Sb_%+kXSR*VXqQ_%98=U(RQXqb=tgEY+lo{i(%xESoJ4q*)5clE)5w z4%uE^e61};+W6gMu5K?u5ykr{Em7CB33NxNW}~>i)4?&Ck|@T1aZ#dL?Y}FW*m0R% zB5Ic}n~oXL;l!VXCJu?f7~mo^w4wvzi7sR{1s^7t4=q3u!=wCAiunX>;UGvJj;d0C z4Z`rhjtaXt-RK#KvthVqLJfliMK8h*LN2b}$D&Nm6(UqdMwN(1iGmI*goD4!Xbr*z zwVimgt+|v#0Vv4cOn}i0F+`yNC{8z~Z(YOZqQra>2`J+c;SmO1UTo)?>4FA+WVF}Y zuE+}!GE?YFZrumafqrN6fGuZY(t|fl5kvs}5VTq-M ztvfNTt>!}0xlnLFv{!CsWPg}tbT1(E5Mekd_)zc=8Wi-3>+>UcqlM9Qi>?}@DmQQm~q`uv8GJ9N|-3=1YN>s7Uh_= zt9#VX1(>e-f;VhSu@0q_^9+MvfFrqhuz$#Fqo&nzy9P=KYnt3^>`6G&6F?S`9|gX&`bi(C;wUzSi@8@vPGx z?@Q!>!ZUJjv=U}D+L#?Wj>s(;y|X66ki~FB6>8H|Gv!ANTw0Qded>oInaqC-Y8Kcu zhe!p&3pW${wVoh+W%KKC9;-q$Tv(6pp@1JrGKj$#ZxR?JTvc3+GK_AsBB|gxABkNA zqzihKw=B_PMG|Rw{q6Jiv*+U@tFG?$7Lkyj_89Eb#mC3S#@zpRx96p!!QjowNCM_d z8GRcvl(KV>Wn|dz2P62=-q_#Ye;OVl_~>{IBf1!I)Zl~!$TAtM`brn{#1Lx_RoJo_ zD~#ItPZaFzZ_h_v!uB)|4^JO%&2m$T44t|SC@BdENryuZzM6!%a6%#G!8&fO`t`%b zpc<#Wdd(M#pU{Nt{6AL3m23@Kv^qHjL_l)Z=1~waN`kRxtMOoK*IG|Y%eN7(HZ7wB zRq1z(Alc)dX&wowDWa2!G@*sBLzbEx1}V}DySR?i6;xcyS-jA1YwpSj|UuWaY*kBk3+IBMG>shc$r_};Kcry1e%zVq~9c?6Ns!KO#V`O9+*^l zGdROjDgFBV!&-wAR}6*34WFn!9-^K8j=$4dWQ+}+))lJ^BH z+n+6MVNJO5+caYD{1<#qx&BCmajaea$|3jH=5F48w}vlX#*?I*UZ_1;tepEj1XUELsXj(GVrG>panG?Omr!mC$^SAN&&btyB1#PSyN+ z-)<#{`vw{IUF+NlyQ0guFgTHg3UN{i|5g$czf5Hn0u=A_+s{&zZl!+3D#U0Qg=24F zaZdH8WNlK<>fN#BMFzueb+Y6y=)NFVw>4i9Y!bhDpvNVTQ)`?(N^b9?*paWWl*zB1 zrP+d#0{Mx7E)NEM&+t^uLI$9%kQYU*DYB2`mB)!R?URZiT3+`sWvcSU13clA0=iS* zyFW{*6cGvWq%D7?!ukT@4^eIyK=CrlMOEe)&8U{{m*kF5jSWY?NJHVFF8`pJ{#(!> z`g$e-oMg-R-6z)MOaj5ni9o|#cQTdM)9D8JdJq5;8A;x2pXpiX`J6`ngVriqcKqB@ zh)!gWMFMqhv*cd$xoCP^p!k6#E5%ZnKwY1o5xxWRLp{VchBl^8;zs1U&o*|me+`06 zvir0)Y3ndF5FIhYgp@*FisxYrEgFb?39=U`F~NEAz=TIe=96ek-;E-f1|>8yw4jn{*JdBfj# z@2gDtgxurJr{K-+SD>%@t3eeO>K?Xggy|KQn>6#AkD9pQJ}o-G#tl6&5#0PfibJIy zYDVG^p^AyjKV(KP{U)AakIVmi9Z^lqW*hZvy2Qse%hHi}$^} zlrwlE;Sz*2ptww*W%@8e1Ac=ZBoPwqY`NOy>v;c;Mj5k%tvXBxSQP4Jbsgr(XeNe@ zfky8s=0h9gpun6 zYpCBFlg8`K+}^8Na5!o{W{2=GOte|sf4_wY4gEl-Ar#MDf`Uf@>34ZkYdWjvI!K^Z z86&lu;hF3Gu@C=X1v#ruTD2J(plkVoo}^?26RmV3b~T}~!g$y*E0z9vlaVSoCfNO* zh^}z=)&N7&@140r)l_e)uK+sfdk=K1$cx36uLWyQ_JAZv-`4&Z9Kz00(Az%E6IT?=363#KsYfrkp#>^xF-3 z56m5}r707gsONOWdd2;`hA`3oZk$x`HF!S5h@8#&SDaY8sY|0OXh#Yw zvcfW&w%WbD<9U^pb`m8i==+2X1^@{FHp{xaj_efHx4bw_BN-K(8-qN716f)Vt8DP- zRP)!DF;Mk51vLhId4hp01q^3TjbY32SS4$M<0cjy{`_~t`SRKs^DWMyqf33QB3p1w ziG)F+8%gEFz#r8t&zf_;1N;Aq<(kZBe_90zMj=nQfvhqCuS0HiGkY}DxVE?YoxD9w z%fFxiuK#cc^6cC1DObe{FKoQTW6j)XL`w3|B_4_fp?TjaotTWZ{4rbQNeGT0g4dtY z?=E(Qw1cUi&vNWEID~Lp+9UoahZec+LUo4{3YOtp^*6#Yn_4)D%wbcN)J``Si9XRF z4AuA(&0>x6QsyT@21(Ly7bxnOG~O)>2TF8TS3i;xD!;a-_~S8$F_8zRt~cYk>^F%s zQU7D{{29Di63&zZEJ8ODWP2B+SX1dCg z0s)nJ+moH%*v7-46A)k zk3)g9Sy|^XdzIIVhRpr21jJ+tge1WR(cG!>E5>mtQ%6osq;JkDCU|yoN8kn-75Jcy z!VHh{G_@!T1GpcjAkr@v5R+KTG?0hi76T>CSnn43a}tbpTSWx=4^eOqpzz`t-CB#l zWkN2L~f?VgDsYKvu$K~718$C5}4SasHa(h*(Wej4o2Y%gtG!7Q;D<+KtZ z#tnm(h8mwKolwpo%$o*edlx`E-xDakH|J{=gAlpX*Z&s{=TMRc(Y{(OK%wR9FL!ze zdZrradwVgzm4X;n`&VMVf_v#C~*qvLb+rExjmW!^N1fRH&}VMtrsN^hTskWSUp{ zN8Nc1no#GA_=3cJz5LgWF1QGx>t58oXee;0?%u76dEy*`Sk2(|&)CUsEEDht9gILm z{k0wrRlOG!afJ$S(4L^~0-6$g*J8b}u~umSU1axI>d!BIw0raU^YwhOl~5je4purD zVTkT^H5KtchnWUO3F+~a!t-e%0{-&kcGN;ZcP4+a>Gg*C09B{3oH%SHcC$IB!2@T;6Eltly1Px&y{vyeRFQ~MveSvgX9l~NFMspH9 z7w|{h2LG*pSciCsNZQ^|c@U``{{~7Y?dOF>^(XV2-YMs<%EDWW$<(?^5)^3-beTjW{(Tzsy5>zI!7Qlhy zL$+uj=vwGL-tfwrzw=HR@|V${fRfU*bWYe z6uI))@bC4-Sr6!PW4O!@f2tBB$3_y-r!xi7N|)`%6c>TTu0J;w8bbGye^1;$bOft= zg29-S$YNFZ<}IvD+*#={Zo|pn>4-s)B)_5`kIi|upJxS0+w&cZ3Rc-}RzfR2%ml*| zs|T-XYe^C#b}mv^67iL|KYpcFh1+~x2Ih*?&SJWw%4>M(0^dN`+4=~68}YM@a$s`bDevr_9U%%|*ajk}W z9{ioULX48rx)?Bqv{{EOCMMadr12;kJ!r`J|R|P#vHFf;a79rwqvCn*NTiZT-ng--Xd}-6GtK&(| zFOgi@MoRGc(WB{N3V3K?Nm4)<-$D9yxWm3?qzQCEojf`2#snU*JJmqJS2k0Yx} z8HU){-)M=>#2)J9(?Ko)qu4(Y_WR)5FV-lH@I&>F-Zi5ly3b@>}!VCcIgtw>&kc`>a^|UVAmmmbcx>UbhC}3&WM!H zb9B485TW+s^h+zjM|YxzbyO4-=0-#1tuy<@Tycs(g*lT@mhM0I6UdB<>>LdXL+*>70ENqbf@*R zc;S>~#$1gZX+9KH(|f)N!En{8Ve8?gT<*&INUANil?Y)?7xlPczjK`0RLgsAGE~jrE>1vdG(>He@ulHDJXXTc>ATzJBZ zSS>e8WJrm6ZPgO1P8bu=5ngJg`g_8<7gc5qSe4CY^`~A=XFj9sIQB6EQhk!w%~EgN z{KLEzlTtU+u=!e;62vI}aV9%zK&gFQn*B><;aN5H*S`&(f0^X>yz$2?RNK3^>>V?^ zckR2n_5ntO`7(T47V6xGO}LE`h^RQWpzv|^1G+j5p;!qeuW)3+@{(Z(iKs@|!qkrP z8VcuLoV=M}v!h5N0MP`aIRRGKsBD(5`-BRC{0cgZiw@STf`HgX|AM!-uTTH}f&BZw zAC$h=BsYg9h8+$9UP$l~>T94a5#=%?F~iG}lYGIV_MF4`q2>GfS)KqZ8_Tpt{S{ZR zq|mrO+2V(x`IDZityxR^=!>4dJlx@oJ@v=`SKEgEBt<<>@LwXeeA!aIwW^N}^ z^-_>0SE9=(ghfh_TdLL5`ie)qy5bBH(995H9-b$;1*3M2x|PR1x&_yE;QidukOtBg zS!pxK{!?cX443?|%8G8Sxr`!uDtQnx zgeWo~0X1c#+@h7f_PY`%O$Pt;|KSsR4}!ikH^eKSHuX3^Bs9z;V+l0Kh>k3b8vPt; z2WyaKg$z|_YG(b}lV%oNGW=A?;ry`>J7-5u48t$df0*t!A$F#zIfx|7NeD1C+f4(- zEYeaC71!ppm4kLyf$d$;bQ8s8m?Bmg7w$NB zU3ql`C>%jF>&tUpL>^+C+Llv#84M$wz9IzR6yeoUT)ZtsZhjT&fo+TQ0$P0hm=d6Fa?3^)ghl3W3r*7ltL-Kf)l_InLL!s{cSDiee-@o$yH@ zOrT^Yrm={g+03u#Ron2h@*}};c2tK7#wfRkfG(|3OPccTbIiBJSffK0b6xmfN`EvFeJR1^+41 z5lxWXlBqJ}5Mdxce|oye`xuNAN#v>U(OFzE=w_BVt6^SU&+p(}@G@(5Y$ElsiboJw zg~wUP^Rjl~H&YDZnKYuQQ z_;;yU{Gp&_d9gJ>Hc4{JcJgUB&x@OMkOt;ARv*BI$c~@g{~zm!#j<3uJn4s%cSF}h z*&(kD4cEJvU>8A;aD-s5k3Va~nZ#po@5le-lDL(DTFKbpr*^n*n<3BM!5-h%+M1jB z2kHWC!k^WzvVGOS!+BQ)4e}37s>RD~nrKk3zBq^E z+>ws}qFBDuIB5)qnId_|(lz1EK)}DrP`3B5oUw>0V|Xc~{97X>3F59{;a3~aGSG?% zGrrAIA1(4YFIh=a>z#UtpajS0P30V#6Wo)a0ESSGL!M-EUYcHR%K%Y(C>8Nf;cb^*6A29}3No;OO-Qi=|g={(o&+5~`dgjE}r~`ocI9xEI>0 z2nI~`TSC?c##j=C86VC{Z)f+<82F5f{A=dcXgm^0ZV_#g$h#J=X@7?Z-=8L^YEY7 z#7i~_w;A0Z^J_dH*k@WT`9wwAhG!rN2hTbB6n8`nB|{&$B{YIyu}`bsHOxtFCaK&K zgXdP=yC_h8t7(|<5{vb(MzfuGpo^B}-mg2h=$=x?tM*mgSUi^D@Gu;g*k;0hA;8@s zW6Kw7*H@)LRJ7@xBBGY@WqjOb#hyx*n7`v#|`C>^4gj)f7y!?MsFxp!)DS;Jd>9QH%+v4aLC@+)>Hu(g^rxct=5e~fQ+F6P8U3gW`pd4 z0tWE(8h4oBnU}B^<25i<9n8`!s%7lwd~5nzRmOX@;yBb75F-FhMp5A^r@Urd0tWU4 z5qSbVi%wPUb?yy0;o}b!JnKM|j+d+Vt_qpuLDk zdOm$4&YEBX2>~(IBJnd;sBr~KpWa@Ox=*>P9pEmw-)^pUWVT!z5N=?6+;C|?4tFBQ ziv`7<(^EoZPwuoOdTQ@8!&!2Ar3+Mw>n~mTUyQt(n(BfEK#ND&j4uW{$F)Gvws~;8 zqtI+~XyGo|35`QFpg=s~5x^1uIcd*+9aXC-n&tD65$2c-+ZF}i^7&RwOIjo;ImrVC zP&SJ%CYBvkZHk%eMy1?7-65>oi6RdK2w_dcKuOA_c6asAP@pazl4sMtR)`%D+y7RgEpuxHmx7(+Tl6*-R|B-Dc;)TFGid8D6Msyg$C6MTO90Vjz~Gk zwKx#msE-lhKM8?)u4vZk}#YK3wT3t8*gnqyor< z^fR}qjh8ggVTGHt($PTVD^VR^Uj4{-U&NS!$IWP0mpT%7voW@RSw43$Wo?P3$e)qL_D?a^vvrS*(q#J%74Ci<`gYN#% zZz$dcul$=1qP1h2BEQDd_q0Q|qN19|jYalj1*~xQe?v_NqeYxM0VgTP0iPoj1^2=9 z!gn+xo{2A~>21JY`{@n83VUt8<#A?Cb=}tIi*qFa+R!dmIQU?l4YuYjaD0KehvbL zP_AuIjYC-A4d-8-_}Qq2-g5W(=6&v*4w(I+qOGVsuaDuXf?3K(w#zr%lZ(0Z`O9MI z<83yfU15gt#Y#s*-PgD$M>xQADuQF)_~}ii7?htR|Yla76>ne$tleyA!&&KaPN&?v-#fPgi&o z=ayIj(8VTdS~kH1XF?Y00_x@R5t{tx01~K%mA=O{pB_&UGi1MYtpe9{;8&*D@Y zzB92pdSEV_L&1PA_^chG~B>lj!+>bu6f=bwyCH%X8>`fluH7wlzg2`{dqTaG{vcEAd zI9s2I7(U|1I7#+_A%)AIaxj#5q6d_7=!o3wYx>@=jU32EG)Xf-_1fI7;+G>Z8a~^s zW#K1YH?;EPJ2Od!j4l@KkQeZ*!KvXC_`VepoH zW7#j(@4@QGPv9+q0cZc-KbYlK=JA+k<8{6SJio3pz|T-V?_5Inkloo;2$}43yPa|N zT6)TtTopU!*~JNE2ZxT2w(#TM0iv>*WWwtX7Iq5!r2{SxuR`jqQO9T?qO{sgtQe+2 z7ARW?T;x5Kp2Xs~4;1py?88ZbgtEcHioYQLUiJlWzIeVs$*A0){8X)W)o4Q+YuNJ> zcs8=J_^fN26hqlK81vn_u&}P9)%;)=h;cHn+^FhZ=!|llA*V$e?S8y(g`ic7PWfq3i~>ic3ggy(5djLLnds=%UH4sYx) zYoSU}$4R$(%zy=`pr&qje42BkbqDFhQVMBn6bmfd$>9b{$DSfVNL!-)B}i{M2+MPW z0Z^j=+3E!FFUF)~DnX|=ZLW^x$YnR?xC}{EC^c#5D;#C4`tSB@yOG zSS#2WV&}aEN4gmL7fGi+pP%2FYulz5|5U|CWlA&;T!jC;sw;a@H(bvCmJlYS*a&2I z@09S4rP`A9pnPDqR&x_U-`#^fJHvi!yd+f0uQ!HuXzRBzX&ZvXVGXyZ;W5FAqfE8b zj2FDn!7S$%b1Qs>l*Pp&!>D1_3OOV3!=CNSKto7gLib-O20;3xS|{TbSZ_K zBiXeh*CLa_dLY8ldu#Zv_efFhHkqtFfsnO%JC{<$nJpu2o2((DYDWx*>IUYF%y}dp?))_%KMU z7946~K#YLvbu747KGImzy`tcZH9hG4VWH+6LYRpE&vPdc-nnJ>Fw%PEgUI9H1cz07KiVT8U9>FWG4O&VV%Ez}(!YxufmrdMe56<_Bsfqs z{3%BF#8mkK{@ccD_iPeV4t}-(zEJ?fC({xoOY-Q1{1|^a@Z&X@=U|MzQx-*bmYd&I zDD~hDrX9lEr2gU--cu7OV!0}6by)A9wgW< zxO9cW2Yl<6zuT?XHdSF)ePC%PZe%6)>+S{fPxfc*;)|0PQao}c&E~wS&-^bad=%a{ zB08PJHbmM<%HQG+dG&a~L3+eUrYcfn!Nt3yMgs;K6coOkpyCX(*0In%dRTK7Vpdq| zQ{MN5_SF2jY$&Y&w(ckA^}5lc$}YIbI${CtJG4=k<|2>>sz)^rL=>_+o3_FqjSDw;)zcSusX{lt;U<79-#%e@|h6a$< z%up{i`7?#t5f-+U7faJLVWu|s+`|ONypexZftu1x4SXb0C&#*hKI8{UwD7z3f?jUo zOdNy-+Y-t-pf=IL;W{krSWB`u#K%J+phJe}bsv=Bk>Vf2>YmVGDRQsNUQL`CfJb=^ z$kO(_kv~p~-}#HOD|o|p5*Xc_*BpE%bquxg#=6lwfY=m`)(+mXJp~O9TgI@hwoHLT zi+Gs=5?F!99Zei)QK~6gj_|s7nE~7$FXSjTiRTHsKLEHNp$eOV`Qhm17GNP~+86E` zhqNk~&AtPp3-VbF=OwS()Ommt#?}?Xh?di`@!vYkDhOt zKSIMaNDt`XT5UpRENrR4f2GL z^^9PHwA)hsbO@q;Mg#$>3XYvzLCMg~P-~B8?zuL$IMCu9<1T z#-^5}w}b~l4X-D)iE?-+$Pt5@OK((Qk!1jA zuSKADxhxt|BwRf;F$3dz+b(C{b6oo`z9qg#r|=}~{J+d8_|aS3kA3o0V~-ByX)}W1 za0aIVSzzDY6MY5M3y*)BSBs(Qf{Igybns!4bFMY2jd+Zx&0byOlf~YyeV&mwVxKRo zi4|iG>WKUdQ|$8;W^T>5E${~>=N%zNC{|^g-^*DC4a8&xMg4VEhWzITbUvas|MJZ} zQ57KoHFJS}?Tw$wkC+c+jN+Hc{IY2kP?I+_3l-NePIMaKX)7nww2W01(aFAMzA8Yi zcTdhkaTMr9!;Q!|m^779p(0Hgb6a_`?&)IcwTLG;>}` z^~H-1K^WpJ(nXrQOzLBKN0iq*uLse_E$p!BkQh86o^J-*_=(!~j;$>OE|3Exop#>; z4h_G1IDD~Re=!EXm=0It;DG@4vbfwc69l#aGyaXJbyNB><}9l1ge+5knX*^dBZo&0HNyrPBB%jK*)3T040$)< zvpBgAH)vbLtCVLJXg-HlC&NI(O&AWpT&g6?$FXli!l(6w6#A|{tkwfV(Fx)iUDtWGnmn*27ogb8p}zj3vnz^jer$v;c#bv)J>((b!?w*qooj--kviw_iF;AgUEb=9>Kt;9RBnB_-^QZNJ{}WXZ zs_%X##8;D7a!!61T$od7oNbW4DDAn7jY7nG8D=OJiCwu0?Pt)>lJ=E(MT4=Z3}oA} z>QQOpDX$t%lC2w)PRDGaTCQp8lY`@qUZx?)d!xwBIA4Q}06{p*=_GE^A|HSymYwpL zGkFr$+w%ef&Mu6-xH>i}Ih_8Jo~g21cBnF{&adf}Sg76a@x+6Mz2+&xpGY_4vsasG z%y(1oB|JK9Jh**WxR6gOD#F#U+3)#LAQ7uaU^~-0$=(A&EEhK|`2bdB&S#si47gk> ztVdc=JYFfVYK4`oZA_ch;-W`F{%7Y2eEXZSq-~7>lj$0U15U)VsiYtuB(%WX%;f+c z+aZW$;YAB<>Q7dt-Xio*nQp8FW?_HcK;U{V9GDaeL3X|v&Xr<4%Obhm7SQV?JhL<$ z(x7H;Z7Zvd5-os&2wLRkR@gTNgt9tUyw#~8Es!O|ucw@*SOmORZ~j1_+c(IWl|1R% zYnd*@4id(RWv(e@K}HuZ8!>^+V%F@4yJ8hY1T-j`<>#2`*MKB6AP8%z13(1)XDd@W zi4&n|&tbsZpDY9jG(+6UT~m?>cSQ?YwJIM|S)f~WG)Gq9)%D zb;V1fsF?W^eT1AgbgHJwn?LOPswo?ZalmXi0<;@oFa5YJizR*m8c(MaTe1YZwSZ z6~`ay{*QB_bND_C&4s2fS3V9LcrT6^0oyF=JO_> zCG2Lq3dRw;CLm#Wfh{OBPDbfGqnh@$S7z z*U+8)>B9|ysq_hYq#af%5j_^SfCP3SkC2mH3lh#?Kx$P%#|Ne?RNnP5cS~ls3}|}A94ULZP!ddD5@qcZHN(7 zhXCj92}(}9qN>PvQsn*v`)#84w(2h(h)wCN9vlNgFd7hwVmW?^Ba9#jF*13N5ekB_>Z27(eyHxDgZ$hKG;E zEs`=WVhz&8i>t5C^}l|&{2c<0ETspl0VFK*SRz zq0wL&hAY>2%_(wwy@T3i>0s%b zK;R9#r#g^bd8Ms)2evyQY1!t0Uvv2lEYDAr>Wzr(sLD3^Md7FpIqNjg z$wrI$mi@XB=ocR0YN4D8#D9Buba6|DMg-VJ`D2R-Bm&3DQ0VjP9fDZ57yfnz10cU2 zZ2*f_^eO`P=}7l!&_eaG-Y_q?>|;TKrmavyhk$H@o$q4q1`tjK0_E{z`)CU`G?*Dk zQi$oe?179Dp#Jqpxrv=P<6zeTK66f(?X|zWc=hV#Kfn9_rJiM2HV{nx;+C4h)quco zuW&bjFq8oVfA#43%O766`1B9BfdwVDGc}tu=x-2eI^n@sM@1A5AegTnL$z)3__G(U zo`3hv_Ya}`_-FKv0O6G#k{AZ!0M;XUaYuk4IuP{BN18l;`QpRLeLzFMPzY-YL#?(K0Lk4A zAhMPdm2>9*?VZm{TUivxwYF88jnysk1YMLa92Y}ZW6MnGfHOR16GA|;ObLNd;=(Y_JLkNY#N_&3YTKMQ zlevF@nEQF>-g|z1kF*=nc|>8Qqmkqj?S7Wmx0;&XV>4_+wq5}y+Fzc~ZUcFlA#I2i0>hNBMhY86< z7Xm3Nxdzqh-pvs4hzN1O+C{9ZUA#mn+aAKs;=O3`VWqF=IQ$Vgw82S)LW|9d?RrOB9uWKUZYuzp_AX4I zY}A}~d;DFzmr8UOj6{AYi?ZtV*@tf8NE)!>bdB6y;sioD+y@>JkVjn>;L$Nps+ZC* znMX{#naZC2;1Z+^^@PZP!*G3wqm8(?`98#!7AkuC*#mN^`9LtxW`!f`U?zxl$CHu6 zw|U`8PwMLM)#6?RxIaFa5K+bft80gk$PxnCd?V>Vw4P5V91_d3RyeU9mBhoLMBGI_ zNLIo6q6Wm_FtZW7*#}m-neLIyG+rSK+x9{rSWLRc&tn#K<6LvZ!(cC!2tAE2S0B}2 zs1k{r5pgJpRoY?-tkTrq=7msJHb4ggj)^D04}_wHwSCrACI9y!SZ8RM<1)bF_OvHj7F_7OGQ$b>BN2@8aoMig@((Hods=Q*=x zsVH&NnXP@-fk5m;p5X@4jbgCm5QdFU|2rlkO=u#f*E*P5zx~+CS)cxdp$h*kUsjq) z2jYi#JbQEzHHvO0jNZH2!dI{1ENL_mcRFT%YGEw?Ez1*ggMhq{bVt2ao7NKvHo0$t>x)X#(;LA}6iR!9pNEG6U&?yKM+BbRz?a#mDin zYNpS00`;=?%x{q-TVsM&EV>FWlpmb_u{&PW})KQQoaZzxSl`Rf|W8XBH~_+-1u898~W0deL|ER3D_n4^o` zAn`L|h6F7R%Cn$4NlG5=q-!aq z5>MJ}3)P&wgI=T?;3YBYOFI&9!5V&O4E>F??q?5l*L}>h1<9Sh@Z0dA`IJioS!U zv&&xq1Y#%BGv>OJOyB&3rjL4iHxgAhO@(ngZ?4VhD^e#E!RwHf^@w{gH4DS;VNZ<}Q1wiZ} zZXg?d&3(Vf;*C3b=2X=ut?Em*%U6s3?CUMY&;$YbR`BYj=;9Q2saz)DZQfdZ(^)t= zx#qgYf#L zAarOUkh2v61iN+a76AcQU4CqVUiHjcpZ8rEfSf09hqGLhs(^NIu%}bxh`3g*(2>y8 zvGJ1P^00^{3w{><`8ju&vK)fp+ z709xH#}w+`RT>c105>>>-cp<0MeSDDeTNkQT0}u zg~)#}0?7naM|}#X=H-R3T$-vJP7(3URt%6v0`idap|74<3or?xEeB*u>d7yFmk<86 aK>h)L{zE+kk&d4L0000+>2RjR!%@3A#7@B3Z2tk6Ah+U(rz zPA~puZPw-P*8Z~Tj%?G1mcC&2idmi${aKsTOXhawyj)q)+VbnVor41dqaX{5fWU#m zzkL#MhdUG&t6N0gy}3e7&(-#*9zopr0EHA!3N{rsNy{(Ro&zx$fcEa!QCkMD7u-}5-mYp!c1+RWrEBONy#003Zw z>+716-m^(hS(;;{*T=pmA^`xV>OhN&1apMHfHw~7jPXDV5CXi>0%#(}82})Tyvwjn z=BtZ~m~>}*N;yF$8wF^;J{+E=aq2UnGg&(5d-QtDJM(+CS&}C*+Kf`}ZdCoK?+INm zG%zt;%tgj0?esfmqStl%=X(M}=LaSxHR`tNzt0{N-g(SShrPhRIA0L|c&1^pN5f<- z;38~MBTN5akz#M_>TrGcBzuS#}J? zp40eocgMG`i}-kS{^_KHQchqkKFg-#VU5`3G#hpL2dfQ*vg&QCtIEK4?HfI-HVst< zTHh@SH~SymojkX@zNBO^vJXD(8yrt0&I~!lpGpV~*4WnI_z_^Z-+!wpR86X(4jcM- zwdc%b_uwk^?YliImOLqhV9(D%F{@yv{H2n52cCVJA1Xbw2O0Yd6c_l~{J)^~iFa$v zwli%f0%kLN_7;Y%oumqB5(iop-@CgoksaK9&M3P_PQ8Yb)>==1qX9~%`Q>2lTqyni!N%Ub zeNd&3MT+(Cl)+bM3L@(SXa0%st(3p4k?W1md&O1LO{ZuGEBwphD{2h0nx34ld zCNJ?mNo6YxKe=97D%c zg(}OtVX{EOrxD3)mctZxvMA(PJjBy3Z!j-?j~|n(Rx3GZ@RpivQzIL1pr{eg-V<*- z5cUey^o$xMKI(}vt3a?SUKmNZYI*MaG(7ZReYfHA2Zy`YA(;b3{%>}b&nUj(P_P`K z|2|2`AGh{%7znxgx*9n~7dtUfX1?4#xMTiybZEF@%L_U5Oxl=f&bzz8v>mbyp`RaU zp~Y(?7ZFE!S54H0&Yv+cv16EHSkSKOQ3>|O*e?Q6X|VL-6+vtwhaW~y!XbnX-R*fC-zbC3XYUG~0_R5VN z)`sqx*buQ#Cpd05JAg&CwzU7vS+(}!2@l^V z;9*gR&LF>g5KQjM9V3(0WLc?A!GKGt&pP`B-8w7COgg%Gv`uHoS{^?C)EECLl96Vx zUHf)f!JzQ>wFfH?Y}b$NR|(h2oZvRL^3^wOQ*2YZkVeTLrzofwxWR6RQ^ISNx)zvZ zhV^7XR>0?2O;$j`s_|;!8RhSwFp7x6&POdK&lUqlwfnKGT{x)a$*~9HU5of{Zd9D( zL8>1rsn@>9V|eN$`Zu<@?ccCLPkvi;4y_H^5-W1Ip7|7bjWNQRbr33()pz=MKG}!! z+hqZzlE%bhPc!9{lDTXtzK=hRETZqt*1*ps3fMIORvvV4m``XqEF1D{tuI91zINMV z(5z%YjlRrrDdd~F;3MW9nJYQ=3$N1B#(dUPMr|-p$P`-|(P^`&$Eq$^L+V2FC-FDJ z#74H#vCUE3T=;~~(*Ai$&hCmlzTsVd#H=u}TLsUnVihx>f2=bWIJpJ+Sk2{-YkZ z7P57-6_;E$rN_;w6~HiH$whfj{auWpq?Lvuk*oAOCr%8)k*Axd+2%_dnGz7ySI`KE zq|}y+6YJ6Oq35HS*V9ui1c`FQ7h)95CSnHOmF^UZpqCZWjoonEau#=eVOA0wYUiSFIB>1y`95r$}GD=@ENpjq5@2!tS@R2 zRWbYVAqy*SZ=ufY2Zc6Aj*9iyL7?1gb(Nphgl2US`E~;YJxaSpXuj5%*V(f3p+l7y zt=_MQU4#UFj?x?>(;tXc*RO6bO4343_lGbg({I2Cl<)D#p5w=~JGQ2-y*E(pUw3>E z*zviz;SSF=%f)uan9kl=3|R77VU%%mHvy?h{=xv0dAEd=fAxtYHdsCZKEgCT|L&Pi%Jp}lqbusb`X3hLEnpPA@>BLfa_nLKYDmSRyqo0vn zyo2e)v{)Wc9ijq~NRPuLOB!YkeLz#0g+2G@uT)ivj2aOa8RGm#@lRKGmNHj;YVD>=S#F`v!$x;&dAjx(K=2c>5jI*Jh@DWNgKg9(uh<#a#82 zvTv!`dat9rQ>f-e#v;n=ZhT`&cd-c5JF{vJ*LImmKb4B2hZg1dDKTHc-(X3wpN@63 zJsw8GlVls=!`H-hPnD9BW8(Q&<+tS5`cBU#Aon!} z?@7pgh&IDKTMzXEkQ3xM#JMfqxXz%qlla=aw1dp-HkS|@FQ#4`2cd`cb-lcnG&Dag#boTX(k|G4b7tb$$sXHimSWwi=EzPIJ=JOuht9-VS0xBK ze2jsb%RTz^vZ|P1i^gp`TjR8jXN*w=C*p=(>0b0E9hcNQ4S4cY;?9Q>J*1BH>UG(h znY-wY`*i#-_#LklB2vLG5xk3yXXC{aqXhfv37>g+X&F+ag+HM`(1B0bl1T@GxNovH zeJ8IrE~S}tVSY=LpS!uvMaT7Kq4TOEnLxf7t~BbY)I*3pn-j;Vd;6Tbt$g}vRs{ow zudxw3EpJUK(tDlHCL+zWTI3{CWTUq-?9yp(#c)bQ6LlL-6f})h(1d5PT;CLpe#|1C zyJQBLqCr0;#z94I4=(gw?kmXGOl%$H}c8o<51Atj$9w$R+~{GOtvC zPs8$UhxtNNt2fTw5Et+I%IAM7>q8{4tVU=rve)4L$|WI-mHqFj0Z#1NPcIDWKNEj( z?i!8acnb~LUb3l+>#6CbJycG(67oeP^E3s+WK~M-iKkE<&RrU&H`*tV)4+WLOx%?b zp@NhxzDRE&Ivb?kC+u@XyR`ZVyO(7f&ZvC7FmwG5Ue07gAyMX5d%9%LIWWwXq2da! zd`4u(@VeJ@Tjs1biaq>hQIfaFVZooU9zx3e&;hDcW=Vk7M2=Sw3^`N}UE*%J&T0Z=XK zlCRaK4FV?lc^#Yyi%ss;AGc1pU-@u+cYUq;K`%=eb=8v>Z2niXr^U#oy20SBwSXH0 zX7(qn`|#w%Ed={%kEHmT;9OhD zH%B2cB9%znorPz`B~K%Fa~+M~PeP8{qgTT9lMLie^f?Nu6?J70cNrp!4}=oF4!SV{G>4!vz z`gD#DR!5k8!^%3BG{=raCBDa{2PNE2{3f>CHo|qayU7Of{mh~crqsc9-{Dx?+x9L# zizN9uyFpEX%g1_Og+eyB{dgme3Cbcejp5orz9zcSu(p`6+)aJl{Q%CGuizJuqTM#6 zcH+kI7L~V+u2;`c=KH-+uvCBs2gdT!6o8N|C09;v$9?QfC!ckB3c2ga-fd*`UWmKf zRZ_^{V$VZx)aFfls8+QEHX<>bM(_kxDmb|E>}p8#_hi0|h5oh!Y}@F-rl#hllPIQs z(bis-60>W!g@mZKskszZp5#$>SqPXe@*bo4as{GeSw3~X_-0^vK(HQXF(-YJ*OC%9 zC7PPbS#w}rLhf#o#Py8gw0J}{f_AngyEB~)ReX-k*IeY7@w&OLm7e;@^flYD{RoaQ z-56CjwwFqN0>ua*Q^gK*42_O=diSfu4`Dk&F0*1{Je${>r^3@DYNI%x4JwmKM))NbeqGo%?c z>CWWyA6x6YMbzJ;vKA)xJYL_L4+YS|Z^cr=l5$^(v|C-kR&zz zmzun<9L^UMSn@6jOef~3ShKMC3`nx(Xg^@H@degw(%-(>h!MZm9-VM zl@`lBzz0V#CnTNtk;ZD%Li8R>@ZaY5Uj^ixL*IUiz?{2&W|5z)enOO(`lCkU>|>iJoGdKSF6oGomc^Fyna`R6?kT_SC^rZ;fw}cj zMW3oECGNkj`B>fvMmj^lXL*I$4f%=*K&edbbrB!CcZ3cO9DmWBDrNU~-0tHy8-O;w zX=&9acp3?5$Kxk-Pd~`HBIzOG`VL-}m~)Vy5Zb$C_|d%W0-10t43e$ocQ6&YR%=_N zEWaXp=BDO$%uFx?MUH*8wf~(}WGm(_pvh&*0K2Ee-MA_KY3mug2Kl#V;W3NP?k|z0 zsWvv=6wEA_qQcO<5?Z^gHT`oh0CiNOesuXz@C4t=(()v@ z72&6ljAmD^{ z!FZ_&t=2XO31CoaLRN~#Kx1zmv@1qG5Qjb=Xkvj3bVou_LK^CHFro^Hz!ObC2oOCz zyznYSHK8M371HO!ZaE==BM8A=P3WSrnSc%!hZazfRgeYBoFQWTKtk$t0x%rPS;bse z?>7okOHIg?K=4+Plk@lYm-PqBVsS2V@=z#L4hWJ1fn-Ps8GL{j0YQ}U!V4c#{N&I@ zRwHvxn75;){UIAMJWYC=M!ae?2+Nu$U!L{jH(4i4K#^mqbF4o+$)kmf@I$bo=B zWf>qy1_YJ+)t)qJZ2X6{7ydUDNqWi=5#Dn0vOqac&%apU31|HN^!K+Gcni|49XWF} z9_x!kqR;rDy$Hg;I`#JO#sBKl7mq&dI*Qu^d5o2HfQ0+W`Ecm%IJde^ggmU zME{CJA^*U6`{F!~Fes!P+5_!L3ItDLmj4T$fN}nFf&Ma|!;$|I2+7?a{C`3JuGdj4 zN4l!$Vv)XwPT{(0LWl9Hps+{`O6BMy)EVfEkat#)kp}_MG78Ellnm4ffsz5r0}=90 zAS7B@N%VOmf;giT!C)i; zi2@%{9j=3lmKj`42qX*qv&YN>L2$<6Jk^AZFkZgIKL;!@p6K%g#Gz>N5TF7Ss;H!- zs0;!@p$dP3EYUbTDaj8x<$M39mVUiwbEnBw-PU zwLxM&>L<-bMF)pQ5U@B4EY?F!=rC4+L&~F)5`g_yEGkA=zotx8EvG8{r~% z)P%|XC-8q^I`4}0_xgY1`4jpFix!UHkHxv0;!K^~&`836=J`ADA57+?y$(;n1;GD@ zN&R0q*w1v;C)r|g0l)Y^kM{Z5`ne%_V2-2`5IEWdR1nCY^5YSHXw=aXkaYYxgmguC zxu8kA?Qf;_dq3uH(in;aE1;F+fifTjsT>p_ia;5t0uUkNghE4|p~`450_gl#c0AUZ z;E%wewOmL!Kw4)~X&tSz!0DsxllUvz-xW>D5hRhZus<-fuGV*guG{!iw=!hTxoV7&uKMe9m1_4oRZ z?*9h(lfejsM0??}|0(oeAwOmL<-9|R`DY*Ld`3Dw%KdSE{4FgGE9Zal_$}T37d?=u z|8??@^!-<^f93i|3j8DRzuEP#T>nUce+2$FyZ*n)Mfc|wHrk8yMbDpf{mgnxhxBVc z;Fy!)SzW-7!$)>q;Z0Hxt+&24p7e(R;ln2xu|UI*)JaW%8=s;6NKZ#YD=acn(+mL6 zOu%)uEQlk^w}YNoU0`c%;7RP{NpX4Du0y6MK@rU6BjK<^ZqVetcKo{YO|aWY3Tyg^ zPAka$KC4Xexqy%Qy6M?hb~#;}R|Kv}K(r(%IZX?K>c&h%tK*j;dv_*x$Aes)oI+i? z?@VG>D+ecBvU~?Gxi8Cziv0bt-_XqFOU@@6V$w$muS`~f+X!Of@^2-_-y&q>GbX3% z2ev@BtMUgU*eTMPJU(c=XOgDA%pAKN$4YE={Ys`m@lAwLOkrqE1%Mwu^#jnI(92v= zOJM>TsR}y>&!l}L-x&FEW{+%Hun|5dBS$^4>H32z7S26x+Mw9zIMtgUcW?__)VQ-k zq0UHHUs%oI{6YEc%xh?C>V~akS7F*)HO2vp?7C4`lA52lD5uCA@!9tRr<597P{BYJ z*J4LAEWnC<`~`a3R`WbrkDM@)Z{^jkQb&`17?gvp*SE4}m3)L>Q^k{FjB5cYz9#fU z(A9x+ zqx|aM+7SVtHZvzcsyPwg9TdJ*QsshZj_-!A?=*7+i#Vhx2$S?{Ke~ zbTiPTTR66j3(u53t2fpC#%?`%(e{3>p)~OAx4DrYy?`h)wbHeZRxvhHwO@mL>G@u! z8Flwbo@ld%7E~$~huU8n8;^CV>z}c|l$t_S;^wVk|{eSmWh?R5{D7!f0SE=zBe^uT<>smE-Z zV=A_GR4Knazwn`^+^zY+k4ox_tVhO!{!!yY6rNvTj_oxi;4g2(sf$a@;yaSx6$LZD zuW2TB=Y=#da!R*P?#a+S8`Z0?*h;>Xx+&;bJ#_0;;P*6;(#DlbqZi@!qB89;U;1@K ztrFbLHT=$!wG!)#So$|WCo4~>zy>*k&*GR2{E8U7NQ5z&aE4wzmOnYZykWHS3v~Wc zvck*Ev=KCjO(Wg=QT}Xqjo8#NxPJ!jhvdU^<__~?xXb|}yp=7sL9Q^QG~ZU#UIbJ5 z>IPD@PPm=9p2Ohz**;=+7QXZ?c&F6>7Fu{Q_+w6CmBPUobcl6RgQ?|iPB+xwGrnsg z88Wx&tMKxA&iz}-o!!Y^6%F9!%P|wG%3l~AE_sNv*$_F< z@1tF9&3%SUN{b+T5reR&)83h;E^l0?0-d&r56c<+;g*=BVP~ca>uJ7HRZKG>d_rGI ziPm~fu1a*lN_Vd2?YGw#PMcTJm$JR~w{(|oYP2fxJg2!=TXet9lYT!~(a`CX})D!f&tK$sF$r@cr_`IZosBlTGpP+Av`? zL3QZLN(k?8LdFEv^luBhr5LZ8C=sQR-yOzl{>NTvtXlr>!aVi1z{AM`c zrQ-oa)Y;rMR$~*th^oSUS2EA6w+h@Lk1c_X^%LMYw})iMI_yaPH&}5bywY%hvpxWscWqd-h_9)5Tx`d2;JY0ioyz1qbm-ic8#cU?88$Br4 zRHb;{KzVDAGhQmEaf|;Th5UIY?0F#_%<=-7b7LX>%1G4}GN!fZX&+~L$&I@IFc=0g06SDa4HOoWkr$4}(=;ybc^Uzfj_EyZ zOw|tSkDW_N_YDBK36Ce-3P$)7J60}f`d5hWeI5j9VpVA!opmRkrixfRU)d%`Q59bL z?%4TWx|Th?wG;NOg530ZCsS zGPWvaV$;{W(I_fshc!(pi?4LAUkpM0eVtDy^OrBz-b$AB?XGhUF%->0z!;OeNttW? zs{Ea?c^$sAp4|Q{eEAN(7LU^J^P1HXY&TEjN}~y09#Lwa-S*-X^Hr6 e4@&qqrHsS^)~`*8s-!PJ0Qeab-BNAGEB^%Dcb>C}3 z!uZ(H86=IQy^NqeeE9IjjT=2ZJ*Q5cI(qcz{{8z8A3l8I#EH(%&c43B!NI}S*490H z_8dEQtgEZ5uCA`WzP_fW=FXivj~+dG`t<382M?YEiL{1{S6HbmoHzguC6|R{`|#@7q4Eu+TPy&`t|F_ zj~{!z-l3tP#>U2b_wH@lw5hVPa?6%2zyJO_?f>_Y{?42npO$Kjv&2V5=pcalzVx4T z453BP!ruJ$&EM3+zw0;J%yf*i*^*_PJLVsSSr1?0*w*y98GnCRe89U~uLuhBbEOTg z@7p~=dTHjkb%XNNFKtcNbOB#1n}fmhJOs}vi~u;Om;-`x4NmfXeFT01{{Df1a{y@m zlEopxMG<7MR-q;KGd|pD09Y$mvJX%BfFV+!oPxleu&Ql}n2sQ`u&&7xXkZ;jg>BIJ zgb6l(RsC7S9V;mZ)tDULP;q0ke-VywLqfLm+Hl6gMGc3O&1W5!{B{}_s}4_z%qtwh z?fW3?p--)>!vzj|uj@z101i2tbj5bOpU+q11w6DSNfYR&QI@&OW$*b&p893>gW~D3 zci+9Y)t+v7zyc9$y7gF7ZBGy*v{!E!Rojt2krp$}8Z_BxQ9Wn~39BLX%z$a9zDQ9@ z?Kz20jwGW#^goG4|C?waMU)2r?x}o3)U;)iqRlH3bv(4pTl%i}{M-(DDIY5Gr_T@n zcqX>b()UHFdDc%UuaY;$YDdi$T`U{GMN;V(pAihE7{kO6JcERV!dxdT|6~SQzC|dm zoZ%>E30dmu8KJCQb(Y3tKy=^$vM)u=IK~huk0djXGY)@kLg3)DRUJmSr)Tzg1N{Qr z(ictdhv`@PkbXMS+XvCv2m)S>7JrAx&>!SKA`paM1%8yhQvJaB1rF2qt{Fs@Gt4`%rI{Km*tiL*;Hc^7EmR+{xT*l@8#)?&5{9eQ|S=eyB^Wxcg|$$n!C z=)j*CY5#>$Z>KQUm_(suzV&B(}E_kh{2d7 z;?&pU!F!Hi2kOxyd-fko1>oQ|#{5&M7<}%W@=Pjv9zK1+1kvd*T{Vz`T%{v*H;f1y z`7ZOWfr2QrKcdN=$e=rqm@gq_^D_?gFTd9*qhut=1@1U34zIU^XgltRg2vR3k3 zOX){DN=}+oh)E2$9|g~x0q6q*p?zoM>;dG%!IPYCzrA{EFHdqu>rpG4D{pQf#oVR-UXLn2fqq~)Ut0$`mG%O2QyD;>Z zFuZupVxMJSNNaL)#;}*-3{HOO{be62P-hHUva2>^%(pYXlyV)SnQ0mXPFOxi$og`g zoy+5C<*AZ<7tEMMM^$n;hbhuJ9A#RkfrMasB`i~B$%9MEc%sDykiPE}xWtGO&1KMg z2*U10*0&kK(z3`%Nwi8PJBO%yQ&3(=6l2%*>yY?Xs!(f^L(iV={NcS6kO?uFk6yNk znQ3AAp>9ba7v(Rj{+QT;a5*B=Y%>Ee0bodqo6nQG*Q8pkNhMbQvr;9+HTllv zAFZz>;P%y}A1L%4jSFALEyU9=d~^cK8!78th|ed=-Ve*&|MKe@HF4IY;D8IO7dqOX zuD+=t7@0G1gzid!r>*#Om6jm5Qi?HzunQ6pF({X-P{t}pS-I0lh9WYglFn4ll*6Um zW*)<01o!OQr(S3jE1IdNffhkMknCW4%jsi0CwQR;yOfNc?cyOc5)DO}Ox2B)U~iX1 zgFb&Q`5{^E4G9seQL&gqA1rT^8d!mmFlzI9ow)*Nn~0387TZ^YQ5Oo#s%SC*zs#H!b?tK+bj6+&y?;(Z;(yN~;yhQvoIf$yOr}2UN;I6fRz?P)Ku( zB*(pe1G+t44okIlb&O;qhN5maHqR(NNN_u$#uHt8Jtnn+=-_H|F!7Zu%IFZYU}djTazrK;|<-oZ+fpg=U|(23?^9YS4pF( z8U6uIP7Q4!kD6uf2nsT#xPf)2Jwt*yt64#ZrvJLLW^Ve9tc#zliCY|AdSFiOTU`i! zi!z%3rLE)`@(s=)Ta3{8_>$#egrHwzE}1Jv|8Cs=cdLzX3=#LzS!MZS zmmPZ=HBty$GbCaZUA*L@<#R==>4cy4Q0W;+1T> zP%701CmS`O3IsXgviSziszVAb)5yoLlPBl38nFXf#pzu9{545$yjVLhAPdzg?f-()A*(`ctn`BWJm4HN$;WpYi6^}@tu7WFE+hN z|H1%oWq)+ml~LPj*-p(g?+&md#~)3zUe<4zQ@VRr&(_O37vtX?NjcKy%YUl^y0=6F z{~$^gsB`_~qE?44VCncbM2SYYHg|D?Zs~hf-wWLO#=Vn1-S)#1!I-V~r6`|#>Z*I* z^eX5ELqx|HEaGs)OF|Q-vDsfTN*xSkYB&ah`QidGCf6u24g!~nSVD#~zP18XalnjJ zCE^eXkHg6e<#Mc5y?cR9#n^FN7;5yzH8r&@od)a}#a+0YFS&{1{R0N^;58|v^9d)P z0C(;h*i!9Ft%SxO9;Q8um%lQX#6|-8yj9(Rm z6MbyJKS^g^c|e$zHd+C675itN%bNB?>GTbZ+MPbn(Xii$%H=1|CdA}k1bAiRz120d zQ@6#8Qm@hL+X=QME<{&4;KUHqrb{b$juG!qAD16nA#MM`aVV8ITM@_lj7K;8HNpOm z#@ATH9s0WITgG=b#=waRUQesnql_V{W~Y_$W5-_2+pwGrK=A_Twr2p;(cIzBg|Kmh+aQ{ChOaw(~8)<;TLua5VdyR@}WznV2dJ;P%* zd6<0Fs1+X^%ehxVSCAzYJ8BwMuZzf2*gXp#l%#A9oPU}as#ZG*Qw+Lhe|2^rII+lP z8}{l4Hu4(BG>1BkR}dcQnO^8IIS+>Hp0S8%eS8h|?&Xx+zS2S4YoPbO!^`B{3|Jt^ z8s0n#C?)@U?|M7LC}XwIk(lEH-nOBMos9#^+7`_W2OU|#P`!K_5E7N z$@j?)qaOk#@u7R}FGD_O)1k~%A3mR=wQRO(E;_@8(CLUKML^+k72&uvbkr0f^4v4jUSPK6>a&Cad;(~KcgwE33 z>M^=f=$0EE;I5m88(lBdO2=73YbYs}(e4-d3#a-`8!GE40;389WNUecWAYZmczr-3 z*oIX{u*TY(+@@XYQL%abaP)S2ed>p{@&eM}+`)y)^_r?}z)>1clM7n|j{?(iHcjQ@-~`zz`BD$?y||`S>j10}@R-xDs9EK%mJ} zC|VKuh5YL3@CjhK9ce_ICjUK~1qD7C63%vndYE|zirI)wetz?fn5>a91)P&DEgt_4 zlkzBhj_wS)9(3W-6zm*)2h{}`Z``yn@4&&q*6$vC-+w2V!2o}{g#7$7bGT#dixIuF zYLP&M+sxUzZdaaYq%J#P79E0xiWH}eoH?E&tRjb+TlHocY;X<|nVtlxe8x#MGWym; zNNYaJ+8=@jC(?xDhlLT0_BGke-F zQDtH7N*Nt80dsrrnd#M2ii_UaaRe+(k?ns;5*QJ%xHVlJruVIE!$qH=q2dos6s$!{ zW=&0;YyUiy$;3@TL9RstAD?B*ajXPoLiLl46@i6*3jlCiJ@qE@8!H!>4SL#R!tvO``9#Zh0)=0C+YjbuKo#ODDn;niL%f$wlyV+Z9C$uw) zcMouZf{hwuRH3pwDa?V z>q6OynV!j?gtQf|w0S={35FQvW|P%dw!BpY4J{0&SO)!R9o&}enxxPsKQ#?xS6Ob~ zPss{aRs@ zoMr@(RZ&Z#Om+sr&Rau!+2a0Wkm5>*u34~eVLJQv6eS(L4?i?2A3y)mfIWZwLL+7b z|APDz!O^hloEaDgVpdl#u|o#uxwg@R`cfb_=b=ScZ7$6P7m7zZz0E*eGY_88C^}YS zG^r7Isky|{4sr-{;|iY4IbqyXkqq@6_5yTNyYOqTE@wk=P5icTWX-|$2~+V*{rXKm zb>?ie&gj-fx*z8HBYU3?_u4b*KEhbhYvYKwORS0(hEPb~W-aAIbzNBz0sgbVMpjkt z$Gq9orYcAx?A%u~c9xHLxb3yM+1Mk1{8lAyOJVLADf`E6d^dw~J9Z#@{D`!P%d_N| z8U)GgY+nE`Tq9mvl1&y-TNR2`Zu)8~issZN9NEDqp+OttQTBiHB@jU9^-;D>G=JTm)+$DZq0rONEqwOVP7bg`dcDNojM znvJikbm^npayO4kIB7k$UysG8qmt0W1<~JK*dSzutw=FEwA7B0mh^rm{xfZ1h{nvctKZnvrLE(nY%`MiG{BQtQD*4N&2tJWGG5IprWZzt+90N6_ zQ}~(<8$oi@WRgOx;$XFtmD?$@HM@)?xDO4jJZMyln^5%$qa1BUIc)|F%Islw(Qqa- z0QKURWmmeCi0tM!Z6IIv+s$!H1wgNoo$poenkiFz`y+R#CSxeeArtfad3^I z3YXn_u>d5n#rlveQ*j_qW(#oC(*X`4AK8m|jKu+gseb7tpEw~pJW}VS@oNEwaa@Tx z@U~-00;>P;B)UJYA*lpTj7j!bGp1B=$+5(kL)pnN-8KdzWy&-@VMdkH-4IxoE{GwY zt#=OANMl*O$G2YrHgU~%k=@|8;ce$rD;G$Wsrb*%rv_En>6lH$^SWYW(Hof!3E|`F z{oX%3?9BXNH#X{)$SPkxakn8PO}xFcu0&*FSs z$uAo33%HaRUOpZu!K%VHoA~l75R}xKlubV+3@%)zPf3X;i;e`E*S5#-cofM%(v^|D33KBksBr7*mf5uP*cD^S_+1Oh@fm zzIpLsE)tM9*O&^Za+a*$AgNR;w;1^vs+8rm2L5(Tt=>rs-wPhzFFC9_ZdAeIJPn7^ z;=|gINFFN>6kk9waYzX7GPYQPUcau9T)xFv%waM^Ls62(9=L_h!l*z7G~lesX!N7{tMFj8;5?O=TGszc*SJBkRH*b)#~ zI}L(#Y$Drc)q7-ev%#)!%W_R%3j=IkOSL{}ZE$K>vc?kIuZ>G7~qnH6#+*GKi;S}bd&~FUT zW9jY581x(Sr(QPZaX91nT0?-kRl&%CWWTV}y{$J(1X9-5=H?6t;DSk*P3N^IxPBie z3f!MD(?tXanrpMFLmV)BGRM+uipw9hF?iJ3y0q$so-IgP<3heQu6J#@dQnq?-2(uA z_38i*5Zbq6Broh8NY)C1uzFg&PPXeWKM?iC&cYPSe+LBASs*NE-J3d1imIA42~J$E zeCN{amjyjDvexEUmv>jZzcHp&5S@4Wo^&{;HGO31iY!8M>4`u@%v?iDsW&5>UurhDaOIdSc57FB{akHgMa@=;5q)sw^U4Toc+ za&nEOjQK%2z1M#6Z%{bW*z`K10EB+E`8{kZ|85E|F1S03PXmzpq|L9!l3*i^cux>^ zcA9-&1-UNk}NlJ?8*{oz<g$3EnfLV6A71Ck3nJ6DNw>&ZFH6H`ejHnRMjllnjGh}57^wYXx$4XK+@cZ0 zvSqUiqDJ_yU%!q^lot30Z1wZgE+Yt6SwKX!K)_I}RWQq<1Yw5{BZ_0%-~&FiQ#2pd zajjHd;KTRTb^A!Py;>o?fKT$nulYnl(oUgp$|rnW0+3e?xKQd9iZ-W^)N$;U#5X?F z%VLQb4Ye!GCDslXU#Snkte$UAfk>hnv)^&j0J@L6#Ab?fuz$l{F(kOHg@=jAA+M+Q zO?0TxBzrR)9GFGK+VmZEJ-{S(XML}?8AczB=XWe#awlcNiQu@q8_ zyop*xApx#Q2vTg^D0hbh1#Jn^uB-VnFEH?{pg>`bR;qG0><zajkpMwZ*A zP9vzYjUqQ`sx0oDnNjhzgSTB-@AT-eHRcF38^6j>x>CQaV9vhp`PDXocy_P(g8#j8 z*@{I=S55e%hY2{(tRl2UgnaD;L{X`=Z7?yko0wmjgxUt_m)i`|-BRKDo$9^9L)d<` z=+L1ZJK^Ij=c*u65OjzoVy_MAR(FF!7NcLeD)4G|P$0k%^)qCv{Qa+9HH6;mhfhum z2)KK9M0|fR{HiHkAc*jL`lC^d4<4N0nd-)9Ax(qdflw)p4F{1nnxj2*M(*$OS>umGC4x8AN9i(uI@LxT1p%7k3CAz zZ2xz9$UtcS$DIb7+)+0_AuUE061db)NGLjUBYj?fq)4VQ|7=mRjpulmEe* zS)v;WmW+sXg7Hfenbg@Jk3b!Xlps0@1z4B}tx>EMmnTT1+BMowH<`e%wrcIQMj6F8 zc!voV9uyWd8AOMKWk;h~2U)Dfqo-IR5!MpTIMaRdD4lVMQQ5-mXDn#}8B|&ZD_ekD zUYJ=Pl!!i{)4P@i20b!K=weJ1n zu3NMBOzp0&Ur$$e&D5@*d0&3t0#IcoWh4PGFaQ7pZNU2uc%F=yn31xolBA5h#6JS? zID0b(H%Jx$aBy^YRh1GY(bmx=f!P8O0T=)dAO?s`%-ozsl$GUy|06vt{zV6XImW+O z|Ho?odjW#Eg_{}Z1Ytp9ZZl_BcMvZO;uSsJo&WN2ARg1q+Qb~h7lC*NSI`N9`02mp zrvKsh{_>0pK<4{r#oj{r$BN03enDpf~AXeWxUlIWIuvasQT4WCH+75CAlH|668S3;-=4 z8{=#^o4A_%(+&h^4{m7*05_!ofTRNe7$952>;1pV|6^|u_FsIUa0vj^eE~pz0szu8 z0DubA9*X|^8XyKhK|n%6LO_9bP*70NFo>`)AVNVvfI~z>K}SbJK|{m9CisAXiHC)T zhC_;jM@UFaOpNh?jDn1af`Eva=r0HuNEHSe1{oF>nFtdNlj#4N-unPFSg@a9KOw-# z0B|%g2sE(w0nmg11Hi!{!2b5&zX%!z5(*3)0v0621$i38UvK-Dz#%}=_f-H9qyvr$ zfeISMHsOE80KiV|;|aYumr!-=$iBb0EW~H@=PgA!s`r1^0r9-2(HZ2`3{WKGropbtzmGO8>VOxPZZs1)OD$#`n1tQkvVCr}MVdTls#$ z%Jk?&Toj0P{Us;nGzACd?r6@OSC7E+<|SF7(B_I=NkPz}D(+BS!gfZ7E^PMa71G(F?w^UJh7 zM$}WZJ6A``t26)1#%@?g^!E8vBbftBLmB(h%y(2-fRMdD@$094yotLVv{AOw^TKkZs{N4T|T9k|8f8m41TOw8}muOU3=o7`FhH~!l$4Q%BbwQ?Q$-U-s+Q{jH zeXpZWxOdydy8M}J*DU6Sg(^?;0RuIvD#g)>#2nhQu*ZL4z%jm;y{HxB#7X+esIh74*!u;7YC%eINvnGjEs2y;^$88@-40s{>YdblyX0cEbxI!M z8==;B6^ZK+LiX#5f?lX*^dls>kv5-%mNjc&an$^$b zLI#+LoGP(SFHCv+msvan){ir~XDjl%H;BHy!dBDhYSk+zqY^siYQw35ojC?wJe(=T ztoFED;7Bc}(GGl$-bR@^yKUdp3Ig8JgH;lT|ZDf0V%4>ggK-K`eX4G*>rDePk64@J`B17O#J$ZgfK;O3QZtwPRDRFG3 z_UyI}m^uBV%~or_U&w{FLeHU>RzV$zWUY+x@tZX61Uv%+%2AE zw#`&;Xc9TNG0{83D75JwUp7+XuN*fv&#@iKbTk*n|7|?nBNBOaf2k8K4eMbTS=PdG zzybsUF*Pe_dv92WVp5i%o87k7t$Xzx%H`nxfot()L9DrXUQgi_PTJ{VzY!`PrjSz05vTOS2i*Ved-c9hZs?J?&6dhl+(TSWsC)zGaZhWn`}6J1|DRD;eDr8@gCo z$I17(ZD-0(kb$Iq@aBP@bCv5Lx#ag)8YMNlS4hT? zWQ7;L>EySYCEsClH?=>rf5d@dZY8qn@i@m{GicPF=@E5|FqGXINTbj+UM%pbhvg?i zTOr6&0PL?g zpFQ++c$xyCuzY5gyg|{$k4em0xfWF)`?9WjCw~7xP-VRJxGzIYtYS}_v1YeRwbM8c zT8g7waW8bq6;R6fDoLECzlkJ{ttrcg2&q9t`b}m0H3olV}$6KuSPXH(>!OEIMkmdd-)iSWk znQeXQ5y(YPS0@z;g+qG_mW~Y2n1PCwJOe$lv}`FX#ytd>e>_StTCNu=lCJ}G)#JoS_CznXMky$$*gQxE2sdEq;s z4GK3@Kp}Vyi~B91SK*rU`~^mO)N|m|stZ)=n&Mkg{4!f`Aq>PV(86Xa) z^4|QO5dnCz$GqCXV?|Ib;>^OwIv7XXS0*-!y6NN@lY%R)f@CpLrxMT<}XG%6Yf2@E*NknS-jWMf#9nl`LLuI^uY^s%O5TD{&!s8(d`cLZ z2B_h!Xj`E~(!23Drlwdu4_vWruCbt+(fp0euH>Z3e=?XYX_ej+-FP;6yJ(j<6{oh? z7tS)0u=s`QLp21GjGB`cBhmIa6d8K{HvbF?R?4O>uad979m(qUZKr~3Hvxgku;L9o zj-Ot|o1JB4xI|QI&@UlDeA_dB^W|qizi>X3H?PpD7+-(__ZT-C&)!^fWw9+%kX;O$ zzJ7_U9$sETin^nf_=wj1dC&00Rs?yZsBv+krmL0(!GDai0M5gk1aI28$ll!HlI0rG zK2>}ws-m-Ag@YTX0!i9IPuO(o+p_q!>DysOaSq~V?6FXaN2+l$~rCYnI zeHE92a^ax+Dr8^04)jwvW&2O|v1aVeIhjw$Su^RcAlOonF<2+x>)y#xypDzMwH4SFTSN}~(9u~`>lr3#jtvXKQivXdXHwuz7B6Vd z;P$O@z`j^MSrDmbpg3+hX=#>5yA}b$2ztE{a|_Q~&sz2W@mv1C3e`=UR~D zlZZ0Qu|RLZFl9(`K2{P7@(^p4ycL_%Cz;MVO|fZ&<*rIGTvfA>)R0y}%hP7Mg>5(G zbSaA^Inr0@DK~SSV&IPDyTcQt>X+X6pOU+ zOUpRb@aELoxmLaxIxukCJfU`4;Am)K`|IMIXrJ@k(#iGLO(MqRNy77_a72)aV!RE; znw#KSNWvua7-GJ{<~!sICK>1>w-J1Td9o{8o2xE zQ_5kej{|3BPbPmk?^bG@qvtJ8F*~Q>9jIMxVl5ES{AgkkZ!T}O0v~TA-Z~y=5Yr5W zZg-SOkuvl{s`6WFczAY8>X^v&T^c` z!p<-6lfheG;lnS7-KQ{SWs$d=IZmUScVJ`iOgH&hGXKLqLYGBt&*yacOAaT?$zSgP z<7beW=EuQadHnvOXcNGTAc@=gGQ9&9>bVX>Ke|n{T z%+9+!XCeFwt{Qu4z+`{H`bgE~jKZu(JF3}pJlNGUr~P=Vj_uM)F#N#u!yl7MbIUVs zB+A{juA-iL)K(tX^m_^=L7J74_44!A(}tS>i(r4{rgHB-&ov`wbqT4Z@OuldXw4?P z>_x{@y!o4Eo%|3^{{(83<%qIELt*d6@x_+Q4f`azE*7j9L=tYxf$*&?%EzDH+u21Z zh(`)|EqEqhxz&uxSD!-6*3>H)yT?Cp|46N(Zla&%?z0`yobVc3jKIo18?ZH0K8(<& z*j1=O*Ihn|G2`H~sII$gOn+Qsm$!GhCb71X3Kd7jzOeqq$`ZF?8l#yigI{k^2PV-# zQIH<1D=m3) zSB$tTbr#vKuelZe2ygGS(gIS?T1{61rF7w!NdCo-?T=R9q9TOMZ1=B_&;QhFI0EOG zLZ;FywyQV`I>=IJ)xJ0v&3xgFrlX?+nOwT3 zdF>41Ub2|FhS>MlBBvF1dW4aTUeVKMuCq(%rZ1z+kxHS|4zj>hCEMb#9d>=R>aEE7DzYk)a%_J*^f~lN82>ikB(S|@7E{jO2i!6 z;gng&bcjZw{n{_jSCX+x(ZL>PUtWshkRBarSNn<0tn8EDr&8iNbuQR7F-NojJ2Ohb zT>cs&oUkwkxp35qA#$6xP)@NA{?W=X4gGTBQi%bwO)se8W*P)J^MxW*yv+o676a9S z(|%#bhBg-ZqKp_cN2PrmN3IJt z$MLR)ZQ&{FmAboTo`Nr}p){ykV$uu3N$HsZH)F5Z`_W`;O{jp6-*dhGGwDS((6k0( zRb4{Pz_9UGPRc`u?0IrU;S?O=Bhio$MJ?{~$+2Gigf*vxKRs&;$wb7M%?rgPck6!9VFTtJ!TmlH(UhoiK9eiumE)X9ma1 z7@1-P35?syzEE&!H`k;w#%LSjnqg0QPe_sXfm#~5UfAPMS)AFmXVl5X33&^ESL+vOA<)&FkT+A{)kZjrHKeKIi*+o`>*d6(1T$o zn&7fT1HzY(GYVD*=UcLmnvSAX#0&0ORo@IM)}tlan&Sl&pS{2d2}85lnLb!FS0)sk zbWaG3(@j^I)y>PH^>1ci^zy6eJOyhkO}4Om?1JU@E|>8&PQ4m|hY*yV-`yQB-cK+r zaA`a!JR(7|>DZJhU<`KFfh#lg^9V898mjwzbzS0XVxIfO2m2MBtYuEOg7AKVJy}a* zk?|1`)r_=h4nLanCS7D%nDnoM-Ei)|jhCad9rIOc748tbi1n&-;dxRb7&n(@)GQ-V zy~NrXt`VwMb&{(zVoPLN)&6*>&C{p~U)G}op zrSA(5ieO)rW{^?)VteOvA$eWx*=F~~edR>=M6$Ie>#|+52Aj(H(f(KM(#WfluW)hE zpEB%)WC>STIv9Di&aOM4bI{Emn(;9KM&$B0`);(7c`rw`Y#6I=_D>oz3zZKSG<57Lt_;DxZdfYmZ%#$tWT3wP5KfiF>C z`$p%gw=ch>RYs{oy!n#Ank?NK0h@9i?##My(lXbwi&^$LCGXuDp(%j^9vo8kog4$qft{ zygRm>S3C3PGghVRxH$?-LI-W{0LK*5X|});uWyYvm;O7D(SkZL$#0Noich}2FCX~{ z&e(|YzC#FMe^6IpQ9c48wEOMZzP?}Ej z#B7QE#c#tl?bV7&KK+vp&hL)n#=d@%uA?Rpyv_k=t>Dj<23THBZ2m;V8b#l)jS@6j zQ!;TH#%t{vTzR@uB|rVk>8~?%hle9EX5rqK7p?1$??9yV>w)e0eP)2=IhT~dO!|mTp?%%)06$V>Os^<{!gdvZ-W$cCd*4jv zeJ-^$F*5E!^Vv0-qDFhC0iFJeZvJLJxkXr5xSKaE`;TdV-{09e0ZJmpQu{T_*?YxT zvx1xGmbdtN#IikHB?s;M9v@IpEp^U@zSW^THGj%mVvpXS&~!pRl<`5T@FgNu*ehIE z*(+Mh-Y>ek5MrdZ{C#H-rB|`Cx(jK?jPQFfkA)=v%8Fj$9k~4zT{t^#Ag*(P9n*}M zQPXW{pfxIQfi`NUwYei-Q55t+SYSg@*hb~55jPB;lkAV@KA}8!yaSG>TvGa1&&b)- z2VQCWw`xuJ8Us0onf);VA1<|gqHno8-RjZ0MSN-Ax-MG=611FqkH9@<)1o~`3+z9 z*qoWQ;#wp7Kc^YLt9oIBXc#2eRnuMUxmj!$617)7&rxpdp2Y%M4%(hGYfayQ#V_~v z2HgbIk4jT*73j0zcjGvFm9vLBP+v*&E;sg%PuqR{8dr*5eWgsMjzhM`+}h5s)*d$p zz2bUSn~KDXCScwHiIvZt<@}tl?|>61ox*?yT7LrrhlKwBR^he^w;7Z>4 z?t~q28tp~2qFLh=Yq1n=!=BXLh2nVT$W|7Fx4;|jvi^F&Xrpf*JI|mO07XEqxG*PC#(_j7P!Pqf)QpC@t-W^PSC7wjTNt$^XhdiN&ek@x5;mLgr!|qZ~8Hd4JujoX?Bj!`J()2jc4^~yAu1I&z+uO*x&IHUgdoZxCkQ6n1-0l zkaqS-0}GWHgkcXEiy+kKM>QOA7}dC!9+4shcQ@aDE8l9J~w)EHJjUy1}$JvL|fcHIh2M{8XbH#w0#!2lGs)jG}PMt z_@z2%Ff-T=Y~JqXV*py3+zItC#j#GBQ^nPvsKR1P6BHRip3+&!0bZS}Tw08xN=kcP z*MnSX4AcOl=*CFTWHb259vH^4QY9!}RBtz>GPu~^!-M0B6ia>em0;tkPm~6zes*OX zxlgQgyw^GUL9e0Ajeg^bHQw(4$Q|fG?f?b>1`7^yhyNrf|H~av|M~;S8OT&H*#2>c zf4l+wuQyzk>!lu;G|5XD%*E2BsczEH&Aoj2GOPbK*+4EBr%T-Gpf_(pUkkq0A!74j z+$>F#chB|V1U*RGFv_x!7^Yf6G%wmYaEH9|7*`AtS2yO6wW!qYyr7^aQx0lTV4ASL zlF!x)t#@7+E!c;9u3V%gL0z;u2rDA%n{4Tx{#aypYXl2x{VCmWo7oy1wIPRTySV>0 zo!SGhJKY7&Q48If~jad#WuSS_gjlMrp68P>v$I35DC3l^T`&|@55&xWI zfBoPSZ~fW}%v!RJPRYjaqi{>2NM;LMSmHs<|3OtD+QL~={^R4`w4&~NL^B$cN~pMz zE$~T*bqLywoT^TP3_kDYRSWA}DT3(->X^kG#XEA=^Q2>iV1!GQKiy zE=OwMi}lVgG)3DANH)8`T4Wwz84OH41c;@Hr&i`?^hoTlN=5u;DqV&{GrCYtm zqwyEB?Jm@&={AMip{lOUiG6DVV$73IA+j{< zOcr*Ae)h+u`Ilv0s!ZrqyuDC^L2lfdwV?QTe9~3I`l@=1Iqr5NF`328(|NJ_+^l*l z6YJI*iREE^s3f-o%s{%)12OhW-3!6C%y!dRGT&6RWSt8rhcO`%J<~6CAHMpO)B!^b zn$wD)j5#<2I1Ca5JSb@n1$vwU^b>$YLnp-`V--;`ae+c5A!iX)HFf%_tabNAq+zS;jurb7#XKPUW}OQK#EeoTgHgQN{@&&3ka zADBfi{9TAd_N0vKY?-4>Sz`UIeygWi?$WT(6>XNK{AoW6O)Dn90>XiWl$>SL&fP*G zQgV7eA4xDPhJ=3j^rJ$rNL>MCyz_kS2Kt<7xO5%?w9>{OL>c0CV$EGt@~G@o#U)7P z$)X2UdHt5;`qn{9>o>IL!C5DwYvl3qaqih)*bZ2hQ}D8Bx>-71qO2Kyp0HhtrMBeg z4(<2#{-)*1%JU|{$FAKf6oUJLx$=TDA0y4uG8c&ZN2+xf+a+cb?k;0^g^Bv(rDXSG z^$qs1_ctBpk3}m-G%amPI%FeQQZ1F#mAwZkmL2jNRw5U8#AdJ|HBP9XBCE_MnmMWJ zhva#yQzedE^t6aMWLD)3sHXN~)9c>>d8wbv??4IJ6Q>{f!Qv;HVa(m??50T9L@@tE z^FB1eFzd~B_1q{9repPrxVS98999S9d>Nj;!hMWgVwzZ-$GKgit7q2uUrn3wj-}I* z*xnL(PNm$v-)TbZG_FOMmiEzNsYd^>YN|IQR}Pr$s~0NzO1=ZIh!+(B8`Afvsl~tK zqAN-9_m_UjS>w*zK+5T67*;0kLZLUWDM`d%N2`wSfwBIb9G>~SBi zIoTG3ySMs$qvCFJ`&|>m7z>$5aQzF?jM40RkTBiunh%S4JUWt9^AKNYN8N~jX`l7> z;T@oj&1F0Mpd|Nu_yhNpTm4{rvAsJ^7lKe|y_Ch=F@c$#RF?j;P1p?1V zDZhYlUbdO~>+R$@$VUYsoHFh(E>>sDt5I|6O$4qI1mCpvCjY{(m$3am88v+r_4$IK zk~&z0zzD=kme(-jg^ zwSBCX{AxI{g|pl(egrW*m1b2D!`JAf)N(F=*hd`SXa4gvIA1GH$<=1vb*qQj_AJSnTP_1Tk$_xEAY*Q-b~v(L{vM z7_5)z>VB1MEu>+x8zGORY$VuW`HrliGj(D1Okveoj#gtfkk%$Bl3&3>`BuZMadERvh7CEDcT# zR(F`U&FrB1X&9G=FBwyZA*WGS29jWhdq`C=O?7|Kmbt-rjN!U^V45_=a&0`$gObNW35;`Q24Q9EL$jSH|$g1tZkz|6^Lc)}VCf4vQ_{IX3=&JHO?>4>x z`W@2FwkPN_(T3l2_b&PJf-p~ zTJ*k#)(=|Ic+PJcPBX%2z2)o?_^Io>qZ86@=~njC{R8VMuwfh@M5v{$r9LuRwG=L@fcOoO^vOckN{RnA085jp3HnAO4R94z1*v z-7a=X)&=zMLTk7X<}62fU%f)K?~&E|;JmWn=!!lS*bfV?DXVQTxTvWZahJ*)q)c3i zG%E$@C&+Z6x7pvZPzN@7cuaMzefuM|+&M30IQHe(m>{{Ctq-9EvhQ6Gxb79xD(&D` zy0qxRgxgFRfBrO4=E+lK2!W9Ms)kKDtyZT#gzlhqRDGoy2;NRRj!mhm%G*<=m!U&l zKsIg-hu6|i(>=-TM;eTfz>QdF6EiLZ>+K5zK{!xq6D5UO9Tu7|Tg^Qh!s52UHRj!S z_aj-SbtAiD(pr=AZlJA4T)x3O$=T0zSa>e!M~m<9m_UJxZ2kya+V?M|ewrFn!2ub5 zJ6TTqMJ|&Di~1UU`Zs7Mbs2ZcWc@&~fYBnOe8tL9RJ;Wo99}ED^PY5-i6$MIMs_E5 z;zC#lW*CZ*SNlm>Jf?$0$sIL=ddA>TyaY{qG<@Kx_ zdB1W#3<5RroJMm}BDhE;0rMuc|9KeZq#ZznUH4Q{sgs1c3Bgpt{a=qvl+O= zl(zcUMF{IlUeEd!k&D>GQa``+KQrnd1HG|BzayPeJYDJWhE)lygv506cERy;YAkFO z9qA*9U%`mek9(I4EsL_li_y;u7(YyvQc$dPLVW%XE|cqThBMgor{d;hf8{cij~_c3 zWHx9*LThgUv3_$8FX~2;hgLYZ);_!vVB{oQ=gP5zy_%I<nnZEeqk)Bm#-AO-sar&BOmt5cQhe9;_}06qM8*s#_QEa zTrWl&maW@bPyuVCX?zD(YPS&!)rnT__UK%NE%mQuIPq4pdKnn1X>ldB!S0qd;}hr- zl8XlIjTeep9d*VH((2h~YT{jD7>!S+Myq*_kw3a^a5EKFB6G}e1oQeYpn4ujh-*P9 zgwhAq%wb9cw%g0N^~TiV8cWK{r*v@^o;zP=HRbpG>_ee)7aARS5)Rna`obQtGn8C2GXs$)6 z&^6S2+pD&O84oT&_9?;lm81>*HJkaw!Ek_Pf6zGuR%H|P%3`2W=+Chi6qrVzh9?2` zb#0;?0XK;oY_y_ovd3Fli|oGL?L+F2K!Upp`~ann-X25Y-nq7tu~l&M7x5xxWN2Q# z+rc(1xXb(~f?4`K(>Xa9O|OO>@X*#KriR44!7vOP&;Ws}JKIfr14k1sQFvW@KF)d> ziatcw2;cqHY*?C6IIl;yBaf(ugy&~D*>fEE^ZJI)m^wE*Uuy8vE zph`)%Xl^ry(1LjMI@)R9bh}U})P3`+J&%LyZL30-mCP-In^*0N$pT&3IHm=pkjkm* z>qL`IiO>}zq1k@6kA@e-L3rA^qYh!5VE7i>kB95sU^pahd;v?CR zRtcRJSuzEN^IvK(*a#s z`%{sKhG*Z4DAnTV=7K~-tufZwMo)#z7aL1*7 z3hOF}U=h&EMu2@X!P}B0x6P-N>4Khhr)3Q>F(}F^jH9hVd)EJwX^&f8vBjvdU|y3C z28St)3E>N0?=YypN}qxyAb1it=*T`^&XPiziPX%7^d@Lf$|qChw+8OsY=f1O%SRwJ z#DvU0>%)B<$>!QPtNa;lKrorUFgIZrUnYiekbIJn<@^(#6w!+EDeT|tZUpVXnRY}m zU?&no`wsjhJC!SOiN^W4BptymM>kzoX(t<6*h!W42zRvrCNrq%SRO>nb`d0d=f$qb z8j<)x;=4~sT(xT`zz{H47dWvYQw0Bs!DvUi zR?sb5n?9)5%(=J`Km61WgL!e$N>SxK1x4#&3p0|^z2jUKU2u`IL(cpBc>XsqB~Pl+ z02XUIevw;6Q0G!$^>m6+Bad~(s=y^|uVZ5yq`qc+fLEkyBAjcwq|5qP!VA0GIht!B zQd>-F;sU!?u&>~8U0%^Ibtn|CNY_hBv$@oQKyrPo%At}*txMutRC2xI5qFU zT9BnpaM@kR7~xeL(zwfPV?{Ve{;2kY!*Yu5aZZZX%bRVny*6IEp+K_xoTn|5DW>oN?(gRwG-jR}S&c zstjz(j4oTP^x%9h$OvT2{w}CisUq!66Xd9cF>;hPH_2HiSuB?e*0Z%rhEiWG`o{>l zE-AL=%H*--OcVO)svUts*Als4bD2S{D3DFY%ViZ+T{N(Z*Wr2Qvi#XoDe)-|!Q<}6 zO$L%8Ln`x;Ap=X*Wq6&oC+&^i^nUZCD4)2~ND{kdGuAZyD0{|v@SnADq83Z{P0ryC z3v}ZH1wGa2<)7A9nbE?2rB0bh2HUfqy%dZs$Tx>zlZU(O9%aVP5ntSY6CAzg=NXzb zWC_rMY<`h;f%zEx!IO0jh6yPE0xor(iohlRAqc5iYoqncbqOHIRS(}-(v>_i=` z7oR4UG%9AIq7zD}3nPOa79Jr#Hm8edc(Qg*#@*Jev=b3q;NUFvJ1f~XG9his-<*ClHU=ol<6g^JIHDM7jjR`uyh5X{Y2v(=%*T))vA%iRVEgOa^G$V zdWdPpGnL7)O7YH^?_;+QN9G$Pv@^}mbkNj45D@=_zmMTn2>2F+`B_>^wVc3AiM+jj z;AdVo9!1xxO$+J=HF%j$WP`gRQ$G?j6qN)0uz=&e zDuudK+PFi)>+{UGn_tpDbNx`Fpt&?Su^!4lc!nc4?C6V(6y@g69OyZFBx(=EJ5ZOa zKTDXGq^wNu?;tU@j=7Z*3N=D1HW}Z|B(Owrr(zVjTK0t!dXo4VUvy4&R%}%sR3C?8 zek>?o{V4u3edH^4&KqK4cKtDWjeEMx%|4t<6@HI(Wb7r54wvR$mutcGMp}J4Beuqz z#BtzW$A}9xuhNVno(`B*PGmBCfE$rsb=u;V}EB){*I&WF*lIvPvHlcV1E{ElWx!yd0dsh+-O2$v6Z1$3)`o zwYV@AD_TX3x384z-s4K`?WiBAjuOuNyyOJ1iu$0#Fa?8bj508g|c4s>NOO z3ZF7wcN%Wbqa#$!&|L=4(q7FrK+mYRC*oQPeKdQOx*L9uJ&L_2qK*0vx=o~*-Ijd( z`Pm#)c+9H7{tOCvVP=KC#Swo?hTZiPpl;YFk52N(hh=r&(c>k+HeFMs95|M#dCu?# zv(^}-LK7lsGKTM=Dy)Dw##WS|7TGFvGY3;;RK0MSYk+q^RIyn?_L4l5{+jM{IJL-X zQRS&|+3K2>NbKcLCI?DC$$uu+eh0jbP|CbUKc%h;vqqCGpP3G0p7l#Pm~I?Q>Md8o zM zAN|rg`=O{}Y~-*~a-@MZS8I&#LU=ZqW%ufv&d>1GjhjxFl%L1=`wPD*~vWBS^LB)F{KzEkxffs0`ZFBLFE*S zaJf@#T|%@Hp@D;D%l+^u;_TVl{#TloS2FZ2(N`{ zpkM4`>}-^xNbxWfF*9{FivvEhQf;n7>;^1=TLixYs3W6uis4zmRzfsQN-)(7UBv}A zB|J8?arKquIof6yR5w4Zec`#K5Y-;r9&$e2l5b7)!3HiOb%-S7(xS0y=Gy-07mXWndVk~~j7 z;Pnm+z%@pFYIraMnlwiJjQTUbsAW)(>gjEm1xApa;Le; z_P+fwabR9o&X2X!l#;R0b91K3p(Zy+DmltZJEo;2+`PS4J3VR!WpEWgi3?szG?+Z{ zGCarsNU*ymsdNNCGa8|k$M9(}Uq80l`yu%-R=&qteY?k8N3+CnM2sKNsPZZ~vz&r(oKks*? zV!GmyvBGswTANW!c1e2p*S3P<+l*wg$=NwQGa3HmZl0e>Oq0eTGIBx!|F+$6r#}pI z7#Y6w@4-1*k?D?t)e;@JIeRgA*{^DPu#1XbmO5Oa(-=L5W^(U9|Kqc3(W79<{I3=i zE^}Y`+elFj|Ho<-_g3d)*WV50k^%5GND8uyB3Knz*;Ke0?X#iWxXHP3NvXIA8oP+! zr-f0+{i*4<$n>gh4E?a=Dw;J<@Yye7w@dXUi%aUGxknV^5KX?QP2DYITE2XTA6bPj zaCFd#SG);uD#^kvoH8XjS|Zx~n5lJbe7Mw`ra&j^fP9pdQX`rid)Xgn5!LK$^ZSoy zv0Aw##Y}$Evva_c?9H~kjR7jZahKeNbI!KsoVi*i!LxW7v3af{MDnP4|1%3Q+6Zq= zWY~zBchZsFE)uyYaI=;8c#&&}S(SHKR`nS0Q?`D_Udw6DeB&o((QnXnkwZ(z_T$E& zpPua^b6JOz4d_7@wek5S=sW6EmsScTRdsy7Ejx<1h_+)IbX?njp#?NX7ZAi7@!=8^ zL42FshDE(S{IkTg%mY`-Te{>8@eZiQhQUcaWl<93GgUsT-Ms_0W?0Dduo0F#17s$J zuAMD2G+EIR*(H?4$D%2N)os7sklpoEZoZZBkuP}*y#)2CoYV&2u4Wm1dwt7k({kHL zc9*C9mNJi_?|!^UHiwe0Tj@s^DZ6oyhw~1gXFxWF=$0SW#$^k6DB*bP8vJ&mcs&gp zxk{;RSGgsi4Y9V{y@y6N!Rh~{J(l){yO<-HV4v31&P)k=)NCtH8H$R!`Vnk|H@8t$ z?GU(2QM|z_w;KQYl8rs@m%ndq{2YV1c{5zQ;JSO+4NX~|_k2nIWV@uneXac8FL4Nwg03xw?|>w5 z6Oz{(4L|JTv&P?(qKl_K-xsU&(* ztF!b?x1RDb+`)@d(WrEVDZ5d>`KM7)uoX7E;4>s~>vP>V?-Z$W7Q>_SsIo7l#3X+} z6p$p0DmCYt2Ez&{t0duY%MEx-f{$2&=hW6#VZE|I!BD9qpy;yS&f8eUivRMc-v-x@ zN`n0wn?IdUZ~Z`uEl1{?Puh2cCdq2{61Uwa{sHu)tUiq>wUp9DTEu35c9<&(ECH5S zy}thTV|&tdEaa_6*_OE|G6um4_f{1A<{&tLi+x3M4VjJ{^xSM}j+%hGMcPmf>qn%Q zFLDp}gK=KLW9JLQHM-F1H+hzi0j_kURhH8u14yRdX_kK+MH|j5*VyO8cM(!ik%A@S z8RJo1G0ZgttK6i0F+$i^m?SCsA*Y9Y1cAwB&H)#x4(%juO0!6JgdlOkAIfyHijxUR zk|3Z2zAjU|1vQ$Xizx1cA;vxL_*Gax4AOLWvtW42KuG7cYUGMVL!e zQX6rM1%^P5oh{6ER}~&siisjf9@_i?MFQs{9NB|L56x>GR_+TV8Y(xe;@Ui-?pfYa^T#w6Xzx|<<0T&8(V5n zKu^QKIY}rl`*S|gSr&w+`Mo^1yyq4|q*ORfpcv}$9{7Rm#~sq-$2k8s)E)BW#xF3l zs#x$l=~ z$wbFQ;7Dz72o#|#Z12P>LPI;FKEL049tkoQS;8~tALQ_*T@+={x=`YZ+Mcf6{qA} z=Mxr#px8(mSfbvhv**52MZ*Y#jWGPccoP>tN2<7eAv>Qaf3vq)>1sct+8n~sMfAML zu3q4NP8p{xq_%N6X3zKuyR8^*tCI961sU->y0ai`gW%Y2Z#9ZKci}=3044WZp;y*3 zrkhjl8_#s`R_)kG`NT-@2lI&J_*FTRn4l#LK=mOa`EO<>Y^uc)^5q)y`30G3LA~Bn znuy+JehmxDrJP9xlZ&NX7A-lpAhEFNzW}SQj<`Br9wT{Yw3ZV6!?7*61tw?g>K{IVSqD+aESq|6@#V>wS)P= z_!NQWc1?U=LW#rei0Ni8A9T7gCi|A8MyfK%oaFYc8Nx6(v?4Y=*aKGqqQ*E#3}w_b zhrXd*pB>zHP3Z1vX8hjQW{nUX~Yw=}osxfX3UZWx~ zWa?-aj=<%vYX{OF<3kH69%I|GF&}z_fq*&9)k(rWa^f2S&XX`Pka{ckpy0Wmy}+=V zC0 zB$*LN{pmXWgwpZ$;i3A}PKmtp;L}6VbL?-Ro~NYc$~twB$D~IDe92Ovk1@EfHv(i} zw_tO7lw*lAck|HV0DYvTLF}^cl^aQ9I|_OH;&t)QI#My!gYPjLJ_Szl@Bm|K3UBcd zK-8i{J-_`pPNa$SK@G6>I)s+hQs`?nr*ywxh3 z@#t$)^d%!tov*PesmD!UWWWQJ4&_&6SVem3*{FJ^0{R##XsH^k2I`R_+?gd}&#m2k zQOkm%UyUYeM4PW;J~6>yB;*=zhlJWC*NntJmTIvnYg$kWq?4f4_-(dJPb+UxyP3Un&DiGI3J6NlCM{V&bLM}L>~zW z6A5G%XIl?u0CNa&Iq}_2u2lK(1_ShYi!*-8mr~VRe*~}r0NbybP;My0KbAzo!CgKT zlIjHFQDGj_6S@ZH+*8|M;NBK{2BOhu{Rtqcrl^)QZbq{B8V+J4~_kMlJ3 zL|W*w!%B(vn!8&_NwV}9SYVXKgm5uMS|U0Un*`CWCCe_>gDd)%(su(z^DY9cN6V>J zJ4Tg7E);|Ts{o>E$ux+#NI{dM48W#+zQ2G*)DV5{9+7*Idw;#ofs~f8T9C1HA;nhS z$A)Mm5^1*sO}u8{)1}vYaA2bHQcCp-tBN}L-0FtIM2WKWD5J!7BJhH;k&_yngpEqq z@WQ0uy9)!nUHqt$aY)s$-BEt`4gTeD~3FAK3m zG{cOPyF_k!22GI3y!#X{L)Uaf)3F|bn6!0EDmGF|kYn5c3^xILP>g(~&>MbT^J`!0 zTZ~3X*UDiFAY3m?g?}NHGAO)&nXaI%f_zlJZZx>#JMFOo)|w!f$e3*pi80bXBO5H!CrIY-?B+XXTFRRP#}_h z%$%qaBz(YafX8e5HN3N^=+M@Z8ZlN#7R^U1Ar11=n-b!pk|C^7$o?ioq+FJKWPqis ze-R9Ptlgu|Gi(BJ%52YN4xygRBz}X@xFAcT!A5RE2BXK+$wJj3{Q<=aAJ~bxpOQ&U zUYaQ+l*hE>d61&&~V7#3{1cWn;e`5_% zM9d1B2qd=etzncB)*Y9UiGcs+!BlMM^4r#i~%S~k}C`s_Xl;A=aNUA#PS zmHbCd4HLLk3P-pAnmSco0=S|cWfTGI(NXgXCBp|*%vhVs!5TdgfM4Nq7?IW;jD3Yt z4ogz(q)&()=()jCGY9b5_AHZ5`|xN|ZItlk9}5{f%3n#}y9a{+H`5H-SxW=s5mb^laZ zt%U`Ex>QG$4H$%55Wn(ZLK`*vRsDq=(KdkQbF4*SVq6amYR2NLj zFT?JOG&N41k{2TUD$EP)#NA*7V3keBH^?&M7G*EcqNL_1gyMc8EzT(W6umTfKTfHu zzy<=T8#eHKOtH*Y4BiqfH~y3CQ7if?Dm**$|9G0O24 zW%kn3!I?Arx+snYv(h0#A>m;0#q)~`Azz@i50&;+nOwgEEzE}gq~e)R-uPVuPza6A zDX4m3Eb7Q6w41w-p0(kCmE+gLHwC`}H5D5k6*w0Voo@#G2!%R$z9G5@qEO1P{vtMq z`4-FGhzi&bE_jJz#+(u0bUm6L5fy!A=B0e*^4>HboyDYdZzJE~1ua$28w6$&D^mmp zgy>zy2@lk2p%-LjGMa}JvxWAldNM3Az)t+uGyBA@*XxtTp4q>Qp6f;qfI6X;=(ogJ zZ{UC#g!}>GTO_5{(r=E)$l~{1DxMs_w4tQpaQ9f;A~HpGx8Ah{1v}sop%+w29wYla z#`u{ABTx?jI^^@#8RbBH!hZ^ZSl45*>7~gYx+cHN@a%N{ebN=sMM9B|~ynd+(`& zB{|s(rgyW~304vo(?Qgg zNaEhVGPR;G3cO*R!xv&c5!|Kw<&gpCFdrp3iQy8!(xy0YO4^Xw9c3hUYmV*pF}AjR z@+I|s3Q@kf#fhELnnBiEayJ*O#eegG`cb^gJ_rsErQP9-SbQKF7X0z`$eAi%%om;g z>bG9qbTPCV!o4-Wl$nT#%aH>WU)|t)v~Ry*PJ;_GGhBkeb=2S-#fOV#6bTqZ#$QpY zH+M^DhxdYnn?o_UU|Zr8w?|xaQ@FkJnTpmc0;-V$y`Z>1E4>SUUjKT0OZsj7gA_8W zwHC;o9QuOAz`ssNX4G3@11J=Q?fg;nD6%VV-%XB0B{K{vCa3x(nQ4ICQSZEBrL0@j zJ1;V`A+x+8q931NWkE8vx!i#@gPj2oPT27LFMu|^1+CqmJGeoAlbm<{gyNNZW~XUx znfY?_!a*U3(dj-5%m|0xg^pqcDbPe|$&5Eijg(3xj6}g#uc??j2~;=@c*DWr>1%f zrN359Lr#2#!8X5&d=K>qQka%aF-dF;W^#7{DL7*D4oWj@&L|;; zH$}4O85dE+7jM1BigZOKErQUJO@OC`QoG_?GO`8CO=#a5bAp>qp9X>;a%CWRlEmdx zs_!vj;nIC(i2OmpEspZ3d!}F_p$|ryYY%MDQsi`#Xwh%xh@T!wRM1cHl_Q8YoO7uw zKPE{j7jNb?{_Cbtmx*1);JY?WF{%b0rzGsU1Kuoe;tFc=?J9l&j2W*Tck{$z;oTo5 zlu>pGPs59DnI7lt)OFLEUSbvX7M7#I-AB{X@O#`iWMMiyG_fAwu#k*{hv~cRQgI;X zy&omVJZ=x<6nF5b5eF4ErRQa4w^Q}4K2Lh@R^%IWaun9=Gk;n%_*_6%#iNLnVf zTC^}r3sOwK{=oREZOr<9?2Q)>Qv8He*se5SX^!e7g+7sKLSP1Kl$(YfB8OZFoCT7;hm%j z_@}0Rfm{Kh+p(Ojgu6WK584r@x_2l1^KTivZGN2gNrq@6PpA+ls{cgZAeJNvEl-Cz zdx(Ii6IOF^)OVF7mwHOmRgmlQpO7O?C-@jNke(CBDz<)!lwu=gnW^*-bv_xo6GRp)zRFs83z!JVfeR*(0~IHbTX)YdUjnu!MkezA0{;BW ztbT1aGm1R!Qu_<|!rn1(*9_2a-Mf?lB)i>;)}(jfT$$$t(_L{V{fS@hoTTnPzyBb& zay|b7&VnY8iQ}SoX*D_G|1;^ozpn(5$GZH-|7Z*;to`Y~ll}rO15WRhHbS)Zk2a9* zzi0>UD*X$%HQSo_r|l>GO#iAHdD^r5Pdox|0x9xuN%a$;lO7}jUs6{0C)?Ov4gbjh z=THpzpu=?9`(H74sUxD5x_mw3|99jBlJYBZ;uYNUzr`nz$I!OprIr5_I3an^fjk}J zI~@l7cTTHY!_x`5pLeo-|0(v^b$@0;(%|Qvg_!=Ugvsm)Wc{DEe+~bi%p!OHGQ#U5 z0eN}{gz!M__)Kwd@&6a6)g2ge2ZRW*q3<9N)SZh?P+?N-)KS;JD9Ar9m=p)O6W3^y zl4DHxg;#hJy<^4)EqPhTlrlnnUNqbDpOK_CSAkQ10S$d@$HMVanx|sf@|iXDySBl8 z-^oz2A4kWRPyn%*$?`z^Py~64f$qpn9V@-^tmDchy7HgeuMi*_vLvK- z>C@1T6CX=5?wTqpg^xrm;Zif_BA{1XUsRLr4yo)&C7MZ!9F~Zlb$Dt~nZ5kT<0opL z`q6!LMvzQMFXK6tUH^MjW7MOK>DhqZLbk-cxs}jvKRr{D$fehOPiHFm%ZtwF5b=xp zr5dmKtiI9E2uk9+6V4$Rv{>qT%k@i@HD^qLwyLOi<7U5=owW(|MBdvtMLLp*@EMlt zoTV#LCVt0HePT>@w>IsH+9`{r$W^rWiA6u^wZQlIxM%bs7Q}#}o1d&~PqY;^ z(?@M~pQ?wc+>5$nF2j5lN(toN0WJz-pka+`FF)q1m)pPWRmnp`GvF+A2$FR`yd=MC zr0?j4<-(|38kbYqH#*hdVi&e6Dq+a0Y1eezLFF1MT`Qz^f1h_mv8Vov{kG@PepO3l zQ?5^1yj&bpUyc$7&PWGJTOIuJaT}fbqZPzmTDT8k@KxV6Pg-tbNfqjMb8T9Ux3UAQt@KokD!pA^Iw2} zg1g}rAwxhvJkPbx(Q{vvJI^#A{DJGe-px4!)58O0#%7^xWaDM1*zLuke%jK}xx3Wk z8zTE_<_co1>5ttNc2;RWFB(KyB78m1%^ynKbw=)gIwLNC;EtI0f3ffWi^~1e7w`PV zg4~k|r)rP>?Ti07KvEn;PAg4D{~Ykboz;eENALcus!Medv}Co2(W`>Jry6*;%X*1E zFxg0yC7(UAI!AsBEb`(wrZlepS_un$)&gwuh+JguvUi;#D>l>lU;q_N(}imh#pT)R zH*Gq+=z9IhstNh4jgdp^G46-u+dpz`cf#$AjYN|?IIRE6BG zMK6-lYA~G5SZfqo0~0-}e7Bb%C5`*{tIKe-tXbkFfGX2#8;w{v*GQ^BR*7aNIJg;0 zt(h)h*5dwk_6Mnsm!T>myK?L^xBkvZI@w#T9ShJI75m+_WKN=<+m~hixh4az++$~? zr_}RV53XoGpv$r%upnr=a0}fd0u|{r!GEb7$JwIGxVN-irtHz4kRDlVeL_cg)8aMB zaZz3{x(p(rHTwyKheci79u>(wEice>`3o=x=uPZ6U1b~?vzrWtw6sJx8#sMpVRsE8 zQJsE-fRV7%Mxx>#%^FCBQ2zzMNExMm0gt9$@{>D?hR)OBeXR`pn9=nVv25h6TvHoix5X@3vK6mU+wh1R{56F5+i)4pp+bmFY(WuC&q1f&N{JM|Xq9 zzo_pW9Utx;f!{wJ>|fj$2Lkvn)VH<^afHH5Z5;m})R*Em5fP<{IQoJ8oC^|ZmQl0f z>MBiCCq6WwGm1GJC3*^feo!*j7g9RIIX-ls?N4!Mp$o?-1IR3VPb6@YnhMt$n>RhI zKo|#JXx+SyQ43YN7YkDs$s(FmEr6Z=$$G#siNiPhm5{wz?T0jXhTdlY%AdlW$o-a! zI5HO=F>qd{=@QGlpMn=TZxgM0BJnZLUMXsh=Tm|t_U0Q$1(NHV-Y{+hmWn@W*n#=- zCN9<$f6ep{^l^h^;@IsNH~$1rq-M$F>sXL~s{V@u+=4R^ZP@Qu!fRD|Xk4c_eN$O4aEK-+$8e20i zG|Y8SSywe}oABMAesYdSmA}{^y1Urq9x#jCc#}^OQWrR)h9RHKtR(qB6j5`7fj%Vf zhA+RaeB~fGDccRZDoHmYx@;$vY}N%1a*&GGmB;2kD+Z+s;&Y1+_B=5T`B94=_^lTl zD8E$8)>R0M%zg!_y>Gi!G!`~KwDo|)Bq*z{Jf-S!v}iira+Rb;)wD7t8c`G)paKjh0#2sC#+r|M!fM0xQWgF zDSN7?isT;VM=+Cmi{G=f0&+Fp<+p=e;UzE7%3D72hB>dm;X=N*cD_v0mi*;W)!MO` zBq4vE(Th_3;UpRVXH<+}q2ch=me6d*!IQ+i_xdlW0z*ZEiRoFwEhfF+P)#R4Qut}a z%(sAG`bc{NKDr}^rxNYm*}>R;%x3M~kqX*T^6DFu?q%Lgh$0m?Lmy+b?Mhx`Pko{l zU@DjHlQ-lfaX?x}T696S?H9-LT3vW-u;h`XPHU<*#cSduD(^P40n8Jkhn>$Na=L%Y z7*ad+w!Q;@#Z=FVZoa>Lt0Bb4TB1!pUEGJNoa(Gxu^MB47Af_aJb4yR3-<^ls9dUr zXpIQHB)%heVE`%O;Zw?|s`Q$L2Q=IZCJ|FU`IO>r^kxKPS@+KU16l5~&`l+~)#Ux0 zZXAVU+#&NQ>+^82#HYN%^rAPEw4a^HG^5Ico69tp$D}1$jKt$Yav6}O<~_Xbt;wGEsI1Zwaa!c8)J;k3SDt1s^cm6AgJ7uqzO*LnRj$^GoVy%)*RlJ z-ON8)w~H5bbs+)MKZ~#!Mrc44Il~GWTZKn7xPYdp^@IZXxK#S$`{BqLa~oz`Ky^w4 zRGEtP_m2GFJJNKQDV1jwKrs==-KZGnlLYab>WRWb7#d)2P5*83wTXG8l!I0Cq>4&U zj7U<-+ikopPp5Q+a(7eGC|*MRh{Q9nRvL;}#hhy>*De~%h5ic&OwM_1C%+?uHxq$p zUgIa_SJ+<|GF7C43xJX9gw%5K6pbk?}vh0rn| zxPSb^8)nMf@;$!6jr&m=p?|Iebi5w|zjmqQ7<7Lfp~BMf`Y*tqosn-pcC#pCoYGCA zu~Josfq@K=Bx*;;+pmPATR8XQ3w^kiNLqj72 zbWQb$m{2thYGw<O?9!M-$0I)qSLB*;Dq*`l-kHuV<_;RhTJ=40Wk@u5CG`2H>;>~iuSBk^vego|u= zO!0Zf)P5X&!4NI78N@EdB^4MR%QIk8RH&ONC6gf+E*rM%WlRF-=Jv8!CBAlk@iw3Y z{9-mO5|9i^ZCQK8>*qPj2@+@{AZOF7wyMjgf|i8~NAP_a&t}@+Tg3@q#3QbAEz+le zs;c_*7p`S_;3NWZ`62BN`~Ze37N)75)H72bA{)>(xvNydk8OaE$} z^9l=C?mXRl!Xln|5y9638X$b4iH|OIzd?yl9078snT$Vn<<8k+c$__t{E70H7#r46 zsfN;W=RS2Ay&ZZy4z_UDZGP#*GY39+DWI!ac%$b7dKb-@t8f$lDJ_%ias*vhdL6}B zC-XWsh^6ofUusxpcq0A8cqdAY-Tzl>j&_(9EZ-tgLEh z2rW8<#7Wlj*81rGlok>nfe7Av``eE};^Sr=XkG&+Q$;cPVa8yS=pr-U zYrh$s`+dYqrBfSjE-PCkfd;)W<8a*;$$qXz?MkmQSZq9brTRuJgroiXGb@OpURoHV zk!-%EOIC&{n>$7Pt`aAP2iVyavJJ6DY$eVZ^L@7Sb=LH)lGU1dei*y-iJfxBj=pA@ z%wd@G&J*LN0Yh&^)l1b5(b(=iDJ9ibJ|NL)!Deo1ZiaBYwK+pf{NBV@C=#;1m3ikzrNNWoTt z+(ED+rO{uZq|by5^Wf+@%nI9yp&{mS%Bo*^yks`EG;y~^x@WG4>32N?9WI@~fpuW~*C?BQU5^%L<ti|!ahM+2zK+qPrRzX zx)c-t&>I>wt?0?b%-r*R`@0l>C;uZ{EUf;WV2R;B!CBAvFJK?7o(+^4CdWAxG1gv+ z3`{;?ND^HUy9u3E%_s^V7T2qbOLZ!LEZ`6o;BP4v=|vIP`r!O8Kq2rOADjZHrtJW$ z>~ppe?3D3?l^cs-Snoje?q-@~gRf7$>WW8ICpM|Ngz|BETgOwunMUk=jSl_6xkHd3 zww?{yy7TD&3aBYz;FGIg<(XH4zl}ax_-TaY2421z_T>x}Q780>OT`o_gb`mlz0!VJ z3hx4+VVdE0!cI)lE>90tqbPKY9!8Z4;R-*JHMYuEJH3OpzAg9Fr(7z(-kS;Dn-pA% zpH7`u@f%^AH+9@$j5=3qLf>`C{|@C~{f_JB9l8$c>$=-7XLSUBe{!&J!%LPYu~DFi zeDTx9s)lc!iM=@G3UCKx;W?GlmYA((h|p_Iidz-9R?Tb za#&u^Gu?dkrb9-cKmFwmH+t)lFkOuDW!VMT2Eb{Z`qNXw$vSL4o=-W3GNdQrd1AvB zX^vcXjlvY(?Eh}snX+nOxDj*cZ<5s9t><^|?1^=0cI=-t@>_Hy$RkS?Nx-!nla)Qm z$uF}~aif zkc45s1^?lj9V8ojdWK1ChgD8@#i0IP-q>F{rJ^z)Xbz27wqaG%tVwHQ4>SN0Km_S|j{)=93=&Sylof!R%e-=AzF+~=RU|Y{EhA! zcyV@CW+X}HZuZ^iS7hiW9WQS-X$p}uGD3_5EJ@rz@nSaucWI^7`s{xjq^v_j!Y0Xoc!+0B|z%JF1I93IB^XgDN$!;=z1#0i^KO`ny>2H;HH4eoJlMHadw=ZmG7enW{{(=LJ2FZ@8T% zVjPP67H{|7M)Wb0pEj^CB&d06I6J*{s7a|{o>h?S#-3I?b8e;g)P%=w{i{Y?1s@*? zNhOC+c@9n^c;#@r&_yBe>%r5w(aD~jg}0Un8;6dC{fBfph{hX*E%W$J1CLcjTEz>J zp+24r$?=VrQpV``e(VhoX(&(FZsL8pgSga^ScY|v4^%>{^t<}!H^{<5CVQ0WLxCL0 zJjY^^^sd>;QI*p+W8ZiD1WxM1tTe--_X0kKC*VJh7I*VcHS+ps739i?&l8!a_$`f2 zpf_GiBRcv4AjQ47-at!8-7DYAK}kW$^IS-a1E{9KSHi?^(oHp7LO|ME`6L#pjVe)T zA{_N)?c^ZzF5VoVbLxBhI>m(1zd)ao*vy;b7@xRqb32bx$}svJ#S(){_M?N$ZrKYtuV`%<{!e|~CMqn9GfdB8Vs zBu|^PnXPD<gxA9UKTq!ojsc+uX zCPZ?i&o+eZg z?taN1LW1^sc5`vG$2}M;fZb4e)>YMl(LkJ2m7qSXjGW;uZ3ESZ8$NnYL7eUDa(8na zJX=DAIcGcF@t)ahld1Iq6d8uzbe6Pbd}n~2`IbX zX6q0%dIT|^O2Ct&aV6$leI(zM_Os~aC_v~VN~*Y2ZQ-ydbk%rsGnEd_q6N5tnhI+# zfuo{h`Cl^w;*(oSd79i@Y>RDDD-^=|yMj844?JLEloOi(EJuoNX#NGpUZWbuT#_%A zx6aX5yPF`4)qQp|Nn*nBnA5jtK@Xr4l|a4B)S1%4WLjBiX5q&3tw{`jN$scJDK>4x zLfXKxQgI`!WJrF;rI-!amnr#bGp5(lJNAD381Y`ly_!tqBw4l*lJ!uBiRAVtD*HXk zdptM;fs31d4;s$AODvzN>hA~Qp_EZF{yt6xU~K1uZY`?@j&2y?mS^CIaBPm6ro*Sk7vwV@tYNfj2~axivk_o-Zn7Met{WAu2P zuK6E2HjmF~--X?67ouUMQNiD%Gf;~Mr=_>vtc(qVw!BDpqBElA@};dm!g`aBP|7Jx zYeIU(*ah*$?aViV$6s?i~+`M37!+zBp$k2uCWB1*)=N91@h@fBr9A7OxlHT z2F+$6L_?Tg)7`Ot?&D%Vt*$Y+@u>bTgdH-ji18YX4Z09>$CXaRb-fY`TViK97~rWO zJzf6n5|bgd4CpZYT$;+iS!>%VEBhW-K%LPCl#RLD6o?T?Lfayq@s>h=kgF6$+duP8 zxgPA@B%SPfg;99E3g}SLt}uKi5<}S(Xln3L5$0n-m@GZZ%@#8!At#>`K;YC*A59sb z=3U(8;le=_k3H4IP-LEE2FxUU*D!%K156%t+m%Nz0>A?w4=x*Zqr--^gL?IfIT+}1Nzj3FRK&1r}(@7pt424W4#M>_AR zph@2MuQxhQ855_)dGnSk?wK$UfRahLL<;M06@j%V==Yy(RYQXBU&jZ#-2c2ShWshI zS!Ji09M6P{6>`B#b-|p$(u4(x8$PS<#IoYC)vs&#(YT*b$mV>AnjeL#6|lH}e$SM$ z^M-pne>7}dUgZ2y%Noy@w#ICMZ=}bE!0@o90Opao!AV&ktZPj=mylcsL8YWi+%SG} zWM-xWLAg9H4pD4tE$yb5I2(~t3-P^>Aw6x32uK}o#aV@Oq4yCsa<52@-r(b_6o2_2 zf#ph8D<9M?;`6Se?0(|^q|_$xV30|_w@rbc8kMBnSTdOq{CdM^O0Z|O%|*RqC$ceK z7E-UxdooDYHp`-IEOyW^xD}}9@TJ5=xzuETvc}*qphEc`mdCPS@_QPkYV|Q-huQS! z991qI0MxIH16D77jfBQ{7SoCJj$YuMc03Iy@0O*K{`AgGCHrc7UzkW-tI58uhLm!K z3L6vK!01WxnB^sFYMlsh3W~qyt%BjI^_GfU z=r5q*WVOC!4&7G%O;9S;qv(UzXK55%|JF^J86sk*Q=pj~ti4D0+G!v8Ap5gAp;GVj zSM>H`sEeLWOd6xDR~Gb19lbBcfcYJ}rVR1=#{bmDHA{%C2tG z?F{$!gXZcU3_$=AC3-Q$r2x2gA7i`RFdRhCQz`!ZRdm|{UVXEhMzU}bwwDxKUb+|wZV}^%QmzZcYu2F})LcmC6D|@Xm75Yu9qVIMjwQn40eHW=@IQC=%PQ5iuv>DI z2{B%o(U&R~z%`7E&0Jn)q!T{)3t)SgE>x>a5okj<6YxD)TASk-K%7XR`-JAjhy288 zdY6XReIejj=By#ZPAN6t0u@*C>H_>&8jc%od=%u^-u!vml+LevkjkE}+_SJpo3-SJ z)=2RScyxb=QNbb_ZGCWPJQjjm{Kl};^1#5`-P8l=M$uiq9W_pBM96I%Yi8|g@#Y-| z?-Jn1ztHmLrDoU-2ulQXXLx8`?>?r~`ku|b)2gr_rWSR2W=P;4ojWX)t|Ta&j{YF0 z=xF49`~8WxBmU7n)wA308fW%@0hTkjJHM-E)9K3GGG)3a>05P91~J8>Pw;jOI|CG% zXJ_Jh#kX1bB334-HFOPq_3l}xGBi_K7x6Et2_&x39yLTEN^*LY7G;ZPhLYy#iR#1B zH2~)(5HB>$+NEAZ9t|_BO?xOXAjZyuCKO>S+E~3$oI(2bT7Q#pv?K6`l2@e5yFY+0 z3__&tAD@e=yx0YEQ!l6h){^z9G=Z&Jk1hK6RFVb%0!pH_y1o<5%;nyiw-B7J8R z?`N5SR}M=`zXX?8P82&bIt)i)q#mmCXGNL??k-|43MqU-G<2XD`cj3DPNw@oKfdy+ zY@ud3iLEtM_Jb^SerhFAaYb1b^XM85NyQ>$g7_r(^{o=0D`mbxOt)K609TV2M`oL) zba6}6y|Q>(KI%Eo-)J;Uecrz9cpH@fXoEUEcw?O1tU;-66!u|yCx$C6ev&BF2%NN~ zGaKXVF2ky-#`&IcA5-;sxsl%VFx8vF;(=<*y^-MrGy#?fFnDfT0t?Kd7Q)u6kym?3 zMUt=dsm!vSUE$pp{w7JGR1i||qn{4b*p`xNW7;&Gon4Qh-&f&u&!~B!(~wz0{eDfz z+7m3T*MqjAWCTd&{7G51Bp?iFF=my<9c3;RwI06x>?O4sCOs{@oDMt()cv;HtysudphP;_7Ilvz9_LCHk5WmH)1T@CSMY%tvr@&6-}vU($A-Q|THP$~ z83(6oCGtXgP(Q_NINz^J3tFE*L~%{yQ9W?21IgPmj^6wz!!CIZzQr0x)VHgNu3u4| zvqB20rX-c@tG0F&j}E$6vvQNe@wmpSs;zH$#+7r|-@z_1Pv>y6@xaNS%5hEawKdH> zE(_UqR`IXfKVzBF4jn3UP14~cEw}1C(|t1s%$r4fNX0Mn(g+CBRu`C= zxqd^%nAP1hpd(2)eE2Y)tttlSQkjUn@VDmOR{O%ImiRnzBJ(VSEp1$&NS$W4S58(< ziZrI2M4XHSrqxMtdFypQK>-{5DQY%s>&hKqUxR9VD@Co^-QK$ym3PsEM5aJU2S zZsa=lkS_AwR~AttlX3>EFU##>qWenUaF-R{bc$!u?D(`YV(DUE6S1LSPhiaM!Ch(Z z8{-Ws<*BX`^HLr$Xui1h_zI9XNW;roPz&q!XK)#sKz}wfGhR7LQ)NDJ!11eBaAej|94c1Je8G4~GWfAT*+u<6 zGrE(0_Hyn*1bb6(ipDc8$>_2E_KyBKsPtX&A6~LSmY*-JT5jf2bEMIlGDK8B%*>84 zPOlXxd~^SR<}!+_&DH&LDhaoL&C|oEOQ>`76a34RedJ0=(scnT%;QVo)O*(#1FOR4 z`i;+LR~Ez;@S=YF1&D8KF4xKX7VslpVopBw8snuwq>3o4ues;HthZq37b6O&22;Q6 zhtVrBm&mohmH1Qr%0tLpCScw&dX%x|B!8?~Xq5r>aI&R+)SrcFL0t$kT0Ck*8oJ7S z3BJW?>)M(s`6Z!VxZcSuTl7j0?~i|u38iMVuQ+%%fB+})m00DRg(ScQE@Y|I00GyK zKdfEFp`ZIh;2oh<)>*o6c2SR^{74;CrL*#-8b!{hT^-O~MKi;^^B3@P&}J*g{6z8Q zwAAfz%$1klj#_|gyLt16__^j*__j4OGCHWthkc+8*dFGk1jic@EFcbM_o`oVb9@*O zbo;xb+t1+UC~yGB((1eQsD7Mo3aX65R*ygJdEPL9@t$XL*e9M4ehw?ftso_-ANSQ0 z-%nZM9Z`<@vOUP}&#U`G`ygsHLQb}z8}MK{r1XXAk9&#)`#jgU$9-V>tkGIN)iJSq zvH6_2ueqbR1YHtkk~a&c^XrATydOOH*bxL7dKr80t!v;>Xk~wzu&adb5)ZWPHk`WV zQvHvwKlUr$$!(?ia(Fr2h(ajQNz=&QGbWXzUvCh|J{=Pa8HpQ~pMMu&|3CbH4g5c( Gf&T$Cg9y_A diff --git a/assets/img/next.png b/assets/img/next.png deleted file mode 100644 index 511a02c4a6c24c57b0b21ad4743cfb2ddf9d9d6d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 986 zcmV<0110>4P)#NV}$WzV|YmzI0Q*hM~YG6rm3Tp5DunC zkX%zh5RHttyZqbux3znjLYhj7u~kM7DW;b3}m9^_vuY2?Dn*Eu}XsQUyY4neb zJ4TDI^XD&e8_zqR*B?n6V zG};w#-$|4@Cez<_$$}O6%;gsFKqdI*3UTG3uF0f6pb0R^>Bqf75sj?TmGo zrkjk*Ok=S7^7Yw#=ACcEG+m6HO#@Tu>$!B%%6#UnuN#l1!1DHdX;VZ6E?KmqpaMtI z$_UB`U}0!7vH~;Mw)5KgYBrxX?#Ec_3F{^U7Jf>|(8K;+cYdY3n@>J#-OFkss{bKi z%4Aw-=U~pg5mq?iznD!KW;Fw2vN-hYwJ@VP>EY#G4`a5j3i{- zI^Tt(w3zL8JgyQqzU1G7xnz_q^nW@iulotnsM&65bqTN}lJ*bfA=%3xXXnj?tq>aP zYPRM)`bchrBsN_65IcE9D8W}TD%^R_)epb(;5+BsWDPQdl=`}L5%+_(hLMb8B9p}5 z7#c|E$Nk4I4H3$zqe#sB~S07*qo IM6N<$f_tayBLDyZ diff --git a/assets/img/prev.png b/assets/img/prev.png deleted file mode 100644 index 487ba479f5ccf48357fa7cda447965d306fb0403..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1028 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 - @@ -43,7 +42,7 @@ Cancel - {{template "prt_footer.html"}} + {{template "prt_footer.html" .}} diff --git a/assets/tpl/admin_edit_client.html b/assets/tpl/admin_edit_client.html index c549c94..41203ce 100644 --- a/assets/tpl/admin_edit_client.html +++ b/assets/tpl/admin_edit_client.html @@ -6,7 +6,6 @@ {{ .Static.WebsiteTitle }} - Admin - @@ -83,7 +82,7 @@ Cancel - {{template "prt_footer.html"}} + {{template "prt_footer.html" .}} diff --git a/assets/tpl/admin_edit_interface.html b/assets/tpl/admin_edit_interface.html index dd2238f..3a0380a 100644 --- a/assets/tpl/admin_edit_interface.html +++ b/assets/tpl/admin_edit_interface.html @@ -6,7 +6,6 @@ {{ .Static.WebsiteTitle }} - Admin - @@ -99,7 +98,7 @@ Cancel - {{template "prt_footer.html"}} + {{template "prt_footer.html" .}} diff --git a/assets/tpl/admin_index.html b/assets/tpl/admin_index.html index 717218d..25be8a4 100644 --- a/assets/tpl/admin_index.html +++ b/assets/tpl/admin_index.html @@ -6,7 +6,6 @@ {{ .Static.WebsiteTitle }} - Admin - @@ -92,7 +91,9 @@

Current VPN Users

+ {{if not .Static.LdapDisabled}} + {{end}} M
@@ -191,7 +192,7 @@

Currently listed peers: {{len .Peers}}

- {{template "prt_footer.html"}} + {{template "prt_footer.html" .}} diff --git a/assets/tpl/error.html b/assets/tpl/error.html index a9a76d0..611381b 100644 --- a/assets/tpl/error.html +++ b/assets/tpl/error.html @@ -6,7 +6,6 @@ {{ .Static.WebsiteTitle }} - Error - @@ -22,7 +21,7 @@

{{.Data.Details}}

← Back to Dashboard -{{template "prt_footer.html"}} +{{template "prt_footer.html" .}} diff --git a/assets/tpl/index.html b/assets/tpl/index.html index ca90f8c..c2e0a10 100644 --- a/assets/tpl/index.html +++ b/assets/tpl/index.html @@ -1,13 +1,12 @@ - + {{ .Static.WebsiteTitle }} - @@ -26,7 +25,7 @@

Client Software

Installation instructions for client software can be found on the official WireGuard website: https://www.wireguard.com/

- {{template "prt_footer.html"}} + {{template "prt_footer.html" .}} diff --git a/assets/tpl/login.html b/assets/tpl/login.html index f441f35..8277098 100644 --- a/assets/tpl/login.html +++ b/assets/tpl/login.html @@ -7,7 +7,6 @@ {{ .static.WebsiteTitle }} - Login - @@ -46,7 +45,7 @@ - + diff --git a/assets/tpl/profile.html b/assets/tpl/profile.html deleted file mode 100644 index 739bc96..0000000 --- a/assets/tpl/profile.html +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - {{ .static.WebsiteTitle }} - Profile - - - - - - - - - {{template "prt_nav.html" .}} -
-
-
-
-
- User Image -
-
- - - - - - -
-
-
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
-
-
- Go Back -
-
-
-
-
- {{template "prt_footer.html"}} - - - - - - - \ No newline at end of file diff --git a/assets/tpl/prt_footer.html b/assets/tpl/prt_footer.html index b7ebe9b..612e74c 100644 --- a/assets/tpl/prt_footer.html +++ b/assets/tpl/prt_footer.html @@ -1,5 +1,5 @@
-

Copyright © Prolicht GmbH 2020

+

Copyright © {{ $.Static.CompanyName }} {{$.Static.Year}}

\ No newline at end of file diff --git a/assets/tpl/prt_nav.html b/assets/tpl/prt_nav.html index dd83a5a..afb071a 100644 --- a/assets/tpl/prt_nav.html +++ b/assets/tpl/prt_nav.html @@ -3,7 +3,7 @@ - PROLICHT + {{$.Static.CompanyName}} {{else}} -  Login +  Login {{end}} diff --git a/assets/tpl/user_index.html b/assets/tpl/user_index.html index 4fb3173..a8d988b 100644 --- a/assets/tpl/user_index.html +++ b/assets/tpl/user_index.html @@ -6,7 +6,6 @@ {{ .Static.WebsiteTitle }} - Profile - @@ -100,7 +99,7 @@

Currently listed peers: {{len .Peers}}

- {{template "prt_footer.html"}} + {{template "prt_footer.html" .}} diff --git a/internal/common/configuration.go b/internal/common/configuration.go index c147f62..6d581e1 100644 --- a/internal/common/configuration.go +++ b/internal/common/configuration.go @@ -55,31 +55,39 @@ func loadConfigEnv(cfg interface{}) error { type Config struct { Core struct { ListeningAddress string `yaml:"listeningAddress" envconfig:"LISTENING_ADDRESS"` + ExternalUrl string `yaml:"externalUrl" envconfig:"EXTERNAL_URL"` Title string `yaml:"title" envconfig:"WEBSITE_TITLE"` + CompanyName string `yaml:"company" envconfig:"COMPANY_NAME"` + MailFrom string `yaml:"mailfrom" envconfig:"MAIL_FROM"` + AdminUser string `yaml:"adminUser" envconfig:"ADMIN_USER"` // optional, non LDAP admin user + AdminPassword string `yaml:"adminPass" envconfig:"ADMIN_PASS"` } `yaml:"core"` - - LDAP ldap.Config `yaml:"ldap"` - WG wireguard.Config `yaml:"wg"` - AdminLdapGroup string `yaml:"adminLdapGroup" envconfig:"ADMIN_LDAP_GROUP"` - LogoutRedirectPath string `yaml:"logoutRedirectPath" envconfig:"LOGOUT_REDIRECT_PATH"` - AuthRoutePrefix string `yaml:"authRoutePrefix" envconfig:"AUTH_ROUTE_PREFIX"` + Email MailConfig `yaml:"email"` + LDAP ldap.Config `yaml:"ldap"` + WG wireguard.Config `yaml:"wg"` + AdminLdapGroup string `yaml:"adminLdapGroup" envconfig:"ADMIN_LDAP_GROUP"` } func NewConfig() *Config { cfg := &Config{} // Default config - cfg.Core.ListeningAddress = ":8080" + cfg.Core.ListeningAddress = ":8123" cfg.Core.Title = "WireGuard VPN" + cfg.Core.CompanyName = "WireGuard Portal" + cfg.Core.ExternalUrl = "http://localhost:8123" + cfg.Core.MailFrom = "WireGuard VPN " + cfg.Core.AdminUser = "" // non-ldap admin access is disabled by default + cfg.Core.AdminPassword = "" cfg.LDAP.URL = "ldap://srv-ad01.company.local:389" cfg.LDAP.BaseDN = "DC=COMPANY,DC=LOCAL" cfg.LDAP.StartTLS = true - cfg.LDAP.BindUser = "company\\ldap_wireguard" + cfg.LDAP.BindUser = "company\\\\ldap_wireguard" cfg.LDAP.BindPass = "SuperSecret" cfg.WG.DeviceName = "wg0" cfg.AdminLdapGroup = "CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL" - cfg.LogoutRedirectPath = "/" - cfg.AuthRoutePrefix = "/auth" + cfg.Email.Host = "127.0.0.1" + cfg.Email.Port = 25 // Load config from file and environment cfgFile, ok := os.LookupEnv("CONFIG_FILE") diff --git a/internal/ldap/usercache.go b/internal/ldap/usercache.go index 1afb7ad..6fe5593 100644 --- a/internal/ldap/usercache.go +++ b/internal/ldap/usercache.go @@ -18,9 +18,6 @@ var Fields = []string{"givenName", "sn", "mail", "department", "memberOf", "sAMA "st", "postalCode", "co", "facsimileTelephoneNumber", "pager", "thumbnailPhoto", "otherMobile", "extensionAttribute2", "distinguishedName", "userAccountControl"} -var ModifiableFields = []string{"department", "telephoneNumber", "mobile", "displayName", "title", "company", - "manager", "streetAddress", "employeeID", "l", "st", "postalCode", "co", "thumbnailPhoto"} - // -------------------------------------------------------------------------------------------------------------------- // Cache Data Store // -------------------------------------------------------------------------------------------------------------------- @@ -28,7 +25,6 @@ var ModifiableFields = []string{"department", "telephoneNumber", "mobile", "disp type UserCacheHolder interface { Clear() SetAllUsers(users []RawLdapData) - SetUser(data RawLdapData) GetUser(dn string) *RawLdapData GetUsers() []*RawLdapData } @@ -95,14 +91,6 @@ func (h *SynchronizedUserCacheHolder) SetAllUsers(users []RawLdapData) { } } -func (h *SynchronizedUserCacheHolder) SetUser(user RawLdapData) { - h.mux.Lock() - defer h.mux.Unlock() - - h.users[user.DN] = &UserCacheHolderEntry{RawLdapData: user} - h.users[user.DN].CalcFieldsFromAttributes() -} - func (h *SynchronizedUserCacheHolder) GetUser(dn string) *RawLdapData { h.mux.RLock() defer h.mux.RUnlock() @@ -152,30 +140,6 @@ func (h *SynchronizedUserCacheHolder) GetSortedUsers(sortKey string, sortDirecti } -func (h *SynchronizedUserCacheHolder) GetFilteredUsers(sortKey string, sortDirection string, search, searchDepartment string) []*UserCacheHolderEntry { - sortedUsers := h.GetSortedUsers(sortKey, sortDirection) - if search == "" && searchDepartment == "" { - return sortedUsers // skip filtering - } - - filteredUsers := make([]*UserCacheHolderEntry, 0, len(sortedUsers)) - for _, user := range sortedUsers { - if searchDepartment != "" && user.Attributes["department"] != searchDepartment { - continue - } - if strings.Contains(user.Attributes["sn"], search) || - strings.Contains(user.Attributes["givenName"], search) || - strings.Contains(user.Mail, search) || - strings.Contains(user.Attributes["department"], search) || - strings.Contains(user.Attributes["telephoneNumber"], search) || - strings.Contains(user.Attributes["mobile"], search) { - filteredUsers = append(filteredUsers, user) - } - } - - return filteredUsers -} - func (h *SynchronizedUserCacheHolder) IsInGroup(username, gid string) bool { userDN := h.GetUserDN(username) if userDN == "" { @@ -231,45 +195,6 @@ func (h *SynchronizedUserCacheHolder) GetUserDNByMail(mail string) string { return userDN } -func (h *SynchronizedUserCacheHolder) GetTeamLeaders() []*UserCacheHolderEntry { - - sortedUsers := h.GetSortedUsers("sn", "asc") - teamLeaders := make([]*UserCacheHolderEntry, 0, len(sortedUsers)) - for _, user := range sortedUsers { - if user.Attributes["extensionAttribute2"] != "Teamleiter" { - continue - } - - teamLeaders = append(teamLeaders, user) - } - - return teamLeaders -} - -func (h *SynchronizedUserCacheHolder) GetDepartments() []string { - h.mux.RLock() - defer h.mux.RUnlock() - - departmentSet := make(map[string]struct{}) - for _, user := range h.users { - if user.Attributes["department"] == "" { - continue - } - departmentSet[user.Attributes["department"]] = struct{}{} - } - - departments := make([]string, len(departmentSet)) - i := 0 - for department := range departmentSet { - departments[i] = department - i++ - } - - sort.Strings(departments) - - return departments -} - // -------------------------------------------------------------------------------------------------------------------- // Cache Handler, LDAP interaction // -------------------------------------------------------------------------------------------------------------------- @@ -398,58 +323,3 @@ func (u *UserCache) Update(filter bool) error { return nil } - -func (u *UserCache) ModifyUserData(dn string, newData RawLdapData, fields []string) error { - if fields == nil { - fields = ModifiableFields // default - } - - existingUserData := u.userData.GetUser(dn) - if existingUserData == nil { - return fmt.Errorf("user with dn %s not found", dn) - } - - modify := ldap.NewModifyRequest(dn, nil) - - for _, ldapAttribute := range fields { - if existingUserData.Attributes[ldapAttribute] == newData.Attributes[ldapAttribute] { - continue // do not update unchanged fields - } - - if len(existingUserData.RawAttributes[ldapAttribute]) == 0 && newData.Attributes[ldapAttribute] != "" { - modify.Add(ldapAttribute, []string{newData.Attributes[ldapAttribute]}) - newData.RawAttributes[ldapAttribute] = [][]byte{ - []byte(newData.Attributes[ldapAttribute]), - } - } - if len(existingUserData.RawAttributes[ldapAttribute]) != 0 && newData.Attributes[ldapAttribute] != "" { - modify.Replace(ldapAttribute, []string{newData.Attributes[ldapAttribute]}) - newData.RawAttributes[ldapAttribute][0] = []byte(newData.Attributes[ldapAttribute]) - } - if len(existingUserData.RawAttributes[ldapAttribute]) != 0 && newData.Attributes[ldapAttribute] == "" { - modify.Delete(ldapAttribute, []string{}) - newData.RawAttributes[ldapAttribute] = [][]byte{} // clear list - } - } - - if len(modify.Changes) == 0 { - return nil - } - - client, err := u.open() - if err != nil { - u.LastError = err - return err - } - defer u.close(client) - - err = client.Modify(modify) - if err != nil { - return err - } - - // Once written to ldap, update the local cache - u.userData.SetUser(newData) - - return nil -} diff --git a/internal/server/core.go b/internal/server/core.go index a8aba5f..d3d2151 100644 --- a/internal/server/core.go +++ b/internal/server/core.go @@ -56,8 +56,9 @@ type AlertData struct { type StaticData struct { WebsiteTitle string WebsiteLogo string - LoginURL string - LogoutURL string + CompanyName string + Year int + LdapDisabled bool } type Server struct { @@ -71,6 +72,7 @@ type Server struct { wg *wireguard.Manager // LDAP stuff + ldapDisabled bool ldapAuth ldap.Authentication ldapUsers *ldap.SynchronizedUserCacheHolder ldapCacheUpdater *ldap.UserCache @@ -88,7 +90,9 @@ func (s *Server) Setup() error { s.ldapUsers.Init() s.ldapCacheUpdater = ldap.NewUserCache(s.config.LDAP, s.ldapUsers) if s.ldapCacheUpdater.LastError != nil { - return s.ldapCacheUpdater.LastError + log.Warnf("LDAP error: %v", s.ldapCacheUpdater.LastError) + log.Warnf("LDAP features disabled!") + s.ldapDisabled = true } // Setup WireGuard stuff @@ -141,15 +145,17 @@ func (s *Server) Setup() error { func (s *Server) Run() { // Start ldap group watcher - go func(s *Server) { - for { - time.Sleep(CacheRefreshDuration) - if err := s.ldapCacheUpdater.Update(true); err != nil { - log.Warnf("Failed to update ldap group cache: %v", err) + if !s.ldapDisabled { + go func(s *Server) { + for { + time.Sleep(CacheRefreshDuration) + if err := s.ldapCacheUpdater.Update(true); err != nil { + log.Warnf("Failed to update ldap group cache: %v", err) + } + log.Debugf("Refreshed LDAP permissions!") } - log.Debugf("Refreshed LDAP permissions!") - } - }(s) + }(s) + } // Run web service err := s.server.Run(s.config.Core.ListeningAddress) @@ -233,8 +239,10 @@ func (s *Server) destroySessionData(c *gin.Context) error { func (s *Server) getStaticData() StaticData { return StaticData{ WebsiteTitle: s.config.Core.Title, - LoginURL: s.config.AuthRoutePrefix + "/login", - LogoutURL: s.config.AuthRoutePrefix + "/logout", + WebsiteLogo: "/img/header-logo.png", + CompanyName: s.config.Core.CompanyName, + LdapDisabled: s.ldapDisabled, + Year: time.Now().Year(), } } diff --git a/internal/server/handlers.go b/internal/server/handlers.go deleted file mode 100644 index d62ff82..0000000 --- a/internal/server/handlers.go +++ /dev/null @@ -1,588 +0,0 @@ -package server - -import ( - "bytes" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - log "github.com/sirupsen/logrus" - - "github.com/h44z/wg-portal/internal/ldap" - - "github.com/h44z/wg-portal/internal/common" - - "github.com/gin-gonic/gin" -) - -type LdapCreateForm struct { - Emails string `form:"email" binding:"required"` - Identifier string `form:"identifier" binding:"required,lte=20"` -} - -func (s *Server) GetIndex(c *gin.Context) { - c.HTML(http.StatusOK, "index.html", struct { - Route string - Alerts AlertData - Session SessionData - Static StaticData - Device Device - }{ - Route: c.Request.URL.Path, - Alerts: s.getAlertData(c), - Session: s.getSessionData(c), - Static: s.getStaticData(), - Device: s.users.GetDevice(), - }) -} - -func (s *Server) HandleError(c *gin.Context, code int, message, details string) { - // TODO: if json - //c.JSON(code, gin.H{"error": message, "details": details}) - - c.HTML(code, "error.html", gin.H{ - "Data": gin.H{ - "Code": strconv.Itoa(code), - "Message": message, - "Details": details, - }, - "Route": c.Request.URL.Path, - "Session": s.getSessionData(c), - "Static": s.getStaticData(), - }) -} - -func (s *Server) GetAdminIndex(c *gin.Context) { - currentSession := s.getSessionData(c) - - sort := c.Query("sort") - if sort != "" { - if currentSession.SortedBy != sort { - currentSession.SortedBy = sort - currentSession.SortDirection = "asc" - } else { - if currentSession.SortDirection == "asc" { - currentSession.SortDirection = "desc" - } else { - currentSession.SortDirection = "asc" - } - } - - if err := s.updateSessionData(c, currentSession); err != nil { - s.HandleError(c, http.StatusInternalServerError, "sort error", "failed to save session") - return - } - c.Redirect(http.StatusSeeOther, "/admin") - return - } - - search, searching := c.GetQuery("search") - if searching { - currentSession.Search = search - - if err := s.updateSessionData(c, currentSession); err != nil { - s.HandleError(c, http.StatusInternalServerError, "search error", "failed to save session") - return - } - c.Redirect(http.StatusSeeOther, "/admin") - return - } - - device := s.users.GetDevice() - users := s.users.GetFilteredAndSortedUsers(currentSession.SortedBy, currentSession.SortDirection, currentSession.Search) - - c.HTML(http.StatusOK, "admin_index.html", struct { - Route string - Alerts AlertData - Session SessionData - Static StaticData - Peers []User - TotalPeers int - Device Device - }{ - Route: c.Request.URL.Path, - Alerts: s.getAlertData(c), - Session: currentSession, - Static: s.getStaticData(), - Peers: users, - TotalPeers: len(s.users.GetAllUsers()), - Device: device, - }) -} - -func (s *Server) GetUserIndex(c *gin.Context) { - currentSession := s.getSessionData(c) - - sort := c.Query("sort") - if sort != "" { - if currentSession.SortedBy != sort { - currentSession.SortedBy = sort - currentSession.SortDirection = "asc" - } else { - if currentSession.SortDirection == "asc" { - currentSession.SortDirection = "desc" - } else { - currentSession.SortDirection = "asc" - } - } - - if err := s.updateSessionData(c, currentSession); err != nil { - s.HandleError(c, http.StatusInternalServerError, "sort error", "failed to save session") - return - } - c.Redirect(http.StatusSeeOther, "/admin") - return - } - - device := s.users.GetDevice() - users := s.users.GetSortedUsersForEmail(currentSession.SortedBy, currentSession.SortDirection, currentSession.Email) - - c.HTML(http.StatusOK, "user_index.html", struct { - Route string - Alerts AlertData - Session SessionData - Static StaticData - Peers []User - TotalPeers int - Device Device - }{ - Route: c.Request.URL.Path, - Alerts: s.getAlertData(c), - Session: currentSession, - Static: s.getStaticData(), - Peers: users, - TotalPeers: len(users), - Device: device, - }) -} - -func (s *Server) GetAdminEditInterface(c *gin.Context) { - device := s.users.GetDevice() - users := s.users.GetAllUsers() - - currentSession, err := s.setFormInSession(c, device) - if err != nil { - s.HandleError(c, http.StatusInternalServerError, "Session error", err.Error()) - return - } - - c.HTML(http.StatusOK, "admin_edit_interface.html", struct { - Route string - Alerts AlertData - Session SessionData - Static StaticData - Peers []User - Device Device - }{ - Route: c.Request.URL.Path, - Alerts: s.getAlertData(c), - Session: currentSession, - Static: s.getStaticData(), - Peers: users, - Device: currentSession.FormData.(Device), - }) -} - -func (s *Server) PostAdminEditInterface(c *gin.Context) { - currentSession := s.getSessionData(c) - var formDevice Device - if currentSession.FormData != nil { - formDevice = currentSession.FormData.(Device) - } - if err := c.ShouldBind(&formDevice); err != nil { - _ = s.updateFormInSession(c, formDevice) - s.setAlert(c, "failed to bind form data: "+err.Error(), "danger") - c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=bind") - return - } - // Clean list input - formDevice.IPs = common.ParseStringList(formDevice.IPsStr) - formDevice.AllowedIPs = common.ParseStringList(formDevice.AllowedIPsStr) - formDevice.DNS = common.ParseStringList(formDevice.DNSStr) - formDevice.IPsStr = common.ListToString(formDevice.IPs) - formDevice.AllowedIPsStr = common.ListToString(formDevice.AllowedIPs) - formDevice.DNSStr = common.ListToString(formDevice.DNS) - - // Update WireGuard device - err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetDeviceConfig()) - if err != nil { - _ = s.updateFormInSession(c, formDevice) - s.setAlert(c, "failed to update device in WireGuard: "+err.Error(), "danger") - c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=wg") - return - } - - // Update in database - err = s.users.UpdateDevice(formDevice) - if err != nil { - _ = s.updateFormInSession(c, formDevice) - s.setAlert(c, "failed to update device in database: "+err.Error(), "danger") - c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update") - return - } - - s.setAlert(c, "changes applied successfully", "success") - c.Redirect(http.StatusSeeOther, "/admin/device/edit") -} - -func (s *Server) GetAdminEditPeer(c *gin.Context) { - device := s.users.GetDevice() - user := s.users.GetUserByKey(c.Query("pkey")) - - currentSession, err := s.setFormInSession(c, user) - if err != nil { - s.HandleError(c, http.StatusInternalServerError, "Session error", err.Error()) - return - } - - c.HTML(http.StatusOK, "admin_edit_client.html", struct { - Route string - Alerts AlertData - Session SessionData - Static StaticData - Peer User - Device Device - }{ - Route: c.Request.URL.Path, - Alerts: s.getAlertData(c), - Session: currentSession, - Static: s.getStaticData(), - Peer: currentSession.FormData.(User), - Device: device, - }) -} - -func (s *Server) PostAdminEditPeer(c *gin.Context) { - currentUser := s.users.GetUserByKey(c.Query("pkey")) - urlEncodedKey := url.QueryEscape(c.Query("pkey")) - - currentSession := s.getSessionData(c) - var formUser User - if currentSession.FormData != nil { - formUser = currentSession.FormData.(User) - } - if err := c.ShouldBind(&formUser); err != nil { - _ = s.updateFormInSession(c, formUser) - s.setAlert(c, "failed to bind form data: "+err.Error(), "danger") - c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=bind") - return - } - - // Clean list input - formUser.IPs = common.ParseStringList(formUser.IPsStr) - formUser.AllowedIPs = common.ParseStringList(formUser.AllowedIPsStr) - formUser.IPsStr = common.ListToString(formUser.IPs) - formUser.AllowedIPsStr = common.ListToString(formUser.AllowedIPs) - - disabled := c.PostForm("isdisabled") != "" - now := time.Now() - if disabled && currentUser.DeactivatedAt == nil { - formUser.DeactivatedAt = &now - } else if !disabled { - formUser.DeactivatedAt = nil - } - - // Update in database - if err := s.UpdateUser(formUser, now); err != nil { - _ = s.updateFormInSession(c, formUser) - s.setAlert(c, "failed to update user: "+err.Error(), "danger") - c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=update") - return - } - - s.setAlert(c, "changes applied successfully", "success") - c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) -} - -func (s *Server) GetAdminCreatePeer(c *gin.Context) { - device := s.users.GetDevice() - - currentSession, err := s.setNewUserFormInSession(c) - if err != nil { - s.HandleError(c, http.StatusInternalServerError, "Session error", err.Error()) - return - } - c.HTML(http.StatusOK, "admin_edit_client.html", struct { - Route string - Alerts AlertData - Session SessionData - Static StaticData - Peer User - Device Device - }{ - Route: c.Request.URL.Path, - Alerts: s.getAlertData(c), - Session: currentSession, - Static: s.getStaticData(), - Peer: currentSession.FormData.(User), - Device: device, - }) -} - -func (s *Server) PostAdminCreatePeer(c *gin.Context) { - currentSession := s.getSessionData(c) - var formUser User - if currentSession.FormData != nil { - formUser = currentSession.FormData.(User) - } - if err := c.ShouldBind(&formUser); err != nil { - _ = s.updateFormInSession(c, formUser) - s.setAlert(c, "failed to bind form data: "+err.Error(), "danger") - c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=bind") - return - } - - // Clean list input - formUser.IPs = common.ParseStringList(formUser.IPsStr) - formUser.AllowedIPs = common.ParseStringList(formUser.AllowedIPsStr) - formUser.IPsStr = common.ListToString(formUser.IPs) - formUser.AllowedIPsStr = common.ListToString(formUser.AllowedIPs) - - disabled := c.PostForm("isdisabled") != "" - now := time.Now() - if disabled { - formUser.DeactivatedAt = &now - } - - if err := s.CreateUser(formUser); err != nil { - _ = s.updateFormInSession(c, formUser) - s.setAlert(c, "failed to add user: "+err.Error(), "danger") - c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=create") - return - } - - s.setAlert(c, "client created successfully", "success") - c.Redirect(http.StatusSeeOther, "/admin") -} - -func (s *Server) GetAdminCreateLdapPeers(c *gin.Context) { - currentSession, err := s.setFormInSession(c, LdapCreateForm{Identifier: "Default"}) - if err != nil { - s.HandleError(c, http.StatusInternalServerError, "Session error", err.Error()) - return - } - - c.HTML(http.StatusOK, "admin_create_clients.html", struct { - Route string - Alerts AlertData - Session SessionData - Static StaticData - Users []*ldap.UserCacheHolderEntry - FormData LdapCreateForm - Device Device - }{ - Route: c.Request.URL.Path, - Alerts: s.getAlertData(c), - Session: currentSession, - Static: s.getStaticData(), - Users: s.ldapUsers.GetSortedUsers("sn", "asc"), - FormData: currentSession.FormData.(LdapCreateForm), - Device: s.users.GetDevice(), - }) -} - -func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) { - currentSession := s.getSessionData(c) - var formData LdapCreateForm - if currentSession.FormData != nil { - formData = currentSession.FormData.(LdapCreateForm) - } - if err := c.ShouldBind(&formData); err != nil { - _ = s.updateFormInSession(c, formData) - s.setAlert(c, "failed to bind form data: "+err.Error(), "danger") - c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=bind") - return - } - - emails := common.ParseStringList(formData.Emails) - for i := range emails { - // TODO: also check email addr for validity? - if !strings.ContainsRune(emails[i], '@') || s.ldapUsers.GetUserDNByMail(emails[i]) == "" { - _ = s.updateFormInSession(c, formData) - s.setAlert(c, "invalid email address: "+emails[i], "danger") - c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=mail") - return - } - } - - log.Infof("creating %d ldap peers", len(emails)) - - for i := range emails { - if err := s.CreateUserByEmail(emails[i], formData.Identifier, false); err != nil { - _ = s.updateFormInSession(c, formData) - s.setAlert(c, "failed to add user: "+err.Error(), "danger") - c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create") - return - } - } - - s.setAlert(c, "client(s) created successfully", "success") - c.Redirect(http.StatusSeeOther, "/admin/peer/createldap") -} - -func (s *Server) GetAdminDeletePeer(c *gin.Context) { - currentUser := s.users.GetUserByKey(c.Query("pkey")) - if err := s.DeleteUser(currentUser); err != nil { - s.HandleError(c, http.StatusInternalServerError, "Deletion error", err.Error()) - return - } - s.setAlert(c, "user deleted successfully", "success") - c.Redirect(http.StatusSeeOther, "/admin") -} - -func (s *Server) GetUserQRCode(c *gin.Context) { - user := s.users.GetUserByKey(c.Query("pkey")) - currentSession := s.getSessionData(c) - if !currentSession.IsAdmin && user.Email != currentSession.Email { - s.HandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") - return - } - - png, err := user.GetQRCode() - if err != nil { - s.HandleError(c, http.StatusInternalServerError, "QRCode error", err.Error()) - return - } - c.Data(http.StatusOK, "image/png", png) - return -} - -func (s *Server) GetUserConfig(c *gin.Context) { - user := s.users.GetUserByKey(c.Query("pkey")) - currentSession := s.getSessionData(c) - if !currentSession.IsAdmin && user.Email != currentSession.Email { - s.HandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") - return - } - - cfg, err := user.GetClientConfigFile(s.users.GetDevice()) - if err != nil { - s.HandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) - return - } - - c.Header("Content-Disposition", "attachment; filename="+user.GetConfigFileName()) - c.Data(http.StatusOK, "application/config", cfg) - return -} - -func (s *Server) GetUserConfigMail(c *gin.Context) { - user := s.users.GetUserByKey(c.Query("pkey")) - currentSession := s.getSessionData(c) - if !currentSession.IsAdmin && user.Email != currentSession.Email { - s.HandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") - return - } - - cfg, err := user.GetClientConfigFile(s.users.GetDevice()) - if err != nil { - s.HandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) - return - } - png, err := user.GetQRCode() - if err != nil { - s.HandleError(c, http.StatusInternalServerError, "QRCode error", err.Error()) - return - } - // Apply mail template - var tplBuff bytes.Buffer - if err := s.mailTpl.Execute(&tplBuff, struct { - Client User - QrcodePngName string - PortalUrl string - }{ - Client: user, - QrcodePngName: "wireguard-config.png", - PortalUrl: s.config.Core.ExternalUrl, - }); err != nil { - s.HandleError(c, http.StatusInternalServerError, "Template error", err.Error()) - return - } - - // Send mail - attachments := []common.MailAttachment{ - { - Name: user.GetConfigFileName(), - ContentType: "application/config", - Data: bytes.NewReader(cfg), - }, - { - Name: "wireguard-config.png", - ContentType: "image/png", - Data: bytes.NewReader(png), - }, - } - - if err := common.SendEmailWithAttachments(s.config.Email, s.config.Core.MailFrom, "", "WireGuard VPN Configuration", - "Your mail client does not support HTML. Please find the configuration attached to this mail.", tplBuff.String(), - []string{user.Email}, attachments); err != nil { - s.HandleError(c, http.StatusInternalServerError, "Email error", err.Error()) - return - } - - s.setAlert(c, "mail sent successfully", "success") - c.Redirect(http.StatusSeeOther, "/admin") -} - -func (s *Server) GetDeviceConfig(c *gin.Context) { - device := s.users.GetDevice() - users := s.users.GetActiveUsers() - cfg, err := device.GetDeviceConfigFile(users) - if err != nil { - s.HandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) - return - } - - filename := strings.ToLower(device.DeviceName) + ".conf" - - c.Header("Content-Disposition", "attachment; filename="+filename) - c.Data(http.StatusOK, "application/config", cfg) - return -} - -func (s *Server) updateFormInSession(c *gin.Context, formData interface{}) error { - currentSession := s.getSessionData(c) - currentSession.FormData = formData - - if err := s.updateSessionData(c, currentSession); err != nil { - return err - } - - return nil -} - -func (s *Server) setNewUserFormInSession(c *gin.Context) (SessionData, error) { - currentSession := s.getSessionData(c) - // If session does not contain a user form ignore update - // If url contains a formerr parameter reset the form - if currentSession.FormData == nil || c.Query("formerr") == "" { - user, err := s.PrepareNewUser() - if err != nil { - return currentSession, err - } - currentSession.FormData = user - } - - if err := s.updateSessionData(c, currentSession); err != nil { - return currentSession, err - } - - return currentSession, nil -} - -func (s *Server) setFormInSession(c *gin.Context, formData interface{}) (SessionData, error) { - currentSession := s.getSessionData(c) - // If session does not contain a form ignore update - // If url contains a formerr parameter reset the form - if currentSession.FormData == nil || c.Query("formerr") == "" { - currentSession.FormData = formData - } - - if err := s.updateSessionData(c, currentSession); err != nil { - return currentSession, err - } - - return currentSession, nil -} diff --git a/internal/server/handlers_auth.go b/internal/server/handlers_auth.go index 65a951d..8684641 100644 --- a/internal/server/handlers_auth.go +++ b/internal/server/handlers_auth.go @@ -44,7 +44,7 @@ func (s *Server) PostLogin(c *gin.Context) { // Validate form input if strings.Trim(username, " ") == "" || strings.Trim(password, " ") == "" { - c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=missingdata") + c.Redirect(http.StatusSeeOther, "/auth/login?err=missingdata") return } @@ -55,12 +55,12 @@ func (s *Server) PostLogin(c *gin.Context) { // Check if user is in cache, avoid unnecessary ldap requests if !adminAuthenticated && !s.ldapUsers.UserExists(username) { - c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=authfail") + c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail") } // Check if username and password match if !adminAuthenticated && !s.ldapAuth.CheckLogin(username, password) { - c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=authfail") + c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail") return } @@ -96,7 +96,7 @@ func (s *Server) PostLogin(c *gin.Context) { } if err := s.updateSessionData(c, sessionData); err != nil { - s.HandleError(c, http.StatusInternalServerError, "login error", "failed to save session") + s.GetHandleError(c, http.StatusInternalServerError, "login error", "failed to save session") return } c.Redirect(http.StatusSeeOther, "/") @@ -106,13 +106,13 @@ func (s *Server) GetLogout(c *gin.Context) { currentSession := s.getSessionData(c) if !currentSession.LoggedIn { // Not logged in - c.Redirect(http.StatusSeeOther, s.config.LogoutRedirectPath) + c.Redirect(http.StatusSeeOther, "/") return } if err := s.destroySessionData(c); err != nil { - s.HandleError(c, http.StatusInternalServerError, "logout error", "failed to destroy session") + s.GetHandleError(c, http.StatusInternalServerError, "logout error", "failed to destroy session") return } - c.Redirect(http.StatusSeeOther, s.config.LogoutRedirectPath) + c.Redirect(http.StatusSeeOther, "/") } diff --git a/internal/server/handlers_common.go b/internal/server/handlers_common.go new file mode 100644 index 0000000..85b72af --- /dev/null +++ b/internal/server/handlers_common.go @@ -0,0 +1,188 @@ +package server + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" +) + +func (s *Server) GetHandleError(c *gin.Context, code int, message, details string) { + c.HTML(code, "error.html", gin.H{ + "Data": gin.H{ + "Code": strconv.Itoa(code), + "Message": message, + "Details": details, + }, + "Route": c.Request.URL.Path, + "Session": s.getSessionData(c), + "Static": s.getStaticData(), + }) +} + +func (s *Server) GetIndex(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", struct { + Route string + Alerts AlertData + Session SessionData + Static StaticData + Device Device + }{ + Route: c.Request.URL.Path, + Alerts: s.getAlertData(c), + Session: s.getSessionData(c), + Static: s.getStaticData(), + Device: s.users.GetDevice(), + }) +} + +func (s *Server) GetAdminIndex(c *gin.Context) { + currentSession := s.getSessionData(c) + + sort := c.Query("sort") + if sort != "" { + if currentSession.SortedBy != sort { + currentSession.SortedBy = sort + currentSession.SortDirection = "asc" + } else { + if currentSession.SortDirection == "asc" { + currentSession.SortDirection = "desc" + } else { + currentSession.SortDirection = "asc" + } + } + + if err := s.updateSessionData(c, currentSession); err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "sort error", "failed to save session") + return + } + c.Redirect(http.StatusSeeOther, "/admin") + return + } + + search, searching := c.GetQuery("search") + if searching { + currentSession.Search = search + + if err := s.updateSessionData(c, currentSession); err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "search error", "failed to save session") + return + } + c.Redirect(http.StatusSeeOther, "/admin") + return + } + + device := s.users.GetDevice() + users := s.users.GetFilteredAndSortedUsers(currentSession.SortedBy, currentSession.SortDirection, currentSession.Search) + + c.HTML(http.StatusOK, "admin_index.html", struct { + Route string + Alerts AlertData + Session SessionData + Static StaticData + Peers []User + TotalPeers int + Device Device + LdapDisabled bool + }{ + Route: c.Request.URL.Path, + Alerts: s.getAlertData(c), + Session: currentSession, + Static: s.getStaticData(), + Peers: users, + TotalPeers: len(s.users.GetAllUsers()), + Device: device, + LdapDisabled: s.ldapDisabled, + }) +} + +func (s *Server) GetUserIndex(c *gin.Context) { + currentSession := s.getSessionData(c) + + sort := c.Query("sort") + if sort != "" { + if currentSession.SortedBy != sort { + currentSession.SortedBy = sort + currentSession.SortDirection = "asc" + } else { + if currentSession.SortDirection == "asc" { + currentSession.SortDirection = "desc" + } else { + currentSession.SortDirection = "asc" + } + } + + if err := s.updateSessionData(c, currentSession); err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "sort error", "failed to save session") + return + } + c.Redirect(http.StatusSeeOther, "/admin") + return + } + + device := s.users.GetDevice() + users := s.users.GetSortedUsersForEmail(currentSession.SortedBy, currentSession.SortDirection, currentSession.Email) + + c.HTML(http.StatusOK, "user_index.html", struct { + Route string + Alerts AlertData + Session SessionData + Static StaticData + Peers []User + TotalPeers int + Device Device + }{ + Route: c.Request.URL.Path, + Alerts: s.getAlertData(c), + Session: currentSession, + Static: s.getStaticData(), + Peers: users, + TotalPeers: len(users), + Device: device, + }) +} + +func (s *Server) updateFormInSession(c *gin.Context, formData interface{}) error { + currentSession := s.getSessionData(c) + currentSession.FormData = formData + + if err := s.updateSessionData(c, currentSession); err != nil { + return err + } + + return nil +} + +func (s *Server) setNewUserFormInSession(c *gin.Context) (SessionData, error) { + currentSession := s.getSessionData(c) + // If session does not contain a user form ignore update + // If url contains a formerr parameter reset the form + if currentSession.FormData == nil || c.Query("formerr") == "" { + user, err := s.PrepareNewUser() + if err != nil { + return currentSession, err + } + currentSession.FormData = user + } + + if err := s.updateSessionData(c, currentSession); err != nil { + return currentSession, err + } + + return currentSession, nil +} + +func (s *Server) setFormInSession(c *gin.Context, formData interface{}) (SessionData, error) { + currentSession := s.getSessionData(c) + // If session does not contain a form ignore update + // If url contains a formerr parameter reset the form + if currentSession.FormData == nil || c.Query("formerr") == "" { + currentSession.FormData = formData + } + + if err := s.updateSessionData(c, currentSession); err != nil { + return currentSession, err + } + + return currentSession, nil +} diff --git a/internal/server/handlers_interface.go b/internal/server/handlers_interface.go new file mode 100644 index 0000000..860e87a --- /dev/null +++ b/internal/server/handlers_interface.go @@ -0,0 +1,94 @@ +package server + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/h44z/wg-portal/internal/common" +) + +func (s *Server) GetAdminEditInterface(c *gin.Context) { + device := s.users.GetDevice() + users := s.users.GetAllUsers() + + currentSession, err := s.setFormInSession(c, device) + if err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error()) + return + } + + c.HTML(http.StatusOK, "admin_edit_interface.html", struct { + Route string + Alerts AlertData + Session SessionData + Static StaticData + Peers []User + Device Device + }{ + Route: c.Request.URL.Path, + Alerts: s.getAlertData(c), + Session: currentSession, + Static: s.getStaticData(), + Peers: users, + Device: currentSession.FormData.(Device), + }) +} + +func (s *Server) PostAdminEditInterface(c *gin.Context) { + currentSession := s.getSessionData(c) + var formDevice Device + if currentSession.FormData != nil { + formDevice = currentSession.FormData.(Device) + } + if err := c.ShouldBind(&formDevice); err != nil { + _ = s.updateFormInSession(c, formDevice) + s.setAlert(c, "failed to bind form data: "+err.Error(), "danger") + c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=bind") + return + } + // Clean list input + formDevice.IPs = common.ParseStringList(formDevice.IPsStr) + formDevice.AllowedIPs = common.ParseStringList(formDevice.AllowedIPsStr) + formDevice.DNS = common.ParseStringList(formDevice.DNSStr) + formDevice.IPsStr = common.ListToString(formDevice.IPs) + formDevice.AllowedIPsStr = common.ListToString(formDevice.AllowedIPs) + formDevice.DNSStr = common.ListToString(formDevice.DNS) + + // Update WireGuard device + err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetDeviceConfig()) + if err != nil { + _ = s.updateFormInSession(c, formDevice) + s.setAlert(c, "failed to update device in WireGuard: "+err.Error(), "danger") + c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=wg") + return + } + + // Update in database + err = s.users.UpdateDevice(formDevice) + if err != nil { + _ = s.updateFormInSession(c, formDevice) + s.setAlert(c, "failed to update device in database: "+err.Error(), "danger") + c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update") + return + } + + s.setAlert(c, "changes applied successfully", "success") + c.Redirect(http.StatusSeeOther, "/admin/device/edit") +} + +func (s *Server) GetInterfaceConfig(c *gin.Context) { + device := s.users.GetDevice() + users := s.users.GetActiveUsers() + cfg, err := device.GetDeviceConfigFile(users) + if err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) + return + } + + filename := strings.ToLower(device.DeviceName) + ".conf" + + c.Header("Content-Disposition", "attachment; filename="+filename) + c.Data(http.StatusOK, "application/config", cfg) + return +} diff --git a/internal/server/handlers_peer.go b/internal/server/handlers_peer.go new file mode 100644 index 0000000..dc6e55b --- /dev/null +++ b/internal/server/handlers_peer.go @@ -0,0 +1,318 @@ +package server + +import ( + "bytes" + "net/http" + "net/url" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/h44z/wg-portal/internal/common" + "github.com/h44z/wg-portal/internal/ldap" + log "github.com/sirupsen/logrus" +) + +type LdapCreateForm struct { + Emails string `form:"email" binding:"required"` + Identifier string `form:"identifier" binding:"required,lte=20"` +} + +func (s *Server) GetAdminEditPeer(c *gin.Context) { + device := s.users.GetDevice() + user := s.users.GetUserByKey(c.Query("pkey")) + + currentSession, err := s.setFormInSession(c, user) + if err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error()) + return + } + + c.HTML(http.StatusOK, "admin_edit_client.html", struct { + Route string + Alerts AlertData + Session SessionData + Static StaticData + Peer User + Device Device + }{ + Route: c.Request.URL.Path, + Alerts: s.getAlertData(c), + Session: currentSession, + Static: s.getStaticData(), + Peer: currentSession.FormData.(User), + Device: device, + }) +} + +func (s *Server) PostAdminEditPeer(c *gin.Context) { + currentUser := s.users.GetUserByKey(c.Query("pkey")) + urlEncodedKey := url.QueryEscape(c.Query("pkey")) + + currentSession := s.getSessionData(c) + var formUser User + if currentSession.FormData != nil { + formUser = currentSession.FormData.(User) + } + if err := c.ShouldBind(&formUser); err != nil { + _ = s.updateFormInSession(c, formUser) + s.setAlert(c, "failed to bind form data: "+err.Error(), "danger") + c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=bind") + return + } + + // Clean list input + formUser.IPs = common.ParseStringList(formUser.IPsStr) + formUser.AllowedIPs = common.ParseStringList(formUser.AllowedIPsStr) + formUser.IPsStr = common.ListToString(formUser.IPs) + formUser.AllowedIPsStr = common.ListToString(formUser.AllowedIPs) + + disabled := c.PostForm("isdisabled") != "" + now := time.Now() + if disabled && currentUser.DeactivatedAt == nil { + formUser.DeactivatedAt = &now + } else if !disabled { + formUser.DeactivatedAt = nil + } + + // Update in database + if err := s.UpdateUser(formUser, now); err != nil { + _ = s.updateFormInSession(c, formUser) + s.setAlert(c, "failed to update user: "+err.Error(), "danger") + c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=update") + return + } + + s.setAlert(c, "changes applied successfully", "success") + c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey) +} + +func (s *Server) GetAdminCreatePeer(c *gin.Context) { + device := s.users.GetDevice() + + currentSession, err := s.setNewUserFormInSession(c) + if err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error()) + return + } + c.HTML(http.StatusOK, "admin_edit_client.html", struct { + Route string + Alerts AlertData + Session SessionData + Static StaticData + Peer User + Device Device + }{ + Route: c.Request.URL.Path, + Alerts: s.getAlertData(c), + Session: currentSession, + Static: s.getStaticData(), + Peer: currentSession.FormData.(User), + Device: device, + }) +} + +func (s *Server) PostAdminCreatePeer(c *gin.Context) { + currentSession := s.getSessionData(c) + var formUser User + if currentSession.FormData != nil { + formUser = currentSession.FormData.(User) + } + if err := c.ShouldBind(&formUser); err != nil { + _ = s.updateFormInSession(c, formUser) + s.setAlert(c, "failed to bind form data: "+err.Error(), "danger") + c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=bind") + return + } + + // Clean list input + formUser.IPs = common.ParseStringList(formUser.IPsStr) + formUser.AllowedIPs = common.ParseStringList(formUser.AllowedIPsStr) + formUser.IPsStr = common.ListToString(formUser.IPs) + formUser.AllowedIPsStr = common.ListToString(formUser.AllowedIPs) + + disabled := c.PostForm("isdisabled") != "" + now := time.Now() + if disabled { + formUser.DeactivatedAt = &now + } + + if err := s.CreateUser(formUser); err != nil { + _ = s.updateFormInSession(c, formUser) + s.setAlert(c, "failed to add user: "+err.Error(), "danger") + c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=create") + return + } + + s.setAlert(c, "client created successfully", "success") + c.Redirect(http.StatusSeeOther, "/admin") +} + +func (s *Server) GetAdminCreateLdapPeers(c *gin.Context) { + currentSession, err := s.setFormInSession(c, LdapCreateForm{Identifier: "Default"}) + if err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error()) + return + } + + c.HTML(http.StatusOK, "admin_create_clients.html", struct { + Route string + Alerts AlertData + Session SessionData + Static StaticData + Users []*ldap.UserCacheHolderEntry + FormData LdapCreateForm + Device Device + }{ + Route: c.Request.URL.Path, + Alerts: s.getAlertData(c), + Session: currentSession, + Static: s.getStaticData(), + Users: s.ldapUsers.GetSortedUsers("sn", "asc"), + FormData: currentSession.FormData.(LdapCreateForm), + Device: s.users.GetDevice(), + }) +} + +func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) { + currentSession := s.getSessionData(c) + var formData LdapCreateForm + if currentSession.FormData != nil { + formData = currentSession.FormData.(LdapCreateForm) + } + if err := c.ShouldBind(&formData); err != nil { + _ = s.updateFormInSession(c, formData) + s.setAlert(c, "failed to bind form data: "+err.Error(), "danger") + c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=bind") + return + } + + emails := common.ParseStringList(formData.Emails) + for i := range emails { + // TODO: also check email addr for validity? + if !strings.ContainsRune(emails[i], '@') || s.ldapUsers.GetUserDNByMail(emails[i]) == "" { + _ = s.updateFormInSession(c, formData) + s.setAlert(c, "invalid email address: "+emails[i], "danger") + c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=mail") + return + } + } + + log.Infof("creating %d ldap peers", len(emails)) + + for i := range emails { + if err := s.CreateUserByEmail(emails[i], formData.Identifier, false); err != nil { + _ = s.updateFormInSession(c, formData) + s.setAlert(c, "failed to add user: "+err.Error(), "danger") + c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create") + return + } + } + + s.setAlert(c, "client(s) created successfully", "success") + c.Redirect(http.StatusSeeOther, "/admin/peer/createldap") +} + +func (s *Server) GetAdminDeletePeer(c *gin.Context) { + currentUser := s.users.GetUserByKey(c.Query("pkey")) + if err := s.DeleteUser(currentUser); err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "Deletion error", err.Error()) + return + } + s.setAlert(c, "user deleted successfully", "success") + c.Redirect(http.StatusSeeOther, "/admin") +} + +func (s *Server) GetPeerQRCode(c *gin.Context) { + user := s.users.GetUserByKey(c.Query("pkey")) + currentSession := s.getSessionData(c) + if !currentSession.IsAdmin && user.Email != currentSession.Email { + s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") + return + } + + png, err := user.GetQRCode() + if err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "QRCode error", err.Error()) + return + } + c.Data(http.StatusOK, "image/png", png) + return +} + +func (s *Server) GetPeerConfig(c *gin.Context) { + user := s.users.GetUserByKey(c.Query("pkey")) + currentSession := s.getSessionData(c) + if !currentSession.IsAdmin && user.Email != currentSession.Email { + s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") + return + } + + cfg, err := user.GetClientConfigFile(s.users.GetDevice()) + if err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) + return + } + + c.Header("Content-Disposition", "attachment; filename="+user.GetConfigFileName()) + c.Data(http.StatusOK, "application/config", cfg) + return +} + +func (s *Server) GetPeerConfigMail(c *gin.Context) { + user := s.users.GetUserByKey(c.Query("pkey")) + currentSession := s.getSessionData(c) + if !currentSession.IsAdmin && user.Email != currentSession.Email { + s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") + return + } + + cfg, err := user.GetClientConfigFile(s.users.GetDevice()) + if err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) + return + } + png, err := user.GetQRCode() + if err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "QRCode error", err.Error()) + return + } + // Apply mail template + var tplBuff bytes.Buffer + if err := s.mailTpl.Execute(&tplBuff, struct { + Client User + QrcodePngName string + PortalUrl string + }{ + Client: user, + QrcodePngName: "wireguard-config.png", + PortalUrl: s.config.Core.ExternalUrl, + }); err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "Template error", err.Error()) + return + } + + // Send mail + attachments := []common.MailAttachment{ + { + Name: user.GetConfigFileName(), + ContentType: "application/config", + Data: bytes.NewReader(cfg), + }, + { + Name: "wireguard-config.png", + ContentType: "image/png", + Data: bytes.NewReader(png), + }, + } + + if err := common.SendEmailWithAttachments(s.config.Email, s.config.Core.MailFrom, "", "WireGuard VPN Configuration", + "Your mail client does not support HTML. Please find the configuration attached to this mail.", tplBuff.String(), + []string{user.Email}, attachments); err != nil { + s.GetHandleError(c, http.StatusInternalServerError, "Email error", err.Error()) + return + } + + s.setAlert(c, "mail sent successfully", "success") + c.Redirect(http.StatusSeeOther, "/admin") +} diff --git a/internal/server/routes.go b/internal/server/routes.go index 476d013..6da31d0 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -22,7 +22,7 @@ func SetupRoutes(s *Server) { admin.GET("/", s.GetAdminIndex) admin.GET("/device/edit", s.GetAdminEditInterface) admin.POST("/device/edit", s.PostAdminEditInterface) - admin.GET("/device/download", s.GetDeviceConfig) + admin.GET("/device/download", s.GetInterfaceConfig) admin.GET("/peer/edit", s.GetAdminEditPeer) admin.POST("/peer/edit", s.PostAdminEditPeer) admin.GET("/peer/create", s.GetAdminCreatePeer) @@ -30,16 +30,16 @@ func SetupRoutes(s *Server) { admin.GET("/peer/createldap", s.GetAdminCreateLdapPeers) admin.POST("/peer/createldap", s.PostAdminCreateLdapPeers) admin.GET("/peer/delete", s.GetAdminDeletePeer) - admin.GET("/peer/download", s.GetUserConfig) - admin.GET("/peer/email", s.GetUserConfigMail) + admin.GET("/peer/download", s.GetPeerConfig) + admin.GET("/peer/email", s.GetPeerConfigMail) // User routes user := s.server.Group("/user") user.Use(s.RequireAuthentication("")) // empty scope = all logged in users - user.GET("/qrcode", s.GetUserQRCode) + user.GET("/qrcode", s.GetPeerQRCode) user.GET("/profile", s.GetUserIndex) - user.GET("/download", s.GetUserConfig) - user.GET("/email", s.GetUserConfigMail) + user.GET("/download", s.GetPeerConfig) + user.GET("/email", s.GetPeerConfigMail) } func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc { @@ -49,7 +49,7 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc { if !session.LoggedIn { // Abort the request with the appropriate error code c.Abort() - c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=loginreq") + c.Redirect(http.StatusSeeOther, "/auth/login?err=loginreq") return } @@ -57,7 +57,7 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc { !s.ldapUsers.IsInGroup(session.UserName, scope) { // Abort the request with the appropriate error code c.Abort() - s.HandleError(c, http.StatusUnauthorized, "unauthorized", "not enough permissions") + s.GetHandleError(c, http.StatusUnauthorized, "unauthorized", "not enough permissions") return }