From 0bbfc7433dae688de74b05b62b062c8825d99e22 Mon Sep 17 00:00:00 2001 From: BaiJiangjie Date: Wed, 18 Apr 2018 12:48:07 +0800 Subject: [PATCH 1/2] =?UTF-8?q?[Feature]=20=E6=94=AF=E6=8C=81otp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/static/css/otp.css | 146 ++++++++++++++++++ apps/static/fonts/font_otp/iconfont.css | 25 +++ apps/static/fonts/font_otp/iconfont.eot | Bin 0 -> 2564 bytes apps/static/fonts/font_otp/iconfont.js | 1 + apps/static/fonts/font_otp/iconfont.svg | 45 ++++++ apps/static/fonts/font_otp/iconfont.ttf | Bin 0 -> 2396 bytes apps/static/fonts/font_otp/iconfont.woff | Bin 0 -> 1592 bytes apps/static/img/authenticator_android.png | Bin 0 -> 2443 bytes apps/static/img/authenticator_iphone.png | Bin 0 -> 191168 bytes apps/static/img/otp_auth.png | Bin 0 -> 3491 bytes apps/static/js/plugins/qrcode/qrcode.min.js | 1 + apps/users/api.py | 76 ++++++--- apps/users/forms.py | 12 ++ apps/users/models/user.py | 18 ++- apps/users/serializers.py | 5 +- apps/users/templates/users/_base_otp.html | 87 +++++++++++ apps/users/templates/users/login_otp.html | 87 +++++++++++ apps/users/templates/users/user_detail.html | 82 ++++++---- .../users/user_otp_authentication.html | 37 +++++ .../templates/users/user_otp_enable_bind.html | 54 +++++++ .../users/user_otp_enable_install_app.html | 32 ++++ .../users/user_password_authentication.html | 25 +++ apps/users/templates/users/user_profile.html | 32 +++- apps/users/urls/views_urls.py | 7 +- apps/users/utils.py | 47 +++++- apps/users/views/login.py | 73 ++++++--- apps/users/views/user.py | 131 +++++++++++++++- 27 files changed, 935 insertions(+), 88 deletions(-) create mode 100644 apps/static/css/otp.css create mode 100644 apps/static/fonts/font_otp/iconfont.css create mode 100644 apps/static/fonts/font_otp/iconfont.eot create mode 100644 apps/static/fonts/font_otp/iconfont.js create mode 100644 apps/static/fonts/font_otp/iconfont.svg create mode 100644 apps/static/fonts/font_otp/iconfont.ttf create mode 100644 apps/static/fonts/font_otp/iconfont.woff create mode 100644 apps/static/img/authenticator_android.png create mode 100644 apps/static/img/authenticator_iphone.png create mode 100644 apps/static/img/otp_auth.png create mode 100755 apps/static/js/plugins/qrcode/qrcode.min.js create mode 100644 apps/users/templates/users/_base_otp.html create mode 100644 apps/users/templates/users/login_otp.html create mode 100644 apps/users/templates/users/user_otp_authentication.html create mode 100644 apps/users/templates/users/user_otp_enable_bind.html create mode 100644 apps/users/templates/users/user_otp_enable_install_app.html create mode 100644 apps/users/templates/users/user_password_authentication.html diff --git a/apps/static/css/otp.css b/apps/static/css/otp.css new file mode 100644 index 000000000..4c9ed2606 --- /dev/null +++ b/apps/static/css/otp.css @@ -0,0 +1,146 @@ +/*公共样式*/ +*{ + margin:0; + padding: 0; + outline: none; +} +a{ + text-decoration: none; + color:black +} +li{ + list-style:none; +} +button{ + outline: none; +} +.red-fonts{ + color: #ed5565; + font-size: 15px; + text-align: center; +} + +/*header样式*/ +header{ + overflow:hidden ; + background: #dedede; + padding:15px 200px; +} +header .logo a{ + float:left; +} +header .logo a:nth-child(2){ + padding-top: 13px; +} +header div:nth-child(1){ + float:left; +} +header div:nth-child(2){ + float:right; + font-size: 12px; + padding-top: 20px; +} +header div:nth-child(2) a:hover{ + color:#1ab394; +} + +/*article样式*/ +article{ + padding-top: 50px; + padding:50px 370px +} +article ul{ + float: left; + position: relative; + left: 50%; + margin-bottom: 50px; +} +article ul li{ + float: left; + position: relative; + right: 50%; +} +article ul li span,article ul li i{ + display: block; + float: left; +} +article ul li span{ + width: 150px; + height: 4px; + margin: 15px 0; + background: black; +} +article ul li:last-child{ + padding-left: 2px; +} +.iconfont{ + font-size: 30px; + color: grey; +} +.back{ + margin-left:-15px; +} +.active{ + color:#1ab394; +} +.clearfix:after { + content:""; + height:0; + visibility:hidden; + display:block; + clear:both; +} +.verify{ + text-align: center; + font-size: 14px; + /*padding-left:70px;*/ + color: grey; +} +.verify span{ + color:red; +} +.line{ + width: 500px; + height:1px; + margin-left:100px; + margin-top:10px ; + background: grey; +} + +/*输入框样式*/ +.form-input{ + text-align: center; + margin: 20px auto; +} +.form-input input{ + width: 200px; + height: 30px; + padding-left: 10px; + outline: none; +} +/*身份验证*/ +/*安装应用*/ +.verify div{ + display: inline-block; +} +.verify div:nth-child(3){ + margin-left: 58px; +} +.next{ + margin: 20px auto; + display: block; + width: 214px; + line-height: 34px; + background: #1ab394; + text-align: center; + border-radius: 6px; + color: white; +} +/*绑定TOTP*/ + +/*版权信息*/ +footer{ + text-align:center; + font-size: 14px; + color: #1a1a1a; +} \ No newline at end of file diff --git a/apps/static/fonts/font_otp/iconfont.css b/apps/static/fonts/font_otp/iconfont.css new file mode 100644 index 000000000..bcc9331ef --- /dev/null +++ b/apps/static/fonts/font_otp/iconfont.css @@ -0,0 +1,25 @@ + +@font-face {font-family: "iconfont"; + src: url('iconfont.eot?t=1523776860888'); /* IE9*/ + src: url('iconfont.eot?t=1523776860888#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'), + url('iconfont.ttf?t=1523776860888') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ + url('iconfont.svg?t=1523776860888#iconfont') format('svg'); /* iOS 4.1- */ +} + +.iconfont { + font-family:"iconfont" !important; + font-size:16px; + font-style:normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-duigou:before { content: "\e632"; } + +.icon-step:before { content: "\e60e"; } + +.icon-step1:before { content: "\e60f"; } + +.icon-step2:before { content: "\e610"; } + diff --git a/apps/static/fonts/font_otp/iconfont.eot b/apps/static/fonts/font_otp/iconfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..41bc9e0c6e479b7be0806b4b5e06ffdb5673af15 GIT binary patch literal 2564 zcmc&$U2GFa5T3d7ogF(k@j14WK$5c!Hjv;X7ylqqoDd)(CE_Q62qX$Jj-3SgPqqVz zL?yqV^iOKk_exb2wGt8nQ7Tm`QfOcL&{ma7)mBjpty)>C3VrKCrE=-)-C02Dch26u zZ@!t`o!y(=9g`K{Tmt|C2khZM;59(Z3NkctLYJS`8ckyM?DgAE*&1qwc}PMYa*)>Q zGAZmZ2`QL`1;{`Gip02KnwTP#$eP*8&<`Q#BTc>bM6*F{PyrtL#->h>+)_V!Oypi- zM~+YRD${=*kx9Nv@^mtrD15_nUy%HW$|tj#rF8Ho#~J{Chi3R_E|r*ZI4U2L?JrcK zb3~XQ@XLhnQrSM2EiZchAO!dwRjrwPGQoX|RcgOVvOSwvET9WsA$=>!axRfg#ZRfP z0*LQX|Jg#mR8BrReHfsjne<1Q?|fyW0_RL50#IFBLQSSh)EHuKUq73@(Dc&ZVAE~C z|EKdDGt%E16%+MDqQ&@JdQT#p1c6y;tQF{+!iFa6dMk8758$EEiBr^4?^xGhW_~t7 zoFS2pX^|KtpLCXL{$E_75iH#6+*icDt-tY4P>|X_jTzEC_nb%{o`GRltSb8a$@dBw z7_(E)dv2APnl(b`;#IK;WHQ0Gg zeVKb7U0wcdP4PYIc+XIaB0DWt8YQVslHRh2f?(k|Qp{d<quoLOtL<*5)MjqpVrgyP;@{qaSAMs( zIvYCzztL3L?2X-Nqus`0%j^bE6k-8zqUh_z5XPeJF2p!`+#(0->O(<5@A^7=1wnn7 zR&O}N&ilONY75{!9L7MKOX4e}S%1b-y{6ufr2D#(tTc0d>`(d6`2!%qHX7R>@O$lO zc8IP{^u#e1Lf#u0aF`=8#{h01hw5jMG2{ew?Vf5fsh#Qv%WwX$%nu=sMbw9qb4?|e zNcW^~1i#!lI`rhn=f?=wA4Bx}f)V$C5{-nrF*2Y;!hWAQ5>?!;7BhL`YN<2!%;3f8 zaQs+&e08Ajo&50L!FSIu9hj)&${9sI-nTzo8px2ADq-RjxYT>{3^5jrZ@1og6=Me)7WpQ*YF9qgK* z-g>Z@N|aMG^2H^2KAF#@^SQFM1}0Op3zOyEKKs~gzge!InQXQiBOh{F4zt;x5*>oGppchk%7J7mMaprqghq{wC`EZt zhOi6;O?Qd.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}")}catch(e){console&&console.log(e)}}ready(appendSvg)})(window) \ No newline at end of file diff --git a/apps/static/fonts/font_otp/iconfont.svg b/apps/static/fonts/font_otp/iconfont.svg new file mode 100644 index 000000000..66b68a6a3 --- /dev/null +++ b/apps/static/fonts/font_otp/iconfont.svg @@ -0,0 +1,45 @@ + + + + + +Created by iconfont + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/static/fonts/font_otp/iconfont.ttf b/apps/static/fonts/font_otp/iconfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5be075c12eb07be88f28f3ffcdcc8539c415e952 GIT binary patch literal 2396 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 CFO}Z_ literal 0 HcmV?d00001 diff --git a/apps/static/img/authenticator_android.png b/apps/static/img/authenticator_android.png new file mode 100644 index 0000000000000000000000000000000000000000..cb357525d6bae2703b65ce994d75bf235a3c23a5 GIT binary patch literal 2443 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4mO}jWo=(60|RG}r;B4q#jUq@9rKPT2(TRZ z`+xaleV5e|<>sbAeHFZCrW8vYeD2Pn==CU>vqj*5kP}A&r(%ODnAy>O@K=r9*ZO$< zQxDVYepmbNdH&K;<*oCje{<{7yzeq`M;u@h+t9$Olfa0ZdCmLih6ZnLo8{cU(j3HI zDQcZ3paE*)>$xsb`ctOeTlckzEv%AA%fvQJ5;D)Ujh;7i%QerROn4oJaQUTn(>sf| z7G$4e5+cGwAbo3APhOhJr~A67SpM4CN?zjZ(Qmx^Y5Ej_W0Ko%s8{UCYfPO}2(qrn zm=73^(+ZCP1M!$7JJ77t7LP$;B$+O=;a$hqy2Y)xYsIS8PRhG7b44`1phozpYF$9o zq^i9O6mRV1H@_}VD2^Z+S=DYH+O5J^X}sq3d!u?EVd8Y%y~uny=*{xy6Skx+zhAs? z{;My>{q-G<_!9-xrLT7$&DpszMb=}&^TV^gEbMya$c*p9hNv z%)DVwl)eL1>euo;B46Z3=`Wg{FSY9BS{CA5S}ykMi}7T$MA_-noFaELX8v{NAugSz zc3#Ybk+6SI z-MaPZXVzL0m2vZ!Uq9P7OQmFTQsUXDzKETbb#QIojFI{k!ACxtl*se7T z=il6;vgYok$@cBDi@#e;Fq4fqFlqOJO)udla7SQ>os3P`yE%coyV8nr^0VtjJbtJD zss4=%JC8XH<7Vhqr~C)%@>a59 z-n;DVpFfBTmut-lYwus1G(|vn!gi6vhmM~xjVCS^P5HyPy*y`ZU9dWT{qFmq?1B{9 zIGLvZr+;D0*tybe9?W^F6Ja4UDII2pCp^tRxycftYwK$M{kTS3o=yI{SR(6*g-IEn z=PsZ3`L(xn0^`r~mWq);TP!(rPQsc7(_~ntr9cuoikN^?3fl(s_7JFPl-C{qr*N?= Ur>mdKI;Vst0JY&~aR2}S literal 0 HcmV?d00001 diff --git a/apps/static/img/authenticator_iphone.png b/apps/static/img/authenticator_iphone.png new file mode 100644 index 0000000000000000000000000000000000000000..fd5b4e8ebcdd7919fbd582293c716eeff57f1893 GIT binary patch literal 191168 zcmeEv2S8KT{{IbOC=OIm>kL)|M;#RhE`k%OTGXnvRf`rC5dm@Gzyxj8Ix1Fct)oVz zb<|es0Nioo1T9tsl<5{^$j7n|tje5pCc5{dtdi#c=mI=Q}<_b5E1b)ca_N z#}Gy!V3_srKSq7^VheE@;YFz!rRjaTPSH*d8Ki%LvKO%@}5- zA7ifl%*XI2e!Hle z?SR|9blU~DzogrEo_4dxj2;M|nFNod_Tp8RFSJ||E1t=Oj50>23d9^^M?*38KZ z$zQ7TKLIo6!Tlm%t>M45r_UZb8veAy`{o}s47Y2;_LaadKlX&}2C#j{53WeN{doGk zp<`hiKVRxIXCT~5rV+!K)t)kW&@k9;4%_bs&iokfBmUfVn%@u)*oJ#=HpXwR#~9d# z-!+>uJ7~0a|7HuO&Kdj({2Y#DwqWvX*rK0VJ8n9f}g{A zn%$c3Hx5TZUD&=q$Nv*N2R!ccMbii3&&g+I&l`>B49{H1Oq)FfuMs{YbO@LOTjaRH z-qZa&@bhpE!r^nLj~NQv@Vmk2)I5%^< z8IwQ4cN)&Yygf6V8ORJ`?3sD+{UsB`%wYWDvSYI0Z}eY=Fq4_tj4v~l>8SmQ?l-*1 zzXZa6KVcRzIZPmYW=_TL4IuwIm6;DeS5^F!{%%KR8u{1xpA-hRtw>pr2|A2WhUhjXNNWh&e;y{j{KguQk* zA7I`O_7D3uZ)Dz->1f^!j?~{=49D&cKh;0ic@S#ZPYpBtr0^Tz5n-(GobamflrV;| z7s`dlgg1nzaa()*A>`TkRvc|Kd2;@6Relwp(H(Cx`~;7QPj^n216`hVQEKSm zF1(M-z@S+RXZ!h1pKISAX6vc;9)UicKCtiF@FwCn)WKI_}x>otKWycw& zpNB@HIaT?yVJ8{p$XbT6POSV{+b?1M{D5JuZ1tHpdp_NtIsRe6STi;-QkpO=nbu5O zxT3DG!t`O>n8D0Pj3+aO`HcAj?(=kbPiHeBOenLIS;4G<)o~NEm60(K%s%FK<}ee( zoMO%~mznF#E#@yK1>TiM%nK%uDP|M`fxt>oM_?;xA`l7M2s#M53S0!Pg295}g3*G{ z1d{~5f!5YB^!B)X8!G6IZ!EwPk!BxR+L6RUt@Jvu3C^r+D)i$#;YhmVS z*4fO(tiRbXvoU59&3w&*%@&z`ZMM$rN3-2#znMjwoin>`_Lo_@*$cBGAtS6MY$R+Y z>?m{*4itI{KNn6D&K639tA#%ZcMA^+Pr#E-5T*;Wg=OZJ=Co|)fTJY ztmIaIT0OEVtx>Cns7BWsLu!0pBe2G@8b8$dwZ@qmf7Wmt&Cb_wHwrKUwdHf3AN|dURQf>?X$I$YQL&er;cMCw>qEKnOkRFo&9w#)Jd&V zTGzI2r@9_>r_`0!-Bvfc?w@sE+SIgZW7FT}3!6nYKiC|xiMPqFSF@gDy@B;6*OS!S zR_}Pd#Ck>bZR>wn-?M%|{k8Rft$(%t(*{-z92*R7Fr~rQ4I&y`Xpm(qv~6uW(ALNH zE8CxKFWEk_v$S)v^RSy?x5n;wyEwbNh7B5aYdEgqf`;1~o^F`lNZ81+kw>GMjlvrp zZgjU%dE*w1`!}B2cva(rjsI+1@?Nv|+}@k|-s<-v-}~#m@+S681~>6<@?DddCJ&mL zH*Mc^RMU{AJDOfk*GJfiuH=0z=9ws3DTtHqWU z7g}VuY}C@N<&2i!w>;DGnaEb;D)JM3FOrL%+c&iDZ$H!iNBfKRd99ka8ro`Jt6i<) zS}ERldVlQu%ifQC|3Pc()-J85wf?^K`PO+3B8L$Up$-QelH1g5hzwIhtndb15Wqb)@dtl8{Bqx+dJ(n+qty!Z}(HXTkXx-cW*zv{kHb; z9n3oP=-}7krw+G2u=t?Y2Z0~#{vfeq?T-CB&hL1jV@9V&orZT>)+wgb%gzp+Kkxiq z=h)8bF5SD#?DBJ$ln)zx_|b>UK0NVZLD%+OeY$SzddInr^AKmL^D*cAZXLQ!?Y5)a zz3#T%M|5A={d{*-kDfhd_c++&Sx<+alX`CNndoBYGSX$O%avXhy$1A>_Bz$8tap#z zb9*1|o!h5lAOAkT_IcXZsqfUjKljb*_kO?0{dVDJn9 zirXHyC;i*?pV9xf{x1h~88CZ5)PRzKy$3ECcy^F*ko%z3gW?7^7(90H*1@Sm-XG#S zg|cS93BT6p+)?Dxp~sOLvZKDs!p_OMTeZ5@_5y#4Um!%vJb z8}ZSIjU(=V+~(uJk7GO;PY=(Ho)1PkjSL=na+Kw$k)yVadOZ5W(V?SbKe7G9`;%Wk zDIPOmO!%0Tv2DlB8+(47&A18U_Kz#^8sxRXEB({XpDy|I`e#i)^ZV@B=T@JO`+U#m zMdJsL-!%TwgzghoOt?GIY2t#3SHEcb#mp~Gd)s(V_C7MnVv^USUneOidrsaxx#&xe zFSmc0=QGe}v(JkuZd1OW@@#6qsT-y~nbvRGhG|cI`}uD4eKuV@{fFtcrPAz7G33 zYq|UK11svTn7tx#WuKL@Z_K~(`zCIc^QtYY)T^hgzP6^znjh9E*ZQoz_U(t?{`jqC z-L!Ra;XT54thZbrxc=^U;_votsJ~&+hOCVvHb#GM|NZLk3paUhy81)6A7q=YH_zLg z{^Rf;k8N?-vVMzl>-4REZ5yyHa(nabtG1W?H07s1cew31C~F~GBP-wOxAWdE_g&Gu z9e4i_VHq($;_=VpevbXc<(K_?n(kS%N3}O_Z~DH``!4S9xqtt!&3_F)V0K{MfoH!> z`0dv3gML4Du;am9k&PnP{K5P&?~mt)CLOwac-Z0dM_i5^Jlf{y_9)w^)zO0Jh0%F2 z(__+)eRk~j@sEz5Khftz)X7dK_nvBXYTN0Cr^C-!pZWTXM!ra1bavL+>~p^7vd(*- zPr2~vg*z8VU5vXl?9!FXLoQ#4?H?<@((lTttG%xtzvgl+=6a9o(Kou^h`QPRW>j2{ zxajzv@yBlUx^?1q-`i*Y6#scHVNk;5J05qg|Ml@-x9^UK>=ilnbGE;1`|@b6Tkeg#PxBt-&(7BrtbNtw)!xEx zh3AV#7Nr&k6e~(rmo_QgSLRX{Tkci|F5;Kf$j#)114K4MFYM5So-~G_P7PjMWK-QO(w96hCP+s-Gd& zPhyy}!89ktPh$?cvm0*x*=Ru+{;B_^Im^_qAuNXf3EpSS>I;PR1)7VD2+rLC0u%Vs zeOO>7G`Fy{s!`Lr7JMM89%CjD3eC)g78aPu3zopYGv@Uz8npVbzoqS{$yVj*1p9y#Vx|?_}DLErE>*1rvPo6$|o?q~)u&B7Cw5%Mj zOTY-}WmR06!FAP#>oPMp7n)n)bqUNu;9FSV+@jTomJRxkvYH%h`+nD@HS7j_x9iZk znysBj=QaFt_HFA%4&5@^uLDlV+@x>6v3HX~m`Gc%!uP$;ypw16+m z8W3$+TGp_xLB6cDU$x0sUHr1C_|m{m1h5Ztb8{>Bf4y2YYt=LQOY;CSN;ge1Q(Gv2 zmq}Qk5i=^~?$u?&;-b=DvgFIs4u`Chv{5?W(J+UU9uXR5+L8^qS65_RR}A^ZKgj9j z%LYTY&7Iv_c_n!J+IlsZa`kGpn>^!bT8yvCE?yZiUBf7syDR%OE6&z1ySHeV?*l{{ zX8k^4xoWq3edp3eJ#&&Y%wT5?(>7VGVOovSFg8O}C&f?qNFOzG*Dw#;Giv$t$cMGm zEx%RFm);((VXpNE*DzPcNH5%$2gaATla&p*tzpWn!^Dxv9m>o!%*qJK16koG;(~AC zYzrES@(P1ej;L4rsz)7_st4?}c~Sh|d5Ye*OR^j04~vyHl}^bOh7K#;vR}i@T(I#| zb%J7I+QvHaEyYVddY&qNkn(eIr+Zn0%SQS(>E*CNP^7XOgcjvruclmpZ59Rf|;JIo?FZI2=)I+Mh#V2uhX*U$E+vWv}ay#buY?^c60H! zJ?>_DrcU_$YKOoL?m=7PoR@|%B76Q<vDggbo!m&10Jd#ocFK|T<7k+rLOaL%LI(b`?mL$#@rFWc~Kdu_ffAa zqCdPYb#9^bQ*As|C%<#%w7sfBnIrHVLnOm{sC|=)IviM_VL}4GyLi&)+s2RjZ=dpT zgJ)tPGitCMd`n%#aPMCzK2AOxE-M8m&Qa{x}++)lu*Z4gT zCb%oEFaGq=#KS|rEjSj}Wp+(-+~exD%x?L1QA%QQk?J=|Ey-QiUZs9{@v-82>Si!% zkGNj-Q*GIUw=N+d&_VU+(r+80pE=Ag+n(j}9wR8Q;@-CZy@}(ARk1a^>HeZ`#Zlo) z&om7ko*!QvyP_yOM;w(bo#qz}PrWeZz&9FZVUU0G1JOfopYdvPxo&@Z4dZ@C``km9 zDi$e6=ckBKd1dRfdc9{~ z4<^=z%Bo%iJRpluctDqBNqx?yHBdXwkfyYBxGO%YPK4)!XFK{tOnAxsXU&I2yWEzH zX>$2v4+oXh&BC;ktFKJgF#p#U$afcJJhZvjA#?LObvO0ZRLj!M`OcrH@8vs^B~I?x zOzNCnzUi-1(vvYmGbbLpyry7hT-VtiPA|%tw!2p|np-MmWSBU89|R{~%523iYiXFh zYuYQ+XBZ8$3?}NUt3x5mIo4UdXM5vzV6N$&SNt z(F~gB@cRjm`jRm&aUU}dp-bH?q~7wpVc}&TdajU`G}SN(mvW@`V^tCjbFP#4;U(85 z8s@&ghVg8CR8YbnMS01Tqf38D_gt20en;I#Ih=&aJtNA*{wE8j;ab~ue5~T++zsO% zID8{--`v*;`a=E8R+kEKmaVB_Xh3^N7tl@?lYsWF7~@$+N?wvBQu>nYS=_$VOzy2= zW{8vKWX0q}t8y$+y(QPbTop-Hre$(}(XupLnc(vBdKQaU z6iQ)bs<^e?Qb<6;zPHkfm1&`dxzu^}7v`lM(?@t_dSAThsSsUQB;HtjQts#<-Z{s% z4Gt6!3`N!y?7BRAmcU+kt1S!=2rmDSksQKbq)j1gbt!}oNg?vA)YkA)J0<2jtJCG> zzK0{jIuvP`;t17d*FKVDaf?$U6vNavTf$TPIyNI_t>ltw)32)Ncu$3A@sjPmm&BL% z*wyd!wVB6beh{Y)IvM?P!me>*cux)9<1m}jHnB>X_XV-yTj{eM#q!8x^(6V!czg)i z6Z2Cph>yy1I>SR4c&6EcJ~{H_6Yvmr5$e|B?kiZsIS)%^))6x+UuP~9I9Gym~< z%gN;`vO<2XVMvMFezt{R4C_At0xBvOru%>D_2a0pdtOiyMg8qGH-{$Q!+0eT5NF zp3`Mna;!E2N@>~bt{6hhkcT%(I6wm+jG!J!rZl-cq`!Gs=u(`xaIlxFwS#?lNmQN% znfJ_(!NmBY)K|jxPg^w1EN_)_lGoXWiY1CgkoiP~SDZj; zGlkEEMX|-cm2-4?!0((%TH^N4O}&xg(qG83VsWbGeh7TFqz7e7>pWF^Cch-hO2zm* z!J5Ea-Kuv{Vx)#oKqX0hWufRKxh`uV|1x)7Y7^BjFqKPdYnbyb@>0&F1*re}OOoQd zZ^<^rfVcqH6jd|N9hr|x?%y9QdYKt;GNwm8XPZRsTQbrrtwz3XMO5-5%EZ%`(><(Xy z+Z&I^%DRjPS&6z$6e2X`$e!&%_M#zOAt0gfCud#}N9lMHya%%ND zst76!a9zA+bK0C>JL$<)(J$}cbGyNa_a=8px8V$Do?Fep1PM#ma>&4I=I=eb)t4sV zm?-th-Ygk@7jE_A#S6tDv%X{9DhT!$8odU!e#kwif7{E$ja^{Y@}^@+FLa42Q9&ff zd6~TmEW8tQl?_Ge;h$vss?JFZ^QU@WX^3SGZ)Ha)l|Vk~YM$TxA z3wK~LG|?TZBt5;iSO$N$zQ4W5{-2Ej-$9`ICbK1!Eog+bI+R9OJ39O4mO~h5xXwOh zU~Uy^ZX2B>7= zadN2{nY-Bm_;jpvu^$p&-&&NSX;y8i9ba=_V2bd%PcT`^t+~tuD{*;?}zC9H$yC(ZEUuVE|;| zj?(*$Aqj6ZR=E_`U??B38akYM<=8w6I|0@>I87(cL7>9HkWk;C=VVx~^kzo6iIQ?n zXuoVMB9SCXCG51RMBgtG(+UN7R-DBbSM)Jbs79=kl|Yh_aA}j&ehlMP1c8|04(RIL zAPZLS44LY(yfA;Un@E~?MHCZ>?Sn?pKA4l=Ib+9ORe0tI?3h4=)RSar<1s|qFeoMB zyTr!>*FKyS=AKw&crI?DF!4WhmmwW9s9T0}s={7pAr0fa2$4I*Q{$7vX=UVs_=tN> z=gc5l8NsC7>)vm=%E--q33e0#Izk;;n?IDPP%AMmTX9f+X_9jLGjo^ zYw0n5?uI_0alzXGvs-B0cBdKS+h(S>NK_&SbxLJshit)+9wkw+rovX}H-8X&qNbKN)7>ePvO}3lqkd$$YnTYOQ*7lGR^> zcHKcK6uz)<2!*d6uomtPbEUb=c_}SO{w6I5z*Gj(HY^*?3s;Ybls|=R25XUe&w(T| zwy*<8+Bf|KWulD`WwphbdtN*i3}`Jf7D^kpc^)cc7z~7DP&37J5aP6Jafh&I`j4Do zVwmKP^zf6jvU?}fJy*&7q)8DM#Ca=2J@P{C$qz?)N2ksZO;g`GwbJvrh8c3l=~(oV z;=~5AiYwa!bFQznzED)oTTq6g*?;P!pTYZ2lF7gAq(_E9r3j)KgDx<)3V$Hox~(u* z!*IuO;Sv+NA{yqs*Oh4b@|-lD+3iY0Xorg+{b5a@-qJ*<4Y13>usXPj_*Nn^E0Z!% zWq|;OB9xTf)Bfefs@@vUA+1VJG)( zjKXde6m71iRkW+HYSREJ_(`%UC)Eje4)&EFoiQ%DK=Ly4xjr~Ub%rN0W+0-6u0%OJ z#~|7`&U4&S**7jrn{rfrBOwrWIlQESq#*MyYXZY8nGN6|b8`+L6`&jj?FwjX3PZb< zZpo5ize1EO^~c=>DC3Vji}9+jE=axG%WLL$5upd2*EV0)Umbn`5ghD57%I_pmOQ0l zGB=-3GYj?0bKDDk=xJL>spP6+;@+RSV_LcaoA;+?A53aJq-=ukd%euRdI}2#JX6-h zL5dix0S+m$o!|*nVkC`9ZPR0*5CdR?!gx{&3rXtO_Zi+%a`_n#^$`$maO23lS}T;4 zByd{Y>PwPkr=0ma=8}@cuy3Go8<>wZ32hV7=8C}?@lIvlaW*+G@?y@)EFcwcuIyIo zyGQC%8bq3saJ`k_hraqPbJdPSzV!eaF9gCc7(fPw9S?ZL-?(bfkf&!7rCiDvy4A9!TAzke24ildRk}@eM|y7%gFICfTO)1?>w%J>cmMc*>TXrrtzSQ|%2LL%} zTW9e?G657w5<^sb320_Z$1K%@(|2$J0Jv9HFBj&!0@Bmu$s$avG+*Hx*+&JXLT+2Q zTv$ATblxtdwK(Mrowq-+rSf&3jF_*^q^J&H=WV2-XYumw$xC9&d&v4-x}JOd*lO{E zizy+2Gwr8|5^{K!Z*3&YYLyu_yI?gJs8T~aVjoslvn0c==Wh$sra&JSS6uQOzGijD z%}9U*T~Ag`j?U;%WX>B8f$~N9s;z+Xf}W=nBCrxGddYzsK1%udyT*?og-SPbhZ)_V zr(iH_A0j?6jZ^0~t!<@TI+$trRD{b_ZkFe|Lb_b#gg5X9KtRX7PjPuYkE?Y?mR@sD zB<8C#*I|%DkSdT(?BNs~d{1xuo3!TU|e;NUQ@&D7X9$_vJ=0#6=mc0eRRg^2S##SFYI0 zGV1yWtY~~&aQyPDS^b58d0;oNy&U~YiraePcCp~%Mq1^q%paB!1eK))xu2B!OQIQfLJ%A?Kk_~wnqn62Mh?B$9 zJ#!-RoGzvvanHU@iX;xD1U}+*FR~kjkE9OVH$lVv{Jh@y6vylS3#X_aLY3sN>zklT z!kzj_kfC4E2Fx} zI!S8ldnk^N56t;}W$lQfas-V-qeAAbZB*>I7ABU;4aOP*bxnXjhyeXSFj^!Vf)wUc zC8Qc#ppGWP6pk(@>I9O$ka;BYIc6^>lvkYBu3c8M5UUJq#8YB!iD8a;%L%?K!_SXSA0ZyG=lWUR)!IN~`Ji_W+~(|(Z}^1o9d`ynrY+zMx6Xxit9 zk96+g`GBhK#HBA9cf3~wNHX;Wkb6U!CY^GG;6^Z1ackQ zAZK0Ape>h!mu_$qUv5assINgxm^3ZPNFh)U5GCisLxqWtq%GGz>)1uUl}Hz?yD6MW zvR#Xyoj=7~JLi>4T$ipxtzMJ^OTn-W11~MZy-SfVih15SS0>V}bbMY6pz%XP_CSUU z*%@N-apHb55Zm^Rk8Ld3Y@^@wFH`Z}B);)v(4Y5C%&`MBbaq2LAg28!r^b8`K&pWL z-}@5DfzbO#;~aOzC?y5KY%89w+S+%74g|AC(cvRSLkfa% znO93z_|t$|1O)Se?9`^{m&wQ6eszU24|P<6c$?{fw}C^| z48IWco(k|bcqm!1EL+hls#Z|pPp-8Vf1flb$WC^0Q%FE^(tv9)lGz1wH_?@bB*kOb z;1Rpau9x3}QOwgGiQ?^!kU(()1d5HJFxE4tGX#ph>Uz1WTc%V4t=D_T< zw%s9@f*cdjfyy6Aj`>5r`sIVxkYhIOeC;cjt5+RlV$u@euB`s0153vfj>RchE-NV( zuwGJr%I?M-dWse}YCs0`Clz8qr~_D87uoLP44}}Z`NIf8zZ8;%3V=T3Y`|TFB3$2g zoEjWMvjxQCR07@rQGCm_5oOl2cqXeL)+lpzl&|GET*lXfM^z(+RNFnLO+>@`AC%lo zE9*1r_-dtG3Jn5Ja#!0?I!ue_(h4>Vvc;f~SS1I(j zxlmmIisr4%iy?@E1-Zj30S8!uBQ(`PojEL3xGS6{7?7O!D(4X37z{C$RJ9oz%}d46 z;b&#F)qg^|1X*~-hou{{M3A)1crFh(m7q>g$rkpI1*pH>8y_@tTgTrs!%KoY<(%Bs z>9cRFUI_=|{J?5TL}HQ&#lDYAJ7SiZ321Yy_Gvjt#XB&0*HX9|kJfA6yFoUA!HiL% zj{qlEK>m3~QsiI!R0uF}!~lz85Y}rE(YiSg9&}L$All^pf{cX|>)NC=pDu4-JIl!d5}>v1@q7#>4@j>J zm(fH_ua-dszbqBfD+;be5IsOx0bIEWx;o!{LBN%U98yB>wBuc}-WW9k9Wr))z#H9< z5h#~ZmMJn#?To^u7h_KfKPTQ4A5%eWA@tFhoF;-Z7VAhzQFguz*!Xd`pO?xA*4ojwN5HCE&$l?;h#X zJD|sm{A-|`@0i=80?j`wAL-XAkAYM=WxdU-uec>K2DnE{nfZU;FdSrGD7KRDsn^)`xQ;77qR4^FDbg8hTN&R7OqcJ%|`A>ebi$@ z%5_oh`x8?_I*mL~&*qAC2gLJms+~ZFB+LOF8&XRbgw`1{2VikqwE^`h9(aIU!yxZX zb)7ec)dal;{Z(bu;3a^gBXrJgr2wP@1Stk0fFXmB8|j?UI-W=HJOe7%Kgd=E+M!9k z+W&RB1s?z;5da&8^kaYIZN5nlgb_vlWr$)WNHnbH0BrO7000B7KX6w3X5Ej`0_EA; zSOziRUtzt3UHQLdRgFt4GwKWAIA?Aq;8JZqixMp)pC#bZt0bRAY}WhAeZkweGecoI z=TMA+*VYj35&)yww}s#sjN(-dwc0t#1YXq}aeLt`5nI00wVN7sL>FXbwgFwE(Ulw-i)Kv?P8adjmQ(l$xw7vY9J}$+oXwk1Hn@rObh?y zfkQye2kVp^))pU1<8Sa+h9d=#kBA&MHt?xomB0L-n{>G~HVO1W4F;w>BKKYg(lrJ? zf@Njfn5Yr;rhJ6Iuv@lJuY1o3Um!Kh0K!py$g8_=F?r(cI1)VoV>=S|W(K;>Iv zoRExhV^F9`G=8EYp#+ta14v}0njG!N6ZtV`j{2AoAdzgzK*G4fIEyXvS5+?1J0pcv z`4j<{hS)4Z^e3Gqt@<3rW)Y(Q6pmb#%@vgG<1Tcs7pdWk8LEK+VT$sdD2@xv#(dOZ zSp{Sy#8jLH4xv#PuY{MsoC7cT2Y7L+9Uuh)N`WZ9lGl@BNKq96b#rKg54cBukczA0Pi^yrmdB8UCo$0@ zez=?Ot~j=N{)3yFKZq?`%3Z(sxGd?SC{gzDJg4GLrM8(t$==1&^Wv4Q(i()eDDIs7 zyH`%k)|E-}qn%qNyp-OKs`;{`Y)RC`L+S#@l#{bF&i|p3@Dzm@CB)(e?Q}%v>S=%y z=2CWvDMX{DZDvlCBnHkURf{G4jIX}az{-fpasKe!xB1sPk zk&4YgYzYwK6#}8qL#mXSD;dR-k0g>@dfd7Qt}KBN^nZ$I=Rzk5i`5;i{3UjXr~nYs zOM~a>s|-qAx|4#w0aggeyvBfyOd14aM6p{N3`r8Iw5k*@Hi4a{lK$Ty(@1@R_+b3} zOt{8|a4aj6XvC^>;nj8d@M!pNDXuN37L9sm)d5?VCsQ-g3?jkAk?Qg zEoUN5Jmtlz}m z2RL&83d0#hORwQvD*6FtpWrtTvtI$=Ko;J~g2|OU#JT#tObMy9b4f$4aB&9mgm7!e7;&}aXYC8LT=Iu5`%Q3b}85Z(JhLF6c-lx6h^#sS7H(4U*140U&rSB0!zX7Q+PU9@3u)bwG-G z(-qtSl$!=5KkCqbL}cyzvPoZd0KhTi4{;k$iJ(#mGbMzsrktEH2qU}$K(sj=f~w=> z7A$z85MA6V1|Ex!iqdowsjjoQDB|r++mcEYWNCVh6x1+R5}4+^HcRLbCWYN$K@9ZfAwQw?H!VjIYAibF3Ml*|y6W4HsKP&c z+>knxKGg&W)wnzKA4uwx<%y91^&ilIL2!4K59)LFYg7jm#yMpO5ZB|53&Nwo)W+y{ z6~;P(KMdbjidbjqI1}=msh}Q^$uMr{qxuCbU{0YZL8NsHpvEE8E`SgwDN}SYi!oVt zj#Ah;%C49kkUi=!Ibdl(DD~h(qNDii|D4P5!5yNb1oCcK%5>T!+8{m!UOaGIPg72< z_JJ1y`n!QY0%w5YO5$a5oR#d?PsCY{$QVS9CVQEK8eq~KLZAd(d|o^dD2J79)8gWT ziVq@zlHlSu&S;eOtaoT*f{Sk_4nffO9~s|E=E(xluDU-7u69?hQ>Lst-V2@o;4kwBu{Yj+LVI0 z*Nmqs3`CVRL0s|1#8D9jB7WRb|1e-(Q}rO^4d~D&ot%0W2ReqFFz$k2Qo|GaouEwk z)J(aUaBMR%XtuK;&rjQFs4ECuwh5 zl!|6LphSZrr|-sTih%QsFN6R#j|^7heXwx2vKv}eXX2pD_U{(8aR+! zPGFJ7DJ#_gkpM2Yf-CrRlmc7XD#_9=-ch0)_xu!SM&x7$B}EiZ&+iG%h&rc8GonNG zxJB5E_#op>C+VN_YIN%4z2xS_!;*q=DJN%SnC&a$Bb6XKj$8f!h2_x@TM~ukJL*cq z9}tbw8c|UxE4B}nJ%3PXQYD3v`bKd;K!%!+MDLiWc!h#Ge1(V?@V_9s??yaF+7O~S zs;W#+>##P<%6`2Y5r^k*G^@LuX#^2I65q}IWHi7>Gl-umjK-))BHL%ojnmE{PFXlL z8p=hXm^0PPix-(*J;X!;6sro?vr5AV86j2V%|a;@#qJSgW(K(dH75gfz0S~W z$6VCb>6U_8`Lha8TPNi)=#xxhtgie$-2~OjaV!DmSf^Kjay?Y9fMAYya4g~&Q4%=S zRfQH={gbhTpV@0FJ^-jVrYa6P2q8s6l7b;giB{9tf~VMH#A5${62}Z9ZYh;A;FxSZ zt_7Yf`_z3Ts0=j%^i6KIB>E<(3@tR`?jy48JHNylU*z5MK3Ft&hRtDM1t)6>6*iKMwn&!~{ ztEL(vOoSZp$JeYyG6+wv%>RlWQ9#lk%lufH0B|A-qsIoNxM#&w2&4a~?2#W%h0&9q5pd!WoicjN@e!MQ6o4{1fD^BO z-#!+Uo;el!w`EO5>IQxR& zFHtdpe&A&psF*W&=Uioi=cuY~8>!Oe7>- zpu#huGzSzdL4`*vRAeh!kxCRTL8z!7Dq0rLqC!Q*CG8GBPDP=j1}{^N&tCqilJD{2 zatuw7%*{s66J=(`wK??d(s3I#?O+svw@`&+s9Mgemjmu@ZH*VO3tOl%#yobBvGIp6 z{wcHjv@j!x5Yh5eQHNejzz5bGZH-g)L3U@H&(IXEkSEz8*kFSI7n)QObks}Ei@kbJP2HH``!eE%eD(Psd0r&J%dqTim*9>4RTi@clzM}3VHpYELg9d2SgD@yf>p&#ha;p8+3m+_DLK5x=Dhjh?zpT zGlHB5M!YW=(4yjvClT$SAgAXJ=>;m4#;LF7@d&t%2XXli5uqJ;3?lu zRyG8R(q-0ikY2$EhwVtoZ!YwvHC^eCO?e^+qL;7dI7O_s!SDy<;n8XvOliRbqQdbm zr{6~)O+Mroe^M&>c~y*i>&P1`n_M}^gr_{`aE3^E_%(?l&;SfUOcWNe`U|M=Xf@;d zfw?;3@4#|e`6DKy)CMTSLI1!YLS^6pHI~0kNd)%{9sp16oZpNMou!TK9ZK9xLTo`} zzXGgZB(AhVDwuo>T?sx%2ad1xb>+m{MO@z^Kv8jI75KpBgk^;{T}QG4M|&a%58~Za z7W28+IF2r67QaC%X3yyH(^T7GMG@(sPa%2sdolt=qzNT77jFo0|LSGOSct-37i@Slmw)B=&COAErAQ4g)*-{+;1#@u9bpjML`{3TZc=Sr?Xn4UZt!hK zrsWPhEPSzvmNHUP(OWd3I|=|*d+7{u(wub9?1|{fa8#asJ404F@Qg!gpKR}>&m&WU zy4xI)PXA)y-uoIRVutPg@vaL0RX(bRBVy)1y0CmQIx-j_AyjI<{E(Ljhd@sh=0#AZ z5R`ehKn1UYG`f+Z(aHM%vp%0nIhn6kkKSG!SP5~}WAmaIKmz~*%p~Z_BBEcEt&?>U zp{x@c4C)l5`bd(+=s%=$(TL56St|kmA?z}HDm;sqY}dMIoW3^mcnrE|oQ%$yxoaxG zgxSqntZW(PgM9XoQAAJejNI6ab&lLaWi0LQ0X<#BOTO1Fnf@z9qb7@IC+u zs7MLy!()l+ExFUNzlh?h=LpI@IW|2e$47^9*MTw+jZ|qg_xV+jZF3LW66d@$3{=|; zW3zBheU0yZdks|)lW&fdAdE3%<)>cXr=WslS)!130d!t>y( z8kv*{GdB}PZhk7ZCZ>Vj6a?}(BavaeU^GVYtWo@=a3oqG5IY3I<~Zj~=@&Xz5?x@U z)4na=L|sYPgi+F=(pSmn5y^KGeIo`-7WBo`nG=Jl3S|ZWkROdPE2FhF``PZ$SR~+K znPTM!Dy_id9}mu7;;8VYX8=rxSa0p1h{MK(>$Z8s8A#t~vW(wx9n>lQ6TXGc*lRnd zBoeP*&<-Q$dTREo0wprf-u~!v9YBaG3)=+zIW*^0ugkN#Fv5rA)#N7lMk|bqF!^F9 zM(@nCl?y+BayJNuOD9Dd9TZ2RzyT+#F~eRNQS>zl_5J^im-xtxZ&>7hSH?>uxgv&W zn9}N^wF;XXYGM{Knnr7E7c=4u4_K?!zUom?;pzdP(**D3KWle|n(D?`mZ@_)@YDwP zK{un&W;tbvQJW(zp%Gw?RF^md>C$XjVp(Du+!VG=9AiMeeGU6yz4-8x9CxbAk|6^q z^kxEuMqL&VN9d64MRi#m?{t*^Ik&VE(PbGec?nQx|H&8HmXO7q4UX^v)lUO##7HYx zb4;fEL5f4f0FdNVODFD!EOG~^6d?x_{PQL(luo}xh_a`D(|1KemXio{6gjjhn2I5} z!8MTfNwo|_3Q)!zk2XCz`sn*AW?BDTe1(3-n4eX;RY16kF$#AfFrov2wZDHeVTY?r zrXy?d<*L>HAm3Ez^%Lb;(?^dHyv%n%FpAtMo*JJtCo3jqKr1qhT;) zxz^*5hPgr+e>_8sgYtk>@LxC)yfJ?gVd=}{gyNy|d5B$66i$5}0?@6T$iGf>Z=bWF zr}q}i;P2M=cU{T_i;zs7Bj2E&B!81%+84@~ z@btl;cK}24hYYlw>WXPdr`$Kkt1|g2^^5WH5-sKBUgYP@9OQyjZ z{pRu*iKKQw@hDh3s06M7QVKkgz^fXQuRRaO$0|-P-j7Mknu1-IXU`IV;4!VqlSJ8A z*U7$=6UCkxu&!&sVfH!;4$fl99VuFaqUPKx`Lyt)2(ShvnsWf_IttdHepGXA<;dgU z4SL6(XwDT}Ra}J{@`26m_ZD#d3wZ1c(8r8B$`AyvFFo>hIGrZbB~cndN#I*rWdw@f z5+j+LxJ}ANM{(>X0n{NjAfWT>ta#fS3n>$yyvBny0^k!*AmmS@9kw+NfFX-9&3i&c z!GKu7B_ut#I8cw@iUohT1U!D}f*+*w_#Hu){1mkKT>wiy%wCVL|KJ00 zD7zF&&~vQv8Tb|}m#SbfM<=X9A!lW_5^%yg;toz&bxSv<=N^(yodHHzcU*6(b}g(c zo1^}ApJ(t)Mb5!C;U(Yoc#yKWZo`!ixs_aGJ#Y~KTs2py`Y1ZV3PHU-nqZ;LPf9#2 z(Cs5Y{TCM6sM|+?Yl^Cw=MLibaY^(t6Fe+>OgK^x-9F%vvU@Y8Hgm&lz&RMKdGVE& zb5RKZ!Ylo6wZG%!B-+9$YT|J4l-VU@@HvJ0(Gnz}qfvJO)T;m;P4pekQ*<w;Trq@a(Shi}n1F4t2Q4?g)(@IF8}?oOF=NE4>A4{44+t~~ zY{65S1UCPv|6C{i-hY@)B=RhHrcf{qI+C_p+h_#4d616P_8HOY0cxZuLSju;>3!&D zyry7hT-VtiPVt6aMmG_#dD*GI)SGrHaMV;|c^bXvxP0|UlgZ%PaYV+FQCTGZqLJ$l z9)x~(FtrcDh;A40X>LKe1F8$`Hq}!bBL70r#uf4}6m3l9Untsm4wZjtM9{_{|I+?t zO4Q-y--k)u-VNE-+Z*0!LcA|Nq!S;*H5t^4lNTn82l1?gKq5XC3%N_TpiP&9zgj<3 ze49HsUQ1&{4=D&}@|@5Cok%@BligD{gTf7oXs~S`F@i=k`h9y80Bi_-KD5jN%mk2r zwLaJt?cATxSYt^K>Vu8W8sLNN`(7{eWr6~Ws!^Yzm&!4jD*{k@Ef5AmPg=`5m0(b14%)~Lja|>j=Axq$=S1vjoVHe-nUnA%2>pQHq_zJarxvlIh`1i zt|2l5^Qelbn&eS5s{V}TQM#a-X+k^%R3QxF*d|qwR!9n3@-6vj`s+^G%UL@F8=lG-tRrpt<;_uN{ zdoXyQ0D%xg$&M<^0Ei8BeHH3c5PSe-17scY9JQ1U1P~C)Mt3xQBLWP4H$*>km|wO% z%jG==e2$QAAb7pOgegjF=)%hDO9$(UAqR`$t*e3B5A>3OT$Oe>MB7WQ)UJzLL7<$p znaol=1LZhfBE}M)js%VQBHZ7%;=>_T)*<6ceF5f;4{ek9f5b@btesaH$w>7iV>x}R zqi@Mbl^mKKNE?>tA`xN+vebtFhYJuhs=JREL`0YY8f+U0XHF0}*l1KXY5Zv5+K1_8 zZqWZC=nG=KO!`<0z^&Xb-9iTBtKVwaH^4dz^4FIVT?jQs!v@xCctOt; zM1aFwoh-&FTPMH~o9Ve$LIpTp5opvytpLaA!!x5}P=EuVQ4@}j6~mOxQZmHJjE8TM z$Fa9HO~{r(Q3bfXOPyDLVP4uXeS|06Q-Il`RuItFsXLo(8?<58H)pG|xm<2}2LK)z z(zi!(YvK$~QTzZbv?So54&W652QdJLvIhhlTK{itz(M@KA3?xzFz2LKr?H+;_TZ>? z{x`GL#HHhr01Oc3Jpdo{-A~b#fWHL-FybJ6V>XtA)YLEDm0P^d+b${B1Z7-hI*D9> zC*UY8`wS-gvL491BDtU;b)M%%=2SR%l>1DspWB0s>~84Q?l#Hxmal$322C6btj?zq zm_$O>Y3Gc+7sSmeXcJM-(D%k1jZk{o;+{n+233y?Db>`|cM&9gsP=#fS*6?qQVF2f zQHW|^T6e`07&`>YyVlNXZNJP-T+jbq+`xaHyu%l%LBSgXE<#tvfQwixU}{KIQco64 ztL&=G-7xNf!#DEw&3&Di28{Sfa#g1wg}UnOQa4asAOfqQAp{Cf^;KI!o~qSP&O6PK zPnO65Nx>=RL46nEkRWfetRZsxNhVkTp`8o>r$MJ0mo);Mj>6I1coYJs6Cd~AK4nV1 zp;r9JX`U#oTnMJGn_a0elry9ff2v6;fx=J#*@aX>D-4yqB&NKFtRE4ES}lHX5mJen z_R|k4T- z0CjBfKKFeZ04lcUNj)G-A0z@nfo$6>?LA22pN1ws{yE0Z266m|8%*i@0}+uTE50jXu4o z01BWBSNSSg2{bGd`fb8T2B=Rx2^#r!k@1GO4q6;4yCD}D)&cl(#v9_=3l;sU$ZG$u ziW7`S?lG^*jY6KIG&zwc*WAKRA(61xF+mw_{Qy7!+*umJoYyQ%uLb;9976AqZ)bcqvEw;3TxCWDF0s>NiTabDq2<7u|k2&3~GVXIEsH-_)7cXCxf0J)T^KN?dQCR=IwtfX)}_?A%11osx}y#c5BILyL>A9!^eet6@S=XXo&vA2m#u?mX)-$T+hr z)~AGTR+^6d%aFZB`lzgn;erwJfRbB6i!rQzA5;kpd??=$>G4fZ5rumQnFE9aY2^iZ zK&0}5aF;m0J-mR)$Y@eyd)9ndw99SD zm?oD$_HfWJ?~Ubqb0CT4>)LYZi(nVfu)+_65XE#n$@Cy%UV&zMT5tm;ey2XyM>n7y z2A5OB4ZWeanHNKx?P@Imxg9VgJe!7Cc5p<#MV`0i=;iX$sWz)AH?Vp*RAYZij{& zW+b%$F{$4qwR99W>d;49^r|g;(1a`@08O+m{ifgAQHl6o$B~dP8E}*h)!Mxo@Q9Ie zm~F37oIn{o@MZYc6=d+*D2LzCFgm0Wb+#lR8J4kSs2XCLx5>359IJQXnvDKoCg}yJ z&hP|jsO+t6H}WJfH0?0N{xK%wWdJV2O>Kdd#9CubavrEq4sn z)M_YPf{Pt%c>)MGRJ+rG&V_dY)Oil&$6xSVA?c-#%}Wg@OkKDO*wWobyr9^o6Hm)| zvTYu2HWrqQck&oTJ+^=zV_=2M?*QLYiZsHduH8y)NqcZOd60kzOdtg3zE`>YW`c!B z+&%F9uf-h#{ux<{d6Z-P5&1^JOz>+N-U^685?@=k2~i1}oS+ z_(g<(n*u?QxbbB-q)GqxiuypI*l7cmC8GskP{AW6lEkZER5zzGU{yG0ZL8}irMkedtP8Hf%X1#89-P-V zBW@`*!YDT>*AxMeA`(AypmIvJPfP@ODVurwqO*9l-%}`XaDK3+7b@)z_q^$eYCb#Bq@IsYuLAYfIdEw`{ zv{D)^Cwzt!7m>%wq8%{CGn642VFL>yNEyy4kus!wPeav{*L{_zm-HIH-zdmFP^kw6 zPN(C!A_Zb)0tr_sh(`UvOBz8JQ=ReSfK zyX+Pml9%ov0u_>7-zH7WZJ!#no+Hz8x!MOd)k;c6PB7NDD8pC`80wA!gDl zS`ZK%QM8~SIHG8=06}m$uUz7~bRB91sRF5r{)a9*A+P~mWV3jNn6gD`t8a>q$_Y#p z$Rg2z-+TtJ3Cb4fu`58maql_87I}YT$!x%Bj<4Iq9U4IF;VlPrB9vJ4)s8WS03wg! zu!kCzA=?pRUI=PRPzjICP9qTm*aKkLMCAMy$w*pVI3Br(2D8PaaZT?bZ2dnq%d4-^?4ayrw1<3g`oR4SNPZCW6 z&>ZvKXJ`Z8E#&}Bc=Z+JrS5mAXbq}x(YY8@xB%{`HT;FKy_af5?6x}=Uh+dRODaNE z#W(3l+H!~;QSyySEWTXqTZkzRJ!Fnbm!!`#F~n#^_6 zFmSoNv_+uKAXx;%+p}A>WD$&RDOrSYI=gZyor->cp9Q|ddHmn)qFq(5&Kr%hk1INb z&R4BD(`P}2c~p&%3qe*(ViaCr9<^gvaZpaJURv|0HJj7s1lxgmRP@XH_uOuXy%B59 z;w5vGD5c(VNRjpBO|;sorb^eV-S{A-x2$+DrPB$kfa@t;Wg;!8Vj(wx(vB*)0Ynpv zas#xQSXyoX(Zm`I+yD?ri*DXE9OgU&4mzN14M9wxLMio3L+dI~ExBE4TVdenC~gqy zQ`|ZGcT_ErCQFaxwxR}(9dkBLjXDJej$n;{)20yy8i*jn`I>Z_7hJ|uogQg*)&vZVm^Vo@jBB~L z5Py)mLzTs;@yX#-W$}XehELoysY50A0G zG3z@$EScv{)M*9y^@pC?F?cBl;w%WBB&k#l2}PHotfQ4f!a5XnG`T~}&DoYFaW)*} zEqyfr{x&Z@CrQH$iq|k5dW%7%;UCQ~csf>4zQKmo>k{q+m6B&s|9hkb)laIfLMh@m zV2==S;vOq{%e0F{x(1LOQAMWTF@U~wN52)QOut2x2)$vl$LO&ffq?h@HO%mms65`# zS|`NHGrM@vsGHUM@@8mbnhoU#POP$(vLhB{APgr$Ezn6Ijyh>mPErAB(jsB$N4v_& z;MMPi8)He!*LeI#>7qO%%=<=Tuq_*Cq(p}xMd+v zh_b#LAWf*Z@=Eabwe@N+>fE-7_An4%C(DDr#yvF-lzCr_S1i*o&vsk`mKYE?5GYHl zM3q2ULg1i4SuKG>3zQ`U4g|`cjD9)u_%v6oPZ5Vhx(|8UMIclJX((`>bP61phiVl# zNaLASLWAca{iKCtic8#(8s<{4jH#|CAoZ13hnn(h2#Y3mL4n_Aau<{lKpT`Jn?+o2 z*t(s-oy*(|#YhnK1w*;KgiPOcQj{+R$%Lco8wm&|zOlK!x*+wgzI-V)lrvjRKw5eLO<6Hxg}Ytq!#o6~PvwriC|sW-a<-8Fu;JmcqxI`I?R;3~*{Lpre2z(7nzJ zLUYvKwP!J2+}oxOC!J23idRn`ixLU37|N#pMXK9Dh(hc$XGrU~Ixcvk@VT%EK{VRt z8|~V#dvEIjMOS6F8ZGARHLyf=yri9k%K5flt*X}6v`tPJXdL3px8N6I1|0>t{k~+bHGOVZQ1KRH>sdc{Id*alvu`3Sa}^ zana2mMwr@Kcnl?KW3}t*3KZugZ}gw~@T*r^nl=yW7b@YACU_YzHrd5|mI*+9G+5ck z7Ft1F2xF+D608eNSN)=d7>ZiILR|=BC}Jw!E2>sd;ZLr$i1n+T?Bu49faIhB*WASR zZE-r_!p96=FRqC4TeDn0N9>U?R?`y9q2j88kD*$8rTj)RTOFi`u>*h;vUS+CEh%y{ zh<>-Lxz^B9xBgMWIvJsCzPkA9-lVl*FjZlLMSn@-V!B_%s~b_-5%4r7Hkhb70uZ~; z7bc)q?3R}RXS$#-bKer!K3J?b$y*6>d=%YZ9STi&5Z&L=`8AGg#vzTNZk`cDwEQ4s zK%^z;sFPa;84GyOl8~XeK=Ly4Im@e+U-Fz=@*+YW*JaerKu=kV_!&@TEv=t{R#}Vq z84LzxEs(J9QO}vNc~)E{#+sO~eT}(hY9n zxH=498!iecdBo)b6|T<(A&Qy?ro=N$mYjnmQS>g@^w&k{shB9V%=#v+!**XQ^LmUp zXde`q*nJAjfgyV&a8=?E8Y|Y>3WII|L93!Fd9Xi%RUe%3ZF1Stg9$^(a)s@L0Am4m zp(Zg$>LMn>Z`A1ou{qi)PD-63sMAR@fv)a*H1Y6|7q9H&x^}JMAk4R1jp#L4G3QGu z^eG&nPbOgi+^nwPl7*cYP65*U8!_=>p=K6ZFP2KF5NHn~Y-;~R5?y|VJc@4%j$fWN ztG^IfcAWk+LR`q`u`Au8VP<)&`hEr&TJPH>kQfp=FLa5UE4!8YQa-N;fm!MsL{IPe zxEPS0%Dk)Z!m6G%9jGO5dzp{|0u>qKDB-2E3qgT~V(>vDjB(H~D3?{WI!%jLHzo2C@FfXu#0Em8MUSR#~C&IiSID%7p0{CHqCxxSo zxAz5#BQL#{ZxQ9y*Qs_42(6X<2&P`7R+f294lZYJ#q;4cQNdAUAepEp;P(wf;X5lS@0PNIS_2?uUC_>^^3p>+r97C;m2p>F+HNqK&f}Bf>dx36N98fSUE2B=d=BVKru@FCeqc3e16-|^fKfbQ?T$!F<~3HBin!&+*P}#K83oo%|Jkv zZeBc67AOuSwgQL3Zrp+-)IVuN`bL~5w40@dn~>BJzu9W$QA_Lr>Sl@|`x8qPLFFo| z8c}0|`ZC1nC@5t{ntpyV%wB=MtJTfkLgIA&!no^FVEsb+4=Ek{8Dk`?Z!<3i$1+2? z1H>J&-Q*`KpE@|c!B^Zqu4YZ3WBn%?hw=x zi)JMPK$&45tLi*X;356tMf_TUfQFVlTEkXg;)h6v_W(RxQ3ry35Bv_%`Hdfey4u4H zp83o904~ONQI?qTP9h|s&bxS3Mor77XS56FtojWGv$ZsDl z%OSAyQRmOZfhUyKghFe+PE07SgZVmmLUkH>KqTX6OrmrQPp}YYLxR}5Vhdoe zX@l5G*;i=72lka8^@|youYQ^O+++OQ4eu-W1#jQZ3`GDMQT?+f^YM5irQypV@JD9~ zwpkrdUH07(9;#bfys8Y~H(`$PfKVVPWJ?4AduPmRyMKJzsTAMBQ_lvl91lkQf0hCB z-}pj20SG!(ai$dU?qcHaiOsy(gqEzIJYI{(TvjPjrsrIJ7e0>Bzo zeFS{$&^Ofk2Dxt@?o3q$LQF;@*^RW?843|AQ5(}ntVC=KBi3DLszZ3-E7!G2X&#v7 zyf#bd5GH|s4ObPxkbJ~PCPf6?n-CStRUAkSQR@2-7%n3XyZNRE19T8oW%(yveO1yo zlbm?LCsl?lCbkg8889mXseDs#TGR$fB?2T3Dh*)ui8$^7P@PD!QmrnuksR_1F&@g9 zvMR>?{hrsJG&y(9+$ItNRu*%d-t!&cJ&wu}AdW{~S5zj1!dRaQ)EJ#_*`P6R5~XmN zzHSz#+0!hM{;S?Ycx>#a_BwSSL!vzw?)ciFasX3Q3W(FGs8B>4F>2QOa!_I_+n7U{ zew)IKO#wU_;Tk^+1U*bJDso4kBE}0%8=>TCyh(@@5MQL{X*eT-0fT8EGBNB?mu%vn zLKIQwiO7gK8G@rg@09YZv~s5e9EAYXg&R<0+QrYS@)>sX_Ua1bkd1>KrP@OsQ zo3tPRYEhi*@C#{P42rcuOhzz~!%0jA#6S>h!xn*Z0{xrawpo?U-J#0aRNfFM$xSP@hT*aiU=GO33m6&g?xEof{}0%8c@ zA)p}>RHUF7gi=H(gHi!s)RaSr$RQ|a6as=q5=kJOAt47s&NKPeeLs7`-urnXvnL5^ zzy9I%e(!g^Kr*xUb6EFUzlGOFKO&TKB{eq2Gn$Pt^Z=5$=ym_fRTnB|Hs8v1vyfkI zx2-BSl3&3wCGJ}lDW&(Km^cIwkO1-?=khNDkN1k8f+_ctHfoSUsGs4R}SYZP3YEqhZ*%!Hexzj+|<+?`v;+-?ipBi^>Ux-KlwgX$I+ ziriPqV3sBb;9OQ#17>NGxY>2BTYy=Pa{yyR*1ZIhi{^7v5uVJ>7!#C`8 zm$%8tkhsnwN2}$?iDiTsE#L;ujF%bb!~pCk^qf9>BvS8wLn>{-Co5D>)PH{x$(hRJ zufMrkI?gt$wk>({Hcz-VZCL8LJ3hm4{kl-J+r*9(RS}c;CZ^u!H2-jpb zzg*NVve_KYRg|bP$4?Ypum4zgJb2A*7wH8>22}}_Cv=yVc;L~t`4Wg&1~1Zj;d)xVg{fN1v|9Wf>hWuw6!`lYx(%W{i%+$6?)n<*cfOe(Le;f-Zr zG;up5l*1#a0Hqg2`;S=Sb^S+*(1alJ^`Z(vi`N&$lebya>~&^qGf7tnO+~f6v;E9~ zs=q3W+DGyAzz;e~^8j2BZ~`BQNJu@pXJEco{+x<)F2wK#cz!PN0fz4B@wbl8Ka`UD zuOluH4bMa7ZGqnwc^ZxN^3h)?RAhj?IOfY9F_K5+Wl^%JG}K|dD9Arzuh0%f9Sk2j zK?L)X17V&akatJ&o}`1j*nxWnM$a_#)h--}MekzQXc%~k2fGNWb0}hZ7)HD|SA{a3 zA5;s1>I^5vER;WIeUs$GMrgCrNR~#FLjAQUtAc@4%vq2E&*ZvTHexJx(oh|5_{bCY z5*(HRlKJ}LUE2hAtHG8P!SG%lRW?lykT`I<0YCC@E{z|#Ju^#cI9iq0fo;Yh13OL`y91K|g*}p9m3;V{NEh(8`$D$t%j3_+wyLq486w zPz)$(OEUy?KpZ6gP*aEn{*jNtB=b*^x2Zk|_A}`^Mg1LSgxJBi7l-d!4&Fb*SD_R6 zwfXIYUz;1nmD6S3(!54Q zVqBAK&s|fP2|#ULP7IuaBiJc;)j9^$4$fQsG68CH5AQqc+?(7a@9e|R>{_?nQv|2r zxMNM1mkT*cpKRCkJ>%6e9YVd|vO1c58p%9y@yrq^&idMr5FfIau#TpnEZr%MljuD` z8(aCbB^6KsbE^#~U_O*0xZmB<=p^rfd$!j}63g!)X4=7{`dOI+Yp9%|^Q4aA-`D(# zilnDXUCXcV%erOr+`g1VqKkUBMg*b>$jIkUN<9Ns)KE9oL~Ow*gJ&wzXwt{_4+2+scfMey-;;H_)zUer9dRk(9%jA-XVxGs8vrxxREz_YwxU z%i0JSsPMqUmNX-b2xml+5TRrc&S+vG2xm01yzf@(r7=t_ZNW+MKGuhZ6{M&Ee(;az zVb!)7O9S&O>bW01SWb5Q*9FB@56;ox?>iBFfPkzla$z1-*ddWypzac7pG^ChV1QHz zYF70(wG7o1?}gUi`-@BpTZwVjbp*13%To-?%?NyGT29kSc$WT%N_PISSg@7k>CfX+ zvGNo1L|Ac_#MKtbE?}76U9!nhIo)|Grz9Z^>kU3Y^MA@2@=x%%OFx&-3E+nP4Rk-3 zm2bw%vvdjm3Ey90{SPCtyi$|615Hu(q5EUrM=Y?~I-{FtLDWGa3O=GP)Yp-n00c{d z3~^fG&BqPXTJyZeTD4}j)ZdJ)K8~ArvD4QS?@}wN$k|QiW3oPG7lr~d2K=+!`gSq( z#|X966YDiJ+(VGM!v;Tt;{$pxF*REhuO~;F<~&K%cnH;wlFrw~LA86ZZ0<+ZHsaor zFwb4U3tFWlmcC@D0xm}qhK+o6hJDvo<4hbL4)lS;b0NcFn0+>tQkhs9sb z8;_2JWot)&x3l%b6_ARg2~!nrOVSN^VXzbm+8vyChFkn_#bDSK39&JKxUA@3gr}R{ zol$4K3}v$mYV+1h?%_xw+!&{Cw*3YpTE@YBPsE)n6q(+N)wDw?&bTo2-1l2m< zsiuDWb8?%GNWJaKBOTk%j8DajL+{N{kKUd%KB#Vt-pFMLq#Fa#gt9dallk~=C_Z0C z9o`l3FkezEZ*}5E*Oi`c@Z+OL^yg~-UVdymaw*>L?NOLm=So&D_^eS z9?1iDZ*#Qpv=v2?qMPC9Cek7wN=}(o8r#Ikd3QjI%v|Z27UdbZ%IUd>itCII1)y6t zU!!uW^?d^A?D!s+m+d>$KjJWGUcaX0JcgEY?tn9*$*A&6ElBSHnh3YzK!*y}} z_BjaX>Ky59c-4w}v-AvjHP4mAgNyNk@!}|#3v;lRs#VR)wCsN21y2F45QK358zdY{ zlm)ZW(Iy=B!eslln9M#$Qr>V5Pt1tioL%s4RrkWgOlR^7Ny{HUg(^8u+RAwYuP zcPM2||2f?Y`&OH?0o=r^sC zG*_UQ$))8D)cfQ9Q<75Zo1O0SqBe-l`j)X1Tdz?h*)fZX^y@ibh=nPy7>|eeq844= zR2Ihh(L#Jn;GVG7V)wZIl&fa*z^3>_Z7sxh94Jjc5rw5yI{+QPtb*NZF7JUAVGyRD zN)iE#LorjGEz$AHpj@Cd$bCsv@;{xX)AvFBEk!2vCmK}(tPBf-XN0we>z#E@{>97M zvCav*EEOq`&e=7lLW0PqJ=dP9o6WhcnJpm*>$J zoSy#NSnGSzTG1ayKo&9L%L3Q_LboXlHh3bVai5vWxb1`b>5~0`GTIu54xtyNV@g;LkJfIwZzZC z&Z}`e!Ox+sSMYPl!=!b`8ca-mw&Pgsb9ybFS1{dpw6@exRBAIV0tT_;4cC=bs725c zuA*soL7UHJmCi91?WVIYMCw-2n$WDxtoe76eHw_m8@jL}otUzjv+mB2LEJcp4IOue z4$dFiRNEq252~6ZjIP!}!RIRIIzU6L3sv559GS4(lJyCC+AV<%G3jCrLMG_{>?@L` z6&#)vxHipiv^+|+6zbjDhE1+~eTv9I?^qx!FT&(1ZcoDGs^j;*Q`DFaG{!RsT%wnv zH#(4^LU+e+$tIisgPw1wEAaKZ52x@b^#9ze(|8m*U+Y5K@>Twt&!`J6Gj{ZIA;aqy zBBuA`Vg^X;ZUP(cZ{&7a`4{X|ZAmRFnp2hgt=Oo_{>jBmzkgkCpadPtdzXYMsHRgJ z)=R?c;1WbNITS(rDc+dZXOPv%PoQA3a=MFJd+Pp zvHM?24w{7=&Zj~L(g;^10|~+v2Qud7adoJ^E(AJ`@$3eDN)`(1`wN#3hfe~=AyDWV zjf1LB(Jq8BiMPyNn5tu5Gj?Ya_@A|-)48tYQpR>0qVNZ{QH36b0!;AW>h!%w_de$E@)3sW$$a!P6+HXei5k;7dDml+JyI3IES<3rTU+hcN>9Rwtkc{*<}#88a-LYe_06J%yMI5fZ+isGBt9J6R*JC)O1rW1 znQa6R2x#6eU#Dmq^D3P&_eQ`>=pSDpf!vC|G&VbuG7xP2zLVAUr{kFl0(nbI0@;;h^`sUMwLc`? zpJ9omW5#XN*FAe*ksrB*m!WScDnZl(_B%vEQezScMtaY5Q=`o|RJutigzDf5>NRCYJVSv7>UwkkNU| zUT?Yxcst1}A|i~`g(KRhNkh9(Kh2{@ts-~vL8&7*s3J_O!uc=Bb4#eANVL#}M4Ad{ zsent+bbVLb0`9M;v3o7)c8&zI6Uxie*Q`*UeO}*@Hv0It&UiEvOo2E&Yv$Iu?DKWL zi(FH0B@{SrsYbxk1Q@S=T>#8B&r_plnvX-PD^mDF7nDFGH$sc++CO0nPCxA{!+^yE zxYVXQNKC-&0B2=#<X=>mIHOX%%LmgMGT#d{wmB1r*+fbbiSZ+` zkWgdef+eCCI5c-oe82B%Vq;_8*}3-V>FLM8vVXdH#P(JJ^3exx-nEhbDEq~jr>Zgz z*kJ*2rt=xVO*^~ee74=3dZMAE;%G{bntobA!&>6BQRP5kny_PjU)X_r$x!S5C!7z% zJ{(1Wr_RHszO=1f%GXDFZrl;l+~REst_uavtCKefE|s>edh0?()aKd2gUwkRq&(y+ly+5G z3U-!`+X9^NQb6q$XF07t4NiGOf3o_#)bJgPW+@(R9I7zrFn$Pyy<*X!mFH3A6%)07eJq z%<}|RQ^Ly~qW~cw>-6u3Y^z-Rg1($IES&G6Fi}42D4Gt$+chGXj1SYivY6He1b|7L zGY&+^a)U;4a?T|u!a2uP1nV;sZ9o%Km13~8k)disP!>QtyN6v7pIFZ)}MFm~TS{`;CSX45nut(L6*)8I( zFF{d3r`*BsO*xRds%wY+om0Pgp{z@1*Tm$_55=E-G;{5!zizq(E47?$S3hNSX|DA> zzoA&Lv1kzF!|Ey-?s)6Ax;2~ntN+)=yr@MeN}|i2Kro`g9xfc1QAkV_W)$kh?3Ko5 z6n!lw00I2V84ft|YnqNd+`neb5IAs zi!Qr&L%Z>JlV?3C^SSfEE1e@A-?b#uxoG6MABJwIocH%d?jQQ6ymb7_%l}Ghf%(Oz zSft{}Fo>rUOurVx2@QTtAAN^WRM^scoonr94;cEW_fSbY{Re%FxAcilfh=463Tnl$ zC2A_+`6?VuKI`h4n+xwWduffsbNEqGVKOm%gP4}{U)@PkD`~4Go+Rzt`-mqgAb-O) zzqbvj?#{hVMg!F?nxhXNF`QGN5tC?tu|YkaS`a5JmJUxkQLH}g;htr54DSBi!9DSAl1+)kTKvtHRozLSI<>@0b>Q;@+6!6I~A_lWQbc-=AJ> z>*+8Kyx`JRNkhN@n!d+m_AWMyi>=`d12svILR4mHO~U&>{nT6~Y_luuhB#`=P=!ZG zIzg!~n@J@9d3*>~u6m&y0|akA#Gr&H+0J|0-m;cCG1%KiU~dy8JY91?p}mcw_4zmM z@8bOC#Va~<3D1i7qCp2!o;|knqpAdX%!?zG)U*|uVEXKnPFDIe__bAd6rsXpKTWp0 zP7&dB%MoF3AWr{3Eyji^UZ>vuy$kAvv5SAfWK)38N-o#;1B~t;vGNb_Z;x~gsoxWw z8YOD+D;eGmMHMctWpAd%-b{hfP_Oc6@8U69E5Aj9e@vGD0V|);&n(b-XB>jAi;A^ zlcngcs_kRvzt#K-_Xfqcq^T?j8ZqtTv8vVu9Vn~<+K0D#&GR*vR`n}w5nq(^odHiy zNHZd}+#gzPyCuU>ZM%_U1li`8u*5$c1Rf_J`7RD4uPsu`GA0F}3zrBuHkuK>FUVR> z^H^;@@q2kZakMEfV!rPxY{H`H9bq!=EJJ}Gs&ojGfx#2p1At;*H~SMh-uER;#+h?B zZXI`f_)XZ zmX|E|ikVa&#KX4SUESF!D8$UC(R_4M%GW2_Zro^-X~KQXYVd#{{K(AndkN(2yT{k? z{F)?gied8hpDM3ez~t>aHJ;xq|2Xh`4DtM8qcaZNvlHG@qx?<>FX@sHfMJCh(m#sB z5}>IQ*pd?SFXM;{uqDbb7QDbJ3-RUa*(r22CvnCBx_8rm)~?;+~Z(Cw0Bu zv9a@?I@%(5yT;>cOw?)a77)FWTD1UQTmsExoMTcd&+eIwMI?#Q{kS+GJIVhOy zK6E9$@Q@$$5q(90NVt85gD`PIr>()n37r;qSt^ueo%Tk7i95?WEiiE%GS`kf{N;c6 zE^7p6_`EBL>#XI`qv!Tod^di&{*<@lrGav>0nhRDtt1pM?Cl4JV_^6VlL}J!1Ta3I ziI3s{KoL7OCyJ1t!JCLD*W3xpL>WfV~{$kRKHDBj`V>0O2s(}=$k68WPicpRbU zI>1A#n2bT7RGfYx9!ORqZZLpN(;tgaaJY21sqK)aTqb?q+@zH}3B|GgbYs^TDSHEI zM;w;cVA_dMbYf0_ie!Vsg5cfXxrn1Z4a{CFpK;{QYeRkoZC5^b5ye6o^JfmJRVqWZ z?OKttG74j>0ze}53(Qcrv&Vsa{}|#lS2S%_>Dk3#lC}yUu()5kvposN4POoVeyUY? z3#|7M$4!#Q1B%f+8C&^f(ajCbqd!!M$N?MP#QZQ6*x|lr?VmSKR02F!g?cM`)?JfQ z2^~S8bxjQanlMX7Exb5b@&u&km&!pwRhGm&YQ(;-ilkYmsykz6%am-YbfD8KPcm?N zy)ekY={4dmvUQ8-!FY0Y=bh`h`}W^I5R`UC^7CMHWVS(w>o^YSbE|E~qWU2Fn8DGY z84l}?2N*>KQp0D@w}0PUc_W!*M?@ZDA#`LUly=IC)77q^$L7?j3ohptGB(5K^%hq< zb`YKYx4zW&WQ8`Z8iO#a2;#s^Np)n7Kqn<}h{lv%vVd+JuKvu5X*1AZ=rLz;5chz; zs@`XGKE8w$B&U8{FU1*=0B8rir?Z!vXQeIlV3c^RHj#Wh(R zha9ySQ6VqW1r(&>%$i~0=lcBKL@6Mfth9s_W6ENlj4GzN%34=~w#G3*8zi@@!a4|m zHX=&e_-Aq#^Yq7l@R-sFd1*tzW%pVwX_(-~h)Q|eIcaYu3<4slEnHNTWfF5y=G_{` zqIMVzO>bBq`Eo;_k|Br2ZESj2b8>AJx}$)hMxZA-@rxM2p&k#!#HN=Cr3=Iay{)3j z+4!>NmMBhvng+_!D*-!rx(HxL&2X_7C{`2pWR|<4CZBEZKDKFFi@djc#LV3~cGApn zjfa8rbO&eo<5(ZdyC=#sXF2&sVC%X#OWg4?ka1x!VtnQ*1zcav(|Y*FIEHjFhaVVK zw-8P-Go2R%R6c1kAFT*N=A$8gMKsHN`YC=#sEIg2lfRs%|DDNSj+FE+6g1*HLX1l* z2_KpAsMHb;f4TVwL#$q8RLsz0()lKX3Dkt7&Zy0)Z@lbcVF3+t!a>Qzd82`{V2U)N zR2q@|7}u%~dI2O%lOOFm*o`$zAAuJIrjICx6aOSIedIRwmU&klg3|S=Q~zLb)?byb zb@s{jue)2*1TfyNv1da2qvBh%7iiDCC|@-_^m{G!S{F>Ad=Ql4@%dUlh=gL!2T_Pk zK(7rcUqM5V#%v7Kxis%5)kgVYA_QbZvb6o^ll03D@G{#h*W2u8ueQw^1u%1hLqT_` z?d<65C8q34#yo!KXuPfo^Gj*EbJa-z>J-9wtX+Nfxi!N>>|bBSCp;D-bX`8U$VxMzjgDs>2cAC^k^4kJ$IaQRQgipR zNw9s{-MNtm{*sg#a|f9OpS*X;57o9s&$j$wNLb~lsl(hy;SxNyY4TG66>gu~-a>GY zvrLDJvO^6vq zML>Hu3~syi>OX`eIQ#6^1{bwB&<~#2(v@?10`;lI^r$uBE_PAgyxF-fWjwhkrx$&; z`}T27f-uKWy(7}6pkPy49|mF?XmQh1yWNmsAtIE;$rfN;#(*wmpgI8u^Qt!f%Nuqm z=ED3Bs4MT#X<~Pd0T`!Fd(Yb_^!o+Q%||Od0_@bU1TthgNTt{^h?s{0l?%W;VpW$c z_>cyQ4A3qexOwfc>1~c7u5)ze13`t86$53yrApZ6;^4`tBH6p`Wjf3PSx4t2fvkIo z-1p6>0`Sd`bOUwjd8L#+He94%T_6!vzFKWNwP5E4TT*Um&Kw9`9pgp{xONl?T_`Gn znICQl%6ESGPaMCqPU|C4a1SUR9mQyho5#4BQ&azm-O+w8(G4-=IXxIKu^V6E)+;?4 zcOt6CS`p#lWsK74_Li}YZJcFjLZPbxyta;&-Doq{Vynrto4=hMIzKAVX0xHhURD** zjwiV-2qkBA;uvDb$g%z_Ln$L`U&H1ppIK#He#~9(rRShcTg>A z8u!_}s5Rrp%_KBX=IGk}ouj-vjweHyg}x5@5qK*Cg-$r*6a8yu;=y|SwC&}_s`WPe zJhH#klmwKNa1L}sXzZ#p;E8Lg=&!jcNgs-C9(?#}pK7lmuNaf|KmkMRd)ASWDE#=F zvsd=X-J265{P?uh4*vdQ;m6;!bZE8h-=~`oB}4wyr`<=|N;TTT# zck_!O;6u6==piN@(@v^gcFjF?2eyj+hagzSi89~Px$D3^Yu!~B$_}t2p+L4f@}B2A zEgKI!=ZlG+{y+{&MOx*rx)DKq*70`s^4M)LH5sg{%c2kEiwxGv{b9S@??-7Ftg+)N z&yKzs8LZU@GY4OBO`kNg>#detwtVEJDN!9CW8AN?2fpkP=?9Jj9&vH{0b^toc8T-@ za}Kh;)5F4ToptH0l4Gx=ytFoE29srAmmVeSH;n zd5xy+#c2MED>R;DnzmkBqR}lDUcqoViBffbczkbv=1b7to=mu)$T_y#T6cp9%c6brb(D@Dviso|S%scQyw5kU$1hJI^O{`RlJd-9=^A0kqMp)`gg+Xr?y}xzb zn1EVZi;L+ZWDKyvNK%Eq%&jhwK0NL8wxX7Y4#7x)>3m=6Lwq4OOpz5kb40VBQONWRpc+ z+Y5z7UTlWKBF}V!tnUoeP2t-z0wG|-0l9>GME*=?FR~hVRZr0)@)14ZJT%+#v0(!e z8fL431c0RBQZtSJ2nQML)R+ti?5ddIeM8pz-&8FfJAc0I0b6lX3(vyOZ#`hgzx+AE zmMsmpFpR_{c}472mzYN4j!frTvT+D-Mz5|4ZU$@>wPa^gW1xuCheZMDNzkMG8NklK z?x0LbEy`!V{;Ul(rJJN*x5v-`t>4hEKca47k+fX@v5wo25KuW$UMJxWw-cUH*^61n1$0V?%7lYD}>X#CZ|>W`TfQ$oc=Q+>rZP}=M7ymh=kK?C}$uY zn&iU~0gqT@HDf`{E+8v33>Lx7%5{CRN9dX1Gpj0dUBAyws=^r{eXcl+`V1FPXZYlA zW}*`Y6JZ(1*tRhnrF@!afacNO5z=+hOuRJAkVtjX`Y6Ka0xvTvb4x+$93tpLZ^vI3 z7fSarz=!A{w)>3>h?^ToXs4pni7f)~PEx_5Gjl z{;mynv~JLUK)o`X`K`o#^8OgA-t$9El?mB~`A)q*h|eFm_eZyyg=Tk9zJxsc(Kb^> zs!(Y|uL)tg73yFEUdU7(UNujCJze^K^xvrYGPJMJ>i-^C{jFx7=BO57JK($2vRW7= zbg6omn(amA(^#9Tpl$u!dK$OeGZN7*(-Q46O$vLUU8dW3YL}TBc6DW&l1I@lvy0kg z+RQjr>NuJ4dTHyl4H-F)VY*1|GGh)u79VaNj8TgXggiY<3|26;U*Er4KAK>0Lz@_2 z!Hq?z40dDn@XmwA%p@~K`tf_;I{H*X9~YcNPG2*9zvybmL6{KNF$;D4a@oo(4|;{s z#;44*G(@4AL)04K!CNG1jS73@?!|*QPpdV;gEzaRlc+TsbGnV=)LpN);aa1|9TPf5 zA50!I>5({kEMf|b;m z#a)Vj+0kkA%sw%5cZW_&uwlw+;hiJx$k6$zj;x=ww-Qk;F&h_2Evn$HdYAA*JZL_6 z6TnH=b=~<|&mBA3qsDWZ&H8$QSg-NN;bv>PQy6o%!e7gYK+tUtyD}n6kWkm?HXyfA zB9Q4e9rop=?p;-$$8?*4p0hU}OnLs9uYL!>seGmwfhXHEJ|R&K$pNam(;8y6t~=U( zaH%-oiALEX2> z_U7&g{b;;5t6uO<_ak6uB7mK>2J5f~m_}1|OsPRh%b6pX$ORpiTtFhzJW89e@XXXW zU@31T?woY*q$8tm_%3pGtk+y3)bt~Y!??sOH20s{Q?K=28z1|<=M}DebHnFC$1`a( zi}kn9yF5T)_gED`sRYUIL5cuSsVyBm2x6zd`^R5boKGpP{xw&RXYE*NqodxJO( zjHOlzv`6x7S_E+x>DQAJK5dTa2?%YkI3Z?NQ&UGe4=mc`02dsyD{#S+BUfjM+0~SY zHPyC1?(fL6s}|cTx6d1$_tli9o6c7#69hiSY#l4J{UYA0E&+U4PC5|;*C+1)*~y74 zJ!o418Mw9T?QWSvz;75kN^gE}_W4$JIXz-C9~o)tWQ@Z%496rs8{aySHSnz?ekBG2 z95cYT4z-gojas8~-Y^4=zM^)>AGJhT29zk;O%?_#97eK{q73U9tz#HI=$B#EBF%Yg z+@TdOs$rk4Wn+_vT5r932e;mey+gF#av0+;hLq3l zs|3W}I?O(ZrnEes*M_tU=8B$O{EG`-5Uvk;J;xN1=_B(vI3!Qc2tN0kf-9{qVe^kr z2%ip&c!0Q2`^#=LiuwbHESKYVoY$(~c0`Jf$I~2yj#%D0C@)Z(%Cz`|CF-E)g!T9A zPV=LJE^F?Xf1Ma|V?)%XjQPYS#wr>@Qk$y??;&?{Rm|qf3}ZPL{yJ8H6D?kuTQqg9ih(n(}Lt@#WCGijnpeJNpUK!4e2FYy$c%i{EmA* zjL@G;6@#t6$g$!#Pu6}vlO=f%`dQG_g6)d1S+wEeCNFIBwBHJB7HJ`7vSWYdce8&3X!ih1m970 zcniA(&E7l`v-~e*f88-Hl3B;XoTH(Rf$3WC0_(RA{vG=A=pua@&XrDY92owlE2jL) zAxC} zxK>K??IQ%Yq)>?&qAAOSHfm?3SJ*7?o5a^}JGwNCwH^p@;E~wU72o~xGE@~uXn|WI zq@XpO5~>7Pm`gB?^&gYx?vLd|qprR4ve$|lbKaF2S0J>4QduJrV0j{nAd}elk<)D7 zXeP1iDT0*g@iAwj;L0^b!!7$Pa5X==B}}BtB3grQFc_!6`o(`zBeEhq%SmCYeSx+> z03A@3PV0#8tBzkqcPc_Gp1Wj)T4ffi2%yBZZgH_35dTO7u<>}s9^F)v&pSoU0DI#f zX}k5$ecLBYaJL$anF8SxL=hJ?`VGewoys;46Vm|a5xgBuzR<}{6Bg5Wj`@5PwuRvepP*Ou z6d$4Bd+vVk0*ChoJ(jt=Q9@I?Zhha0+iC$cx%@(1ErFgyo>uFYHUw%bpxBvyFuD!- z_5k2b2a+O1bcfnod3#5Xkeb5NBO7_J1wM z&BC-3*LRpt)&TIql4;vo-LuWp{3N-{P_AO+G0-Vm7vcnR&fJ5Zbi5gRckhnhl70QU z3*Ozk52w7gL3nr522}PanLJF&_m=5NBM7}i8%2j( z3(`DCA}hwP2Q;xLsAMR|7q;M#8jASBHy(;F+?^Bg+J;Q`GKw$o5_mLYa!SRYU5&S_ z%wPT4w1|T{u>?NX^O+koM;2Ma7CVo%gGun{Z8ilDa#=;ZNc#HOwit1g%LSxdsRb*) z&s}N|JNQI)&{Yh_OVLW0@{6KP>KEr!eEZmuH^9Mg8k`5>bnLo70Hrh%ba;AKx&88$ zj|@4$rlL_!@&;qYPVxtUmk^v=qKe0wGc9CSl{MDXNJ?r`yB|4NYQR1H;@geiCcr(< z6+@)2wnZJ-q@jBTBK@`HeYoJxV|vs)&I+GAcc4g&VqT32U2icuk^Of-#$3o(%gW58r44#m0C~JUK{KL9rwqY*mH9~%Pk(CD%PVRnXN@)qmBuet z>Q01Kb>MlJvpE$|kcJW3gJopSrc^*o$)_|z<5a+`XIp2Vx+|^?rvh$u#F9DtKZpNe zi%%Vh=M%&%-LQ@T5`rTj2ok8cVop@{({OJJPL!y)(x=x@$2bj)SI`^Hl+I2il^EsF zwlLj6p0t~*La{B7QVUPo6SOU?uIaWP`y!Fh4#q#-cIzMia9PzP+4WM(I>^p-Jsg$# zRqJpjpgS!#vybDwNn{#IwA+gTRLw@#LA&hh_c2@CBn(Z-6~yg2#=)~DP9RfP;b~iK z`(aeItzRjoU^K>{Wt>mIoy9utg-;4CoM#{mmNtc5&%3Xw=cF*XZ|T2Isw@}wcBY>AVE5J{#Yi8O^)7lkktU66^x3Nc$)X;@!rvZr;uzN^wH1=?{6+q zUcBVZ+mN-<|2n?&H~3G{R|i4ap5vqmT2b)0$#?z>tX}A^+|{)ZIoiY zUy9hhB8ic}_jf~)I!UM)28KbCggJ9Z_I29GjanO8jH28RXJ4pmgyCzS>noa&5R;T( zM9o_Tlm}K01X`+*^Kbc+!aBH77B)QbO3-<)FYNe5_TM}^sVr>i#CetX6kQ*7!rgAb zf}8`TnL{76=l>>pZAzD4cM9H4pdgc99YYrLHBhLcCh44k&`gm-j#Q)7bNxHf>N&)8 zdN9-K4JoE0P31@)Mn&GI&hM~Fh(X0S9M8gGko;6DCjSF_lu>?9g`yxw>}h*2!(`Km;91pK`2VyXcH#e98Kw~z z7xfoK>~vhZenV8x-Iv?I&Q;BEXFh6cssfSz}o0y+T6Ef$Oo^P;NrM}@j{JXMQ zN9?O1pq);QUboM)&+VGf-4*Sbz9cQ?xp|#G$w@37+a+)9ye^L?!W3q{t6|K4ahT~u zR069z9Tj1)|LO;nfj{+SEK^J{Y*mv;%cd`RdF)14%XoarEBoL}zPfA{V=JGeJs4%l+Z&}`9*MUVPa|(MPIDOT+VI>GocglVAWeQH`9^cLM~Y>}Ia5NIIQA4LeeZh*id z24e68P!WO{h}|_Yt)lz;y?G$^Pe;~$nX#jX_I%Vn!!+toLUEFGg;$0729a)K6*auD z5h-le+BHuUH$Y!=eWxrhg=PdQuEmQOUBdhb)a;=(Atj3A*e5Xpn->Was}U7}-J25H z@M(65d&&Lg2C8Y-7Njd;H^l@^OsO1<1!=+iB{6x;?$j2fH|JzOKb97x)n|YBKKyG3 zYT8d`Blg`Yt!mLId(~vFg75cmPL%r#XK~>(=%s+&G1U~4(O58i(63>^h=aah!H9$Y z7{P)m;e$T0U|O8bTr+O+W1es}s+nR4dxC)VH>1}^@fB7`p{SC%To|7&WP9987XJVg z-@ehp^F4F*srK-EAGx!q?H#vWJsvzX$4g*AIKDm!{?E7#iDtIuY9KW3*Ra+&fDj6MEh z{7?}lb@Q&Tix$$V0+#l)D9^y;M9)1*6mAk$#Sek%_KM;At8LUbN^m$(E;-mdDyHMv zvxZVdz!7c{Y69aiz2Ai3V{#{D2LjV-xHcHc@G}rkb16 z%N`I&*TJOQt!(ZQmp2fhMLC=E1T$Hmx`hZa^AX%9vmuR8ZWIS4*_g7CJn#@`NdIuia;;}R(g@~*^Uz2%<5FsJIS>~o3* zDFQ>T$oHbg5wc4IDFVXc$g`eHlr5#GL~cji#Bh+jC1YE)?M8$e6rElQ8&GXUgt%!K zQ^vKva;9M&k|3JX-5Y=AcW=V_sFYcyu}$=&^$PB~F&zNW zLf`P^q~xQqR{rV9S6wVMYA1c8Y+!XPK)>V49m!xs{6$zvv;;F_5l!;Xf69R#wo$L>6AyD3#kA$$iibnG`PBrK|z;Z>Z^rL~=B4U$lrs>Us2Z;-=_= z`4Deq(={)S+)iH<icCsTd1}RSFO9gq-*Xcq>fBEkh>;E zI+<;+sjI}fIk0@v2Xw}jhD(IZ;o`d1#LOw|de}vPjlTF->CRLc!95Wl{Y6oRS5D0) zHVPR7cu`;K@=qOGR^7F5Ni|Es$<&`i1%=3(7pF~iURiaOo6rqZXn}%4LN~ObeW8t> zS{Z(!KZn_Fa?EnjrKmnE2(AkBo2eEhR$1hE#tt zJ7M7%4g2@Q!Fgc(!RkA@ZOPN2tP&+r1TT|`ND-zGfnXUt9BftXCR&8_?!q7^%RUmm zz`7#eJ}~X1W?&>OMe*T?2iRbENlnUtO;Q}x{HWQs1-R{04P^`;kjckXUb`W|J;y;+ zd#e+-#a!vxWnYy#H5G&DXfc>R75TT7JeZE^{(iJ)){-w`#?9*zoinR++^2bKhure` z13^a{D?(d&{dd_L;4Z5pp``Gz! zHNV0$1`Q3W!WXLR?+izS{Q}Ig1)SRytogImCgyrfk7A4Y0B$QI5?$8IyhcOOW+kM!fH@_xGc13@_r zQv&s*Wr5^eh3yu^!O?fT%?anF)_45e_*o|9z3OuMVA+(8PAbBwqtdQfNn>5Wp}{j6=2c4%OiF zV&oYcFz({6b+EPRk2g3i==oS;%lP+cTfPbw2&_7d}NZ!cUhFbP+n!G`jCLwRo zsx4&iHpWGJyUcu1Om9yHBULw4RGt*1p=$?6T&?m1sXBD+h{}`aN0wxt%!n)H%9C4w zexa_N|4e$<=8RZH$9u#0UmhPm5m-_ zYbr5>cFC0(tA%!XTvTG5W$hA`7#%X#KL6Li+i6N^9CDJ6o=74_at>@WhR-3Xj^>qw z_;5u!o7`hM4L^yB8z}ak`$S>pCdaDOJgA0c_iku6{_ecgjL(xYpF1DC(mCSsT}v{Z zi$&OBsqe3KH2kyN>_38J+Hj-j|0!g{MNaSEQ)LH`8Xc?vU#S)Et39465}QT z>P<{Dzup!_tzeRa&F)-B!dx}F`(1Ss49l9<#FjU&bFNJ>mL_Tp6Tav*jz|5vHmoAi z>#8$1#Bx0f15VHNUgui-nSpOQQ6h_jxGCphD2Cwo;HB2cBX=tJlV44U4`CXraol8n zQke=Nc+7C!P|_qV&^fFurjKV|GTa1hu*%qac~oNfp{7E3gUhOhL;;r-LG@s=_Ld3%aUQpZ7~6zBX(MnnHM$V^ zwr2dCXaFqboS|B=4fXE$3n~F==G*_etFZX(a33r9-Q2|32M(NM?txMooCyeRSE{(cgYG`=NlU zjeE(KWcmad4w6UJn`Gkv?AsEyZG(U3@^HhP;O1g`jwPJ-HK z;1#DAE5RT?OK!)>56C#ohz=FgTt2caN|d7HEXhU7?1Meb-qem+2-b=3Welc^PBo}$@p3c zS}Rpk={8E)6UEi~Vv+iW$vms9lRs9l+fey@d1_EwRK6j9V&OrORf(hO#Hht@Q7ycw zdZK<758Z$5kLykPdjIE7|4e(AN1qKf;z5--ANBRlct4p-(9|8sjC)*H3Q{(@aNl5q z`C=A#iL1gv;ssAreT5-MT+z4{oe6wQZ#u7yB*a&vxYq8EX)gbZQ2I$DIrcn9@>%_v|>&=E>;#FzdF@_2s8hwOb}K=JUo z7^m#)r`Hz!l^p5K$VT%z8PdJglwqlo*Qd6E$p7XbZLsABN#hs!RK1(bu!H^1>r?-h zPnyqrj^T+donKyGkjbcnZ#Ha+V${Lz>+dcfondUpv%s)!ExZ}~i)Nr)>FQ|(hAH-6Th%D7eMIy2!q#_Yn+OA1O99bf% z2;s5M#(a#(5=I)vOo;Eb<$K_fguiF2>-y7TK0!qugtIQcow^q$^L#=-RhiU#9X@gIdE_!=Y4NZvt zAdOqu23ud!dU<^BW<*O7i^1GdR3iq9mLeerinFgTCKKN;H&r9dOUP>o9$#**PAEPW6~bHUat40{tclx#D0fmJ^$A@=U0M_jDi zp}EGfvRfv|F6VA0)MWD8gEHHX$nNykk)V(n@!_H*qCOjpp|bc_8-ndpf;1p*Lm!8Rb$K3rG11eX@(}{zf_TJLVQo*jn0S}J z;x7~S@x1bEd-t(T`sQ$qD{a@DsJ2m~QA>U-8`PlxdEGn;|Vew`MqG0fkpdVO2_i55B%j2q^0>tI7+5ajZ1&3#0(!aO$-l z;c&|2Nfap-{go0(O^0l&T>F9n9))+w&km2H3b+-=NX-WH+TLN$Q>mnQiXl^t;>klM zVk-~+{$nv@+N9~7Lutx16?*3-Bis(L%>bZ8ABVhYaGaU+XxD@z}AbK7izBcxtI!rh$DI zM9l@Mt}QNaBb{`4BTikk=UZIfG)`UgJ(=!~5w9|*?*5KvkKyvR`q#ftOg{~epxIC@ z7%7I}{>wpMW{aMLBkMyCFWKbywWzcro0oeom+wucU3$yMx)DHN)n66!atc1+JW+qM z!)1z*n5u8t%E-?w=zdG_hgF=)P z0D?fk3`WZTz`&R>VGl817)ZT#2X3PG#mwFv`r4~k*bg3)JGtOpp)e99xEyEz4Myu03L?5MZ-y5 z<;IW~sNCo8ski+)s-vw%??xMB*993djNU4rmQBS_9u=$H(P&0oh--_1iw__*+h%>r zoYOd={(izR%2?ex9+$|xVXS<>me$|AXnO-&f0gqpSP-m^ur=Nxxm>t^k8@r3f)eWom`+BP=H9h$WV?FLAo z7|tllr556HUCt+v>WtqtC6&IwBLtGYC@t+*0t_c?kJ)vpMXE|*QNUE;V{}j9*b*Sq zBxH+7I=3yGIW={&1^(3?MrU-R&}K8enX=7u5L03zJz1%()^S^HVcO=@H(th_K;y$?_AH_xBvcuAe{PDr&-g(gHK2a3m8Q{rn>Y?*^|DNW5bUyhu2)iK>!Eg@fQRK z&EhW#NFSjk z?3KrL-8kTj_;wIy-F{$|-w2v_uk(L5EH`p! zV?dFp-|+?&rhX_WWcv6Xm3;V=X7cW6hIvgfz7f6^;|oEZmUbydt}MP^_nPv|(eG?t zJ8XKJW60PXof+XmHwKM4>s0eC#z)4ss)oqR!&{e<#8^iSQ?OtO2kuF}kMGw5li)i# z4VmDG9W(R3%th~{ABT(csf0e~UXuhwJj$02xuANN6m-ryPO5B<$fe}N!Px=_IQX-6 zcEoQxd>r_ru-Upb-R}cVS~nz>-Uqe z0l;Kb+dJFOI>9G7-3Wurph-+fcYvtb(oyyC8GF^dT5|nlp7TZNwIL2B2~jd{p^nI4 zTX6bm#vI|Gr3X~d=>oY1Vo0uXdhWr4WOg%lLQCTuq#gXHmqUm*vu{-7|55dg*=U$x zR6*7|aL>+gN60Ppxo8FhnA!ku^U|N>XT-;Si5kCsJ3El_YwgjrsYIEi1eV;G6LbA3 zR|`*Dfr%l|O!@6B6XhLsAN*2pSg=7SqEowj@n5;>!qU&?bewL7 z;K7dho|#;Pix=-n=wu7c0k5B8IkWl@Ru6j;jl?I@ueN3XJKawneX)GTkvp#q`PF$h z9Xm+A7C4L4QTAVhYa2ye+pA78AOkIW`$hwhF>-ZEi}<3P@A?jTVdmX&yT{JI(q^-H z5@kzIDiYX6fB@Ii5qG3pS<8pZMiz!{b9ez~&fz1ePw)7He+Tb5W|Y1lCE;T2bT?Mj zgEtV91y!qy`P-(>l)?u&u+Awmh8Bt)|L7)4Z?;NFEPd$|CD!-GKhk#VpZm5?&~!TU zMRwygmQ6_s!DownyUM_-4B`MlzA!xxMY+)&N@u~4l0O&p*c37KJvfG@c5`B=tm*Ol zKmGJ3_c4PYZ?i}46ldVFxU6iXvi7AkRRXx^At1%4P{RfPGi z7|tY=kpUCZ>tr;XNncWn>ER<&U)t6#AD*Pv`BVg=Y^d~*$&{K^An2DAPt8Fl- z3O8<3zn=ZTG(3?8lLn9yNS7F@N*Z7Q#OMRfzGxV9l?Ud`;k<8Q){=sKIl~dV0o`i{Rpu3sV~~>DW;5Qr!pDQ2#{3rLy_!k#bQQsBNiS!tQ&tUjMOJ_y_2Z zspV+JXmSJz4g@X7J=em&VfmN%<$bRW^%L{gBU1o>44UBknq9N@SX^WL_!qRN#``Y@ za_zzl2Q_*@$D!Q;BBmzfaHH3|q(VOXC^vc;6MIBT^fy*W3!^!9D%@r`zAbhhYu5+9 zq1zfFBtv&5qfRP=R$-MF_>Z56s{H%SrXV1DR z5o=I4yuagTt0$JEx7b#>ectH2uckEJR1_!;2P@(DGz)M=ja4~bd5|-BqcT&PRBd}I z35wG4GGgzxJG^9b8Ydb6-z7N^5mkooa@Bjua)R%+J2rOyQ%73_=Ul~(UDG6B4;=jP z3k4D6ikdHuM7(|80lB`{Rqo|E{fFUhNX-{D!-dj;2CtEM3XaX0I{YBNHIC9ISIpL>nJg2>$ zq>?CeFiMNj8jX>hKvt|#wOObAa!<)%vY#ZRrS|2}FRyrGSe#ZpzUczl0kGKxPAAQM zAZFYBo+!fVV;b{6B0NM!>n^wVB-^*eWcE3d@`iJGVn*aV59a#e@AG7EIQ{S0bHzf{q5)jR=8q zL({ec{;Kq11`ry?>}Xj=mRYL?&As7`tv<^BBs{~Miu^L==pnA*N=NRe<*^%GE#tpI zKTV%EoQGLnTkd@3+0uNpUY1w$NAdI%w!pX|WmE-#$p_O;BIcgd)7&a5yvV|ItqknYk%l-X~f>SJ znNt`_6d?H;EPx^W??GH9zL{KyB%* zxg{cT1v1PXbVR5h#X27`6@adUV5i6a@@ zWR~|d=t{rovGYXRFTS`e7bAGXW=epGi?v}gK~w6WtV&yN00V(D0H-Se(! zJEgc%@pnD$=<~W*#W4J_QLL9DSL}onZ#PaouGzjVlB~~Sgf5(Fr`zgS**Ru z&`^!BKGs)1em=*dd?>ZK9jk5kBT{`!#K9oqlBrxLkGV+g zA&uMsc1O$!>a}@zsAU?-=>R`*$GFzQ>Jas|(@Vzpe9c{Z_ssc~rzqqbkDi;Buy#oVAP(6Geh}u{7-eM^6mxQ~8yF`ww?p zLQ~0zmBa;4=CQ?$+wguk%(YD$1Sean>Vw|rhlA8P-!6R!5IRKG$v|FDI$23|r6PH$ z)s>1WW9};z!Asudv-Dpka+kJF(o9DCB}^ktMgg7Uhd-GgPBn@olN zaOSN{*I@o8HUWs28kJEXFGbg-VCIVMX#+IZfk(o8A*RLbemWNzB3vXvJ~@4#_KGx4JYv0iGht!k4(X zdggp5#NjJuf6Lu7dPZD4mR3_f)5Zh?JWv(H(WALZzfxS_((7G*BmGv9b>T*Gj^B88 zWL-{|KM@V1(>@&b7k-fkDEa7t>}(-CPNh7eZPygebN+z^pKMJ~g?A?ZxZwsF66<}SL18B(Y9n<=74$PC=T)6+3JP=|J8Avbn#lh#u z@IJ!hXQ}>{L>5D^YWp?xnP#Q_XE31{sL2uRi}=f##o$JhBg}x}YRj~NF8|hpgHK$Z zpxT9SR_8q}B`I&17oQ4n#I}O0L;;TIPo$kt%6Yu`_|4JZJdk(p&aGo^x7h+Kds|7U z_!3ulBxU%L+6~UmH~A@GM`AZJ&UKGfjf5K+7FL|=R`=@h@dPv;AtFW*1%{_)eW$N3 zXE!n*BLpVVu_AWTh6^>l#2AEnbox4BRN35mM)&dXkN+nwgnva*WP^h8F^aF91&l-}jUpt^KKVUH?N zde=4glTNvhiqgBo`@1;5dGU%aTza=6z6jN&&mL~H)aG%GlFnhNQa&#-JpL2_)ShSW9qnBX#@#A5P{7KD@L*;gdV66n(B?7}GCjJ9W}j_RW|hV^AzIn> z@lW^M{l_aVn}}H$KstIi6D zXviR&eP5pHQUH0ud}mq4RjP|nr8=VOrGm8Fy{J;1=V@twBkOmSpy$sn>D1ei=QvRE z{IjjIPu&&Qy7bl+$FpvA#NKu`GkL}2f1vEs*E5L%p8F(HGKXU(g*6-BQsWMK4+*>9 ztAQolpwC|yHxFz4UGg3d6P%E}xou||HJFAs{z<1y9&*JmA#6NC5HrJaCwd6;4 zrPi%6@!@jZ%+2cM|A!s!G_Y<|@3|&yd6JN+m=WR_k0~zo!`76I7W81Nfp!+>Np81H z_JJ5jxi)8?{S=F>uSo2re8#3x9MUu*1Tw>7)w&H`>kZgyiAWQj)vQ4~9Zbq=Bu%xn z1c^yAw!wvDJ?CjdcLwdCW+0+zoX#SG87dTs^Fgwmi@W!h<>ka=M3#&wNUQ4LxN6<7 z65L@1=dFI(nc-ZSdwAbj=icNdd1oJfX4ksqo}!x%ri?q*ba}b_#nsZL+!)l*uUw*% zs;&D&+$)_|R@+1=6}aZ##%F4!RMBCX?sgFixRmOC``Mi6wJF`3hX$w1^CeWvoL6WW zWG*&;7G7)D+u5rVx5ZqrcKxm_`iNavyH@V=>~p)KG;3GPxOrWokHOm2C2#GVTL%3- z=%~`0)*$b&%;ty-h)l`G+?L|TBdHs%5EbN;Qt5{896oVoeq}`e3t-zfmmYg!#UIa{ z3)%hq-(S&@D_e{+l|7JksKwh4+rf5$G1lhE$T^ZTiuUOIYH z@dN-x{mSMPq(R;;$Rgx-{*Lzu`JEQi@$A_aBy4?NwLSE$w_a`HJ$j?+;%9Nx)P2$a zJ9>vNRlUOX(_PE2k2Ddlk2tON75e`5>ECGnu~ZR`OIj+evIW#viLzLUfc{!PpCZ&tp8*6v8F>ag(X2pw8d_y`!~%0Z{I()nm^fKt0nylfdq_huz$hI zH?RB9eE*QF`~!+J$|ZjjK+TB56lJRm&7fHlbX><{^D&BW3kEp}Ay7&1C< z+3QUgRZNu6ETGYvvPFYV&=Hj?C%t`uy`Fb3KfIo@cUqoLG%g$Cd??@XHh=|LZ}YTJ zP)t@T&TGI)bs?@nwp~uzj(ubH^&%>H|UO)ZB-B+4#1JQF!;88RYh#57S z2ri7M>A+eU!F2&#jQ!?EI^^L^XDVvdofLgue$a=1g{M#MVpV@j^^ZkW#M5#JY|3$1 zZ;0jqMQd8O;j}ma;6Ll?S@7Fs<&7ZRnaz2%3N0#( zalTAsV+=TKzVgY29fB}kzKia(%IVHiIVD&wE&73(eMU{V-n61XyCM;}RQC1@OG>-BNyMu3B!K!y zJgr7?ni&B+E$_PhFPgzmn8`dX-Jt6=(WckZ+om^++VtpM!fDIw5lBDxnm_&kXOs#h z3sRBBc(lW=V)6l#feb6g?;_rCmR?tF+qWeTI<5dh${LLtRg1qLc811cBhJ{iF}r3b zrsx{n3>Q-A`Fg%?)g;v|oQqE`W|WC^DjF4iPWUpCVH`!qZbPVfL0XJ zHamGnm4CtRR`#)LeAEkA;&Y41H?uf{0jc_j*RhhxM&vV{=oGQP#jo6kIRo3`x%-!F z@ipbY#Km28ZZWG{2id%9EQMCxX++!MP|ugnVk{0tD?fG?MVo%lr2n=?;JO`_e0Rpv z(JH8RW-fV17U8M6uq-bHYKPALKHq<^eP*U}!yD|3cb1JTNb4si-Eeq`nxcx?pU|Rc z=qW1yLUt4X#PPil7)-4~c8RgufF4CeYDb-UiymSIhoLlYq7xxxI_`^LjL$K` z8KxOWA34kDL??T342nId_98jab5GKCdq!f(P~jpd?2*^CvfKD5%|+0r>_v!2g(*_GuMop{Fo=4EpMh^^qwFU#n&HjVWP5PL3@pAF6wlHRjqyX ztLLD`9p=yEMBE`ct5XJ(G(tp#?gn_VMtkkD$Pgk@P^d=)DlpV+*24)9#~(~Q17TU3 zOD8}=+fx=wqo|Hdc~ruvdJ>n43MxR#VF+Cj>+O{sj}i;8#i_$yyu(YFR&=@>V=-F=$y&C51^*f(ilk!hT)_rJMANu~I zjv1;D?y3O7V3?=$i04d;{Lf(FV5K@Nc!S8CLfs&k4AD-^U6HSRL=M(Bi{9TlZcM;U zmAw^Gj28ItB*N*Y)gWp_G`?;q-ar!p z?(qWmtdS+Xb;G>$lnf`3j_V;db+%qkB%RFTfb%>(Blz5Fh%TyLIQW$%chaTlnx*W6 zRqFU8ETV8zn+yM(A%mDtFc26sLk|<_1b$4`pc5!EZpyW3L|ymGOVtAzl}gS(1~4&t zy$T`_08CU6C7zoFZO993d~DKrFf3&<_5_GJ4O(-^AWas4S(Pteb*igpkl;DH{pBt+Za&gwvxgyD%~Nf=g; z$#F%pBye1D_w`{X-0cP|$U4@2Xy#Y;{NFs08QW#RGW&K_qClBzqbndzgH*Oecfb~& z4jRH(iyIZwRsXtZ5jWDuejWINSUR^dJReF&%7^k_SK=Y5}Mc#sH~xu)4bx&0*0?*!?SL~^xVFo zwa034R&Y3QCg)D=MQguoYHXDypb#_=#zb!i~C`No=S9VzFHCUS`{}x3`pkYfm zRYziodgNuYo(`}DmAoP+Xp>-w^N%BWMNsiX5=Rs{y1@d*XP%h^P}0e|G+1rxlWkP( zNTimOZ-wGVpA#yL2-MR}wAe7Hhgxh-F{p=IY>pZV|5T)&A6qnSA8;NOHdf42yn)n> z*)^~(t%7s^r=Lh2oLDd76h~y-~ZwQJ|27X`k==SboTRzs6q6R=fHw*hqsnbP*&tc~9A@|8$7uVz?)XB{~ z;-d2}B1d^NbHGXQ+|8Sv>r#@9LaUY83R$R$`-S7O)G%7v8~;2_eXfg(<+3(9exphW zi6@*?Id@&~p%lB0Q^9KaGAz>}IF^^Yrl4aPAzYI&m|hpXiVHO@1+GVd{@(HIJ!^=0=^=sV9Na@7hYr{ zvWlRI7~^N9;_t_iV|iiPrieFQMuplN#dPHO%Y=PY=qO&&O?o7tJw#X&B;i7X-Wq33 zM1sbK!lzgDrs7_}hw7*Er-$9W-*X|jD-PAOq2~${f+3Hu#9#8Qg)FD%t3okPP4~>r z$5GxZmA0&GgZ#HwDs7%WHSFq2A(igBH#a+@%?u%xmbOmYkdZ@DY0Kz?YsMUYEIzyr z0X)_Bs_2>Z%Or9|(00as;LA3C=Grl;Haf`u+U^kkkY0Y`uE1MJKECLMZbYE9S}aA9 zL-G!jYYhvYEI`qa4h7XyU7kl@aC-VEwoSbwkykMm3-*6PM-8J|usfKJX}R86y&bc9dQAQszkxb6 zIZ0U0pSl-GRX26Xen^Fb&Gf#DyO+Q}5ySri|B3r6N)`|db5Vch{2FMO;Xiuys+{Pj zIvP)+jcNU`u_Gi5a7lKA^v)yO$03NXV*WyilnVnGCh&-cvOmrf;cR_HS>nkG(FUl( zkNGnNzbvrqOCMKOKt#;mB~TWhN+QalcjmJzl!s~1DD!%q{u zdZ`*ZgaTBdhECO3iW)mrLlTj!IK4X;o)~mRn|<$j0^x&;WxA6O&MRe8n&t5T(D8Z;|McU3nE&+C%_Fv(7Neil z;iH(6z4%Xdoi#u*kOV@*IZU}l&_&63IH4)JnXS4g|NBnl8S@8f#`V^V5|0#rt;tgat7lSfX~}tlAJTpC?fC2BLiagp zQg^%GU>wG%u^scf+|YB*b7R-W?GAnG%9#Pwg>F7%{e)XQ27L@dHWu#qYoTFdqMcal ZEG3EFH-lm1|C9g!!0PY*_Zd_l_zUOkiSd~qNUmE8#% z012qBG6Nr47Y!o~5a?Y3^KW}PK&SFG)wu>L#tP1XK-4d97@BJt-t{Kwf=F8AJ9==& z#>PfQM(}w2#KZ)}4yb^KA}KKdrkrE-zXJf*u?k=kaI`=aBS{lPzUBw=pd#r8h(sb+ zA0HnlX_Emt1?VRnMA8S5oDU*3$-R**4zms$27)y~3kwT}oD}8*2?Wp1&i-o$*eDEu z*X(duGCxonaOoXc4#I%($%z#EK|f^m=!wX7it!+rtWqwCbUZQufJ4p#B7u9H3s^Kk zWPVC6g~yQ0a5yrhF5m@Rlt>-27hnSdfCGTZ$w@N%!2#a&2#x}@zyks*5CC8R0Pz%X zSdr4_0W-Nb-~x?Nz;WFJ03?#%L?1LgJx!?&FaSmZfk2@J3Q@=ba7a#JIOaJRaB6Dm zxV__vD2afixG3EMZBUpgM!*YL@V|K4fS)Yh&(zoqB%W3?)}zzxa?yA45A5Xf`0V_` z)FN??FtIcCI2U*r5#(v{m8dN@A9st`Ear^81V&l$P5W+rUd< zF0BhuUj$rB=IY}kgz%m+Dv!SR;UwJ~VyQD<6xgZnpNq+OZd&}+%A96|4>NDdd6onY`dJ*f|05dtaCS4r;@msq$$D0Of7OT38&uZ z`WEM)kfa%RlgnW7rDsr`irj8})JTByMtbQC?k|ZI$QU{W8Q?$4zS61U_5G}-5K>`7 z2zDiucyi;B`^3@GVD?U#goFE^_84Zhbs>EP8|*|T#l{AKnms3_Q9VnMXG zjBj8;%fD@}qH#S`nB-snqKe+V@zaO;4R%sOJt2gHt&DpyZ&xj}Yo!_Oc)O1^DS|Qk zgmuc~X3vD-jOA_SLCtnnQo={9lP9_YF2f;vF8nsC1FzxU80A_j8J^A1jufaKaKKPa z6V%-;x=^l+xTS_+Gn*Farv2o%^eXz)3--}=b9rB?>@w{{}9L+Rn{$A$j@jU zhla0zRaYPc&q zPiNRSJ`$rkem64B@oz^$sL35VgBMqhnkDOlRhX!hxB;vjx=5uYY)4PjZe&3LX&IN$ zGhf`frG!NWs9h*KjW9(NDn@b)S6V;7ZU3WxSqb5Tx>`NRFCrEO4T{NycRAwti#p~V zQg3wRM7L@e&}&IvOPwC{V{RPGl_1W?eE-&8$~0g;nx+?1cn>8jU!I!`@1|$5$xHo9 z3NG`8$w*cOMYPJ+%YbuQdF1+su9qt)*}3eXBwo0AxalUz!(DRk^Lq@)W#@Eu1bo)% z<%%3KW-u~IS}w~q^?Bmg(4eHTmqYrRZ!X!O--0*XG+arp)vIzgK)w=9YI5DIi7rjL zHS1jAw`O&-uG2RAMfb^i$v!5q_{e6aIq##gr^1iq8PddYazqS)VW z^F*Itc&+XHV8bxS#=~zrZgV`WXBUyW4Zb~BWWVwCntqnRFWjUf(;RC}Pa}`8@13d? zcALz7bfW*x*&XiX*Vg$rn@6?EgWVq2)T+q|Zxpz6^E;cIs#BbsFIKpc0NYViGf}^QZl!gxl>wNo`D+&fTAPE4%i^v8?}=w+#Am zAPQPy+m<^mlgpF*A+p-iO)f-K)P&at7BF(hT=FUg#w*ox0(z;!kr;o8Epb2o+3ui^ zaQRf=!~Sf;=Q0&5IUjw8`z>e)7P)o3HNspM5Oc4FGuj;T?s8=r75^0T>d%Yc#hwN~ zN9AE}dx|9DvpTk=#N>Tyl;y9aThVI2Of(-cB>bb3UeJk^poa)FqVg>t31Q);&ip(<(* zt(djjw|ix#R-`-h*S+_fI&oX4QoXiyjc~Qgu;4GH8>51Z%a>9J(eJ4!yM+bJ+>~($ zF(h6-p`TLI?1Ke@GXw_9MI|GNFtDr$)cSyGoB3%(w zkKe&$LLx-{ANgA4J_~M~ZuKmd-!9QvrA-{1LwZ(Rra^fZ?9_I%aL=%OTSM z`-)+9qvrhcvK-n>CcB+_(Hnj4=~VS;pD*y89rN0%@?`THMVZd02eDT`->OL8K^X^{ z@*B9xqvv-zBZ;BWnHZ9U{_Q$1?+^XrCdy`Tf?XgQc_}|i07t#zg)H5D>wQ;M z$|0*$UnN=h{lnJ4oW%+~ihxk#K&7Zb~EF zqFcOwj1xOMMpD^V4cWZJ?%tn1Fl--&yJ>6X7c*k7;JYR+(!9j>ecmzpe(Z?-P)LL2 zuu$a?1A<$>GYeB#JLSaK`DLA!+baJ^S7-JVEdc^4u-=BFNzb!O%3J)309 z<`QU_86N4d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/apps/users/api.py b/apps/users/api.py index 5cedd0c5f..c64c52b44 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -16,7 +16,7 @@ from .tasks import write_login_log_async from .models import User, UserGroup from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \ IsSuperUserOrAppUser -from .utils import check_user_valid, generate_token +from .utils import check_user_valid, generate_token, get_login_ip, check_otp_code from common.mixins import IDInFilterMixin from common.utils import get_logger @@ -129,47 +129,75 @@ class UserToken(APIView): class UserProfile(APIView): permission_classes = (IsValidUser,) + serializer_class = UserSerializer def get(self, request): - return Response(request.user.to_json()) + # return Response(request.user.to_json()) + return Response(self.serializer_class(request.user).data) def post(self, request): - return Response(request.user.to_json()) + return Response(self.serializer_class(request.user).data) class UserAuthApi(APIView): permission_classes = (AllowAny,) + serializer_class = UserSerializer def post(self, request): + otp_check = request.data.get('otp_check', None) + + if otp_check: + # otp验证 + return self.check_auth_otp(request) + else: + # password验证 + return self.check_auth_password(request) + + def check_auth_password(self, request): + user, msg = self.check_user_valid(request) + + if user: + token = generate_token(request, user) + if not user.otp_enabled: + self.write_login_log(request, user) + return Response({'token': token, 'user': self.serializer_class(user).data}) + else: + return Response({'msg': msg}, status=401) + + def check_auth_otp(self, request): + otp_code = request.data.get('otp_code', '') + user, msg = self.check_user_valid(request) + if user: + token = generate_token(request, user) + if check_otp_code(user.otp_secret_key, otp_code): + self.write_login_log(request, user) + return Response({'token': token, 'user': self.serializer_class(user).data}) + return Response({'msg': msg}, status=401) + + @staticmethod + def check_user_valid(request): username = request.data.get('username', '') password = request.data.get('password', '') public_key = request.data.get('public_key', '') - login_type = request.data.get('login_type', '') - login_ip = request.data.get('remote_addr', None) - user_agent = request.data.get('HTTP_USER_AGENT', '') - - if not login_ip: - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') - - if x_forwarded_for and x_forwarded_for[0]: - login_ip = x_forwarded_for[0] - else: - login_ip = request.META.get("REMOTE_ADDR") - user, msg = check_user_valid( username=username, password=password, public_key=public_key ) + return user, msg - if user: - token = generate_token(request, user) - write_login_log_async.delay( - user.username, ip=login_ip, - type=login_type, user_agent=user_agent, - ) - return Response({'token': token, 'user': user.to_json()}) - else: - return Response({'msg': msg}, status=401) + @staticmethod + def write_login_log(request, user): + login_ip = request.data.get('remote_addr', None) + login_type = request.data.get('login_type', '') + user_agent = request.data.get('HTTP_USER_AGENT', '') + + if not login_ip: + login_ip = get_login_ip(request) + + write_login_log_async.delay( + user.username, ip=login_ip, + type=login_type, user_agent=user_agent, + ) class UserConnectionTokenApi(APIView): diff --git a/apps/users/forms.py b/apps/users/forms.py index cbb9beaf5..03b1e21cb 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -18,6 +18,18 @@ class UserLoginForm(AuthenticationForm): captcha = CaptchaField() +class UserCheckPasswordForm(forms.Form): + username = forms.CharField(label=_('Username'), max_length=100) + password = forms.CharField( + label=_('Password'), widget=forms.PasswordInput, + max_length=128, strip=False + ) + + +class UserCheckOtpCodeForm(forms.Form): + otp_code = forms.CharField(label=_('Otp_code'), max_length=6) + + class UserCreateUpdateForm(forms.ModelForm): role_choices = ((i, n) for i, n in User.ROLE_CHOICES if i != User.ROLE_APP) password = forms.CharField( diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 06761a5df..365f3428c 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -45,7 +45,7 @@ class User(AbstractUser): wechat = models.CharField(max_length=128, blank=True, verbose_name=_('Wechat')) phone = models.CharField(max_length=20, blank=True, null=True, verbose_name=_('Phone')) otp_level = models.SmallIntegerField(default=0, choices=OTP_LEVEL_CHOICES, verbose_name=_('Enable OTP')) - otp_secret_key = models.CharField(max_length=16, blank=True) + otp_secret_key = models.CharField(max_length=16, blank=True, null=True) # Todo: Auto generate key, let user download _private_key = models.CharField(max_length=5000, blank=True, verbose_name=_('Private key')) _public_key = models.CharField(max_length=5000, blank=True, verbose_name=_('Public key')) @@ -211,15 +211,20 @@ class User(AbstractUser): def otp_enabled(self): return self.otp_level > 0 - def enabled_otp(self): - self.otp_level = 1 + @property + def otp_force_enabled(self): + return self.otp_level == 2 + + def enable_otp(self): + if not self.otp_force_enabled: + self.otp_level = 1 def force_enable_otp(self): self.otp_level = 2 - @property - def otp_force_enabled(self): - return self.otp_level == 2 + def disable_otp(self): + self.otp_level = 0 + self.otp_secret_key = '' def to_json(self): return OrderedDict({ @@ -233,6 +238,7 @@ class User(AbstractUser): 'groups': [group.name for group in self.groups.all()], 'wechat': self.wechat, 'phone': self.phone, + 'otp_level': self.otp_level, 'comment': self.comment, 'date_expired': self.date_expired.strftime('%Y-%m-%d %H:%M:%S') if self.date_expired is not None else None }) diff --git a/apps/users/serializers.py b/apps/users/serializers.py index 9d14a1d3a..450c63823 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -19,7 +19,10 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): class Meta: model = User list_serializer_class = BulkListSerializer - exclude = ['first_name', 'last_name', 'password', '_private_key', '_public_key'] + exclude = [ + 'first_name', 'last_name', 'password', '_private_key', + '_public_key', 'otp_secret_key', 'user_permissions' + ] def get_field_names(self, declared_fields, info): fields = super(UserSerializer, self).get_field_names(declared_fields, info) diff --git a/apps/users/templates/users/_base_otp.html b/apps/users/templates/users/_base_otp.html new file mode 100644 index 000000000..fb32fb425 --- /dev/null +++ b/apps/users/templates/users/_base_otp.html @@ -0,0 +1,87 @@ +{% load static %} +{% load i18n %} + + + + + + Jumpserver + + + + + + + + + +
+ +
+ 首页 + + 帮助中心 + + GitHub +
+
+ + +
+
+
    +
  • +
    + + +
    +
    验证身份
    +
  • +
  • +
    + + +
    +
    安装应用
    +
  • +
  • +
    + + +
    +
    绑定TOTP
    +
  • +
  • +
    + +
    +
    完成
    +
  • +
+
+
+
安全令牌验证  账户 {{ user.username }}  请按照以下步骤完成绑定操作
+
+ + {% block content %} + {% endblock %} +
+
+ + +
+ +
+ {% include '_copyright.html' %} +
+ +
+ + + + diff --git a/apps/users/templates/users/login_otp.html b/apps/users/templates/users/login_otp.html new file mode 100644 index 000000000..e8bfc75e8 --- /dev/null +++ b/apps/users/templates/users/login_otp.html @@ -0,0 +1,87 @@ +{% load static %} +{% load i18n %} + + + + + + + Jumpserver + + {% include '_head_css_js.html' %} + + + + + + + +
+
+
+

欢迎使用Jumpserver开源堡垒机

+

+ 全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。 +

+

+ 使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。 +

+

+ 采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发访问限制。 +

+

+ 改变世界,从一点点开始。 +

+ +
+
+
+
+ + {% trans '二次认证' %} +
+
+ +
+

账号保护已开启,请根据提示完成以下操作

+
+ +
+

请在手机中打开Google Authenticator应用,输入6位动态码

+
+ +
+ + {% csrf_token %} + {% if 'otp_code' in form.errors %} +

{{ form.otp_code.errors.as_text }}

+ {% endif %} +
+ +
+ + + + {% trans "Can't provide security? Please contact the administrator" %} + + +
+
+

+

+
+
+
+
+
+
+ {% include '_copyright.html' %} +
+
+
+ + diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index 12a9af86e..ae192c82f 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -87,10 +87,18 @@ {% trans 'Role' %}: {{ user_object.get_role_display }} -{# #} -{# {% trans 'Enable OTP' %}:#} -{# {{ user_object.enable_otp|yesno:"Yes,No,Unknown"}}#} -{# #} + + {% trans 'Enable OTP' %}: + + {% if user_object.otp_force_enabled %} + {% trans 'Force enabled' %} + {% elif user_object.otp_enabled%} + {% trans 'Enabled' %} + {% else %} + {% trans 'Disabled' %} + {% endif %} + + {% trans 'Date expired' %}: {{ user_object.date_expired|date:"Y-m-j H:i:s" }} @@ -137,22 +145,23 @@ + + + + {% trans 'Force enabled OTP' %}: + +
+
+ + +
+
+
-{# #} -{# {% trans 'Enable OTP' %}:#} -{# #} -{#
#} -{#
#} -{# #} -{# #} -{#
#} -{#
#} -{#
#} -{# #} {% trans 'Send reset password mail' %}: @@ -277,19 +286,28 @@ $(document).ready(function() { success_message: success }); }) -{#.on('click', '#enable_otp', function() {#} -{# var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";#} -{# var checked = $(this).prop('checked');#} -{# var body = {#} -{# 'enable_otp': checked#} -{# };#} -{# var success = '{% trans "Update successfully!" %}';#} -{# APIUpdateAttr({#} -{# url: the_url,#} -{# body: JSON.stringify(body),#} -{# success_message: success#} -{# });#} -{# });#} +.on('click', '#force_enable_otp', function() { + var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}"; + var checked = $(this).prop('checked'); + var otp_level; + var otp_secret_key; + if(checked){ + otp_level = 2 + }else{ + otp_level = 0; + otp_secret_key = ''; + } + var body = { + 'otp_level': otp_level, + 'otp_secret_key': otp_secret_key + }; + var success = '{% trans "Update successfully!" %}'; + APIUpdateAttr({ + url: the_url, + body: JSON.stringify(body), + success_message: success + }); + }) .on('click', '#btn_join_group', function() { if (Object.keys(jumpserver.nodes_selected).length === 0) { return false; diff --git a/apps/users/templates/users/user_otp_authentication.html b/apps/users/templates/users/user_otp_authentication.html new file mode 100644 index 000000000..6e13111ff --- /dev/null +++ b/apps/users/templates/users/user_otp_authentication.html @@ -0,0 +1,37 @@ +{% extends 'users/_base_otp.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
+

账号保护已开启,请根据提示完成以下操作

+ +

请在手机中打开Google Authenticator应用,输入6为动态码

+
+ +
+ {% csrf_token %} + {% if 'otp_code' in form.errors %} +

{{ form.otp_code.errors.as_text }}

+ {% endif %} + +
+ +
+ + +
+ + + +{% endblock %} + + diff --git a/apps/users/templates/users/user_otp_enable_bind.html b/apps/users/templates/users/user_otp_enable_bind.html new file mode 100644 index 000000000..e97f7cf9e --- /dev/null +++ b/apps/users/templates/users/user_otp_enable_bind.html @@ -0,0 +1,54 @@ +{% extends 'users/_base_otp.html' %} +{% load static %} +{% load i18n %} + +{% block content %} + +
+

使用手机 Google Authenticator 应用扫描以下二维码,获取6位验证码

+ + +
+ + +
+ {% csrf_token %} + +
+ +
+ + {% if 'otp_code' in form.errors %} +

{{ form.otp_code.errors.as_text }}

+ {% endif %} + + + +
+
+ + + + + + +{% endblock %} + diff --git a/apps/users/templates/users/user_otp_enable_install_app.html b/apps/users/templates/users/user_otp_enable_install_app.html new file mode 100644 index 000000000..a47075d70 --- /dev/null +++ b/apps/users/templates/users/user_otp_enable_install_app.html @@ -0,0 +1,32 @@ +{% extends 'users/_base_otp.html' %} +{% load i18n %} +{% load static %} + +{% block content %} +
+

请在手机端下载并安装 Google Authenticator 应用

+
+ +

Android手机下载

+
+ +
+ +

iPhone手机下载

+
+ +

+

安装完成后点击下一步进入绑定页面(如已安装,直接进入下一步)

+
+ + + + + +{% endblock %} + diff --git a/apps/users/templates/users/user_password_authentication.html b/apps/users/templates/users/user_password_authentication.html new file mode 100644 index 000000000..7603a1149 --- /dev/null +++ b/apps/users/templates/users/user_password_authentication.html @@ -0,0 +1,25 @@ +{% extends 'users/_base_otp.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
+ {% csrf_token %} +
+ +
+ +
+ +
+ + {% if 'password' in form.errors %} +

{{ form.password.errors.as_text }}

+ {% endif %} + + + + +
+{% endblock %} + diff --git a/apps/users/templates/users/user_profile.html b/apps/users/templates/users/user_profile.html index f29e1427e..a924b9f02 100644 --- a/apps/users/templates/users/user_profile.html +++ b/apps/users/templates/users/user_profile.html @@ -65,7 +65,15 @@ {% trans 'OTP' %} - {{ user.otp_enabled|yesno:"Yes,No,Unkown" }} + + {% if user.otp_force_enabled %} + {% trans 'Force enable' %} + {% elif user.otp_enabled%} + {% trans 'Enable' %} + {% else %} + {% trans 'Disable' %} + {% endif %} + {% trans 'Public key' %} @@ -136,6 +144,28 @@ + + {% trans 'Update otp' %}: + + + {% trans 'Disable' %} + {% else %} + {% url 'users:user-otp-disable-authentication' %} + ">{% trans 'Disable' %} + {% endif %} + {% else %} + {% url 'users:user-otp-enable-authentication' %} + ">{% trans 'Enable' %} + {% endif %} + + + + {% trans 'Update SSH public key' %}: diff --git a/apps/users/urls/views_urls.py b/apps/users/urls/views_urls.py index b9d6788ee..2aaf4a9ae 100644 --- a/apps/users/urls/views_urls.py +++ b/apps/users/urls/views_urls.py @@ -10,6 +10,7 @@ urlpatterns = [ # Login view url(r'^login$', views.UserLoginView.as_view(), name='login'), url(r'^logout$', views.UserLogoutView.as_view(), name='logout'), + url(r'^login/otp$', views.UserLoginOtpView.as_view(), name='login-otp'), url(r'^password/forgot$', views.UserForgotPasswordView.as_view(), name='forgot-password'), url(r'^password/forgot/sendmail-success$', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'), url(r'^password/reset$', views.UserResetPasswordView.as_view(), name='reset-password'), @@ -21,6 +22,11 @@ urlpatterns = [ url(r'^profile/password/update/$', views.UserPasswordUpdateView.as_view(), name='user-password-update'), url(r'^profile/pubkey/update/$', views.UserPublicKeyUpdateView.as_view(), name='user-pubkey-update'), url(r'^profile/pubkey/generate/$', views.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'), + url(r'^profile/otp/enable/authentication/$', views.UserOtpEnableAuthenticationView.as_view(), name='user-otp-enable-authentication'), + url(r'^profile/otp/enable/install-app/$', views.UserOtpEnableInstallAppView.as_view(), name='user-otp-enable-install-app'), + url(r'^profile/otp/enable/bind/$', views.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'), + url(r'^profile/otp/disable/authentication/$', views.UserOtpDisableAuthenticationView.as_view(), name='user-otp-disable-authentication'), + url(r'^profile/otp/settings-success/$', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'), # User view url(r'^user$', views.UserListView.as_view(), name='user-list'), @@ -34,7 +40,6 @@ urlpatterns = [ url(r'^user/(?P[0-9a-zA-Z\-]{36})/assets', views.UserGrantedAssetView.as_view(), name='user-granted-asset'), url(r'^user/(?P[0-9a-zA-Z\-]{36})/login-history', views.UserDetailView.as_view(), name='user-login-history'), - # User group view url(r'^user-group$', views.UserGroupListView.as_view(), name='user-group-list'), url(r'^user-group/(?P[0-9a-zA-Z\-]{36})$', views.UserGroupDetailView.as_view(), name='user-group-detail'), diff --git a/apps/users/utils.py b/apps/users/utils.py index c8a5b60a8..8113ec358 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -1,6 +1,8 @@ # ~*~ coding: utf-8 ~*~ # from __future__ import unicode_literals +import os +import pyotp import base64 import logging import uuid @@ -17,7 +19,6 @@ from common.tasks import send_mail_async from common.utils import reverse, get_object_or_none from .models import User, LoginLog - logger = logging.getLogger('jumpserver') @@ -163,7 +164,7 @@ def generate_token(request, user): remote_addr = request.META.get('REMOTE_ADDR', '') if not isinstance(remote_addr, bytes): remote_addr = remote_addr.encode("utf-8") - remote_addr = base64.b16encode(remote_addr) #.replace(b'=', '') + remote_addr = base64.b16encode(remote_addr) # .replace(b'=', '') token = cache.get('%s_%s' % (user.id, remote_addr)) if not token: token = uuid.uuid4().hex @@ -181,6 +182,16 @@ def validate_ip(ip): return False +def get_login_ip(request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') + if x_forwarded_for and x_forwarded_for[0]: + login_ip = x_forwarded_for[0] + else: + login_ip = request.META.get('REMOTE_ADDR', '') + + return login_ip + + def write_login_log(username, type='', ip='', user_agent=''): if not (ip and validate_ip(ip)): ip = ip[:15] @@ -211,3 +222,35 @@ def get_ip_city(ip, timeout=10): except ValueError: pass return city + + +def get_user(request): + if is_login(request): + user = request.user + else: + user = cache.get(request.session.session_key) + return user + + +def is_login(request): + return isinstance(request.user, User) + + +def redirect_user_first_login_or_index(request, redirect_field_name): + if request.user.is_first_login: + return reverse('users:user-first-login') + return request.POST.get( + redirect_field_name, + request.GET.get(redirect_field_name, reverse('index'))) + + +def generate_otp_uri(user, issuer="Jumpserver"): + otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8') + cache.set('otp_secret_key', otp_secret_key, 300) + totp = pyotp.TOTP(otp_secret_key) + return totp.provisioning_uri(name=user.username, issuer_name=issuer) + + +def check_otp_code(otp_secret_key, otp_code): + totp = pyotp.TOTP(otp_secret_key) + return totp.verify(otp_code) diff --git a/apps/users/views/login.py b/apps/users/views/login.py index cd8ce8fca..bb18b28da 100644 --- a/apps/users/views/login.py +++ b/apps/users/views/login.py @@ -19,17 +19,18 @@ from django.views.generic.base import TemplateView from django.views.generic.edit import FormView from formtools.wizard.views import SessionWizardView from django.conf import settings +from django.core.cache import cache from common.utils import get_object_or_none from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin from ..models import User, LoginLog -from ..utils import send_reset_password_mail +from ..utils import send_reset_password_mail, check_otp_code , get_login_ip, redirect_user_first_login_or_index from ..tasks import write_login_log_async from .. import forms __all__ = [ - 'UserLoginView', 'UserLogoutView', + 'UserLoginView', 'UserLoginOtpView', 'UserLogoutView', 'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView', 'UserResetPasswordView', 'UserResetPasswordSuccessView', 'UserFirstLoginView', 'LoginLogListView' @@ -53,27 +54,23 @@ class UserLoginView(FormView): def form_valid(self, form): if not self.request.session.test_cookie_worked(): return HttpResponse(_("Please enable cookies and try again.")) - auth_login(self.request, form.get_user()) - x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') - - if x_forwarded_for and x_forwarded_for[0]: - login_ip = x_forwarded_for[0] - else: - login_ip = self.request.META.get('REMOTE_ADDR', '') - user_agent = self.request.META.get('HTTP_USER_AGENT', '') - write_login_log_async.delay( - self.request.user.username, type='W', - ip=login_ip, user_agent=user_agent - ) + cache.set(self.request.session.session_key, form.get_user(), 600) return redirect(self.get_success_url()) def get_success_url(self): - if self.request.user.is_first_login: - return reverse('users:user-first-login') + user = cache.get(self.request.session.session_key) - return self.request.POST.get( - self.redirect_field_name, - self.request.GET.get(self.redirect_field_name, reverse('index'))) + if user.otp_enabled and user.otp_secret_key: + # 1,2 & T + return reverse('users:login-otp') + elif user.otp_enabled and not user.otp_secret_key: + # 1,2 & F + return reverse('users:user-otp-enable-authentication') + elif not user.otp_enabled: + # 0 & T,F + auth_login(self.request, user) + self.write_login_log() + return redirect_user_first_login_or_index(self.request, self.redirect_field_name) def get_context_data(self, **kwargs): context = { @@ -82,6 +79,44 @@ class UserLoginView(FormView): kwargs.update(context) return super().get_context_data(**kwargs) + def write_login_log(self): + login_ip = get_login_ip(self.request) + user_agent = self.request.META.get('HTTP_USER_AGENT', '') + write_login_log_async.delay( + self.request.user.username, type='W', + ip=login_ip, user_agent=user_agent + ) + + +class UserLoginOtpView(FormView): + template_name = 'users/login_otp.html' + form_class = forms.UserCheckOtpCodeForm + redirect_field_name = 'next' + + def form_valid(self, form): + user = cache.get(self.request.session.session_key) + otp_code = form.cleaned_data.get('otp_code') + otp_secret_key = user.otp_secret_key + + if check_otp_code(otp_secret_key, otp_code): + auth_login(self.request, user) + self.write_login_log() + return redirect(self.get_success_url()) + else: + form.add_error('otp_code', _('Otp code invalid')) + return super().form_invalid(form) + + def get_success_url(self): + return redirect_user_first_login_or_index(self.request, self.redirect_field_name) + + def write_login_log(self): + login_ip = get_login_ip(self.request) + user_agent = self.request.META.get('HTTP_USER_AGENT', '') + write_login_log_async.delay( + self.request.user.username, type='W', + ip=login_ip, user_agent=user_agent + ) + @method_decorator(never_cache, name='dispatch') class UserLogoutView(TemplateView): diff --git a/apps/users/views/user.py b/apps/users/views/user.py index 1d68eb81d..af954ba10 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -11,6 +11,7 @@ from io import StringIO from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth import authenticate, login as auth_login from django.contrib.messages.views import SuccessMessageMixin from django.core.cache import cache from django.http import HttpResponse, JsonResponse @@ -34,9 +35,9 @@ from common.mixins import JSONResponseMixin from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen from .. import forms from ..models import User, UserGroup -from ..utils import AdminUserRequiredMixin +from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, get_user, is_login from ..signals import post_user_create - +from ..tasks import write_login_log_async __all__ = [ 'UserListView', 'UserCreateView', 'UserDetailView', @@ -46,6 +47,9 @@ __all__ = [ 'UserProfileUpdateView', 'UserPasswordUpdateView', 'UserPublicKeyUpdateView', 'UserBulkUpdateView', 'UserPublicKeyGenerateView', + 'UserOtpEnableAuthenticationView', 'UserOtpEnableInstallAppView', + 'UserOtpEnableBindView', 'UserOtpSettingsSuccessView', + 'UserOtpDisableAuthenticationView', ] logger = get_logger(__name__) @@ -380,6 +384,7 @@ class UserPublicKeyUpdateView(LoginRequiredMixin, UpdateView): class UserPublicKeyGenerateView(LoginRequiredMixin, View): + def get(self, request, *args, **kwargs): private, public = ssh_key_gen(username=request.user.username, hostname='jumpserver') request.user.public_key = public @@ -389,3 +394,125 @@ class UserPublicKeyGenerateView(LoginRequiredMixin, View): response['Content-Disposition'] = 'attachment; filename={}'.format(filename) return response + +class UserOtpEnableAuthenticationView(FormView): + template_name = 'users/user_password_authentication.html' + form_class = forms.UserCheckPasswordForm + + def get_form(self, form_class=None): + form = super().get_form(form_class=form_class) + form['username'].initial = get_user(self.request).username + return form + + def get_context_data(self, **kwargs): + context = { + 'user': get_user(self.request) + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def form_valid(self, form): + password = form.cleaned_data.get('password') + user = get_user(self.request) + user = authenticate(username=user.username, password=password) + if not user: + form.add_error("password", _("Password invalid")) + return self.form_invalid(form) + return redirect(self.get_success_url()) + + def get_success_url(self): + return reverse('users:user-otp-enable-install-app') + + +class UserOtpEnableInstallAppView(TemplateView): + template_name = 'users/user_otp_enable_install_app.html' + + def get_context_data(self, **kwargs): + context = { + 'user': get_user(self.request) + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserOtpEnableBindView(TemplateView, FormView): + template_name = 'users/user_otp_enable_bind.html' + form_class = forms.UserCheckOtpCodeForm + success_url = reverse_lazy('users:user-otp-settings-success') + + def get_context_data(self, **kwargs): + context = { + 'otp_uri': generate_otp_uri(user=get_user(self.request)), + 'user': get_user(self.request) + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def form_valid(self, form): + otp_code = form.cleaned_data.get('otp_code') + otp_secret_key = cache.get('otp_secret_key') + + if check_otp_code(otp_secret_key, otp_code): + self.save_otp(otp_secret_key) + return super().form_valid(form) + + else: + form.add_error("otp_code", _("Otp code invalid")) + return self.form_invalid(form) + + def save_otp(self, otp_secret_key): + user = get_user(self.request) + user.enable_otp() + user.otp_secret_key = otp_secret_key + user.save() + + +class UserOtpDisableAuthenticationView(FormView): + template_name = 'users/user_otp_authentication.html' + form_class = forms.UserCheckOtpCodeForm + success_url = reverse_lazy('users:user-otp-settings-success') + + def form_valid(self, form): + user = self.request.user + otp_code = form.cleaned_data.get('otp_code') + otp_secret_key = user.otp_secret_key + + if check_otp_code(otp_secret_key, otp_code): + user.disable_otp() + user.save() + return super().form_valid(form) + else: + form.add_error('otp_code', _('Otp code invalid')) + return super().form_invalid(form) + + +class UserOtpSettingsSuccessView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + response = super().get(request, *args, **kwargs) + if is_login(request): + auth_logout(request) + return response + + def get_context_data(self, **kwargs): + title, describe = self.get_title_describe() + context = { + 'title': title, + 'messages': describe, + 'interval': 1, + 'redirect_url': reverse('users:login'), + 'auto_redirect': True, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def get_title_describe(self): + user = get_user(self.request) + title = _('OTP enable success') + describe = _('OTP enable success, return login page') + if not user.otp_enabled: + title = _('OTP disable success') + describe = _('OTP disable success, return login page') + + return title, describe From 12c8cf6b76a3fdebd9066c429db2f1f971bfe949 Mon Sep 17 00:00:00 2001 From: BaiJiangjie Date: Thu, 19 Apr 2018 11:13:11 +0800 Subject: [PATCH 2/2] =?UTF-8?q?[Update]=20=E6=B7=BB=E5=8A=A0OTP=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/i18n/zh/LC_MESSAGES/django.mo | Bin 30779 -> 31646 bytes apps/i18n/zh/LC_MESSAGES/django.po | 364 +++++++++++------- apps/users/api.py | 84 ++-- apps/users/models/user.py | 2 +- apps/users/serializers.py | 2 +- apps/users/templates/users/_base_otp.html | 2 +- apps/users/templates/users/login_otp.html | 2 +- .../templates/users/user_otp_enable_bind.html | 7 +- .../users/user_password_authentication.html | 6 +- apps/users/templates/users/user_profile.html | 3 +- apps/users/urls/api_urls.py | 1 + apps/users/utils.py | 24 +- apps/users/views/login.py | 11 +- apps/users/views/user.py | 53 ++- requirements/requirements.txt | 1 + 15 files changed, 347 insertions(+), 215 deletions(-) diff --git a/apps/i18n/zh/LC_MESSAGES/django.mo b/apps/i18n/zh/LC_MESSAGES/django.mo index a343d985af7dfd3579a03d47852c15e6b2e7ed68..21cce3fc3140daf1c93525e482b6fa13ff0683f0 100644 GIT binary patch delta 11772 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 delta 10954 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 lJnXwL;aR`\n" "Language-Team: Jumpserver team\n" @@ -154,7 +154,7 @@ msgstr "名称" #: assets/templates/assets/system_user_detail.html:62 #: assets/templates/assets/system_user_list.html:27 #: perms/templates/perms/asset_permission_user.html:55 users/forms.py:13 -#: users/models/authentication.py:45 users/models/user.py:39 +#: users/forms.py:22 users/models/authentication.py:45 users/models/user.py:39 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/login.html:56 #: users/templates/users/login_log_list.html:49 @@ -169,9 +169,11 @@ msgid "Password or private key passphrase" msgstr "密码或密钥密码" #: assets/forms/user.py:25 assets/models/base.py:22 common/forms.py:113 -#: users/forms.py:15 users/forms.py:24 users/templates/users/login.html:59 +#: users/forms.py:15 users/forms.py:24 users/forms.py:36 +#: users/templates/users/login.html:59 #: users/templates/users/reset_password.html:52 #: users/templates/users/user_create.html:11 +#: users/templates/users/user_password_authentication.html:13 #: users/templates/users/user_password_update.html:40 #: users/templates/users/user_profile_update.html:40 #: users/templates/users/user_pubkey_update.html:40 @@ -310,7 +312,7 @@ msgstr "标签管理" #: assets/templates/assets/system_user_detail.html:96 #: ops/templates/ops/adhoc_detail.html:86 perms/models.py:28 perms/models.py:72 #: perms/templates/perms/asset_permission_detail.html:98 -#: users/models/user.py:55 users/templates/users/user_detail.html:99 +#: users/models/user.py:55 users/templates/users/user_detail.html:107 msgid "Created by" msgstr "创建者" @@ -341,10 +343,10 @@ msgstr "创建日期" #: ops/models/adhoc.py:42 perms/models.py:30 perms/models.py:74 #: perms/templates/perms/asset_permission_detail.html:102 terminal/models.py:26 #: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 -#: users/models/user.py:52 users/templates/users/user_detail.html:111 +#: users/models/user.py:52 users/templates/users/user_detail.html:119 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:14 -#: users/templates/users/user_profile.html:114 +#: users/templates/users/user_profile.html:122 msgid "Comment" msgstr "备注" @@ -390,7 +392,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:13 -#: users/models/user.py:285 +#: users/models/user.py:299 msgid "System" msgstr "系统" @@ -428,10 +430,10 @@ msgstr "默认资产组" #: terminal/templates/terminal/command_list.html:32 #: terminal/templates/terminal/command_list.html:72 #: terminal/templates/terminal/session_list.html:33 -#: terminal/templates/terminal/session_list.html:71 users/forms.py:219 -#: users/models/user.py:30 users/models/user.py:273 +#: terminal/templates/terminal/session_list.html:71 users/forms.py:231 +#: users/models/user.py:30 users/models/user.py:287 #: users/templates/users/user_group_detail.html:78 -#: users/templates/users/user_group_list.html:13 users/views/user.py:335 +#: users/templates/users/user_group_list.html:13 users/views/user.py:339 msgid "User" msgstr "用户" @@ -536,34 +538,6 @@ msgstr "" msgid "推送系统用户到入资产: {}" msgstr "" -#: assets/templates/assets/_admin_user_setting_modal.html:4 -#: users/templates/users/reset_password.html:57 -#: users/templates/users/user_profile.html:20 -msgid "Setting" -msgstr "设置" - -#: assets/templates/assets/_admin_user_setting_modal.html:9 -#: assets/templates/assets/_asset_import_modal.html:9 -#: users/templates/users/_user_import_modal.html:10 -msgid "Template" -msgstr "模板" - -#: assets/templates/assets/_admin_user_setting_modal.html:10 -#: assets/templates/assets/_asset_import_modal.html:10 -#: users/templates/users/_user_import_modal.html:11 -msgid "Download" -msgstr "下载" - -#: assets/templates/assets/_admin_user_setting_modal.html:13 -#: assets/templates/assets/_asset_import_modal.html:13 -msgid "Asset csv file" -msgstr "资产csv文件" - -#: assets/templates/assets/_admin_user_setting_modal.html:16 -#: assets/templates/assets/_asset_import_modal.html:16 -msgid "If set id, will use this id update asset existed" -msgstr "如果设置了id,则会使用该行信息更新该id的资产" - #: assets/templates/assets/_asset_group_bulk_update_modal.html:5 msgid "Update asset group" msgstr "更新用户组" @@ -594,6 +568,24 @@ msgstr "二次验证" msgid "Import asset" msgstr "导入资产" +#: assets/templates/assets/_asset_import_modal.html:9 +#: users/templates/users/_user_import_modal.html:10 +msgid "Template" +msgstr "模板" + +#: assets/templates/assets/_asset_import_modal.html:10 +#: users/templates/users/_user_import_modal.html:11 +msgid "Download" +msgstr "下载" + +#: assets/templates/assets/_asset_import_modal.html:13 +msgid "Asset csv file" +msgstr "资产csv文件" + +#: assets/templates/assets/_asset_import_modal.html:16 +msgid "If set id, will use this id update asset existed" +msgstr "如果设置了id,则会使用该行信息更新该id的资产" + #: assets/templates/assets/_asset_list_modal.html:7 assets/views/asset.py:50 #: templates/_nav.html:23 msgid "Asset list" @@ -637,7 +629,7 @@ msgstr "其它" #: assets/templates/assets/asset_update.html:70 #: assets/templates/assets/domain_create_update.html:16 #: assets/templates/assets/gateway_create_update.html:58 -#: assets/templates/assets/label_create_update.html:16 +#: assets/templates/assets/label_create_update.html:18 #: common/templates/common/basic_setting.html:58 #: common/templates/common/email_setting.html:59 #: common/templates/common/ldap_setting.html:59 @@ -647,7 +639,7 @@ msgstr "其它" #: users/templates/users/_user.html:43 #: users/templates/users/user_bulk_update.html:23 #: users/templates/users/user_password_update.html:58 -#: users/templates/users/user_profile.html:151 +#: users/templates/users/user_profile.html:180 #: users/templates/users/user_profile_update.html:63 #: users/templates/users/user_pubkey_update.html:70 #: users/templates/users/user_pubkey_update.html:76 @@ -662,7 +654,7 @@ msgstr "重置" #: assets/templates/assets/asset_update.html:71 #: assets/templates/assets/domain_create_update.html:17 #: assets/templates/assets/gateway_create_update.html:59 -#: assets/templates/assets/label_create_update.html:17 +#: assets/templates/assets/label_create_update.html:19 #: common/templates/common/basic_setting.html:59 #: common/templates/common/email_setting.html:60 #: common/templates/common/ldap_setting.html:60 @@ -740,7 +732,7 @@ msgstr "测试" #: assets/templates/assets/asset_list.html:170 #: assets/templates/assets/domain_detail.html:24 #: assets/templates/assets/domain_detail.html:103 -#: assets/templates/assets/domain_gateway_list.html:90 +#: assets/templates/assets/domain_gateway_list.html:85 #: assets/templates/assets/domain_list.html:42 #: assets/templates/assets/label_list.html:38 #: assets/templates/assets/system_user_detail.html:26 @@ -753,8 +745,8 @@ msgstr "测试" #: users/templates/users/user_group_detail.html:28 #: users/templates/users/user_group_list.html:43 #: users/templates/users/user_list.html:76 -#: users/templates/users/user_profile.html:135 #: users/templates/users/user_profile.html:143 +#: users/templates/users/user_profile.html:172 msgid "Update" msgstr "更新" @@ -764,7 +756,7 @@ msgstr "更新" #: assets/templates/assets/asset_list.html:171 #: assets/templates/assets/domain_detail.html:28 #: assets/templates/assets/domain_detail.html:104 -#: assets/templates/assets/domain_gateway_list.html:91 +#: assets/templates/assets/domain_gateway_list.html:86 #: assets/templates/assets/domain_list.html:43 #: assets/templates/assets/label_list.html:39 #: assets/templates/assets/system_user_detail.html:30 @@ -796,13 +788,13 @@ msgstr "选择节点" #: assets/templates/assets/system_user_detail.html:183 #: assets/templates/assets/system_user_list.html:138 templates/_modal.html:22 #: terminal/templates/terminal/session_detail.html:108 -#: users/templates/users/user_detail.html:339 -#: users/templates/users/user_detail.html:364 -#: users/templates/users/user_detail.html:387 +#: users/templates/users/user_detail.html:357 +#: users/templates/users/user_detail.html:382 +#: users/templates/users/user_detail.html:405 #: users/templates/users/user_group_create_update.html:32 #: users/templates/users/user_group_list.html:86 #: users/templates/users/user_list.html:196 -#: users/templates/users/user_profile.html:185 +#: users/templates/users/user_profile.html:214 msgid "Confirm" msgstr "确认" @@ -852,15 +844,15 @@ msgid "Disk" msgstr "硬盘" #: assets/templates/assets/asset_detail.html:121 -#: users/templates/users/user_detail.html:103 -#: users/templates/users/user_profile.html:88 +#: users/templates/users/user_detail.html:111 +#: users/templates/users/user_profile.html:96 msgid "Date joined" msgstr "创建日期" #: assets/templates/assets/asset_detail.html:137 #: terminal/templates/terminal/session_detail.html:81 -#: users/templates/users/user_detail.html:122 -#: users/templates/users/user_profile.html:126 +#: users/templates/users/user_detail.html:130 +#: users/templates/users/user_profile.html:134 msgid "Quick modify" msgstr "快速修改" @@ -873,7 +865,7 @@ msgstr "快速修改" #: perms/templates/perms/asset_permission_list.html:59 #: terminal/templates/terminal/terminal_list.html:34 #: users/templates/users/_select_user_modal.html:18 -#: users/templates/users/user_detail.html:128 +#: users/templates/users/user_detail.html:136 #: users/templates/users/user_granted_asset.html:46 #: users/templates/users/user_group_granted_asset.html:46 #: users/templates/users/user_list.html:27 @@ -890,7 +882,8 @@ msgid "Refresh" msgstr "刷新" #: assets/templates/assets/asset_detail.html:300 -#: users/templates/users/user_detail.html:273 +#: users/templates/users/user_detail.html:282 +#: users/templates/users/user_detail.html:304 msgid "Update successfully!" msgstr "更新成功" @@ -978,8 +971,8 @@ msgstr "存在资产,不能删除" #: assets/templates/assets/asset_list.html:595 #: assets/templates/assets/system_user_list.html:133 -#: users/templates/users/user_detail.html:334 -#: users/templates/users/user_detail.html:359 +#: users/templates/users/user_detail.html:352 +#: users/templates/users/user_detail.html:377 #: users/templates/users/user_group_list.html:81 #: users/templates/users/user_list.html:191 msgid "Are you sure?" @@ -1032,7 +1025,7 @@ msgstr "网关列表" msgid "Create gateway" msgstr "创建网关" -#: assets/templates/assets/domain_gateway_list.html:92 +#: assets/templates/assets/domain_gateway_list.html:87 #: common/templates/common/email_setting.html:58 #: common/templates/common/ldap_setting.html:58 msgid "Test connection" @@ -1242,14 +1235,18 @@ msgstr "%(name)s 创建成功" msgid "%(name)s was updated successfully" msgstr "%(name)s 更新成功" -#: common/fields.py:26 +#: common/fields.py:30 msgid "Not a valid json" msgstr "不是合法json" -#: common/fields.py:28 +#: common/fields.py:32 msgid "Not a string type" msgstr "不是字符类型" +#: common/fields.py:69 +msgid "Encrypt field using Secret Key" +msgstr "" + #: common/forms.py:70 msgid "Current SITE URL" msgstr "当前站点URL" @@ -1389,7 +1386,7 @@ msgstr "" msgid "discard time" msgstr "" -#: common/models.py:29 +#: common/models.py:29 users/templates/users/user_detail.html:96 msgid "Enabled" msgstr "启用" @@ -1707,8 +1704,8 @@ msgstr "任务列表" msgid "Task run history" msgstr "执行历史" -#: perms/forms.py:18 users/forms.py:176 users/forms.py:181 users/forms.py:193 -#: users/forms.py:223 +#: perms/forms.py:18 users/forms.py:188 users/forms.py:193 users/forms.py:205 +#: users/forms.py:235 msgid "Select users" msgstr "选择用户" @@ -1717,7 +1714,7 @@ msgstr "选择用户" #: perms/templates/perms/asset_permission_list.html:136 templates/_nav.html:14 #: users/models/group.py:25 users/models/user.py:42 #: users/templates/users/_select_user_modal.html:16 -#: users/templates/users/user_detail.html:179 +#: users/templates/users/user_detail.html:188 #: users/templates/users/user_list.html:26 msgid "User group" msgstr "用户组" @@ -1732,8 +1729,8 @@ msgstr "" #: perms/models.py:27 perms/models.py:71 #: perms/templates/perms/asset_permission_detail.html:90 -#: users/models/user.py:54 users/templates/users/user_detail.html:95 -#: users/templates/users/user_profile.html:96 +#: users/models/user.py:54 users/templates/users/user_detail.html:103 +#: users/templates/users/user_profile.html:104 msgid "Date expired" msgstr "失效日期" @@ -1770,7 +1767,7 @@ msgid "Add node to this permission" msgstr "添加节点" #: perms/templates/perms/asset_permission_asset.html:125 -#: users/templates/users/user_detail.html:196 +#: users/templates/users/user_detail.html:205 msgid "Join" msgstr "加入" @@ -1856,13 +1853,13 @@ msgstr "商业支持" msgid "Docs" msgstr "文档" -#: templates/_header_bar.html:37 templates/_nav_user.html:9 users/forms.py:93 +#: templates/_header_bar.html:37 templates/_nav_user.html:9 users/forms.py:105 #: users/templates/users/_user.html:36 #: users/templates/users/user_password_update.html:37 #: users/templates/users/user_profile.html:17 #: users/templates/users/user_profile_update.html:37 #: users/templates/users/user_profile_update.html:57 -#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:318 +#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:322 msgid "Profile" msgstr "个人信息" @@ -1919,13 +1916,13 @@ msgstr "关闭" #: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:44 #: users/views/group.py:62 users/views/group.py:79 users/views/group.py:95 -#: users/views/login.py:205 users/views/login.py:254 users/views/user.py:60 -#: users/views/user.py:75 users/views/user.py:95 users/views/user.py:151 -#: users/views/user.py:306 users/views/user.py:353 users/views/user.py:375 +#: users/views/login.py:240 users/views/login.py:289 users/views/user.py:64 +#: users/views/user.py:79 users/views/user.py:99 users/views/user.py:155 +#: users/views/user.py:310 users/views/user.py:357 users/views/user.py:379 msgid "Users" msgstr "用户管理" -#: templates/_nav.html:13 users/views/user.py:61 +#: templates/_nav.html:13 users/views/user.py:65 msgid "User list" msgstr "用户列表" @@ -2231,7 +2228,11 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: users/forms.py:27 users/models/user.py:43 +#: users/forms.py:30 +msgid "Otp_code" +msgstr "" + +#: users/forms.py:39 users/models/user.py:43 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:25 @@ -2239,57 +2240,57 @@ msgstr "" msgid "Role" msgstr "角色" -#: users/forms.py:29 users/forms.py:139 +#: users/forms.py:41 users/forms.py:151 msgid "ssh public key" msgstr "ssh公钥" -#: users/forms.py:30 users/forms.py:140 +#: users/forms.py:42 users/forms.py:152 msgid "ssh-rsa AAAA..." msgstr "" -#: users/forms.py:31 +#: users/forms.py:43 msgid "Paste user id_rsa.pub here." msgstr "复制用户公钥到这里" -#: users/forms.py:49 users/templates/users/user_detail.html:187 +#: users/forms.py:61 users/templates/users/user_detail.html:196 msgid "Join user groups" msgstr "添加到用户组" -#: users/forms.py:59 users/forms.py:154 +#: users/forms.py:71 users/forms.py:166 msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:63 users/forms.py:158 users/serializers.py:42 +#: users/forms.py:75 users/forms.py:170 users/serializers.py:45 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/forms.py:99 +#: users/forms.py:111 msgid "Old password" msgstr "原来密码" -#: users/forms.py:104 +#: users/forms.py:116 msgid "New password" msgstr "新密码" -#: users/forms.py:109 +#: users/forms.py:121 msgid "Confirm password" msgstr "确认密码" -#: users/forms.py:119 +#: users/forms.py:131 msgid "Old password error" msgstr "原来密码错误" -#: users/forms.py:127 +#: users/forms.py:139 msgid "Password does not match" msgstr "密码不一致" -#: users/forms.py:141 +#: users/forms.py:153 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:169 users/models/user.py:51 +#: users/forms.py:181 users/models/user.py:51 #: users/templates/users/user_password_update.html:43 -#: users/templates/users/user_profile.html:71 +#: users/templates/users/user_profile.html:79 #: users/templates/users/user_profile_update.html:43 #: users/templates/users/user_pubkey_update.html:43 msgid "Public key" @@ -2319,7 +2320,7 @@ msgstr "Agent" msgid "Date login" msgstr "登录日期" -#: users/models/user.py:29 users/models/user.py:281 +#: users/models/user.py:29 users/models/user.py:295 msgid "Administrator" msgstr "管理员" @@ -2327,15 +2328,18 @@ msgstr "管理员" msgid "Application" msgstr "应用程序" -#: users/models/user.py:34 +#: users/models/user.py:34 users/templates/users/user_profile.html:74 +#: users/templates/users/user_profile.html:155 +#: users/templates/users/user_profile.html:158 msgid "Disable" msgstr "禁用" -#: users/models/user.py:35 +#: users/models/user.py:35 users/templates/users/user_profile.html:72 +#: users/templates/users/user_profile.html:162 msgid "Enable" msgstr "启用" -#: users/models/user.py:36 +#: users/models/user.py:36 users/templates/users/user_profile.html:70 msgid "Force enable" msgstr "强制启用" @@ -2352,11 +2356,11 @@ msgstr "头像" msgid "Wechat" msgstr "微信" -#: users/models/user.py:47 +#: users/models/user.py:47 users/templates/users/user_detail.html:91 msgid "Enable OTP" msgstr "二次验证" -#: users/models/user.py:284 +#: users/models/user.py:298 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -2408,11 +2412,16 @@ msgstr "Step" #: users/templates/users/first_login.html:57 msgid "Previous" -msgstr "" +msgstr "上一步" #: users/templates/users/first_login.html:60 +#: users/templates/users/login_otp.html:66 +#: users/templates/users/user_otp_authentication.html:22 +#: users/templates/users/user_otp_enable_bind.html:25 +#: users/templates/users/user_otp_enable_install_app.html:22 +#: users/templates/users/user_password_authentication.html:21 msgid "Next" -msgstr "" +msgstr "下一步" #: users/templates/users/first_login_done.html:30 msgid "Welcome to use jumpserver, visit " @@ -2447,8 +2456,22 @@ msgstr "Agent" msgid "City" msgstr "城市" +#: users/templates/users/login_otp.html:45 +msgid "二次认证" +msgstr "" + +#: users/templates/users/login_otp.html:64 +#: users/templates/users/user_otp_authentication.html:19 +#: users/templates/users/user_otp_enable_bind.html:18 +msgid "Six figures" +msgstr "6位数字" + +#: users/templates/users/login_otp.html:69 +msgid "Can't provide security? Please contact the administrator" +msgstr "如果不能提供OTP码,请联系管理员" + #: users/templates/users/reset_password.html:45 -#: users/templates/users/user_detail.html:325 users/utils.py:71 +#: users/templates/users/user_detail.html:343 users/utils.py:72 msgid "Reset password" msgstr "重置密码" @@ -2456,8 +2479,13 @@ msgstr "重置密码" msgid "Password again" msgstr "再次输入密码" +#: users/templates/users/reset_password.html:57 +#: users/templates/users/user_profile.html:20 +msgid "Setting" +msgstr "设置" + #: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:16 users/views/user.py:75 +#: users/templates/users/user_list.html:16 users/views/user.py:79 msgid "Create user" msgstr "创建用户" @@ -2466,7 +2494,7 @@ msgid "Reset link will be generated and sent to the user. " msgstr "生成重置密码连接,通过邮件发送给用户" #: users/templates/users/user_detail.html:19 -#: users/templates/users/user_granted_asset.html:18 users/views/user.py:152 +#: users/templates/users/user_granted_asset.html:18 users/views/user.py:156 msgid "User detail" msgstr "用户详情" @@ -2477,55 +2505,67 @@ msgstr "用户详情" msgid "Asset granted" msgstr "授权的资产" -#: users/templates/users/user_detail.html:107 -#: users/templates/users/user_profile.html:92 +#: users/templates/users/user_detail.html:94 +msgid "Force enabled" +msgstr "强制启用" + +#: users/templates/users/user_detail.html:98 +msgid "Disabled" +msgstr "禁用" + +#: users/templates/users/user_detail.html:115 +#: users/templates/users/user_profile.html:100 msgid "Last login" msgstr "最后登录" -#: users/templates/users/user_detail.html:157 +#: users/templates/users/user_detail.html:151 +msgid "Force enabled OTP" +msgstr "强制启用OTP" + +#: users/templates/users/user_detail.html:166 msgid "Send reset password mail" msgstr "发送重置密码邮件" -#: users/templates/users/user_detail.html:160 -#: users/templates/users/user_detail.html:168 +#: users/templates/users/user_detail.html:169 +#: users/templates/users/user_detail.html:177 msgid "Send" msgstr "发送" -#: users/templates/users/user_detail.html:165 +#: users/templates/users/user_detail.html:174 msgid "Send reset ssh key mail" msgstr "发送重置密钥邮件" -#: users/templates/users/user_detail.html:324 +#: users/templates/users/user_detail.html:342 msgid "An e-mail has been sent to the user`s mailbox." msgstr "已发送邮件到用户邮箱" -#: users/templates/users/user_detail.html:335 +#: users/templates/users/user_detail.html:353 msgid "This will reset the user password and send a reset mail" msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" -#: users/templates/users/user_detail.html:349 +#: users/templates/users/user_detail.html:367 msgid "" "The reset-ssh-public-key E-mail has been sent successfully. Please inform " "the user to update his new ssh public key." msgstr "重设密钥邮件将会发送到用户邮箱" -#: users/templates/users/user_detail.html:350 +#: users/templates/users/user_detail.html:368 msgid "Reset SSH public key" msgstr "重置SSH密钥" -#: users/templates/users/user_detail.html:360 +#: users/templates/users/user_detail.html:378 msgid "This will reset the user public key and send a reset mail" msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" -#: users/templates/users/user_detail.html:377 -#: users/templates/users/user_profile.html:174 +#: users/templates/users/user_detail.html:395 +#: users/templates/users/user_profile.html:203 msgid "Successfully updated the SSH public key." msgstr "更新ssh密钥成功" -#: users/templates/users/user_detail.html:378 -#: users/templates/users/user_detail.html:382 -#: users/templates/users/user_profile.html:175 -#: users/templates/users/user_profile.html:180 +#: users/templates/users/user_detail.html:396 +#: users/templates/users/user_detail.html:400 +#: users/templates/users/user_profile.html:204 +#: users/templates/users/user_profile.html:209 msgid "User SSH public key update" msgstr "ssh密钥" @@ -2585,24 +2625,28 @@ msgstr "用户删除失败" msgid "OTP" msgstr "" -#: users/templates/users/user_profile.html:100 users/views/user.py:181 -#: users/views/user.py:235 +#: users/templates/users/user_profile.html:108 users/views/user.py:185 +#: users/views/user.py:239 msgid "User groups" msgstr "用户组" -#: users/templates/users/user_profile.html:132 +#: users/templates/users/user_profile.html:140 msgid "Update password" msgstr "更改密码" -#: users/templates/users/user_profile.html:140 +#: users/templates/users/user_profile.html:148 +msgid "Update otp" +msgstr "更改OTP设置" + +#: users/templates/users/user_profile.html:169 msgid "Update SSH public key" msgstr "更改SSH密钥" -#: users/templates/users/user_profile.html:148 +#: users/templates/users/user_profile.html:177 msgid "Reset public key and download" msgstr "重置并下载SSH密钥" -#: users/templates/users/user_profile.html:178 +#: users/templates/users/user_profile.html:207 msgid "Failed to update SSH public key." msgstr "更新密钥失败" @@ -2622,15 +2666,15 @@ msgstr "更新密钥" msgid "Or reset by server" msgstr "或者重置并下载密钥" -#: users/templates/users/user_update.html:4 users/views/user.py:95 +#: users/templates/users/user_update.html:4 users/views/user.py:99 msgid "Update user" msgstr "更新用户" -#: users/utils.py:35 +#: users/utils.py:36 msgid "Create account successfully" msgstr "创建账户成功" -#: users/utils.py:37 +#: users/utils.py:38 #, python-format msgid "" "\n" @@ -2671,7 +2715,7 @@ msgstr "" "
\n" " " -#: users/utils.py:73 +#: users/utils.py:74 #, python-format msgid "" "\n" @@ -2715,11 +2759,11 @@ msgstr "" "
\n" " " -#: users/utils.py:104 +#: users/utils.py:105 msgid "SSH Key Reset" msgstr "重置ssh密钥" -#: users/utils.py:106 +#: users/utils.py:107 #, python-format msgid "" "\n" @@ -2744,15 +2788,15 @@ msgstr "" "
\n" " " -#: users/utils.py:139 +#: users/utils.py:140 msgid "User not exist" msgstr "用户不存在" -#: users/utils.py:141 +#: users/utils.py:142 msgid "Disabled or expired" msgstr "禁用或失效" -#: users/utils.py:154 +#: users/utils.py:155 msgid "Password or SSH public key invalid" msgstr "密码或密钥不合法" @@ -2768,78 +2812,102 @@ msgstr "更新用户组" msgid "User group granted asset" msgstr "用户组授权资产" -#: users/views/login.py:55 +#: users/views/login.py:56 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: users/views/login.py:97 +#: users/views/login.py:106 users/views/user.py:460 users/views/user.py:485 +msgid "Otp code invalid" +msgstr "otp码认证失败" + +#: users/views/login.py:132 msgid "Logout success" msgstr "退出登录成功" -#: users/views/login.py:98 +#: users/views/login.py:133 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: users/views/login.py:114 +#: users/views/login.py:149 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/login.py:127 +#: users/views/login.py:162 msgid "Send reset password message" msgstr "发送重置密码邮件" -#: users/views/login.py:128 +#: users/views/login.py:163 msgid "Send reset password mail success, login your mail box and follow it " msgstr "" "发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" -#: users/views/login.py:142 +#: users/views/login.py:177 msgid "Reset password success" msgstr "重置密码成功" -#: users/views/login.py:143 +#: users/views/login.py:178 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/login.py:160 users/views/login.py:173 +#: users/views/login.py:195 users/views/login.py:208 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/login.py:169 +#: users/views/login.py:204 msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:205 +#: users/views/login.py:240 msgid "First login" msgstr "首次登陆" -#: users/views/login.py:255 +#: users/views/login.py:290 msgid "Login log list" msgstr "登录日志" -#: users/views/user.py:105 +#: users/views/user.py:109 msgid "Bulk update user success" msgstr "批量更新用户成功" -#: users/views/user.py:210 +#: users/views/user.py:214 msgid "Invalid file." msgstr "文件不合法" -#: users/views/user.py:307 +#: users/views/user.py:311 msgid "User granted assets" msgstr "用户授权资产" -#: users/views/user.py:336 +#: users/views/user.py:340 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/user.py:354 +#: users/views/user.py:358 msgid "Password update" msgstr "密码更新" -#: users/views/user.py:376 +#: users/views/user.py:380 msgid "Public key update" msgstr "密钥更新" +#: users/views/user.py:419 +msgid "Password invalid" +msgstr "用户名或密码无效" + +#: users/views/user.py:512 +msgid "OTP enable success" +msgstr "OTP 绑定成功" + +#: users/views/user.py:513 +msgid "OTP enable success, return login page" +msgstr "OTP 绑定成功,返回到登录页面" + +#: users/views/user.py:515 +msgid "OTP disable success" +msgstr "OTP 解绑成功" + +#: users/views/user.py:516 +msgid "OTP disable success, return login page" +msgstr "OTP 解绑成功,返回登录页面" + #~ msgid "Add asset" #~ msgstr "添加资产到节点" diff --git a/apps/users/api.py b/apps/users/api.py index c64c52b44..1f7e4f792 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -2,6 +2,7 @@ import uuid from django.core.cache import cache +from django.urls import reverse from rest_framework import generics from rest_framework.permissions import AllowAny, IsAuthenticated @@ -139,40 +140,75 @@ class UserProfile(APIView): return Response(self.serializer_class(request.user).data) +class UserOtpAuthApi(APIView): + permission_classes = (AllowAny,) + serializer_class = UserSerializer + + def post(self, request): + otp_code = request.data.get('otp_code', '') + seed = request.data.get('seed', '') + + user = cache.get(seed, None) + if not user: + return Response({'msg': '请先进行用户名和密码验证'}, status=401) + + if not check_otp_code(user.otp_secret_key, otp_code): + return Response({'msg': 'otp认证失败'}, status=401) + + token = generate_token(request, user) + self.write_login_log(request, user) + return Response( + { + 'token': token, + 'user': self.serializer_class(user).data + } + ) + + @staticmethod + def write_login_log(request, user): + login_ip = request.data.get('remote_addr', None) + login_type = request.data.get('login_type', '') + user_agent = request.data.get('HTTP_USER_AGENT', '') + + if not login_ip: + login_ip = get_login_ip(request) + + write_login_log_async.delay( + user.username, ip=login_ip, + type=login_type, user_agent=user_agent, + ) + + class UserAuthApi(APIView): permission_classes = (AllowAny,) serializer_class = UserSerializer def post(self, request): - otp_check = request.data.get('otp_check', None) - - if otp_check: - # otp验证 - return self.check_auth_otp(request) - else: - # password验证 - return self.check_auth_password(request) - - def check_auth_password(self, request): user, msg = self.check_user_valid(request) - if user: - token = generate_token(request, user) - if not user.otp_enabled: - self.write_login_log(request, user) - return Response({'token': token, 'user': self.serializer_class(user).data}) - else: + if not user: return Response({'msg': msg}, status=401) - def check_auth_otp(self, request): - otp_code = request.data.get('otp_code', '') - user, msg = self.check_user_valid(request) - if user: + if not user.otp_enabled: token = generate_token(request, user) - if check_otp_code(user.otp_secret_key, otp_code): - self.write_login_log(request, user) - return Response({'token': token, 'user': self.serializer_class(user).data}) - return Response({'msg': msg}, status=401) + self.write_login_log(request, user) + return Response( + { + 'token': token, + 'user': self.serializer_class(user).data + } + ) + + seed = uuid.uuid4().hex + cache.set(seed, user, 300) + return Response( + { + 'code': 101, + 'msg': '请携带seed值,进行OTP二次认证', + 'otp_url': reverse('api-users:user-otp-auth'), + 'seed': seed, + 'user': self.serializer_class(user).data + }, status=300) @staticmethod def check_user_valid(request): diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 4674fe519..d64c038d8 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -232,7 +232,7 @@ class User(AbstractUser): def disable_otp(self): self.otp_level = 0 - self.otp_secret_key = '' + self.otp_secret_key = None def to_json(self): return OrderedDict({ diff --git a/apps/users/serializers.py b/apps/users/serializers.py index 450c63823..f1347b0d5 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -21,7 +21,7 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): list_serializer_class = BulkListSerializer exclude = [ 'first_name', 'last_name', 'password', '_private_key', - '_public_key', 'otp_secret_key', 'user_permissions' + '_public_key', '_otp_secret_key', 'user_permissions' ] def get_field_names(self, declared_fields, info): diff --git a/apps/users/templates/users/_base_otp.html b/apps/users/templates/users/_base_otp.html index fb32fb425..cd9c43edc 100644 --- a/apps/users/templates/users/_base_otp.html +++ b/apps/users/templates/users/_base_otp.html @@ -25,7 +25,7 @@ diff --git a/apps/users/templates/users/login_otp.html b/apps/users/templates/users/login_otp.html index e8bfc75e8..80f5dc429 100644 --- a/apps/users/templates/users/login_otp.html +++ b/apps/users/templates/users/login_otp.html @@ -66,7 +66,7 @@ - {% trans "Can't provide security? Please contact the administrator" %} + {% trans "Can't provide otp code? Please contact the administrator" %} diff --git a/apps/users/templates/users/user_otp_enable_bind.html b/apps/users/templates/users/user_otp_enable_bind.html index e97f7cf9e..7e3d19cf9 100644 --- a/apps/users/templates/users/user_otp_enable_bind.html +++ b/apps/users/templates/users/user_otp_enable_bind.html @@ -7,10 +7,8 @@

使用手机 Google Authenticator 应用扫描以下二维码,获取6位验证码

-
-
{% csrf_token %} @@ -18,12 +16,13 @@
+ + + {% if 'otp_code' in form.errors %}

{{ form.otp_code.errors.as_text }}

{% endif %} - - diff --git a/apps/users/templates/users/user_password_authentication.html b/apps/users/templates/users/user_password_authentication.html index 7603a1149..773700241 100644 --- a/apps/users/templates/users/user_password_authentication.html +++ b/apps/users/templates/users/user_password_authentication.html @@ -5,6 +5,7 @@ {% block content %}
{% csrf_token %} +
@@ -13,13 +14,12 @@ + + {% if 'password' in form.errors %}

{{ form.password.errors.as_text }}

{% endif %} - - -
{% endblock %} diff --git a/apps/users/templates/users/user_profile.html b/apps/users/templates/users/user_profile.html index a924b9f02..6cda70eb9 100644 --- a/apps/users/templates/users/user_profile.html +++ b/apps/users/templates/users/user_profile.html @@ -152,8 +152,7 @@ href=" {% if request.user.otp_enabled and request.user.otp_secret_key %} {% if request.user.otp_force_enabled %} - javascript:void(0) - ">{% trans 'Disable' %} + " disabled >{% trans 'Disable' %} {% else %} {% url 'users:user-otp-disable-authentication' %} ">{% trans 'Disable' %} diff --git a/apps/users/urls/api_urls.py b/apps/users/urls/api_urls.py index ce681c146..683638a4e 100644 --- a/apps/users/urls/api_urls.py +++ b/apps/users/urls/api_urls.py @@ -20,6 +20,7 @@ urlpatterns = [ url(r'^v1/connection-token/$', api.UserConnectionTokenApi.as_view(), name='connection-token'), url(r'^v1/profile/$', api.UserProfile.as_view(), name='user-profile'), url(r'^v1/auth/$', api.UserAuthApi.as_view(), name='user-auth'), + url(r'^v1/otp/auth/$', api.UserOtpAuthApi.as_view(), name='user-otp-auth'), url(r'^v1/users/(?P[0-9a-zA-Z\-]{36})/password/$', api.ChangeUserPasswordApi.as_view(), name='change-user-password'), url(r'^v1/users/(?P[0-9a-zA-Z\-]{36})/password/reset/$', diff --git a/apps/users/utils.py b/apps/users/utils.py index 8113ec358..94368e0c7 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -224,16 +224,14 @@ def get_ip_city(ip, timeout=10): return city -def get_user(request): - if is_login(request): - user = request.user - else: - user = cache.get(request.session.session_key) +def get_tmp_user_from_session(request): + user_id = request.session.get('tmp_user_id') + user = get_object_or_none(User, pk=user_id) return user -def is_login(request): - return isinstance(request.user, User) +def set_tmp_user_to_session(request, user): + request.session['tmp_user_id'] = str(user.id) def redirect_user_first_login_or_index(request, redirect_field_name): @@ -244,9 +242,15 @@ def redirect_user_first_login_or_index(request, redirect_field_name): request.GET.get(redirect_field_name, reverse('index'))) -def generate_otp_uri(user, issuer="Jumpserver"): - otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8') - cache.set('otp_secret_key', otp_secret_key, 300) +def generate_otp_uri(request, issuer="Jumpserver"): + if request.user.is_authenticated: + user = request.user + else: + user = get_tmp_user_from_session(request) + otp_secret_key = cache.get(request.session.session_key+'otp_key', '') + if not otp_secret_key: + otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8') + cache.set(request.session.session_key+'otp_key', otp_secret_key, 600) totp = pyotp.TOTP(otp_secret_key) return totp.provisioning_uri(name=user.username, issuer_name=issuer) diff --git a/apps/users/views/login.py b/apps/users/views/login.py index bb18b28da..d7a98e174 100644 --- a/apps/users/views/login.py +++ b/apps/users/views/login.py @@ -19,12 +19,12 @@ from django.views.generic.base import TemplateView from django.views.generic.edit import FormView from formtools.wizard.views import SessionWizardView from django.conf import settings -from django.core.cache import cache from common.utils import get_object_or_none from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin from ..models import User, LoginLog -from ..utils import send_reset_password_mail, check_otp_code , get_login_ip, redirect_user_first_login_or_index +from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, redirect_user_first_login_or_index, \ + get_tmp_user_from_session, set_tmp_user_to_session from ..tasks import write_login_log_async from .. import forms @@ -54,11 +54,12 @@ class UserLoginView(FormView): def form_valid(self, form): if not self.request.session.test_cookie_worked(): return HttpResponse(_("Please enable cookies and try again.")) - cache.set(self.request.session.session_key, form.get_user(), 600) + + set_tmp_user_to_session(self.request, form.get_user()) return redirect(self.get_success_url()) def get_success_url(self): - user = cache.get(self.request.session.session_key) + user = get_tmp_user_from_session(self.request) if user.otp_enabled and user.otp_secret_key: # 1,2 & T @@ -94,7 +95,7 @@ class UserLoginOtpView(FormView): redirect_field_name = 'next' def form_valid(self, form): - user = cache.get(self.request.session.session_key) + user = get_tmp_user_from_session(self.request) otp_code = form.cleaned_data.get('otp_code') otp_secret_key = user.otp_secret_key diff --git a/apps/users/views/user.py b/apps/users/views/user.py index af954ba10..99c45b19c 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -35,7 +35,7 @@ from common.mixins import JSONResponseMixin from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen from .. import forms from ..models import User, UserGroup -from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, get_user, is_login +from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, get_tmp_user_from_session from ..signals import post_user_create from ..tasks import write_login_log_async @@ -400,20 +400,31 @@ class UserOtpEnableAuthenticationView(FormView): form_class = forms.UserCheckPasswordForm def get_form(self, form_class=None): + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) form = super().get_form(form_class=form_class) - form['username'].initial = get_user(self.request).username + form['username'].initial = user.username return form def get_context_data(self, **kwargs): + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) context = { - 'user': get_user(self.request) + 'user': user } kwargs.update(context) return super().get_context_data(**kwargs) def form_valid(self, form): + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) password = form.cleaned_data.get('password') - user = get_user(self.request) user = authenticate(username=user.username, password=password) if not user: form.add_error("password", _("Password invalid")) @@ -428,8 +439,12 @@ class UserOtpEnableInstallAppView(TemplateView): template_name = 'users/user_otp_enable_install_app.html' def get_context_data(self, **kwargs): + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) context = { - 'user': get_user(self.request) + 'user': user } kwargs.update(context) return super().get_context_data(**kwargs) @@ -441,16 +456,20 @@ class UserOtpEnableBindView(TemplateView, FormView): success_url = reverse_lazy('users:user-otp-settings-success') def get_context_data(self, **kwargs): + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) context = { - 'otp_uri': generate_otp_uri(user=get_user(self.request)), - 'user': get_user(self.request) + 'otp_uri': generate_otp_uri(self.request), + 'user': user } kwargs.update(context) return super().get_context_data(**kwargs) def form_valid(self, form): otp_code = form.cleaned_data.get('otp_code') - otp_secret_key = cache.get('otp_secret_key') + otp_secret_key = cache.get(self.request.session.session_key+'otp_key', '') if check_otp_code(otp_secret_key, otp_code): self.save_otp(otp_secret_key) @@ -461,7 +480,10 @@ class UserOtpEnableBindView(TemplateView, FormView): return self.form_invalid(form) def save_otp(self, otp_secret_key): - user = get_user(self.request) + if self.request.user.is_authenticated: + user = self.request.user + else: + user = get_tmp_user_from_session(self.request) user.enable_otp() user.otp_secret_key = otp_secret_key user.save() @@ -489,11 +511,8 @@ class UserOtpDisableAuthenticationView(FormView): class UserOtpSettingsSuccessView(TemplateView): template_name = 'flash_message_standalone.html' - def get(self, request, *args, **kwargs): - response = super().get(request, *args, **kwargs) - if is_login(request): - auth_logout(request) - return response + # def get(self, request, *args, **kwargs): + # return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): title, describe = self.get_title_describe() @@ -508,7 +527,11 @@ class UserOtpSettingsSuccessView(TemplateView): return super().get_context_data(**kwargs) def get_title_describe(self): - user = get_user(self.request) + if self.request.user.is_authenticated: + user = self.request.user + auth_logout(self.request) + else: + user = get_tmp_user_from_session(self.request) title = _('OTP enable success') describe = _('OTP enable success, return login page') if not user.otp_enabled: diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 790c19eb7..e0ecc634e 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -54,6 +54,7 @@ pyasn1==0.4.2 pycparser==2.18 pycrypto==2.6.1 pyldap==2.4.45 +pyotp==2.2.6 PyNaCl==1.2.1 python-dateutil==2.6.1 python-gssapi==0.6.4