From b704707abcec2f166c09c575bbc0f87d685a50d6 Mon Sep 17 00:00:00 2001 From: xds Date: Sun, 8 Feb 2026 17:36:40 +0300 Subject: [PATCH] init auth --- .DS_Store | Bin 10244 -> 14340 bytes .vscode/launch.json | 41 +++++- __pycache__/main.cpython-313.pyc | Bin 7828 -> 8239 bytes api/.DS_Store | Bin 0 -> 6148 bytes api/endpoints/.DS_Store | Bin 0 -> 6148 bytes .../__pycache__/admin.cpython-313.pyc | Bin 0 -> 5109 bytes .../__pycache__/assets_router.cpython-313.pyc | Bin 8907 -> 9142 bytes .../__pycache__/auth.cpython-313.pyc | Bin 0 -> 6114 bytes .../character_router.cpython-313.pyc | Bin 4072 -> 4174 bytes .../generation_router.cpython-313.pyc | Bin 5192 -> 6019 bytes api/endpoints/admin.py | 96 ++++++++++++++ api/endpoints/assets_router.py | 12 +- api/endpoints/auth.py | 122 ++++++++++++++++++ api/endpoints/character_router.py | 4 +- api/endpoints/generation_router.py | 15 ++- api/service/.DS_Store | Bin 0 -> 6148 bytes .../generation_service.cpython-313.pyc | Bin 20267 -> 21250 bytes api/service/generation_service.py | 18 +++ main.py | 10 +- models/.DS_Store | Bin 0 -> 6148 bytes models/Generation.py | 2 +- models/__pycache__/Generation.cpython-313.pyc | Bin 2496 -> 2538 bytes repos/.DS_Store | Bin 0 -> 6148 bytes .../generation_repo.cpython-313.pyc | Bin 3939 -> 3967 bytes repos/__pycache__/user_repo.cpython-313.pyc | Bin 3428 -> 6208 bytes repos/generation_repo.py | 9 +- repos/user_repo.py | 46 ++++++- requirements.txt | 4 + ...pi_protection.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 3338 bytes ...est_auth_flow.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 11883 bytes tests/test_api_protection.py | 22 ++++ tests/test_auth_flow.py | 107 +++++++++++++++ utils/__pycache__/security.cpython-313.pyc | Bin 0 -> 1745 bytes utils/security.py | 35 +++++ 34 files changed, 527 insertions(+), 16 deletions(-) create mode 100644 api/.DS_Store create mode 100644 api/endpoints/.DS_Store create mode 100644 api/endpoints/__pycache__/admin.cpython-313.pyc create mode 100644 api/endpoints/__pycache__/auth.cpython-313.pyc create mode 100644 api/endpoints/admin.py create mode 100644 api/endpoints/auth.py create mode 100644 api/service/.DS_Store create mode 100644 models/.DS_Store create mode 100644 repos/.DS_Store create mode 100644 tests/__pycache__/test_api_protection.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_auth_flow.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/test_api_protection.py create mode 100644 tests/test_auth_flow.py create mode 100644 utils/__pycache__/security.cpython-313.pyc create mode 100644 utils/security.py diff --git a/.DS_Store b/.DS_Store index fdc02598cf750cbe64f7b54ca5d6146083ebda11..ede143e68b2d50ff9881d1462abfe795f8d50bc4 100644 GIT binary patch literal 14340 zcmeHNONsVSk}klftA_7@^Y9*?`*q0T|;;8 zE{~9`7Y~{k<4M7I61{K$FLKeWF{lSkJSYc^nh;KEA{V?EAAePK&s25KtdJN8qVkNnLQ7ane15|<#1nnh7SI;Z7SI;Z z7SI;BMGLUG7mq}jDD_oaKwCgtpkVMafB#hLd6`NQs|f5J8U5(+rMf&M)UhDae5cav(lu;%6v?b4R)~hXcupQeU+N zv<0FT*o2ST_N78skFhZD?mztC$}PcODDEQ|xDVl~Sh$FY^X;&uIk)cj zd#O~nOxMe1uaHTiiFUuVGEm-z?AiI+3pfoqkTG00eQ>X?;1lasFDO-S_vh15#7>`==Zqpsyz(rS62F{(WV2p!>De1fytxvF`IX6i`rFu&SdXI zr!~-lY1kEGSFtrCxuS$rb-7Ye`;j%-x8AB&g(ecctjBlTcklS%ZLX_?P_!UfWPpnf zCh0K@Hzu#u@Xp4t@^lbY-LQlvBI{4hX z!6!7-ip!EG9>TwS2qC-eN7k3!<{9pk5mYk-wMVJ$`vx)#bBlx_=9VsVXVr~k4${i2 zhfeOm9I5C82Y*4N)GF%}KfCGoqd^#IaFWANYlNZ)%p(Kjz(R-_UAyprsY=>Fs*?7N ztNqB5UO4yEwf%vl4SHG9)(BP79FtTZ+5*}F+5*}Fcee!wQ2aR0tWEHgnWX!fdAUuV zN7(!Kl9)#twDK&{s$H2whIoS2iG0~Re*=mNXUM;doTXNo_qD%%3RVvaqPcQtjZpZ* z0y4n-IfKc`iN>5?z#Rv+>o5g|Do^Z_#nG~SKlbK(CWLJk>0h?i%~7cGH|#gd6O~z6 z-o*6Tbto^hj^8TF_vtY&7&`^gv|nq4%s=_`7N?0nO_?L zywTc}KiaBD9s7n!o#c~l9n(yT(_aQd_>?Lk-?5T|4`e1uxav#fJMCxObr@4ko!-5xCA?*t;$5#Lb_Fh6t_6nfFcjR!=R_S;mFI#je z%2PxRHp&)3!-puDjBfJ+B*Y^oQ%UbeT4(q2EY=#K42;hI(YpH37SI;Z7SI;BEf(k$ zT(a!@zo#*WYV-a7ZIMMY(H77axHA?Y@sa!p+ZGAFHwcTtR=#OGkK)NN3wW{v>zo8H z48m^Wti&7kIo@Ne9vs+~V|6U&MTs>$Zu^IT`fHcF@N1Vy&aULjp!5IWL%o~M|NjLb C_WX?i delta 184 zcmZoEXbF&DU|?W$DortDU{C-uIe-{M3-C-V6q~3gIoZI3MH0wo01=EpaR!EDhD0FF zU`S<%-+0lCed2@M&Fma39E_5aCn!rzey1eAd9N}X^W4>T;DWA~j-J2}*k%Os5uo%EJQ%I+F@xK6g$$Rkyf1$E8(k(xH9vznshs2|ms zS9Pv*O!jmjI!X+{A12KptZk;G9}iRPG7ul)%; ze75+x#}e#$wgivGKcxWnt$v`Dkaj1+0pVOXyajvg5H?>I#c$pibQ5K`a} zKy^5lq)9T`cn{@4xEd)hB1rn4Up+5L)y?(fvv?M7Mdb9#YsR0k#Q^)r&*R!CLTuF; zVCPI*&9V>{d}tQ+i0qPSY~w_aP||1AMKx10oX*jCrC8F#m{gg?oHqAC^ltbIo7uA2 zZNB66ujzIA01d8uZ1!Tq0Wi@4cD?xp-wa%vPIxfhL-EQxff}BXxbUvUbwUJp?Thou zv`-H*i{mxRNhCE@ObtV(awidrYmXT>aZmP*rDyp`{>Ex>++Iab$y|$$JO0PhRJF$| zcO3Q#?t-&S`%t`s5R%$pi(KLMYJjw@yun^@Dm7)&r`1AU$x<2AMfeMUg7_W1j{Y_vz2n^RyP}D$+P1QPZGPsoe-aKw zLion>`iKeL*o`Ab@x~sJrD$M21Cg#*kp3VS!b!BtwUi{oNK$-yYh--2_l_(SyTBJ!ZeaDuZ+p)qv zb^eND>1@boWnag>v$QHn=IR7jWQV%gUuqS8!V|-uAw64Z*M`Af4+k z9iWG=uqDr~rhn`Rhk3{~kK1&swevO#+(81n?CJKv_zQBZB1erbtLKMqqfS=xeCGJE S*F7$xTcTmyD%`SQkpBTEE66+m delta 1898 zcmbtTUrd`-6uc z<{tFfU6(9A`Cx)sGL32UK@<1DqA!NTWWj`(%=n<04`xeE^wD#_w)E4XFT2Ti&hMP_ zJLjJJ-E%)3c|U9^R9Bl2EbBM#rhj$4XK4^t4WIe5_#CPCq9BjJ@*slcYelS;UPOk2 zxagrHM38T=U$9$$UPUIst&gavTX6S`a-_{IVlPr0iZdvbYK)PWp(d|$O$gY=m1YN` z6GQ^vc~S+kTE5A?6r7<+;s9&(XzwY~3HH>HJxx0PVP7O3uz6jJ*otqkv#NYWm2Zk| zJ5kcj`6PPO$tB_fPek#DFr6V@z?aAjnGA}hPG^Y$e06Hpm=XvHN(V4yGEHRVOgoUs zIG|1y%>e3Bku2{P^R<%nF_+HXK1Ys^&XrFEw*U-AbZ(LBw@Ug$oqI=??`SI)v3&Pw zAMx{(DBU5fsX8Zr@-*|*gcz%4Vqev zA;E)EC?K+wDOwW{4)vMj8_CR}bajKS#W&f$DQ!9fIRo(J9PFhkyRjf*{W_+B{Hs+i zEWHQ;I>_EJpWT^;N7V{X_ZEtko+$LKx%OD;(IFJmL6*9BPwTi6nMblz3Tag4sNVlD z$JGQ4)mX0ZA4U1z>WZ1mh%?zxbpT6jwYqu!Xj?@yeKBj9E!UP&MSD@Ku58KnGOFm9 znRQzlF=i7McVnf#RGIF1L2hK%E&YK?)L4n8kP$_#ElAUagZ*f6LdCx7*Bj znP~N~erp7eux+cWtpoNyG-lFQleuhsoz8-pST>^OSdoaf>;;4^3tg++S{GT-i z?1Aw`IeriiRhu}GTiw`RUQK4QbP2rqys7Sa+!g~!7ynINSxYSEHsf@Y#v$|-OWC^c zGP`Aah%d0M`YUj@gZfUZT2{9fPp{J~(7t80_LMPBFYuCW`M2#Rk^SI^4Dum9)J@D? zoThwG=qR^MTe&37Ez@LnUS`j?XB^O(^hm)0_Bcw|T zX+VAISQk7qSiKHheRPg;{RM^;S{$I(GrM};QPa7zzxDWtyZ(d#Fa zEHScL4o_#_T5_y~H^nSh&m1q4*f0lhW^+{Q2Bj7U!~t>O#{u3SJQPOXVro!t9jNpb z09Z!14z&4~f^$5JzQxoaMi9!R0!^y2R}5v+(eGKFZ!tA!(n;CNhq7;0_J*SL+cCeV z>7;yvQi}uPfa^fXJU00JKP$ffyGimS4u}K)$^lgl+CdASWY5;z$MIR~p>$C=n3o#V kDX8pmtQ&k3@1y9zn8yvEZ!tB99)$b|Xd9#w2maK7FC}c1$^ZZW literal 0 HcmV?d00001 diff --git a/api/endpoints/.DS_Store b/api/endpoints/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1c85f088ac1c2b6eff02af92f6dbff989d38b9fc GIT binary patch literal 6148 zcmeHKJ5Iwu5S@V(meQo4pxhgv^b{sCC&&Rz;vgcF$Zn9J+lCuZau_-!&P2hR5BUg_ z1_>cFBkjKR?wj%WN#5NdBA(nW#zZ3`D$oR320bRzy-P<<9spTmG&G}zmUK;ZPoTdz zCAklgZA~3LAop$l_3CCkZQ6BbY2P1z&s?`vHD9%JFpBe&_wM%j_2LkFv0ZMJjLj8juIwcSn>*%{^Ge5TXzs*be6YXyvv^^BJK`s$6PJe3hXSF% zz5-Iw!bWobU*ebPE%KETT__L={8t5dP)(}|zRu6qFW)I=Z9=<36BAyR0Sx24O8_2Z gA34=UTTe0uuXN0Y5=G|K9T*n@6(qV);1?A50K0E9o&W#< literal 0 HcmV?d00001 diff --git a/api/endpoints/__pycache__/admin.cpython-313.pyc b/api/endpoints/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c41ee4eadebb624f9a8f0779e785b19c5e0f9981 GIT binary patch literal 5109 zcmdrPO>Z05@$DDCNl_Fj$&xM6viw2C7A;xwM{Ea?ZN?I5(=J!6lSsjqCRY+&ie%oB zj-@1p5(9~Y9wHY_Egw?*(1UW!Er(v*UqFH=L@b;pK~V(0QIUYwITW3FOHz_!7iiH_ zAF#8t^XAQ)_vUjqoK72o@r%#zq~{!j{1rbM!(K-=Zt#SBMr0y$5fbJo7v?GN$8SC& zgheWbB`SqY)D$*TGouR;OV~=S3>PD|u$|f&E=3$+Cv}Eh)WyC{k(RKVy2GusHSD3D za2st4d#RVrnIpb%J8chl&<^%(iFAf{(OrHrE%p)FD%*mbY(K@*u4yZLJ7ni6j&_4i zuE_529=ZqCv`ll;J=0PjneHCtjI)gJ5+Jf0W}Iwh&(4_Vidw|E!czU1V z;pH~Qv$vDL&fYE}`=;%qg0Tbbo9>ETtl!+Wm)t(hPxrCiJ5KSkttox)PU-(vw@%!5 zv@VtQ!%mTHThvi`S3{!QHQn=H?PQX>ckFcF1v~BjAMCVe$4&=ldjh>r@FW3Vw~l4A zxk93#rgZUAS}O!ZAYGk^<%$KB>gEe-Ud^U7-8nuvc{Ow^spbpmTvj(}u%xI3INf_? ztXP;EzM9aqcXBj!PEAmi0^NEpp{bX1DK&$e#5I*_F*TplZHUUoYDsUosnYb!f|9Q< z*V~d*1!a^(GO22sQpo*M&FbA_=g)^^S(&^N2}PCA>!4mpxjYe#PljaB_R_^Gv5Cp? z%eqYtosWekl}P9f*whlfHW{KcM|I)GI|We598=#es#;;u7EI*R!9;34oz*4w0(MiL zsx#?Zx>!ieYK!(@0*)1A`@>QTL*g_8xNBA|C`CMwLh(eBp`?f(Nr>X+FadTOo(&H~ z#y;YQ_@1iA2q9BV&Y0L-(L`iU=4Ih1CyS?qEZ;+>P!=08O9R___mMc6VmStBQ;=hf z0nsbLdLb^vS{k&iw`^*7R#uFQsCk@MxgHYdVr^So4J^Yw+W>S9eK>y7|CY=J) z`@sWJYPOJ0WVA*1wQJXopo1YH3A4JX?qPy%H+)b@f|Kc{lv+rnGrDW$)2yOR(Cy@8Rnu_u#r&++!+U7z2@s7FL+55nL-~KRE=^VIYsoLAh_U@|ZV99Z?CJ@K& znvHnc*IEb4tplqkE3G4I_K~FvRkQtr_o}XaCCk35y=5u#k4-0WcsB?q^&8Zh0O*hF zCgK=ICxi_+sxhXT0U2wCU1JVbLtvhQK#7xn!rWMl>Xx*o zFcU1cN5PfVY_2#v=g+HjKCNkxFSRdt+5;kJFMxnZ_anaLjGnqP9#dYs5<53>;X)`H zFb=4p`hNNnK)rRlY1wXc2SW!)x~5vc31QtNU?h&syhDxpS!6 z_R_i}wAgAyu-a-C;&3lTo@#j14~LF%Nq9F$ilw>a*r;vnDoc0X{QgtgKt0tXoIxiI76OL2a`_z^*$m3}UL5 zhnrKM907J301(x!GsR3s!Qe5X*k)9Vl#(dWLEzYNiiYJkz_`zxLgp#xO{G~eAn9$} zcN;vP%g&@{pWuZ~enFxElWtMij*6n&6lFe_DrQu`9g6aHF_A%C;HyPZX3|tEWYSqM zJ`!eVZXVPs(DZevq?+zi6wFuY(}HT11xxw_lGG;v2;p8RC7BKoV2mfMl)p1n)qWnnpW*H#htR$ZRk(Z7qF z)Vm4c-D}81aje`z4ub@bY1u{VhQ+&7Gdn7#6WCb@{}YgU2y@R1;YNSUo%Epu|%2E8>YjW#q7u2{I!7e!a3;QSfaqTEuEb; z8WqE=m?`La0FZ%vh_iYnu_~l=c8YF#3e{5dh3Lko4O+);x#G zoe9c9*tMKY~R#QDA<$lMF9T)Fe0DCaz9XlyLIwk`C=%fU2u?-3)`k)$o7tf1f`#!tF zqyXz115M8SZxxWb|MAcBzc^{w`zIa<-Bz=#Ku@;idqz*kN(cWuA9=A%KaoXQiW3)+ zO~;@sWd3p-e8yGpqn`X0lHHFkOk}K=n#mQjDH?=B=;nFoqZ700j}n_*wEF88kqQX( z0C2E6q&w`k*$8gDhtRx;=w?kjBZXwcvryI=9 zzA+DXj~jD|D*?bBaom_&e8f8ucXI$2-OxB?7q%Js|H8f$(iovp1ZY$Wxr5NF02a-D zIB#0rX-q5LfzkcNfaw{-#c?qLe6%S5|B>N79sFc)rL$t|TNC?AV&9h8%%zT_CHqiG z9BP_v-%~1s{Al2iZYnI~As-WN#KSUC))1;LetD~-;loV$=M1y z`!yN*n)G~6hQ1-%pNaMj>G_%*_|EECIlMZ%`bOD$V#)NKyMJ}Q>^`&P{2Mt^6I(g= z!@%nKFFU^4QyIEgI()I*H(nz^EnR)1^k%ZGW=iw#R@C=OZ@yQ)`u--))DD=~q6=Rh zEWI4A3`a_bBjvtJn*_+4ZVT7Dd}@=xtL7!V<>Rgoy6#+B^{!^hfzwM}CGJd(cX6hw z-Mc(_|3qouNTqwU0!>LxfcLl05h?+_W+E{2j94>FpxAyUqSo0I+gsuMNE@z;9e#n> R;TMS=MzMR>Q3UIJ{sqTte@FlT literal 0 HcmV?d00001 diff --git a/api/endpoints/__pycache__/assets_router.cpython-313.pyc b/api/endpoints/__pycache__/assets_router.cpython-313.pyc index dd7f063207f0fbf9d6b07396202907918d8b1cd8..2b0a8b29246690f5f07d2da710d048d34d7f6e23 100644 GIT binary patch delta 1824 zcmb7EUrbt87{BL&fCy>ox~Hun*~?zK=OxQ+Sh9GNJ?vr0mTd{fX3sm{MQDppBk()l{mwbR z-}mqGW8%B{))Twk%FxfBpB!Z0@jPsGqYod3{^59rZ(R&7gm8#s1XF8Q+YjnhV4u)F$5XVJ`6UnOw?J1{s&8>fn{>r9v&{; zG9cs%BdvW~R8pHojK#c?D#{`*rHr4k7kxB$+(sd6!Chc?1L$5qwX~+REQ<;bgEs^) zK!(kNbr?(l4ry^P5>wE+sAJQA+?0_5$>gY;*t0Ae=L1^IH;* zoLM8XJiB2;W!SD(iO*QFiC!G-ZzC29%Go&P3!l_uamYY zl9JA)Hl<=-2{&WV#Nz-FGG)8s*HZ*~JPi;BxTJ3b%#hD*!8ToI7M=m#BtP4Z+y<=` zi*iBA%VKI*qLJxYKt!h08>|9 z1S>(7oShx@7&XgQHYeuOyCPmBcby|BNgg?;QH1>F>`X4f1qT3-Xy!XYb0ShE!iIVqhvZ412fYo+_HTTQNc zYA$F0pzTsFCO3O&7n z0;;e!)L!v1tmT1c-*Yhc>9!i2Ec>R^?&&>`isIy}o{gDHaB2}%I5_6r_a3YtzW*fn z%iQ`?`pp(9Gzv7w(r|Xr8!UawqRZ@_ml|w>CB1>!dkMC}hLNdq1D!Ic_Ws9f>g-Z^ zVp)x??0Hr6*3r<#Z>P#buN*Ues&&Xe)7KLN!(O9Ng0C|7{5tp8tWGD(qgT}E0$g7_ f3JyQoE(a%%88dPm8lJ%dm#?B@)Fkmp4wG9U20IFk>5YjZFg@&eSRHjiw*w!_$0aN2ey|j@m^^jgFMLkrNDk=^U+kc>!s-{HBweJnTWD-{Lw{PCeeBU?k zjrXTFe;@bldA)84o`=6a$eC>$z930$^!`)U98rn-sDFsih7p;?wfNLcTvq$gGs9#* zLmM@BRMOPLaayKLS|1H)jkH-y&=xIBTeU#iO@r;yl5L3$9}uyAV1B=r6-H%=p3!_$ zw_li$XxoT4=qgwvLL zj_FxATPAB-N{QtY9mj?(*WFxRFJzZ>Hp!ni4U!2S4_qWko(Z&;uOmex0@7N}#cVNa znrxr{EzAq~w=uc_0F4_v%8KI)XA_XK{N`e5dA5+vMB-3riSkvMB`YmngBN zz(JWPOEczfAqayMBfMxRJP)Nh(8cpxb>pAR=__W=C|Gu#F@r(B#o1~k{BS?TZD1cE z*&zQs)Io;$*P)Po5fWcieh%%(^~WZY{9!m46IagxEidKGq73T}FWwT8AbiWM@H`o= zyw=rE$P`~vqhTbm9Ly-0I>&-+uq3-4 zw6C?Vk9`^DGtrqy1Lm)js)`e#wb1(X=4ZdepN>ucIX1l-+9G%Of6-Szo`LNN;;7yx z2g|m+J=-L{lT1Cju$>&;mAZgqlRgvgJw=jxt1_mlW5*lRZ4%ju4}7<<9Y4P-IZ5ND R@(f?NJi04G#J&Ri{{zyGQicEk diff --git a/api/endpoints/__pycache__/auth.cpython-313.pyc b/api/endpoints/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..882af3a736b1d7e5ff8a428d11087efdab5d8cdd GIT binary patch literal 6114 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 LPrYItS+e=RY*to@ literal 0 HcmV?d00001 diff --git a/api/endpoints/__pycache__/character_router.cpython-313.pyc b/api/endpoints/__pycache__/character_router.cpython-313.pyc index d20fbb0c2967886358a2b921aa1fd556dbf68a92..6fffb5a079d2730b867d4a5d8a79ca6d923dacbe 100644 GIT binary patch delta 919 zcmZ{j&1(}u7{+I^o0x=6Y|}R8%VhJ>#AurwnhGhUB2o`}uyN`a41_dU)2*6>$*$-r zCqasrvUl&rOC|q=UL-+Jdl3(!f@(iMoS0w;oWE6_czZD?AOFQ)py|Yc@eD2 z_H+H^jxUUtc5-if1QFl$#5I+vDx%I5B7SnZh^Z@usJe)GKSD)JCdv{^(&Ww6ku$Dj zl42Tkf<*#lhnS2UPL?p4q6(R&Au@B=RdSJF1Z~M%coCtVAhGlb=rh!R97;k2l_V0T z83B&aSrUb3hWblh5<5bV$k`LfXK9d=d&lx}?BC46lAFZ;L*@Wk8NY#1O%gP~ks_E% zCk17O27r*{1o&F9SiY&`Q{Q-O$Ic9tq5cQ?ry~>RZi0&nV*3Pqv&L$5 zLu1UtH|DJGr`Bh)yo_?maXANKd+JA>bKIP_GHD@0gSxN1acIMPcMLUWe^n>OG9B#{S&9U@wBQpSQ&Qa`@g(bv1^pxx~yd50=5KO zOb}O~8iQHSl!WsVcqMCBxq{F4e<&&36Tt$tj$VMP>a_Ki(JjkYqFF$HG%!k?vyA2-yJG$GT`x3_ZBgzd? u?hCR^e=+tM6+d|bgUE%w<-Nji?atut=KD2$aARxedC-6DU%>J$82$rCHNJ%a delta 742 zcmZXSJ8u&~5XX0Y51k#~1wXjhCVmn-#O5K$MG(mlnMfcCnruY1lI6sg<07!5^%;RE zDBuf3Tkrv>=pr2@J%WNqdmR-}2T?P-Rt$1hy3y{;e}4C$)qbx0EStxsnLx1KeR$<> z9-4VvI$ZvkrCJRUlUP?UnO~2wGA(a)cqfs$dxm0aYoI5o^%&J_h!)nc=ts!GBu$ef zLvtj1qH=U{wt)84eY`diAy%ETY-*jRkOhJqh2&^WlJfLCDZrPdmXjdoW>`xntc^u# zpJst0!TIOPb_`OSVbpJzv}gzPY_{f^BoifmDY5>FX9+~iGTyS^l~BGg4_ zK#1bI4|}Y)8@P-$MY6@;7!6$IpNxX$K%yA_GXCIg0gSZ1yEFD!N$4&@SW%foPCSV0 zRR|*$LIoD9!Y0aiZqS|nq`UL1>-X7psIxjR^&SmiI0joT7-14>L1+pwE~6QmN5KIpvlMJ(bFr9xL_IQ?-alq^mSirKVEV96&^>dg~ki@Z*$`_wAc`Z@>5E zn{WJc*H+MRWVcr_F#dl0Rnp>m=%{DcxQAVj7bcoWlf+;K+a|rlD|@G})2y?@&&VxT zQDKd=LT>e>G~pvYiII6fgYDQ6M6wmKe5I@)!p<;9{IXyFk*&Sql0DcRX7f4DQcehA zQLe=`vJHFWI$SGzLKb|Xky+ta(A8oCM%1Exrdnc8e$|`lp{^_Y=*;6mpF)|RBA88 z<3L$!J%Dxh6lp~lx64h_@|pGV{Om2UbceYzW)Uv-59KbEl?yK00zJBe`v(V*DX3ap z%c@auUCW9VTg(!oq_tR9Rmi$47+*~WmzA_a;#xA3R!I#s5a3KPQMZd4ul~g97dyaJE5S&GK{|EAT37K7toU=UtvMvpvmw{O1h^ua{=KcP zCq$QzW5f$nl?Ov%NGA;rluTdljcp4Fg;w;>?F~X34N3YFdrM!Kx`o|K@Pr0sj zv!<uD(OhT|JYs1V}4*3bC?ga z3R-3?O!UIL+!6{@MAv-@C8cP}sVzy1@l;AlNJ|+ajgMTHl8+F>tb0a}#UZ6L8bq_{ zM1UnDfNly2MT;j>ferrTbWQhL$fkX4X6E*-wMAu>iVRNgm@KohkexU~HyH)=yorLN zz6a(nW|R4YuX-?ff6@^9cRtv;X}E@;@gvVY9s8b}1G}DqADZ_(qx+80&C!=O@r9#l zn=?WayN*f2GWp!*Jgj7#W9VPi30PlVj#$ulC?fDXb>Ibh^{1}GYAcXVfD=t29{n4) ze}k&T6ybtg0qw75l4(u763=QMky;uMNG1!ZCQp@-7rn`WL#SR&4sM#s!50PTQL*Gz zkUwQg8|B`tp|r&MdPA}3RBU)EI&&*Jli&5X%n`$Ymhu`hErXwA64{h8MB;!0hr*~| z>!KLsUNK%hBG!0cF~Q%hqS4s1^Zw4z?wvVfKDl>i#kl?Pt~Hg5i@%`{4_For9aJ%_ z*zPBG7--X}&f)W38Z=M))>SnWnv|s09#=^b5xz7%n3gp`EKcFvTWcCex QA8`zuMTac7WC8m452PD~Hvj+t delta 1137 zcmZ`(&ubGw6yDivH_4{i{5DNYwwp9<6Og2>NwHefA|6GdQ*T3Sq}z~UlQ2m^ytGin za~Tki3LfmG6axMYo}}tQSQPwmFO(u)eY2aiG1Z0r_M3U{eeavM%kCE5Wz|!!R}tXa z-G5el<=s<9#2?~b_PI>sB)&AecNE!9ln_r41ezpLTEHGWTtL`6CsK`QOAB_aAF)cp z*f%EXuXeqN2`YIMS7Lf&rx`Nn}`E zVlPo_I!Z*68seEF1d$<&owtb%v`G_6z^H})Ti3`upT7%0kke4u*Lu0|6unrI63 zW?q!=Iy_cuo0Y0T^RTaV4wMeMKLeCr1;{zh^-W*ZXs`Ai zBPY>mo_Ll`W3X&V0KApei1IJcf_&%g@Gg>VyKF&?$vO`u*@~*o%yMd<7+A{z&{Hsd z%|@%gFTD=YQs+QjMrE#M4y_pvx2mT4x4oQ5Z$Q-cBwB*DqcY#+aFZ?i-|OdH`;!)V z5{KI?ADBfsRt%l6QdoOnb(Cpit8GwI;?}G;V>*CXBQzc0!*tZ@ZTsxpt<}zw+ZRq@ zE!Z!?HNV+d+j?Y_=`!r`pFu0nj>850r=Wgw`#-zmAKmd6*FL#3JMwpT@F;%e@XlfR TgZozJUgRrMPDBxvDTcB?E)U#! diff --git a/api/endpoints/admin.py b/api/endpoints/admin.py new file mode 100644 index 0000000..5c16ec8 --- /dev/null +++ b/api/endpoints/admin.py @@ -0,0 +1,96 @@ +from typing import Annotated, List + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel + +from repos.user_repo import UsersRepo, UserStatus +from utils.security import verify_password, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, SECRET_KEY +from jose import JWTError, jwt +from starlette.requests import Request + +router = APIRouter(prefix="/api/admin", tags=["admin"]) + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token") + +from api.endpoints.auth import get_users_repo + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], repo: Annotated[UsersRepo, Depends(get_users_repo)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + user = await repo.get_user_by_username(username) + if user is None: + raise credentials_exception + return user + +async def get_current_admin(user: Annotated[dict, Depends(get_current_user)]): + if not user.get("is_admin"): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not enough permissions", + ) + return user + +class UserResponse(BaseModel): + username: str + full_name: str | None = None + status: str + created_at: str | None = None + is_admin: bool + + class Config: + from_attributes = True + +@router.get("/approvals", response_model=List[UserResponse]) +async def list_pending_users( + admin: Annotated[dict, Depends(get_current_admin)], + repo: Annotated[UsersRepo, Depends(get_users_repo)] +): + users = await repo.get_pending_users() + # Pydantic conversion handles the list of dicts + return [ + UserResponse( + username=u["username"], + full_name=u.get("full_name"), + status=u["status"], + created_at=str(u.get("created_at")), + is_admin=u.get("is_admin", False) + ) for u in users + ] + +@router.post("/approve/{username}") +async def approve_user( + username: str, + admin: Annotated[dict, Depends(get_current_admin)], + repo: Annotated[UsersRepo, Depends(get_users_repo)] +): + user = await repo.get_user_by_username(username) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + await repo.approve_user(username) + return {"message": f"User {username} approved"} + +@router.post("/deny/{username}") +async def deny_user( + username: str, + admin: Annotated[dict, Depends(get_current_admin)], + repo: Annotated[UsersRepo, Depends(get_users_repo)] +): + user = await repo.get_user_by_username(username) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + await repo.deny_user(username) + return {"message": f"User {username} denied"} diff --git a/api/endpoints/assets_router.py b/api/endpoints/assets_router.py index 8b2ae0e..689254d 100644 --- a/api/endpoints/assets_router.py +++ b/api/endpoints/assets_router.py @@ -18,6 +18,8 @@ import logging logger = logging.getLogger(__name__) +from api.endpoints.auth import get_current_user + router = APIRouter(prefix="/api/assets", tags=["Assets"]) @@ -49,7 +51,7 @@ async def get_asset( return Response(content=content, media_type=media_type, headers=headers) -@router.delete("/{asset_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete("/{asset_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_user)]) async def delete_asset( asset_id: str, dao: DAO = Depends(get_dao) @@ -65,7 +67,7 @@ async def delete_asset( return None -@router.get("") +@router.get("", dependencies=[Depends(get_current_user)]) async def get_assets(request: Request, dao: DAO = Depends(get_dao), type: Optional[str] = None, limit: int = 10, offset: int = 0) -> AssetsResponse: logger.info(f"get_assets called. Limit: {limit}, Offset: {offset}") assets = await dao.assets.get_assets(type, limit, offset) @@ -82,7 +84,7 @@ async def get_assets(request: Request, dao: DAO = Depends(get_dao), type: Option -@router.post("/upload", response_model=AssetResponse, status_code=status.HTTP_201_CREATED) +@router.post("/upload", response_model=AssetResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(get_current_user)]) async def upload_asset( file: UploadFile = File(...), linked_char_id: Optional[str] = Form(None), @@ -127,7 +129,7 @@ async def upload_asset( ) -@router.post("/regenerate_thumbnails") +@router.post("/regenerate_thumbnails", dependencies=[Depends(get_current_user)]) async def regenerate_thumbnails(dao: DAO = Depends(get_dao)): """ Regenerates thumbnails for all existing image assets that don't have one. @@ -161,7 +163,7 @@ async def regenerate_thumbnails(dao: DAO = Depends(get_dao)): return {"status": "completed", "processed": count, "updated": updated} -@router.post("/migrate_to_minio") +@router.post("/migrate_to_minio", dependencies=[Depends(get_current_user)]) async def migrate_to_minio(dao: DAO = Depends(get_dao)): """ Migrates assets from MongoDB to MinIO. diff --git a/api/endpoints/auth.py b/api/endpoints/auth.py new file mode 100644 index 0000000..b1b81b0 --- /dev/null +++ b/api/endpoints/auth.py @@ -0,0 +1,122 @@ +from datetime import timedelta +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel +from jose import JWTError, jwt + +from repos.user_repo import UsersRepo, UserStatus +from utils.security import verify_password, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, SECRET_KEY +from starlette.requests import Request + +router = APIRouter(prefix="/api/auth", tags=["auth"]) + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token") + +async def get_users_repo(request: Request) -> UsersRepo: + if not hasattr(request.app.state, "users_repo"): + raise HTTPException(status_code=500, detail="Users repo not initialized") + return request.app.state.users_repo + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], repo: Annotated[UsersRepo, Depends(get_users_repo)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + user = await repo.get_user_by_username(username) + if user is None: + raise credentials_exception + return user + +async def get_current_admin(user: Annotated[dict, Depends(get_current_user)]): + if not user.get("is_admin"): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not enough permissions", + ) + return user + + +class UserRegister(BaseModel): + username: str + password: str + full_name: str | None = None + + +class Token(BaseModel): + access_token: str + token_type: str + + +class UserResponse(BaseModel): + username: str + full_name: str | None = None + status: str + is_admin: bool = False + + +@router.get("/me", response_model=UserResponse) +async def read_users_me(current_user: Annotated[dict, Depends(get_current_user)]): + return current_user + + + + + +@router.post("/register") +async def register(user_data: UserRegister, repo: Annotated[UsersRepo, Depends(get_users_repo)]): + try: + await repo.create_user( + username=user_data.username, + password=user_data.password, + full_name=user_data.full_name + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + return {"message": "Registration successful. Please wait for administrator approval."} + + +@router.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()], + repo: Annotated[UsersRepo, Depends(get_users_repo)] +): + user = await repo.get_user_by_username(form_data.username) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Проверяем пароль + if not verify_password(form_data.password, user["hashed_password"]): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Проверка статуса + if user.get("status") != UserStatus.ALLOWED: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Account is not approved yet. Please contact administrator.", + ) + + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user["username"]}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} diff --git a/api/endpoints/character_router.py b/api/endpoints/character_router.py index 2f55f5f..eb7b54e 100644 --- a/api/endpoints/character_router.py +++ b/api/endpoints/character_router.py @@ -16,7 +16,9 @@ import logging logger = logging.getLogger(__name__) -router = APIRouter(prefix="/api/characters", tags=["Characters"]) +from api.endpoints.auth import get_current_user + +router = APIRouter(prefix="/api/characters", tags=["Characters"], dependencies=[Depends(get_current_user)]) @router.get("/", response_model=List[Character]) diff --git a/api/endpoints/generation_router.py b/api/endpoints/generation_router.py index 575ba5c..0c3152b 100644 --- a/api/endpoints/generation_router.py +++ b/api/endpoints/generation_router.py @@ -11,11 +11,15 @@ from api.models.GenerationRequest import GenerationResponse, GenerationRequest, from api.service.generation_service import GenerationService from models.Generation import Generation +from starlette import status + import logging logger = logging.getLogger(__name__) -router = APIRouter(prefix='/api/generations', tags=["Generation"]) +from api.endpoints.auth import get_current_user + +router = APIRouter(prefix='/api/generations', tags=["Generation"], dependencies=[Depends(get_current_user)]) @router.post("/prompt-assistant", response_model=PromptResponse) @@ -69,3 +73,12 @@ async def get_generation(generation_id: str, async def get_running_generations(request: Request, generation_service: GenerationService = Depends(get_generation_service)): return await generation_service.get_running_generations() + + +@router.delete("/{generation_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_current_user)]) +async def delete_generation(generation_id: str, generation_service: GenerationService = Depends(get_generation_service)): + logger.info(f"delete_generation called for ID: {generation_id}") + deleted = await generation_service.delete_generation(generation_id) + if not deleted: + raise HTTPException(status_code=404, detail="Generation not found") + return None \ No newline at end of file diff --git a/api/service/.DS_Store b/api/service/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c02be598938e943fae4eaa0ae151d74c4d81b8fa GIT binary patch literal 6148 zcmeHKOHRZv47Fhvm1Z+bmbt=+3xrWQK@U)v4lqJ9YBwy{MdBK4ISYqk!}EtvML~iE zLdcfl=OlLGKB-AeM7+A$EQuCGRG|s77!xAHlT!!oJOi?>(a|||w51DbCl>mPLz4R( z*|s#$EpmU$zujFeo37svmUi>-x#zyG>-DZ*!=pOBzI(d7e?0lhd-DzNVf#J{PsUOT z9Sj5m!9Xw&4E%%v+}R@4+%R-75DWwZuMEifkkACPV=>gD14>H(pgf~hU`s6_G08DI z7DLQH*g}C8%3flyg=0LqUv?~p7EbKN2m8)1#S821SU;&daW)Jc3 zzszKjKMsjrFc1v|NiY1Hv0rwIxw3`)FmR(#zfQu|rnyz>u`v6%)xUF@e)oDXm zUQjU+ebHp1(HBY3=q75!)I@!&-G2b038X=@8~0_iFDRSnu0D9Cz=|iCZ@&5FJ7><-XjKurth-1B zPgOESnImL zAjc@cIJrzCvH}T9pCz!7IfDL}#h`tbQFIi7=k1mLer00?4d@Vo4si+w|G~bS-{>sm zm+Z>^&njyWqs0(8f}^Ms`gg4R0g7N~7eIpcmu@PD%lPSldrImfPB7#?>V$e}^CS&a z>^MQ&D0Wtuqa+H=RD~${mPSc%+vGl*k&LFb5p&c`n_A3Fm~1E=PbRdZzi4Aa?B{qQ zrp43RP-??6QfrHR-#h8;U@XbjHDQ_BlF6=OZ$V|t6C6V!Z;Ph}*N+)t;)d8Y0g`}Q z0t)Z6<%v)WN~}d}MFpG^uoodEax@IqfFxfcicY1?!A~vZt2+Px5W{5S@nN%e{pc;G zgZLb&S*-9EoPyP2yVbL-R?SXan^;oo7uEVDRbN!~Z1XdoaWQ;8 zyx?n?ADbV{>e-Xo;RO#ScCWa1zF(Em@*&la&72|al$;IdC2*JM4(Xm9*<+zSX>%gY z6%pMf<%%W26`|)GL~s|;Rh&v}?Lc!rpWZ}s-^gOSNk-*2pzV}rLthd<|2x!6Zt~Xd zPGwG9pL8^t9A$Yv)7?nCnWx<{+4B#4A-AmVbIG20V-;|E-1}BcOveP)h!Ct}HGjGZ@ delta 258 zcmZo##<+SOBj0CUUM>b8IKHwy^Q`SgzQ-IQOpFW+QyF3yiWq|#ikN~KiUo7ts{B-5ERD1apC4B&p^h>ReqX`Qj?eXg)lCj{L{~!ap7bi ze+x$T&8_~djI!Su#8~Z%oIsi^fP|)gkvfRI3`DHkyeq(qQQ!uM4K{W$h;?VONRT?? Uo5?mo+xcBt80|kXfJm?r075}QumAu6 diff --git a/api/service/generation_service.py b/api/service/generation_service.py index d098a6b..24d6421 100644 --- a/api/service/generation_service.py +++ b/api/service/generation_service.py @@ -323,3 +323,21 @@ class GenerationService: pass except Exception as e: logger.error(f"Error in progress simulation: {e}") + + + async def delete_generation(self, generation_id: str) -> bool: + """ + Soft delete generation by marking it as deleted. + """ + try: + generation = await self.dao.generations.get_generation(generation_id) + if not generation: + return False + + generation.is_deleted = True + generation.updated_at = datetime.now(UTC) + await self.dao.generations.update_generation(generation) + return True + except Exception as e: + logger.error(f"Error deleting generation {generation_id}: {e}") + return False \ No newline at end of file diff --git a/main.py b/main.py index 2a86438..d454420 100644 --- a/main.py +++ b/main.py @@ -36,6 +36,8 @@ from routers.assets_router import router as assets_router # Роутер бот from api.endpoints.assets_router import router as api_assets_router # Роутер FastAPI from api.endpoints.character_router import router as api_char_router # Роутер FastAPI from api.endpoints.generation_router import router as api_gen_router +from api.endpoints.auth import router as api_auth_router +from api.endpoints.admin import router as api_admin_router load_dotenv() logger = logging.getLogger(__name__) @@ -126,11 +128,11 @@ async def lifespan(app: FastAPI): # Инициализируем DAO для ассетов и кладем в state приложения # Теперь в эндпоинтах можно делать request.app.state.assets_dao - app.state.mongo_client = mongo_client app.state.mongo_client = mongo_client app.state.gemini_client = gemini app.state.bot = bot app.state.s3_adapter = s3_adapter + app.state.users_repo = users_repo # Добавляем репозиторий в state print("✅ DB & DAO initialized") @@ -172,9 +174,15 @@ app.add_middleware( ) # Подключаем роутер API +from api.endpoints.auth import router as auth_api_router +from api.endpoints.admin import router as admin_api_router +app.include_router(auth_api_router) +app.include_router(admin_api_router) app.include_router(api_assets_router) app.include_router(api_char_router) app.include_router(api_gen_router) +app.include_router(api_admin_router) +app.include_router(api_auth_router) # --- ХЕНДЛЕРЫ БОТА (Main Router) --- diff --git a/models/.DS_Store b/models/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..961ab77e2290347d45672fd215db0140d3db9a68 GIT binary patch literal 6148 zcmeHKu}%Xq47H)dNnJWJp@=`|!W^pWj?@o0=oRSD>TY3R%YT$V0KS2dU*ISB1;le~ z3MY;Z3&2L8L_`IeAdAr>GTb}0XU+p4=QV0Nr<#^@K~>K} ze{o21A0pd|TDn2*ANg0StMROE)~%*}^zgmsx+%-~s+q&1XrJ$2E^nVsc6l$h-iODJ zuXmeW-tF>p7N?qwYiGb2a0Z+KXW-`y;La8)hKjD80cXG&*fJpJLqHRZhDk9W9Z?sM8q$N0fF6l j1fV15$U&ae`5-p@qG3{$Rb)Je1N|Y83325N`~m}STDmmr literal 0 HcmV?d00001 diff --git a/models/Generation.py b/models/Generation.py index 98cceda..88e87f7 100644 --- a/models/Generation.py +++ b/models/Generation.py @@ -33,6 +33,6 @@ class Generation(BaseModel): token_usage: Optional[int] = None input_token_usage: Optional[int] = None output_token_usage: Optional[int] = None - + is_deleted: bool = False created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) diff --git a/models/__pycache__/Generation.cpython-313.pyc b/models/__pycache__/Generation.cpython-313.pyc index 60e061d77d7d7b5f73e84d4d78a835475fc7388f..e208ed87a3f683878e14ba97b40733b14ca3d3f1 100644 GIT binary patch delta 239 zcmX>g{7RVbGcPX}0}zBx>&Rqc-N;wN#CT+LH&ZjCTd-u2VvIlxd$5!`P(DUPi6K~8 ziXoWKlCelBMj0qB0~eQtimQOd!&mx423RQWAk0;uA}XM1TUCYLm~gZ4)p9 zDX;?(%pk&HazDEoqsHcK?2L@uH^gNcye6<*W|6r$c?ZV~M#ss4oa&4|lPfvh0nJc1 A4*&oF delta 189 zcmaDQd_b7*GcPX}0}#Xrwq?Fz*~nMJ#JFv9H&ZjCMU03NL$IV2LolBuW07KvQVe^r zlsZTyNL(5!t_&8J0f`687OA8&Y07O5VX!yjRv^L} zNT`7rHj~e>NinKUe#y3t+ZH6i3?l3&uV+_dl-qobosp68;p9ggGZ-BvS97W}dQV== G=?(zGcqo_v diff --git a/repos/.DS_Store b/repos/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5b5c90173c06e09b5f2a282001d349e0efb2cdf1 GIT binary patch literal 6148 zcmeHKu}%Xq47H)dNnJWJ<`?=0p*kegwJXwU5&JS5H_tvlXZ&&k! z*6sRh7EcK4;tV(g&VV!E4E%}#+}R?89q~gpCyt8VI|I%@ zmw{6qj^zHoz%SEV!_?epZW4QO*T5wT06K%jRX0T{?R fa+Vi$J%|pwXqXfwip*zspg#mMA>KIyf55;yw>~tY literal 0 HcmV?d00001 diff --git a/repos/__pycache__/generation_repo.cpython-313.pyc b/repos/__pycache__/generation_repo.cpython-313.pyc index caf29439a859f973eb91c1b1c2b4a95ca336ed6a..67b9fa4646dba189e41538a2b7aa7ed2658f78eb 100644 GIT binary patch delta 391 zcmaDX_g{|pGcPX}0}!N5@5r3Gk#`N7Ruv-ygEE5x!&HV~#$YA`#yr+g#v~R7h6vGM zW_6&-P$n=>HJD}ceKuca)?l{D`s|BE*-e4^KmbOEas+cse$KAS$T^vpL!FseF9u|Rc{fxwJ}?7CZ1x JERq144FFwVZZ-e_ delta 337 zcmew__gIeiGcPX}0}$}9Y|kv*$h(G3rHqk*L772;VJd?GLmq1=V-gDkLxgBBqdHJk zC=-~cI@y=YaIz4)95Zt;%VcBr#iFdHKwTgJqeD4@*(SecS7l_MEXbkG#NjvDons0s zOJY%a@#IGw5u96rYF{!mFg%#7$E~O$A~D^6qW^Ufjf)~0D?~1f=v@~vz9?e6!RLU| zWf9lwB3>6oye3cJEE3RhvSd7{$LM6va!}uq)5&tO5Z7cz!^z9I3|WgTfksZg%jL|d zwONt7myywE@_L?fM%T&eyoKCWK)E725aBd=F>jMKFRSzhjUC1tbw4lwnHK^gE<|Qt z49EiWZm4Q}UuxE*>YOF1mz#1qptXo1DP+iZN)iH@_;s24e~1Ck7B%Bmp!V E0HFa_^Z)<= diff --git a/repos/__pycache__/user_repo.cpython-313.pyc b/repos/__pycache__/user_repo.cpython-313.pyc index 52728bf6fc699a5e86ca5beeb63236f8b60c9f1b..997fb7247c959455cf4a41a28afbee18566c36e9 100644 GIT binary patch literal 6208 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 N&2fRx2}-t~e*qoqCCLB) delta 1103 zcmY*X-D?zA6u)=p&g{%&KQbFPyV(}aT4S7G)-@z)f~{gBw1RHHF+K(2t~*&*c4y+f zGZyKC#lG}S!X=1(t$iw#f=^Nk^`-R5KVTq;pbsKKw1TJ)dd~HGV19G%@7(kCJM&Nd zUM2T`Hfs}jUj6W6w`Q*A%JlU5`25~7*-1RTLOf$8g>FK)skk(7=6lZSSgyT(ef%@F zBz*4Dq;ObV49*-~JRU|NU+8uHAd+c%oSJfpZqU4ZNWZf!%!XjfuTwkhk$1;4>rQdo zDx5aRGU3!CJmpcYc{zC{H95zY4bNP*rtx-0VqrJp zgufO($jc*ItF#@mCh2x-C2012p}}c%bd*=*-&&Op$~)Q!9g)wq%M;_^aZ`LJUN={} zL3NQ8Y=WRR;5F7Q?JOoY^WqMz+bLT&|m2uc3q#K35%fz8DxhklYix| zrfRx8m7kQ4a-~|GdfJi($UJ>sI|?G7g_bVR4XvSP$SCO;CTZwjYOB;u$JgY4`Cl&8 zY*$weBqrv?F%oMv?)B6}i6Nq96pO^^HAS?-{qwO8-*z+dRnQBzOZd@A*pCsfL3q?~ zEsiQAS`dDpq`GZhgA?C}pw=^tmXg7a*V@g=$X80RH%jb9?=(QdphiQFXC_sf07;-Lu$NVJY0sUcOT9D8)%_OI&T5Q(J( zL!mwzLjF{op>=t)c;QkNE;UfMRtS2w+&muyhC2uagdv19!WclJb%Tgwd2S%gDNaP3 zA3%$>!m!5|&{IapBdGr$h2x*U1LVabfV@4tBjf1ypVLnXe71JWZ%boSDgrMzT>H(0 zI9$T~AntP=&1|z9c6hU2i!QGDf*bPp(#VSHQf*fi`m}gre$@}#anC= List[Generation]: - args = {} + + filter = {"is_deleted": False} if character_id is not None: - args["linked_character_id"] = character_id + filter["linked_character_id"] = character_id if status is not None: - args["status"] = status - res = await self.collection.find(args).sort("created_at", -1).skip( + filter["status"] = status + res = await self.collection.find(filter).sort("created_at", -1).skip( offset).limit(limit).to_list(None) generations: List[Generation] = [] for generation in res: diff --git a/repos/user_repo.py b/repos/user_repo.py index 4f6e405..a1434cd 100644 --- a/repos/user_repo.py +++ b/repos/user_repo.py @@ -1,8 +1,10 @@ from datetime import datetime, timedelta from enum import Enum +from typing import Optional from aiogram.types import User from motor.motor_asyncio import AsyncIOMotorClient +from utils.security import get_password_hash class UserStatus: @@ -19,10 +21,49 @@ class UsersRepo: async def get_user(self, user_id: int): return await self.collection.find_one({"user_id": user_id}) + async def get_user_by_username(self, username: str): + return await self.collection.find_one({"username": username}) + + async def create_user(self, username: str, password: str, full_name: Optional[str] = None): + """Создает нового пользователя с username/паролем""" + existing = await self.get_user_by_username(username) + if existing: + raise ValueError("User with this username already exists") + + user_doc = { + "username": username, + "hashed_password": get_password_hash(password), + "full_name": full_name, + "status": UserStatus.PENDING, # По умолчанию PENDING + "created_at": datetime.now(), + "is_email_user": False, # Теперь это просто "обычный" юзер, не телеграм (хотя поле можно переименовать) + "is_web_user": True, + "is_admin": False + } + result = await self.collection.insert_one(user_doc) + return await self.collection.find_one({"_id": result.inserted_id}) + + async def get_pending_users(self): + """Возвращает список пользователей со статусом PENDING""" + cursor = self.collection.find({"status": UserStatus.PENDING}) + return await cursor.to_list(length=100) + + async def approve_user(self, username: str): + await self.collection.update_one( + {"username": username}, + {"$set": {"status": UserStatus.ALLOWED}} + ) + + async def deny_user(self, username: str): + await self.collection.update_one( + {"username": username}, + {"$set": {"status": UserStatus.DENIED}} + ) + async def create_or_update_request(self, user: User): """ Обновляет дату последнего запроса и ставит статус PENDING. - Сохраняет всю инфу о юзере. + Сохраняет всю инфу о юзере (для Telegram пользователей). """ now = datetime.now() data = { @@ -30,7 +71,8 @@ class UsersRepo: "username": user.username, "full_name": user.full_name, "status": UserStatus.PENDING, - "last_request_date": now + "last_request_date": now, + "is_email_user": False } await self.collection.update_one( {"user_id": user.id}, diff --git a/requirements.txt b/requirements.txt index 7a19a3b..9e8ec37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,3 +46,7 @@ uvicorn==0.40.0 websockets==15.0.1 yarl==1.22.0 aioboto3==13.3.0 +passlib[argon2]==1.7.4 +python-jose[cryptography]==3.3.0 +python-multipart==0.0.22 +email-validator diff --git a/tests/__pycache__/test_api_protection.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_api_protection.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cee69a1c307850f0e51174d37bd8849224a44979 GIT binary patch literal 3338 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 Na&L3T*gh(Pe*uq%HCq4x literal 0 HcmV?d00001 diff --git a/tests/test_api_protection.py b/tests/test_api_protection.py new file mode 100644 index 0000000..99e8e8c --- /dev/null +++ b/tests/test_api_protection.py @@ -0,0 +1,22 @@ +import pytest +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +def test_api_protection(): + # 1. Assets + response = client.get("/api/assets") + assert response.status_code == 401 + + # 2. Characters + response = client.get("/api/characters") + assert response.status_code == 401 + + # 3. Generations + response = client.get("/api/generations") + assert response.status_code == 401 + + # 4. Upload Asset (POST) + response = client.post("/api/assets/upload") + assert response.status_code == 401 diff --git a/tests/test_auth_flow.py b/tests/test_auth_flow.py new file mode 100644 index 0000000..c0b7f8f --- /dev/null +++ b/tests/test_auth_flow.py @@ -0,0 +1,107 @@ +import pytest +from fastapi.testclient import TestClient +from unittest.mock import AsyncMock, MagicMock +from datetime import datetime +from main import app +from api.endpoints.auth import get_users_repo +from repos.user_repo import UsersRepo, UserStatus +from utils.security import get_password_hash + +# Mock Repository +class MockUsersRepo: + def __init__(self): + self.users = {} + + async def get_user_by_username(self, username: str): + return self.users.get(username) + + async def create_user(self, username: str, password: str, full_name: str = None): + if username in self.users: + raise ValueError("User with this username already exists") + + user = { + "username": username, + "hashed_password": get_password_hash(password), + "full_name": full_name, + "status": UserStatus.PENDING, + "is_email_user": False, + "is_admin": False, + "created_at": datetime.now() + } + self.users[username] = user + return user + + async def get_pending_users(self): + return [u for u in self.users.values() if u["status"] == UserStatus.PENDING] + + async def approve_user(self, username: str): + if username in self.users: + self.users[username]["status"] = UserStatus.ALLOWED + + async def deny_user(self, username: str): + if username in self.users: + self.users[username]["status"] = UserStatus.DENIED + +mock_repo = MockUsersRepo() + +# Override Dependency +app.dependency_overrides[get_users_repo] = lambda: mock_repo + +client = TestClient(app) + +def test_auth_flow_with_approval(): + # 1. Register (User) + user_data = { + "username": "newuser", + "password": "password123", + "full_name": "New User" + } + response = client.post("/auth/register", json=user_data) + assert response.status_code == 200 + assert response.json()["message"] == "Registration successful. Please wait for administrator approval." + + # 2. Try Login (User) -> Should Fail (Pending) + login_data = { + "username": "newuser", + "password": "password123" + } + response = client.post("/auth/token", data=login_data) + assert response.status_code == 403 + assert "not approved" in response.json()["detail"] + + # 3. Setup Admin (Backdoor for test) + mock_repo.users["admin"] = { + "username": "admin", + "hashed_password": get_password_hash("adminpass"), + "status": UserStatus.ALLOWED, + "is_admin": True, + "created_at": datetime.now() + } + + # 4. Admin Login + admin_login = { + "username": "admin", + "password": "adminpass" + } + response = client.post("/auth/token", data=admin_login) + assert response.status_code == 200 + admin_token = response.json()["access_token"] + admin_auth = {"Authorization": f"Bearer {admin_token}"} + + # 5. List Pending (Admin) + response = client.get("/admin/approvals", headers=admin_auth) + assert response.status_code == 200 + users = response.json() + assert len(users) >= 1 + assert users[0]["username"] == "newuser" + assert users[0]["status"] == "pending" + + # 6. Approve User (Admin) + response = client.post("/admin/approve/newuser", headers=admin_auth) + assert response.status_code == 200 + assert response.json()["message"] == "User newuser approved" + + # 7. Login User (Again) -> Should Success + response = client.post("/auth/token", data=login_data) + assert response.status_code == 200 + assert "access_token" in response.json() diff --git a/utils/__pycache__/security.cpython-313.pyc b/utils/__pycache__/security.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67f5cf7b95f3d6e610e980c60b4c528b7c1f289e GIT binary patch literal 1745 zcma)6%}*Og6rc63*Iq9e42Coyf<=K~RB%N@TTn!WEH_{`5P8`u2&&bxJB6iYcQv~X z_~u@!^p>VVaO99fBK43XhaU15L|9Y`8l{b@rib+8imKAVNM&!U4t? z#C(KA0$C_T^AeFj4wIN75?P6mmQ`s0DbWn4NJ?xfL|PSjHLkS4*SgwvLsa5DXj35V zN?U-$ZwN|zfFzVIrDL^2>0ISkxiCZo_HnRJvfWLNbgn1T-EUaP=@5;Yx{rNp3sZ@4 z6PJ8F&C}?j>syYkmuOhEL7@DsT}gA4-`w_LX~~j`>t`L?$2&gl&(6&*+{$UUbK26P zHmfP>om^2SW!qC#ay8eFp-Fc*X|a!XAy8*d^G2 za7-F^OS)xiuI_o;4l!xlhVE@(^It$D*e?^?2(fE4395p4_)gygwu@GpFhWI!UZT6) zWYc#LJ>eE05ZT=}HKTbY@g64Dmz8vgw1Xo_0P~oADn)N*R1Xs`vtxQ06W_xn$IZBv zluMjVZ1`SAx5kVOos6wHex~eOB`@P)qf9KnGVWGrTjR&)HzDu9U)dR6C$K7dDR;k+ z`)YFEzI`mu90@b003#YbONGHPy-Dm|Z+&AGO*VZ85lMn4T&@ciDzN0HIno1)$4pY> z3d0lY*gua`V@9M8aOQe3*xs7lyFYg<4;=|Z%I$wFvgKpsD^-DiO?r zz(qK37pbUt@mjFSXtJ>&JEU?WO<9KnR?drFWF@~u1LuA!HwJq)zu5n_9xG>q3@uFn4wNxn>6-RpnYf1DUN_@b7W_*Q&<4m?_Z zN@_#XwZ!y&u`Z%;{DqLL3CRa||MLghPr}gaj;_7Yhok$;Kg52tY8`Xc!in5{Byx6|&Sc?%>PbzFWFt9;h+SnMCthMpxoD)`9l4fj_nfA_RvpGe9 zXRMH0&~kV0%opKVn_p0ua!O-Zc+V$PSaY0GFuB1*K)+jtA7tSPYQz={Q1s@OW0p&J zhI|A+XC3Z+4Xn;{9QO*1yh7J&==$#{`7;`+M^QXejh(b~R;80`qerOs*Lcst+`;tW za4kMtmFptMeR7bkBUldK4VGtve=)5d<+$Mko bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + + # Стандартное поле 'exp' для JWT + to_encode.update({"exp": expire}) + + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt