From 41cdd5ec7fc9fdab754154cabce99d04a156e7c4 Mon Sep 17 00:00:00 2001 From: Kobe Date: Wed, 4 Jun 2025 13:44:49 +0200 Subject: [PATCH] better password management --- __pycache__/app.cpython-313.pyc | Bin 7539 -> 7960 bytes __pycache__/models.cpython-313.pyc | Bin 27882 -> 29344 bytes .../add_trashed_file_table.cpython-313.pyc | Bin 2563 -> 2810 bytes ...add_preferred_view_to_user.cpython-313.pyc | Bin 2043 -> 2383 bytes .../bd04430cda95_merge_heads.cpython-313.pyc | Bin 739 -> 770 bytes ...d_granular_permissions_to_.cpython-313.pyc | Bin 1963 -> 2341 bytes ...dd_profile_picture_to_user.cpython-313.pyc | Bin 3711 -> 4414 bytes ...dd_last_name_to_user_model.cpython-313.pyc | Bin 1330 -> 1925 bytes ..._add_email_templates_table.cpython-313.pyc | Bin 5131 -> 4013 bytes ...ad_add_colorsettings_table.cpython-313.pyc | Bin 2901 -> 3144 bytes ...te_user_starred_file_table.cpython-313.pyc | Bin 2450 -> 2695 bytes ...assword_hash_column_length.cpython-313.pyc | Bin 1527 -> 2131 bytes ...5d2d3ed0_add_contact_model.cpython-313.pyc | Bin 3308 -> 4053 bytes ...age_attachments_table_and_.cpython-313.pyc | Bin 3511 -> 3756 bytes ...888_add_company_logo_field.cpython-313.pyc | Bin 1332 -> 1679 bytes models.py | 22 ++++- routes/__pycache__/auth.cpython-313.pyc | Bin 9855 -> 13624 bytes routes/__pycache__/contacts.cpython-313.pyc | Bin 21616 -> 24668 bytes routes/auth.py | 82 +++++++++++++++++- routes/contacts.py | 77 ++++++++++++++-- templates/auth/setup_password.html | 61 +++++++++++++ templates/contacts/list.html | 9 ++ .../email_templates.cpython-313.pyc | Bin 5900 -> 6243 bytes utils/email_templates.py | 5 +- 24 files changed, 246 insertions(+), 10 deletions(-) create mode 100644 templates/auth/setup_password.html diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 8ac1093dc67e85e828100cfcfbcac747e5579c43..21fb7fe8337e07909721424c415bd22d68abc5d6 100644 GIT binary patch delta 1860 zcmZuxTW=Fb6rS1j`m&C_c6`M#cunFM2XZkXZ384t#6=>A(qe>ch+eE zwMwL39(qwhQwnPPP~KX#Pt8kJ9@sz$JL?3gBkeb5&VA0D zv+MhB413o-9+|+}(>pSItM#fkO3z+RKiI1g&3C+gtb=u^WXidd44@7ym1zDLVX3Lq zCXaPab&j9pYeFF1IzStGS|HZ6VY{X>T5$hqLJB@YKz9;#Js~YTNL4aJXJ{s}vP>nK z{f?Uc8NHS-7zW^5Ix|X@aL#^0ooczh)a%wrEVqoJ1Q#gH_p?D5tySJ|$TN&8N?fu*eiJ)Y+GrS0yB79A8e&d;Powt2GRZ|=E07Oib- zTJ`WZV*#42SKx|0f~jp`rQ%jdZTprBICFa~SD?SI?&1Y>;o5=E2E0L@_r~}$yPxjo zen)~1@vNi$klaC%BsXl?I!GZ(NZFMewK!U0(&Z)nf)!OI38I0N4x(+uQ?m4?pS7Eu~QCdA@HqMgb&^o(;ghj?S);TvIkltel4^W`k=Q0>Gfr z4hVaBU_xK0&KGLBdcHJY)0tYUs4yey#&oE&1!GoSI$vR`Q7hC+MYW!UVG@R|1QX>1 zJH1?3&cvI4Esi1U<0Hzx_%RfSMvY|zljuK*r)T-4(3dpDgW;EGlAj32X&0Xht5oA3hxaN< zZQ}(SW#+fSA1<8$7p7EYrE)FfHXRG483u)CI0X!6gbgCRjPM$Qh5&z1!e$X(MW|Q6 z4V2nkBaPRUH{X#hfkFg6V-mXR=`wjJE1x_0&yjzVkA1}Mx-UiVO3@pMds1di5Txw7 zNbFsYk$NJp{1`nzdsm%wEGn+biDUi3YQH=dl2(VjV?OD&TLS)Vp9GxSAra}QJeKD1 z*e`Ske;C^~cmcCqKqv#4c4*t;+tcht%w|h3x~r^$J({nUitIfQe!vIX`eOBwa^~|3 zg;F`6pU6;Cn%2)P&SZA;rM3}zf&bd}4n4((;!|f#|6;8)Z)~(`oo0CF`sM3&Vl_OX zb2bNHifpmWwsZ>eoCg;w(~I-^F#7_In4rP=U-8k_N2GVnPH4;fo_}m}lqzed`R?t9 HZJ6MHP^Xg2 delta 1543 zcmY*ZUuauZ7(d^=xyilBZjvrd+Qy{0Y0`A*{$%}=*?;R=JJDvWvfLKL1VftMbty|4 zPEt0-*$R8oAD;_!z|W1__PjV{IRe=i}K+2k}lc zyV7uAo5zDk3^b7+GJR985>oUMf|XY~YYy|fP5&eqWF6Ka34!JvP++FinP*2s#@}8- zA>?)KBa%C8MMzE(iK2Z)`_LBE!IaX6p=FJEt^z*Z!ClHbnO`>SY#agY-Z=SJ3g8v6*ONNM&xg=cJm|1)TBWUqMfts? zXV^XOcxa9X(@OX=r<7Hnx_W(ky z66%{NSY?^;TX32E5YE4_KkxpV(GA{$SI{CSlId2Jb{r=6sWX+$wXN&cEWLv!Uw-`q dcB_Bh^TbZz*lqo(%>wW|Sz?d+FWUGW_zxw*5n2EM diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index acd6ac81742e55ce0e1f8882fd370fe4a75faf95..7cefa31d72d69ca6fda084a41f565addb65c7702 100644 GIT binary patch delta 857 zcmZvZ&rcIU6vt=U-BMcG?ozfb6jDl2s7+OVTaX~N6@i1MTP}rUQ*8qU19cYAgQkrd zO^6B9IRGb%F@Yo4gI@gyOdAf?jU-$&MlVDl@g&X^Oib`)_p|T4@4WYBXTE=e*)Pzz zZ!qWpF;mNxiEsTGqYWxsOU=81VHmTM$gCk!nNNk+EOmH0VkxJh zd5jG;W5^3By{Hl^NJbD1H2_&eBcW9^A=^0T=9S8uQCYq>gJNQQb}kv7nTk&*k`Mqz zNS+9iOciOCN^%$($v+{U;2Mfh*GW-L;h?ZktP|WOO{kelR1vDlyFZV)R=caYlmx&7 zdf{I`+5kX37^N$z;g&HW)c`tFA^$?+H}BLNyBP;~M8 z$pnhaQgjw|lY~sSRF5uFO`DLV`RJWQOtGInf%7M#H}-&($SzWl21T=V*_ZJ>fB4Z9 zcx_vEymD+iTQ|dRr7fx84CJ)|oYx0kRhLh~C{}s!uA|dhVO$(s7+kuwoOqGQs`2zj_+7A2;m1e40zMjM z@t#?Cy=S#&Qz-CV>7k09Vq>CAil1^v}@;3osw zAUxDSwjH(6jnLulX8bkr8>aB?WSuTU6668$k6hVQ5!w#`5B>1qEC<_HDqq@GZHIuk GrQ$!NpW`J_*JR8`U?mu%vTnnr>!e{lUVj(M+1^liTzEDTe{I78!#G10ZpW!zMRBr8FniuBeTXfdPmZif?aT RTX2Js`zwPWqZ1=oDFB~oA^-pY diff --git a/migrations/versions/__pycache__/add_trashed_file_table.cpython-313.pyc b/migrations/versions/__pycache__/add_trashed_file_table.cpython-313.pyc index cf6935cb8a07ce184e913b99baa9648037e9ba90..edbed150dc643d82171cdbffb66bf35edfb89015 100644 GIT binary patch delta 1198 zcmaJ>O-vI(6yE9nba&a^0`eC$SSbQE%0a{^AS&VyEk$Lm2~NaCm{ow_A(wjv}2!vtZavzOhO*75}ByjV8w9C)z;HQ-B`~*mHiNZOa42 zCIi2%1FJ9Ff^LEc5NHY1Q8EbdC_-Y#hMgDGF_4FJA(Uh$U=C&!8z@D9tVI1bA*YpO z?tel4ui!T;ladA8g|nJUvQ(fg$6YG$z#KY9GS*u;iuI)h`ig()%ZjHc|A*dD@@KLt z^o2THgv;n+e7*?RplSLDdPcu$6d{Q}O_m)G&y0*lln|s0=rG_+a5NeoQG!!p6C8XzrNo zQ>UZB=-5PfYOW;|2}bdqS0Rmp+^LJkAutQVqlWjVAKN(<3(ZEuUGO9h7(W5^6gJ;U h0Kg9$1E@WT0M*+>^(UeZZFBp@YQXQFuz`BB#&4m4JiY(` literal 2563 zcmb_eO-~y~7~Wm4*W}e<@GNgR}U^2fVFwF0?(I{NKaym^aKQa=77>Sj*IrbrkxCpI#<~)ceZ+_1EP(T8~ zyvQdB2uL2}C$}j1dKeUt0C@&;o~VEIo6l7>Vk5=MLp8FZ=`dm|86Aq^B7CM51D2`)>OmoO-psR75l#;$YTE$F+=&IZMI9l&O~r2KLPwHq1eU1<6L5k;C2@GN_FK&D={` z0pjqCiJ+D<=3sfsG%PZC&9HI!G13YODxX7tVos^c@cdLN{e+lerRU5HOix4WOWQ1_ zr_F5XaY?sex}fEdVv~c#N5bDj^X{$gXC_qGF81Z`65vTvyG0%7o*jl*wOVW_|)AoxG}hSuM)0;i3U2jmD$d3 zuDBPhkm;OpDH_-f z?1hfJHSVvV?`P1rb91My(p?E0JKxu_?w!D)cDQ^DJ|dcR?AA_uC0gk{cI>ZX{4u!u zwy(o0pnl@sb4MzTALmGolS(T`YGd0t+l5;|G}lDAS{#ZhGK;k+M7Ig<42SNJ8uqK^ z*G6rbKO}8>TQsfImU;g9{Ceyc{yzIGMbGA8p^Unz z>a}-3Ow|ZCYg>q(`52)3zYPx2k>z*I3{s42UM9O4>K5H=MQEs&Y#L7dA!^x0AC`^i@nD^yi^xHh+ETx!iNV^PO|Q zVa}QPyXBWd`j)222u!{|G!^3)^md$C=;{0vTkJ(q-&_!nEVf*oE@K@5f0y@$a!m=K4ZmFb9ouI|!j1>cczO!8x22_V|O- z!h01_beVXh7kMO#EOLlI8uK3}5;78J@q^eBEYW|8d+?!@E3e3vATDVGAa;@LD{oqWj-^ciYj zwJEiW6W>qUvrllqdxa^xl=qj(NJ60uC7Pzj zGrvU+FS5Q?9cdW78%DZrq?gXE8|IpxSvAZ>?{@#KAL@o#Jzvu^?}WpPvulE}skLlq z-K$#n1LIk2RqJ0m)=*;`YFAzDdPE*e52ae_ihW*Z)%-St z^b>#ZNK)qIn{q9BdR-j;AF=Mb+FeT=U&_@~fM)Y%P3wQ(g2bLbKX9n!EZ!DD?N_b; zuWfl3c%Q)Az0l30?CY)pGsUdPt!A28Ny(x)AgrY2jP(2l`yJ**D5Hx150{wJLa2i~ zmn4Cfuaw`^k;RxT9V*Rg$fm9rl&C(}m3n1E& z5l@!fV&$4WL_Y&A)b~#KuY|VLTh#epL~2J>Y(zS%S|btzr?pn)54?^ucq_r<3Z}hq F;a{F_`_2FW delta 596 zcmYjNO=wd=5Pq9|Z~xLHFVV)@G)XC?HBziZg+%I4BmOjqhd!x?sv#z?joAFS&sgwc zP{CUezf8bdZxP*u`weTjBPS&AFY=+`Vb9iYlB#ghSa#e1>ViRFWInJGnxpTyfMyab3T~ z_DixynzJUp;1MxF%=`2`{Sas+E;g?$wnnaQk!wAuu_Hf`X5h!^2kEA7W=lV@L*3gn zyh+2)mR?@or195+g-Hh|zY}d>xJKG$q~>XxdnI`MHTDA$a2j-j L2vh+-Ak}{X4;7UX diff --git a/migrations/versions/__pycache__/bd04430cda95_merge_heads.cpython-313.pyc b/migrations/versions/__pycache__/bd04430cda95_merge_heads.cpython-313.pyc index c583a8efa744c8a5bdc666c7fede14ec45b14b4a..5fbb1ecc9f895a03f3860845b77b2b73d8cd94d5 100644 GIT binary patch delta 273 zcmaFN+Qi2DnU|M~0SLI}+hv@c$ScX{GErSln=zQho7s!Ch*g0hn6-#4n5~FCn7N1p zh&hAVr5K91f;oUBcRI5s=fspgZcWBp?3sDR1*yp;6F>-jB6)6IkVf>4pr_vd(MF!exN!quku(>SuE0xRG|f6NfoJ;(`*RM zq#np2XbGdbnbndLGk*`j?_@vRHPU6*rAr5cjx`T}80dm)z7AusU#Q_5UlrCE4Is{# zOeZjz25^QljeyG7g)TA|5CAgp1D(~GK|ppet=&)sD%;I4K<890#zv@0qL1ov{qjcb znm4G_ynYus4x60ZCd5sVMl!`n$|kUW9Yhhno2&gG?TxT)DPdF9ZAppSmJLzsX{QGJDqn8EF!sr zyhY^A`woBLiYU6Uvnn?~@|5H~Ri&|_v=)@sC(JYS7(L(jF7Q4)7oP9PUy7I0)1#HN zQA``9^mxIK%Zgc*J+n7Ta&YUhj(5y^X-=9~7ADJDx}s%@TBf8K1%I}zj92BR*@2SW zw)If>&G6f6udclwTZomDS|xeCn7m#}W(t12tQfzXKsvPt=27(bvIEpzhD#{=1x5du z^|T#tT+wFkWAll_Q6KxsCq+BRImgl!k!G;{bg|nub+)mub%0GLcV$(PE;yoIFbrYcOjOTQGAGI}mdOvq>=&aR##k zNv?EeO^%5LGbf8M8Zl~3c4t(b{GCmSCz=gp3J3&oOuoP*%FB!-!ZrB-vn($Qk_Zn- zgo)L1vJJZ;rd~EAm89tv=SH{#hfYS;$@iG$CRej7GjdGUVv(Bc#p1)pUc?M^`s5uf z)`BY;Z*e3gmLzAy=NA++f<)C>bGamd3}GNHUOD*}yDX3NMGomXPV+ryc`nGCT*xL~ zFOzqLBmV}6z;zBJshllN+r2hk3EqcXkFYH^%FnuGctSe--lp9Wu>X)rE!m2n&~s z5PLGiaY zBo30~Ovx|LOD{@HNiC8F@?|G~Wlv?~o*d1g!YDYomBYdw6bxXgl?s@I_}9dY0qNos-PhnfZS6 zec#Od`rE%dKJPL1bzKFtsc(%g9H^^C3pc&m*MFKro+pJTBrKT#iC9JwR*-;I+BGD% z0qjKzwfejeRaci@*P@&w6)bxxpDh!fi}7D^R0MLhhxOmA_T#+g3S*An-7A=Xr$60;#s0;{wcao117MbB_Lf$@o~DG zX$Bw-NL36A!VpKgVUV1MO%Eat0R)G69#|gaNx?X+FLQ=g>Ph-eW>5QTYsm&um6~)< z-AW@{QAH#Tb4W(YFfm}eHc#s_X}5$q65xA7>XZYQ?L2m%m08R?QnqB3GxX9D{MkVb z3pBhV&*#fCvjr=M*>M~no0c=P#rzE0VmlgJ$d-!BiM~x_@{}j$k?Va{(WTZvc2-aYcE$LW0i>wWBgai zzmnLL+ID62l73NNDQw9mS<`}Pt>enXrgnPA+qmrwSH0nP`1h2zm5u)R)#SBvpPlQm4G&NUH87E-A+eP!A zfCY|VQhjX=Ct$VOj=_R=1B0lM9yu=eDO7mslQgAUZr)EDy`Roo0nK*;gzECM8)gye zE)Sv>nt5IBqSB=t4-Qf5ZCwE+91PL7ZyVt17-2F-w6@p83Uv$%b(woy@eohoJL~X# z9lqDa8@J*kT{Zjwpr;5A!y(Ey)=Y3ycz7l}?WqZV>XQ*64Cc6~Ifa?BVy64p%M4>3 zW+Zj^LhhMFly~&Za=BEnvN*q(x5}9P-5hnUfbFuYLcizO4nO%xdBfwzA?MXzP9<-u zXF@?&#&D1!n1bOZWnj77*E$^T!I)BO^eXCvN zA_sdqQ=Z?H*pEzi*!_NNOm@Ka)|Sa`;k#~i@_s35kSltx$qwfP?H7ufVs;_F_|kp3 zLZ(Q+h&G1QgFWOI{cJyzTK5$1gMB&#hbNRDS}f(3i}_LfG+m?LAYc!ZM~s%}&!FoM i5qR4w@{Yf&qVM<{snG)!bx-tiIc~qFofGji#s34fL8FWS delta 1395 zcma)6TS!zv7@j%j>^U=M_iSg^%ihl}DasaJGEA4sFp3haEV^--!Xz8U_ zZ9pob=&hiKsJH0)R`%kCq%1u|^wL9Q83<9-%xU1i2W*&8|$Uxt{fa~7kx*rKUPkXPixk`Rfr8n=@diXLo zd;lOI>RX<*39{HR$!1A8K5RK)&LM183b3=P9jby3gVV7Z`%925Ly*O$bX=qgf?^vm z2F^R>I~@LJ7dU#rN;lG+xzx2?-Pw+2@WR_ z@<7Cn>sj7-e{Z<`IY+qJZQg8x*+K}>o+a_EF{ODgq!oT4*~2l5sB+VLyqm_oqAzf1 z4J5uA7zb<;TPE?ZYLEnCGCvy8Err;!Q9lXhMWsA~EykY{W>SPmXcbfgc+FN!sdr^N zF9HVGk`%CxNbL{y%VE$Dqv3Xnd60_VgF1vk?6W=KF}a)8`_I$Fc-KhxaNnn15p%Mz zaQ#14DnD2LVTgQEeN8Rb$pt2M%Eu>h8^PGf>l5BD?rjZiKikjA~E7e)qXcW)nU z;_ILYf8qM(VlFJ;Uca`=V;ES`xNLKpopHAo$*+s_*6muxAd)P12Td|}9hd+rHO)F_E Yn#J50uB84=Rp42;>@I~WJ<7p<04ZJ!qW}N^ diff --git a/migrations/versions/__pycache__/c243d6a1843d_add_last_name_to_user_model.cpython-313.pyc b/migrations/versions/__pycache__/c243d6a1843d_add_last_name_to_user_model.cpython-313.pyc index 5cf7f6d16a16362b873284d5d90cf784dfe94ca7..52d3a78228b99bfd9a0a4c725ec9e1f459b97476 100644 GIT binary patch literal 1925 zcmdT_O>7%Q6rTODy}PzULL&k}Nj8yUFeRzu)CqBDs>J4}4MbJ8hC>XiwRhrJtar`K zIw)}QnID0KK)Hbs63q>vN^mVFxW!3Oq5)1+LcNt*2*HUr{z)u>IQ7C<^XAQ)H*e>? z`Mz;88r2ZA@Fz=K-^&R7!YAD(c9dool#h^s2r>jiTota0MAW%1tx80OQC^j<21o#G z0TMKVBxFcLfvy^%VMH`T0U0hyarMpZW!u&ri~6Q(Rj}@RdX8eES3Da#nzoL&%e3sd z`ju>2FD5U}+H=;-Q!unwFiq?8snkWQFrUDicwWm8Z24Hvf=KJhL^3s%NKGYY^_jVJ zB9TrkOfMu-iNuVi-QlUk1txmcH5fu=msWAnXOZ5Iut;z4Vki67z>6e7Z#n`m#m4E{ zdbdcY7KA2Hsxx!ZMxeWpLn+wHn=9mm^-x>ym4?vuIEP*msdxcV@f>;{JiQ4+KDw+Sm_kRA8B zB#og>A+D}5xdsqnG6&3_z=E{6g)0_QPf>K|V+aAa8g#H*^0%&os<<`Bu?h~3tAyVh zQ%cx33uV_PfO5pbd|LFJTE(SopkVpMEz@#*OiZ8eVwz>!opmNsiv=?+UlufcQg%yw z0VWqc*JWx4NFHGlgkzy@Or;&ut5SY%I&aL(r1Ko@G{5Q_XT4(WTFs$2 zUn!S}7P2V$lWy}_NrmH(FRI5S&8_IHEOFx)<_pL@OQ-5y1FMC@J zAFCq=>d5Zqjq3+$^2@E0@I$+$!|>!uWbimL)`*OKE`FhYrrtPzlYG7N)y{WPWB!Hx zmp2-E{wVUw9|5Gz?yJc^?*-5U&j`P&$sg3@UvvOYbhB?omv23?v@$7uJE^VAhKgW3 zZ9&lZoi%kp#Q&CSkbnib{yWzU`M&WZutqrf3A_vN(sIFh91pf&PdHCoh$p=6l8 vfA2%W(2r>7XEb_OL6PCQa?(FqkDl}of`|^(wR=HDcvWZ)E5bB+0=E7QY9FMK delta 625 zcmZqW-^9iDnU|M~0SG3zT4Z=LPvnzew3w(aC(lyE8q8Y67R+444#XV6Y*GwGoWbls zk}I8AlVf5*%f$aiOcxj?&u39l31$pt2xhWmVq(Z+2xSOj1(}BmqS+?zXO!1wj%I_2 z2Z3#c@gTGkLoiD)t0iMFTQK|Na3;CQ^-Rhf9DcW0N{dsACU0OeVq+^}1}dG*!fYbI zc#9(`u_QSoKEI%t5hR$+oKr6bWC#Osu`7^hV0gmGeVtSG5~u8pv=t$jIE^-B+~5$n z&LMq~Lwb(We9u{)3$nKuZMWQHdBFLivB!nr@QX4LS2!ZSGc#}*U*|Ns#%c7cm>X!) zGz&*@=7Zu~j;id*KrS4Bd;@aO=N2F_l_7fa5*B%7!l4m7If6+RvmA|_;e+Bq#(aJq+`K+pmF93OnEt0U@F!X+(q=VM_8=8ro1an?givT}X?% z0T>0w`$UaHAuM+po9+-E(l#m@U)gHx3Wg{a6A^GiECJHibsWzDn#3AWA+3bbgtr_X4{hlouC!6S5o?T88z=Zm z-!=s(&JLRmXk>&O@mT04SIS0ZW6n{7_r8%QRo+1$?V+QX+_a>)I_xq)zu3R&W=YC6 zD+$dWvAe(!?&rkvZTcW-Yi65(;x<+&FQ?mNv~`UdH`Xby zTs3HlS~{B;PsF0xL|QW)YATvYj%C$UCK=7DMv$&10>)vw-S~pO+~S7#tefnFnjJft z(BjaJ)oobKMo%WyF)f->b(4?7bf#o6UT60ED~4er6@^oW)v0h=)3Y#|(6Xj8q9smG zs+$($Tc&mzm%^k5l%c^s(~g-b;P7ZM^~{sdMa?9}1B_{pr8UjuHd-eTzO!o1Zb&nS3X*$Os?SOFd1?1d|F2BtoIB5W?z8+w>AZAFTVZ_83r$cK@e!mG#_ZJ&X1{>qB!^*`0UU zopb$pw(fBmN70YB1D+6#54l&TMc}GlE%)cj{Yz}Vy!jmSQ!tcwx80an)aTkhdhhzg zIbqq|_9qR*y2ZEiLUTc^SQR}v(X+^2<3Hw?8~gHdctt!|5Oyt&=7pwb@ZP-Kw<3lM zf@iTSF9iREA75^KB`+UX5qsBn0Y0?Mwf^1{>IcU=^s-vfTY!Nky%(F7GVn!#zV?GJ?QHuJz$&&I0s=R zngh`d4B#$AOlDG7;aDQxkM4bXvUp&ji~%m2Ku0|rO=Tdyu}oflN7YapOl4HK9U0&Y zh#ZAGv4~%%fKcom7(}uu#-NJ2t8}X2a4Vj)2a|eCnlZtB0Glvrz{HD4fPniD)A5TU z!~KYwTs-}*HUXn?6&^%-4~BmLMyTwT@5tFB%jLEA>~-rd>xYR5zT6iK*O6b>@!@7B z;4ofx?XE6$u}eRlj3#5J)YMcko`@#V1E)jENV@hl;C?8^zYf-r2uf?S`%~%oWK!*c yL&%1c(mRbiuByhGpFsmY7CUCxg0p5uDmW_;k;-QHN3@UVBi4=*0^x;&$m1VD@76K^ literal 5131 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 delta 871 zcmaixO=uHA6vt;WvzyH%S=w6j(Uj(+iD?2Atj4C|2bQFe)KKY0P;w}V3CSUvGKokb zP^kw!crcD)sfB`w+LHk?juKN>Ei^)=N7BQ?LrFU4glyMQK9y(UvdL)5@OqUnbKHEIf{DUSk%TJ<&~@Qh?a z06b#f6^{?5uqY?iyxiHl=>eK9g;`zkA4eLVLOrvkt_;Gi=-lJI>Kp!bjP`O6-q9C0 z!d@XFcba0q`R^ohvdcOPD2bSXc^v3Mk?2|-2(!f(({fHg!RP0!dyvOc+ zqb$k$D*H;t*tWiG-FBXedeP2FUo?bvy(BuJ-wlx&^g5yJ!etxpm-Y+TIcDgP2>S%Y zP3WM&VF86BU}#T-dW0}4Am&1k2n-8&1$-P$lBK1^g-SY8%uzoV+Ea4TfH*T`OE<6Q zX*!#u(l{4_Y?Zu)W|lE};V`>rwDb3SZVaCj^Q0nS6SO!%$E}>cR?6Ne=FZYE*TnG4 jA&wsiLikfBL;Onx(E15jnakub55SSuQ8VxyX-PySl=}F%T zeNa0u(jOcuSo9(kArV0jKJ}1ufw2sG=rt&2uhF?zBdr7HcfarB`|b~ZzjMFxZneEE zNdiTVZJ~4c88c^hfvaXZF z28FrNmmHhY^wDG@hy5T*J0Tti4R{A|y&5%WHM$sQfNBMMNCN{P%I@KVwRN9pvXjZmbjs0E*4K| zDzX^VVbGisPbXuVl1b|Ljb-{&&{DD_vYJ-n_a~M`CB`)AB`&Vy62r0VsBYwaJ|Dhe zjZZWZTuH8OBlDP~zS^Xjj;W66^ONE2irRS#mL~f?N;O+jpdbZ`Qq$zcFRVKs_`o)8 zbHa>c%CX@(xh>jf`lkA3)ki%Gkq427mseX0V*4L8wd`DRFS?hGtyv3V=fCR2VL83r zv(mrVzjS3SToAj7c}w(fivIldeE(|8#_8^&7(OfpSL=#m*IzAM6oV%7Zi?PK|D=7R zBU%vU-6K3N?7ApH+~Vsu`TF^h)rL*JZ7qan8Lw@<-gzMe*F(ZZ=J_k89)z4^Ajp^s zswU+cvKf#kCCHk)Kt6&?f}*OiL^eH^(a=%D;l>WmFO3qCY)FMP8zv1R2o)~{ zd(b!+X$!4dF}(?bc=Dtt4<^l_5WNU`6qM-QnS^TfU@4+r-N7q$A z*Ft=J`n_0l?uO@UM%WN9N>~a3Q^c|m0ffBd^l0Gsie3dlJ(>%$E z3!Zh@M%m14ZaQt3E36s`NZGkCC0@EKpD5f0p9N_?aVOE}Gd?2qo^n^YKfKJXsEg{7 zqh4*I=$0B;POeNXPAy%19&e&!|5p#JanH2J+7tbi&_pqs-9*6+6kHux)#_(Tjf1fk zI{mkLy`E~J<9{@vh4yz(fejRRcwzNss7@Je3FG zoQK{9vwPt_3YRhaNf=Rx$lZzHLCV`|#o)E;!l=OVGfF GXyz9n_OHMI diff --git a/migrations/versions/__pycache__/d8dcbf9fe881_increase_password_hash_column_length.cpython-313.pyc b/migrations/versions/__pycache__/d8dcbf9fe881_increase_password_hash_column_length.cpython-313.pyc index 3a95e7a681e6c3080f84e7653d1fdb44c1cebb0d..620c6807c9bccb1439a3c4840e08ea4c017e903e 100644 GIT binary patch literal 2131 zcmeHI&2Jk;6rcUD*I%0^YG|;Fnsq`*uP3&}X4lNv zErA0*0}@CGDe5IvFEqCvIP?mCL7jpU4FMs+CAYK@s^G+%tev>x!U-TM} z-n{qbP*p<+T6ptv^=BVKzjCJA#13(o1L7;BBZ7267ng-=A`vxCOUn|Gp_iAXYd+!w znUDB&KT&jv1mF+qN(_;Z9smqir19W~A1^r;!6wC8-K6x6OUg#oq*cvw?dF=J+1RNx zs-aLG->%V`>u5{4tX7^YTcy>^DxRC0_C_$xYSY$CW_E5Sg_C9`vQp)GKqtQ?Q!yWTpMAT)wf9nL{3;9aj3PzvVq<_HBLuXuDX>OxoJG`b~HaS~DS zG`b3!?uagOLpQ{CVJBgvAxCmVT`EZPATf4) z66s3weO>CL2OB=!pAmYr`cFo?6lf`2OAqM5j0}-+^rbpp(oh9H3QzMgC8f@TL>NIWJEZKNGNcjD-Km|99Qq3t7nX_TedO=FrK*?-a zRl~F!m>3N{i$y%f(_kVsnR4q&e(~y+JoD=f0_$%1m~6R@!-Acda|x3`pDA6l0ne;k zr~HaF-dJ@pTP(n-X>r*t;bIQc_Zn`!m~*Y>jiya;ajjM%W&=`%7H@;v5z!mVMy-=@ z#tRPXoW=0GG*Mq?O0y1@%9xD8;)C$hO@J2qEv$Y%|H=HPms+{K{-F)|OSQGMo0!~5 z%(oNskKX=1vG}Afx1CtrpkJgv`>35*Y`yciFZYXbX5+OdN_Mh~9A7nqM=|2w#V2|JQEokz$!?O!x(syH_g`{GE@VNb-u~3Kp zql_iI1QC7%gg-NcR~y2M1>tptr~o$!zrs$|k(aobCmcD$VTi-a9HJb~a)@zw1>hu0 z$Oy13Sa$C?UI-_Ef0e8JDLBBvbjRjT5#+Ob2Qc)G};S7AoU9cFNRnouj-a z+%sF~5DUJq>C{8gLy4Q1J=iA98hWfJ z4}#EiK}#>~F}->43-|?uL@;IT(X+G!DR}ZtgMv`6zTy3N^JeDF%>R8(p2iZBc-%r@ zTMx_r1HGTf;Bvp1&FRt*hNOZ>6PyV;gCrLw&}x$s>v!LT47=JV6n@tWHv|?)XcbLE z>*xtuGE~98O^_8DzA|!`bYWER3jMvhWQALXouw)(Ou;I89ziV_^V)Pz9<{>?EzI)8 zNbdzLd4MAtcz$cG5yiWWAlT^0=BnQa{BWdIDU}3{h4W+~i6i#3@xt?0JKX@QnT5`E*bF?UAX*?Bft9&z`*f5?(!<_wv$(>Zc-d-dJ=AQ(DxM|S#MZM(Mb z4eeWpv)7MJ#Iq-e@Zu>!*4&u8U%2~o5`&^|7pf`pF=bT?rU%nM#vvl=-;@6*+9qF= zCR{0*g6gwOC|pobk6o4*fu*TxQVAokDA)>Vg_J@DaHe5i&OjLPW@lr)EgQ|2%)(sx zxq?1AN734DZQuxPW7VF+R~K)_)`rPJcz|@D9twDK1V@09k}RzW@Nkr1$^; diff --git a/migrations/versions/__pycache__/dbcb5d2d3ed0_add_contact_model.cpython-313.pyc b/migrations/versions/__pycache__/dbcb5d2d3ed0_add_contact_model.cpython-313.pyc index c7d9ba09f4c31a44c3f381abc2d2ffa5f8b2916e..a751446222d18f1128065aa3478080636a7cbeea 100644 GIT binary patch literal 4053 zcmcInT}&L;6~6PcJAb?U7-C|uVM5%s*9AisZ}0Fzquw=l|J>{*&TL* ziH%!#q`5!md~@zS=iYn1JG;>+kKhqjU!D6GhtS_}$A8FbV|NT1KSUBTkwi%31aXNp zNfEcH3Cg6QrYERNAu|NBkQtW3CL>WM3oj=zt;pmh7D{23igOG3^NJ#-4BeJt>P^kk3|+i9HY6(9TsEmBls;AIt!PvWqytJWJ2N<=4h+1+r%YA0RdEc8Au-XL zNOt!oyAyq4@4!$pIrLKR*<@dTebiAa;ffWAP= z2^1WSt)cx^CxrjHw{%!C8S&+{8W3avQCHLWt=fzwN}>m;0Qz8ip#Y-6w^FNN#IVE+ z!nQb+HTIcWYZy@(PfJfJCIzIu*!^ z;-l6eD;$-O^!y-6B0VG>OVc>V$pZ+bLz6E)iVyg4a27fiKpZ0CzAr#djsNr5Pr17T zV*`Ck$X`_v%|c8?cy($9y@0yV>1rSph|LUWIv}v7k{{&Tw_W@S4!myz=}c<#;9w z>f>RThV{`QAeB3Y0kiW^mP>&N9ArFq%VjHejG_g13M6`un@$Z)ry*+A^n{UBr^i(5 zE!!wgj~ThrwNl1du2pA?^Da{=!e@$Vz6?X* z#kO9Aw6cgk6Qb`8-x~hj$l}KmpI(uZgICyu8^zzGJhwsR&v;+FYWpX`62fKx8CH4$=`$=cHCEdeZ^dzU!Gs5 zoQ`uF@WpGs;?zoFb#QrbE#r#)7GL{*kK=womyC4xbyBhjE1G#Y{w?sakuquWNm)!tup&cO-7g5uX(+;O}1_6LYY1J zc}D}sKZv1Iz1y*2CpNq>z7&7-?sU_?7%isUD6H6^#xK}s!87FLWCpNhgOEYUz zn_OajZbx`->5Z~*awpQf9qDi)9hI=Jo%uNX^BX_C@w5EzryOx|Du~Ak`|fdxFCK*8_>ut6ow3_*VoLw*FsSEQL(&wsJ)TbBCELZ`QzMJFQO zJ-!t=iw{FfSrBW|)~!gFx1T?cpb+!jE8n?tJN^lM{ELT;!L1F8jXtg$Jw^ZaSkGt| z^}8;9bcl&ZTqcuIj9ez;GICxmWVKv{rg-b^yqw1zHg9&qrxmth&FVj^8^!KtYTc|U z>%bGh%xWGOR8fnnu2>mE_mZ%QL-Y~ag%VZ5$G!sTx|CO6HOD{(Cka{oP&}Xrg7^m+ zCg^|GA)@8asO2xH{Q-+2t&8kVL;GTMr=b~&XyYRPFf0;p5WAO%2ywzp!MFbbqoe#L literal 3308 zcmb_eO>7fK6kgkFd%gLofrR`*OlT_{LP#7eV)Ikw2c%#UXf`<%B3Wzi#93JHn%Onf zoFXpth=4*ABBiIw+#*0ChhBQ4>S;r0(6qPqk{fNPkb3HywHGgG1fj}Wo_+J)n{VEm z+4s$?*VOO~Jhx8uO@HNQnBQr~e%xhYy$^(M8G*r!;1b*k*BLi+9hMF%k_F8iFPsigZV!hq|MOBE6yRzIZem@9FM}_8vVN ziSYay4La;1ZY?)Ts-4Ktb0DV6$yu*DXg%;>|K3Y8z4+EPX0O3p4a2lU<`-F8G3K+S z${Re~4iBzkv88qb_O{&eY3A#PbZ4V=%Zc|{RZ3L^ZJjCCWaidnQK0S*{_ z3Q{x1GzlnT*+RT1E6U2+J#Zl2+_a{mHM-N|6gZm`)dJyinl2k~P7$xF8AvC5+B%4& zq-YR6pOfG`vSd!XrXrk_B@&SJBph_|Rdn5-*Aeal9ZQ9W5QCnchB*mGeWa|chHEiB zcSv;!HfdPVyLFNH1~nz0Rf&ICHBbg&;u8!ks~N(MqpuCZ(UWjo&LZL;&@=^!DyciI zVI*hN1S-H`s~cF9RfE)>$8uK0g?F{V301z5M-IOoDbYwzCq>18G)iIcq9i5LRu~XF zS8~vDI{oTHWMVKrIRRwj1><)XZtF;q37&AtJkp1Hq82;v!T~*L9g$1JHuD62KFom_RLNd z$Cm?<#p#z^aFuH@xt7vmes}3f`_qm`9gjPo)tRlME8Mw_4$6~(r-?_2$0N^%%+@bg zxUn~0hVNYsM1BcG{?Th-7k}dVdlGKX&u%^u^uKfWqTFTOy_P}P%H0dN!u^&P;l8%< z5j+)U0p{GygROXMr=Mr$#?3^OSECH$jNFnKt#bX{8NZwwW+%RX?Uo zN2(;O<;vwoc&lTTA?^TcspfDebqq+rHKiKqfj+Gh{VX(4BY~>TcCyvvT4yI$xDIRE ztlotc?tryNuQ|r+zd3Sa|LZ zG?|CYIEWr0S+Q-dH?_b diff --git a/migrations/versions/__pycache__/e7e4ff171f7a_add_message_attachments_table_and_.cpython-313.pyc b/migrations/versions/__pycache__/e7e4ff171f7a_add_message_attachments_table_and_.cpython-313.pyc index 3d264a122cd91d20f3ccf902172055f28a5055d1..7c3ab53d7c2d96c3d205ba7b09f82c3d8dc0d36e 100644 GIT binary patch delta 1133 zcmaKr?N1X)9LHxm+wRkB3sPI8N3Ehrz4IX&d^qlm#mYfJW4fXy7)j}_SS?#KTjEQ~ zTP`_{%$p0El&F_ejL+-}{|I%VA0^zx9Ra$VcytdDbwngKWL<0lMA0Rt>L`lJ zGe%L3LPyM4_H{FzrwAT!?H1@IX$LJrsn-HSZr*@DgP;>uodpMBQmp!o+F6@eE*%cK zx>)pYFm(ZdAwVZ#SkU2+LWdADE@mFcyF{c5M^PQR{|kB;e2liz<1P$q$#M<-v5wRl zT3JWlS~YEB3m=0YP?&w1+*n(vzkkwCOVVa~4>hQjE#*8JsIX-ehRqd+1;vd9Q8Gj) zg(kXJz^Lxetb5<)`aVTKEp}@?ZvK(56SjK^V$~gK)XWc$WGsUSeCp%Vd}<_X4qK@) z)246SlY63qBcyYd<;b-Y$`Qv?39=n&B$ZEJ8O~j|d441kp|;1K*vGt|4f`6~{{ncg zYSUd)UBCZYh_5sR{}QNH=v&qTOImwbYhTnl3O%pI(A=RHVsu4PrhQYsXU#vYVDEI_ zRNtS*t?puCHgR*H)LF(y>oj|}`=7+^#GQc!Mq_n)`PN7=Gn=_LVVA=P7tAt_f4}no z3q~2A*qCv3_Ug@S>1-MQT&KKm|Ly)erxxPXzDv06Ic}S~ICr{qv2=P7pJb6s{wjZfnT^Z3cmSfE!xPX(oS)BH*4jcX#nwu%I~v~tn7c$xH^jk+8y zHJ;C9tTZvlOe;_Lk3oLm65`UrrHRWHE{$9yCQ-R^zNCYBM@bpRa5|SAAG63d9;{OIyP;foi*;7bsE1;VQmz%2!7 dxglK8mK*#`w8nz+$rFV8*N(`rgB)j>-vD6)B>(^b delta 1007 zcmaix%}*0S7{+(Hv)|p)0BA0!Yc8iI`^C7?+WMG8XF1utMoVJnpMgG@_2QaGr` zmLY+F2^>peOcM@zF!5wOO5p%$ycoTBGr}L>ObZdj$RzXIcb@l|cV_l|?Rx7p9jmI$ zkdeC|m`#g$lN|)|Ep87)oW>zuW063nsPPU4Nt!??&#*o*Kfi??z;AfZ;$hHa(!e;R zMd%3g*rHfj!#Kow$&KHzl7-b^7kkOUUFqP7Lr^@|1WoJ$+P|wvrylt~9;M=Gw9+c- zrMoOCwm+AxQT~2N4cY8v=|gGoe(gccHgP(1hvbmJea73ZMBF=I^FinpPspM0yo z;yktbPEcKYE6($|YOGl=Ixx?9SW;`Zl?wn~K`gQ1=lqgo2b9IBNM?F=GBuy3Me%JR zz1T*AIx^EiB4wHHkbg0Fx1JywMKQs+DSk!U^?8%p@I zIA;9V9<=qV>?c+3ckXsbUk!`26UauV diff --git a/migrations/versions/__pycache__/f18735338888_add_company_logo_field.cpython-313.pyc b/migrations/versions/__pycache__/f18735338888_add_company_logo_field.cpython-313.pyc index 500dedb91816c387720b5a49ea01b01687beeaee..e1be9f76421f43373d4c29207cf2d55da0d0b2dd 100644 GIT binary patch delta 941 zcmYjPPfXKL7=N#?-P&y}AQ%WLjAhPLgCq)sIR&$jpvWIX9VRYumaUGN?R9xAG3sGE zs|hYml$%CPlsgAvjHoA%3^0(2iHTmkp~Miq=qqF3*SzojzQ6tY>o@0_7Nt#5^Z+W} z$Gwvgwj?#d{?d__V^Co)ph1psexPu~rMQS&L4>DAP~6Rccod#$ZyL!$DOK>w&~|6A zk=K%D>FWswLo1Ggg)XNt6%68B1pyEPU9f^NjKMK?)xUQuu$oa1#u)ATF+^kC+?^@egIjM$=) zF|~xDnPvv34V%SkPP2VUJ(pMUtwdH&>$A{y8Ocd4r+)uS-$z+?Wi^~OCofURiyI z?EE{O*oaox?rIrdDbw?Y!@Hk~b(={S4pF0l9^)ZZMexllPDk!W?wu+2uh%xtu$EXn zzZU9P4MobK$o%L^XlPj)UJ4D(7!SJd-zkTNiUW(%@E7;~naj)W;JR3|Dz+_&ZBIJp zgG*xH+nzOF!>X^f>}!3_ybzuWFOR#MyiqZL>%A zQEYotM9=S#CPQ+7`V41X{p6q%7E=06oF-~YBYtYPS?z2;^jU-rx*R$PYdvCnq7FOT z@ma7*A*j^yr*nF0Dy#L9qtxd#HM*(VKoG(oj0>{ABmf&ffyU3E#ge!tiH9oOY(}u1 H$TaUC8GzS@ delta 472 zcmeC@-NME9nU|M~0SG3zT4cmAPvnzew3w(aC(lyE8q8Y67R+444#XV6Y*GwGoWbls zk}I8AlVf5*{ls5JOcxj?>oKZKzQCfy6wNmI7^6Hdb2J;sG!O{lntXv#l#3;p)siuo zEtuVM@@EzYO%A_Xyv3O%sqw|BB_)}8>BTD@(MTse?MUp_i^yCIs7e?;MM_E-E1tz~|wa@}N zA1t+!;WLoQ0MW)_lbfGXnv-f*qzn`TnOAH)Igm|M?E?n`k5mK44FS0Z-WviUK!R7W Vf$I|+Gh-~{X8~qL-6AERVgOSOe8m6& diff --git a/models.py b/models.py index 5ecca45..202ee97 100644 --- a/models.py +++ b/models.py @@ -22,7 +22,7 @@ conversation_members = db.Table('conversation_members', class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(150), unique=True, nullable=False) - last_name = db.Column(db.String(150), nullable=False, default='(You)') + last_name = db.Column(db.String(150), nullable=False, default='--') email = db.Column(db.String(150), unique=True, nullable=False) password_hash = db.Column(db.String(256)) is_admin = db.Column(db.Boolean, default=False) @@ -363,4 +363,22 @@ class Mail(db.Model): notif = db.relationship('Notif', backref='mails') def __repr__(self): - return f'' \ No newline at end of file + return f'' + +class PasswordSetupToken(db.Model): + __tablename__ = 'password_setup_tokens' + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + token = db.Column(db.String(100), unique=True, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + expires_at = db.Column(db.DateTime, nullable=False) + used = db.Column(db.Boolean, default=False) + + # Relationships + user = db.relationship('User', backref='password_setup_tokens') + + def is_valid(self): + return not self.used and datetime.utcnow() < self.expires_at + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/routes/__pycache__/auth.cpython-313.pyc b/routes/__pycache__/auth.cpython-313.pyc index 5945f2102843ec305e4ded3bcf4828080d3c014c..b653a18c9d173551c801195f74d49e7d15f49e3b 100644 GIT binary patch delta 4664 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 S&EFWzY~y0>Ndvo&Zt-80Jq4!# delta 1446 zcmZ9MO>7%Q6o7Z^{Yh-EtNJgoouBO{ak_0ttHdo3(m+yZ8aoX|ChdX3*70s)m&9vk zcN&k}(?D8ryw*B< zlPq<=Crc3$XL<2T{wR1F5vpT=QGkolu4{CJy)6z(;56eiMSycDw~u)Juw7IngaxpmSvD2;kT(>x6Fp)w>=j_Ky)-0+2cx)>BJi7g#D(LNU^IXKO&^` z;TK1w!Zcb>Be?e^l(@&EY%intJqhbuYTfsmlhod5xt>i8LNBpcc32J_*r{wo=5A$n zH#@kqfDz^ZL7XSmfNFu)JZgHj?KN}Vp<9MX`M&eGna6nsrCGqO6rOhK!D*9CZ)?*S zq$=8ZFJzPygoxmd7f@P6>>NQ}L@XgH2yAYUaBa`*)LnPmp%zV|zKZ&2-LmL1F0+V} zh%g_N(ttqry++&F4tiUzgV(QnG=s(!1djee!69zw&5QUf`!?4PWe;)_1chb4#BbS&tK>Al*q!(n>@{b$)X+Ng{@he7*YyEz6=X%$yj5LW@u z|F3gxRL`(Cj*ZHEVyD?B$4WbgI>J*3XBD>}O6EU#pcOks4?6q;E_qw95A;REO9(#T zQQAbgg}4E5QLBj>KKHBo40*46NB;*Z&OV58`~+&D6n2yEo859Of770$ufY=kZrpS1&vJf3 r-xKtELh+t3_=~WxCoKFLOTT;J?!_NsW3MZJiwYUqPc24Cc)@=GwXrip diff --git a/routes/__pycache__/contacts.cpython-313.pyc b/routes/__pycache__/contacts.cpython-313.pyc index a3236ba4597c6d29f93311de41fa04dfcc10a077..24cac0ca2a99ba79825d8672f1f2936214462d2d 100644 GIT binary patch delta 5884 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 delta 3637 zcmaJ@TWlNG5#7tjif>7LN~B1w#P~s3mJ>&Qw|4AEHY6sp&9!W~lFd@&O4^3x%Ck$` zl55LBpMevk?rl@_!zmoJg$)Gh2S57R04;(BC=d$>kYbzG4vPK+DPY+FiWUgaJ9kA< zR+IAMaCl~R?(DoSKYo`yyh8$y{eF+YKHgs(oI5>yGtfbvySXL1Bnh%0JF=~#ZP3PL zXI2<(hjuQD+2CjhLR@aihDRe1;j$|m9gRV3v;#UuJE4V==8tU4K534>rk>hGEOzUvvJ_-&f-XnH!QORh7K)S`qZdLU`WNE!Zk2;^0Wypm zN3J#0a5LqeipXL5z?N`sYiQ{)TfiQXJ2~6G#@^s;g0tJ#*bi(05}|*yC7lBe&YzNF zJBZw|&xYQqWKTMGG<2=fdY#bOY>I|K7T=x>w zRVo;Yp;i?7$nhU`;6I(3vzMhHTrY?kAt}1t(%qeA1oSX41BFsWt>!_wP*XuE0cI;f zTU}!$T|e*I?iPy<^{uw87&m^ zcen%lJXP1YD|ZNs9BXY9SrR6nTlFtky+wv;VSk8Ti~779HOJ^mEa@L<*voC>F1oxo z?3sL_LE%qPZfEC=p%>z!Co^GRQ_pB}AZnX@fu0R_wFQJ{?UN@O&*pNh^i_UE^lOFY;E|96F%}8SSVO~tvN3~|&PXgO#|B!kA-^OO z{C(+I+>`E<%X*oyi*u^NQJ0W0RCuOSGIEBt?2V z8Hvu~0dCG$3l)XO1)J%gl5tUGs>K}rEV-$_swgGN&?GAm$(U1Akmd`zeo2Fpq*bLE z6?8)?YqM&V9!;f#1ANAy6<-899S+eesYG8DMSL;S!6vu>m{l`)u~1fVpz~Tauh*t4 zs=@O1ZmO&8>!^0ksAYx67Nwu1GCdxCR-Q9FmFLjuS`OhnRlBz@g^}z=@FMWU$CzY_ zHKSP7E}3ps*Jd=R6buNV1|wukH-jzf<{xdmRpnB?s8x+Z(STPNGl7;L*yz$ww0=tp6Z&b-RY%{4u1)l% z-ZcHRSN5%V#Yo_h+w1mx-bK%(7h0z8l`0>)s&r{2?(4fZIlJtgyXTmLbp4C8;ucRa zhtN;ww(mc<2}K;CMc;5qHKY@u@o4c(C@{DyLIq{)9N3V)&Gf%^z9 ztmfxev-$?p{Thvuc0sr?tgtRe#F6)R;F; zv%Eafm+@iTD12%miqbU%EG}F^_$k75gfIf{L0lS3@Ll}`7?H2U(-`k%8GRlG^=ZR`GU7p8F6)1F^*p{Fk73?SZj3%ReTmTCN0LX z38hsdNv{=)?E0ChmCFk~zy`ci$V4}`#x3+PM6Zo?tlPwgk>m}4N!^fB>%1rHXo{;L z+t=lL4|`T;qb9J7t^U2Utk(AH9j3fYnAHS>dgS<7_MfY+o!DgGWhD&{$A0H?WBS=m zY1H(*H4~Y}CPO_A3*4md%b6jb5}u7#o{dTYUSfA*L6=|sFk679z+xzNKqb zSfnS;g+)F;F}iRr(Xlq0rpWd{@luJ;D7;DEKG##mzvVm~rjwrn$C1D*5Z*wzjer&7 z;lmb`FnX)BuCG^;W6gm6wG~J#5hi66XB768@{{`9@IY|Hj diff --git a/routes/auth.py b/routes/auth.py index e2eefdf..566d03f 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -1,9 +1,10 @@ from flask import render_template, request, flash, redirect, url_for, Blueprint, jsonify from flask_login import login_user, logout_user, login_required, current_user -from models import db, User, Notif +from models import db, User, Notif, PasswordSetupToken from functools import wraps from datetime import datetime from utils import log_event, create_notification, get_unread_count +import string auth_bp = Blueprint('auth', __name__) @@ -215,4 +216,81 @@ def init_routes(auth_bp): flash('Password changed successfully!', 'success') return redirect(url_for('main.dashboard')) - return render_template('auth/change_password.html') \ No newline at end of file + return render_template('auth/change_password.html') + + @auth_bp.route('/setup-password/', methods=['GET', 'POST']) + def setup_password(token): + # Find the token + setup_token = PasswordSetupToken.query.filter_by(token=token).first() + + if not setup_token or not setup_token.is_valid(): + flash('Invalid or expired password setup link. Please contact your administrator for a new link.', 'error') + return redirect(url_for('auth.login')) + + if request.method == 'POST': + password = request.form.get('password') + confirm_password = request.form.get('confirm_password') + + if not password or not confirm_password: + flash('Please fill in all fields.', 'error') + return render_template('auth/setup_password.html') + + if password != confirm_password: + flash('Passwords do not match.', 'error') + return render_template('auth/setup_password.html') + + # Password requirements + if len(password) < 8: + flash('Password must be at least 8 characters long.', 'error') + return render_template('auth/setup_password.html') + + if not any(c.isupper() for c in password): + flash('Password must contain at least one uppercase letter.', 'error') + return render_template('auth/setup_password.html') + + if not any(c.islower() for c in password): + flash('Password must contain at least one lowercase letter.', 'error') + return render_template('auth/setup_password.html') + + if not any(c.isdigit() for c in password): + flash('Password must contain at least one number.', 'error') + return render_template('auth/setup_password.html') + + if not any(c in string.punctuation for c in password): + flash('Password must contain at least one special character.', 'error') + return render_template('auth/setup_password.html') + + # Update user's password + user = setup_token.user + user.set_password(password) + + # Mark token as used + setup_token.used = True + + # Create password change notification + create_notification( + notif_type='password_changed', + user_id=user.id, + details={ + 'message': 'Your password has been set up successfully.', + 'timestamp': datetime.utcnow().isoformat() + } + ) + + # Log password setup event + log_event( + event_type='user_update', + details={ + 'user_id': user.id, + 'user_name': f"{user.username} {user.last_name}", + 'update_type': 'password_setup', + 'success': True + } + ) + + db.session.commit() + + flash('Password set up successfully! You can now log in.', 'success') + return redirect(url_for('auth.login')) + + return render_template('auth/setup_password.html') \ No newline at end of file diff --git a/routes/contacts.py b/routes/contacts.py index bf2a3a5..8a30b2c 100644 --- a/routes/contacts.py +++ b/routes/contacts.py @@ -1,6 +1,6 @@ from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify from flask_login import login_required, current_user -from models import db, User, Notif +from models import db, User, Notif, PasswordSetupToken from forms import UserForm from flask import abort from sqlalchemy import or_ @@ -9,7 +9,9 @@ from utils import log_event, create_notification, get_unread_count import json import os from werkzeug.utils import secure_filename -from datetime import datetime +from datetime import datetime, timedelta +import secrets +import string contacts_bp = Blueprint('contacts', __name__, url_prefix='/contacts') @@ -113,6 +115,10 @@ def new_contact(): file.save(file_path) profile_picture = filename + # Generate a random password + alphabet = string.ascii_letters + string.digits + string.punctuation + random_password = ''.join(secrets.choice(alphabet) for _ in range(32)) + # Create new user account user = User( username=form.first_name.data, @@ -126,10 +132,20 @@ def new_contact(): is_admin=form.is_admin.data, profile_picture=profile_picture ) - user.set_password('changeme') # Set default password + user.set_password(random_password) # Set random password db.session.add(user) db.session.commit() + # Create password setup token + token = secrets.token_urlsafe(32) + setup_token = PasswordSetupToken( + user_id=user.id, + token=token, + expires_at=datetime.utcnow() + timedelta(hours=24) + ) + db.session.add(setup_token) + db.session.commit() + # Create notification for the new user create_notification( notif_type='account_created', @@ -140,7 +156,8 @@ def new_contact(): 'username': user.username, 'email': user.email, 'created_by': f"{current_user.username} {current_user.last_name}", - 'timestamp': datetime.utcnow().isoformat() + 'timestamp': datetime.utcnow().isoformat(), + 'setup_link': url_for('auth.setup_password', token=token, _external=True) } ) @@ -159,7 +176,7 @@ def new_contact(): ) db.session.commit() - flash('User created successfully! They will need to change their password on first login.', 'success') + flash('User created successfully! They will receive an email with a link to set up their password.', 'success') return redirect(url_for('contacts.contacts_list')) return render_template('contacts/form.html', form=form, title='New User', total_admins=total_admins) @@ -446,4 +463,54 @@ def toggle_active(id): db.session.commit() flash(f'User marked as {"active" if user.is_active else "inactive"}!', 'success') + return redirect(url_for('contacts.contacts_list')) + +@contacts_bp.route('//resend-setup', methods=['POST']) +@login_required +@require_password_change +def resend_setup_link(id): + result = admin_required() + if result: return result + + user = User.query.get_or_404(id) + + # Create new password setup token + token = secrets.token_urlsafe(32) + setup_token = PasswordSetupToken( + user_id=user.id, + token=token, + expires_at=datetime.utcnow() + timedelta(hours=24) + ) + db.session.add(setup_token) + + # Create notification for the user + create_notification( + notif_type='account_created', + user_id=user.id, + sender_id=current_user.id, + details={ + 'message': 'A new password setup link has been sent to you.', + 'username': user.username, + 'email': user.email, + 'created_by': f"{current_user.username} {current_user.last_name}", + 'timestamp': datetime.utcnow().isoformat(), + 'setup_link': url_for('auth.setup_password', token=token, _external=True) + } + ) + + # Log the event + log_event( + event_type='user_update', + details={ + 'user_id': user.id, + 'user_name': f"{user.username} {user.last_name}", + 'updated_by': current_user.id, + 'updated_by_name': f"{current_user.username} {current_user.last_name}", + 'update_type': 'password_setup_link_resend' + } + ) + + db.session.commit() + + flash('Password setup link has been resent to the user.', 'success') return redirect(url_for('contacts.contacts_list')) \ No newline at end of file diff --git a/templates/auth/setup_password.html b/templates/auth/setup_password.html new file mode 100644 index 0000000..1501c7a --- /dev/null +++ b/templates/auth/setup_password.html @@ -0,0 +1,61 @@ +{% extends "common/base.html" %} + +{% block title %}Set Up Password{% endblock %} + +{% block content %} +
+
+
+

Set Up Your Password

+ +
+
+
+
+ +
+
+

Password Requirements

+
+
    +
  • At least 8 characters long
  • +
  • At least one uppercase letter
  • +
  • At least one lowercase letter
  • +
  • At least one number
  • +
  • At least one special character
  • +
+
+
+
+
+
+ +
+ + +
+ + +
+ +
+ + +
+ + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/contacts/list.html b/templates/contacts/list.html index 634ace5..9bdcab6 100644 --- a/templates/contacts/list.html +++ b/templates/contacts/list.html @@ -113,6 +113,15 @@ Edit
+
+ + +
{% if user.email != current_user.email %}