From d200550b353d54a1666924114496ebea2771d811 Mon Sep 17 00:00:00 2001 From: bvandeusen Date: Mon, 28 Jul 2025 21:57:49 +0000 Subject: [PATCH] Initial commit --- README.md | 3 + __pycache__/config.cpython-312.pyc | Bin 0 -> 1070 bytes __pycache__/config.cpython-313.pyc | Bin 0 -> 1128 bytes __pycache__/run.cpython-312.pyc | Bin 0 -> 172 bytes __pycache__/run.cpython-313.pyc | Bin 0 -> 172 bytes app/__init__.py | 27 +++++ app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1147 bytes app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 1153 bytes app/__pycache__/auth.cpython-313.pyc | Bin 0 -> 2886 bytes app/__pycache__/forms.cpython-312.pyc | Bin 0 -> 2147 bytes app/__pycache__/forms.cpython-313.pyc | Bin 0 -> 2320 bytes app/__pycache__/main.cpython-313.pyc | Bin 0 -> 447 bytes app/__pycache__/models.cpython-312.pyc | Bin 0 -> 1245 bytes app/__pycache__/models.cpython-313.pyc | Bin 0 -> 1265 bytes app/__pycache__/routes.cpython-312.pyc | Bin 0 -> 3053 bytes app/__pycache__/routes.cpython-313.pyc | Bin 0 -> 3066 bytes app/auth.py | 41 +++++++ app/forms.py | 26 ++++ app/main.py | 9 ++ app/models.py | 13 ++ app/static/style.css | 4 + app/templates/index.html | 8 ++ app/templates/layout.html | 37 ++++++ app/templates/login.html | 10 ++ app/templates/register.html | 39 ++++++ config.py | 15 +++ docker-compose.yml | 32 +++++ dockerfile | 31 +++++ entrypoint.sh | 22 ++++ migrations/README | 1 + migrations/__pycache__/env.cpython-312.pyc | Bin 0 -> 4468 bytes migrations/__pycache__/env.cpython-313.pyc | Bin 0 -> 4508 bytes migrations/alembic.ini | 50 ++++++++ migrations/env.py | 113 ++++++++++++++++++ migrations/script.py.mako | 24 ++++ .../77b60347ee39_initial_migration.py | 36 ++++++ ...update_user_password_hash_length_to_256.py | 38 ++++++ ...0347ee39_initial_migration.cpython-313.pyc | Bin 0 -> 1519 bytes ...assword_hash_length_to_256.cpython-313.pyc | Bin 0 -> 1506 bytes requirements.txt | 7 ++ run.py | 5 + 41 files changed, 591 insertions(+) create mode 100644 README.md create mode 100644 __pycache__/config.cpython-312.pyc create mode 100644 __pycache__/config.cpython-313.pyc create mode 100644 __pycache__/run.cpython-312.pyc create mode 100644 __pycache__/run.cpython-313.pyc create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-312.pyc create mode 100644 app/__pycache__/__init__.cpython-313.pyc create mode 100644 app/__pycache__/auth.cpython-313.pyc create mode 100644 app/__pycache__/forms.cpython-312.pyc create mode 100644 app/__pycache__/forms.cpython-313.pyc create mode 100644 app/__pycache__/main.cpython-313.pyc create mode 100644 app/__pycache__/models.cpython-312.pyc create mode 100644 app/__pycache__/models.cpython-313.pyc create mode 100644 app/__pycache__/routes.cpython-312.pyc create mode 100644 app/__pycache__/routes.cpython-313.pyc create mode 100644 app/auth.py create mode 100644 app/forms.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 app/static/style.css create mode 100644 app/templates/index.html create mode 100644 app/templates/layout.html create mode 100644 app/templates/login.html create mode 100644 app/templates/register.html create mode 100644 config.py create mode 100644 docker-compose.yml create mode 100644 dockerfile create mode 100644 entrypoint.sh create mode 100644 migrations/README create mode 100644 migrations/__pycache__/env.cpython-312.pyc create mode 100644 migrations/__pycache__/env.cpython-313.pyc create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/77b60347ee39_initial_migration.py create mode 100644 migrations/versions/79c2e82697ce_update_user_password_hash_length_to_256.py create mode 100644 migrations/versions/__pycache__/77b60347ee39_initial_migration.cpython-313.pyc create mode 100644 migrations/versions/__pycache__/79c2e82697ce_update_user_password_hash_length_to_256.cpython-313.pyc create mode 100644 requirements.txt create mode 100644 run.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e2e9f2 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# _Flask_Template + +template for a flask app that has a configure user system and default connection to postgressql \ No newline at end of file diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53af171e79baddadd69abb855748f9d592bbf4d2 GIT binary patch literal 1070 zcmb7@F>Df17{}kcJK#9rEZRhiO`{GP(ohnsgN;$IaBZYzoE%I@8BCmfcl0RfV)%yt{lE8pzxQ5VzWeI)Nr1)g z&re@FH~@Z{i5E2?d4luy52r_^J^8j!0Erg5$B!=UhW@jsZ>_f^%Y5|2+2)oQU0hP8x!f z2JY#>9^yIz*h@sFKH|2##8f7pBk*3zc=45tuQn3&Q)i^4zsl#Rq^XglnnH_edV921 zs)pX=i^Y&mwulkRRY|2nUC~*zs;NmTmP?h*oI!d^D~hIBwS|P1q8xr{B`b+!iV6=O zOy9TCxU#Hn`)qEbI1>(2I78tx3d3{ppiD)C^z1r8h)M{pmvFUcdM`p7)ok&+!j;&G zMBmCAC7tp)vS~QjSQt)I%<(bYrkF!x_?Xb^+KVM+DYBq0r%_Z%DYJ^EqSa)KP7D-M zNhPv~mJ`v~Tr8rbVu`r+0dyG-{>tHOxg1`z-e;;@GtBX~glDGmiODWF^R literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..532e15c169e3b19f1f1e42acbc2a1f2680e22007 GIT binary patch literal 1128 zcmb7Dzfaph6h1p9aU93|Y6V4Y+A_3?P>58@P*k-wiGdVIs4onWBAo&l*C^QG>`;Y? zsS;f%Lj_~TRH`aQ#{LONS%~ucTL)lDER^)fXpJmU=)l4$c_ORn5GKXFrzbZ?$V{%br*f}dQ+$UiE%p= zX(a@hg$xc9In~el)%2}KyPRtZ*{+pBJ?J>tcQYa1gD%*m3-_Q4LH=evp$AE4_7_D7ObIv&2e1SOalbwkA11++Wo{b!Wq=+s5ulP% zl{{Kh3VU~IRyFa#D3=qawyWWUUe#=ygwnHU6)Gy>E0*o(SnGsVWC*?7LJsB$yRqgu zD>*e!xasFp&pbCPFDrXdU$;}9NhS$1Lzp*&NzQ!*+4unA5i(2L8bU-sXxrMTmR&9) zv{NmWuQUA!Z5r5i%7&?#7EQAj9YQmA4Q!btplc2e(#_zI3t<=B{|k#QNEFBje77Vo zr52Rs0!qtyc~*uBT2(V7)|JSsa%vGR=hB(EOiIpYa#=|3B4j%y$5=-t$HB&0)zNH( z@DOdf-rix;d!qzx^c* z9Bv(K9hwK`NueP$rK$b-i~q*Brj)o{7i>z&+p$i+RuDu+PePw-b*BEbey{PkG2eJ` zDxd17g|pI+&{^%A&$I%d?>;SjGSTG5@dJPSIEzPo?rpb|iqRP1Uv|FJEBuhU^m+r^ WJ&;y_VVIvFe8q{(#9u(c)BFR{3+>zh literal 0 HcmV?d00001 diff --git a/__pycache__/run.cpython-312.pyc b/__pycache__/run.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f16f763d0640e0cf2ab029a550d0830e66f9011b GIT binary patch literal 172 zcmX@j%ge<81pVvmGwgu$V-N=hn4pZ$azMs(h7^Vr#vF!R#wbQc1}277#??^iD27TV zP3D&%o+jfhuH>TB#FEtb#DW4pO{QDSK)Q$-D7BK|Gf2fRZhfGLeo<+jUP0w84x8Nk ol+v73yCM#t93v1H3j&D`%#4hTU)dNKr5~~hH0Lxj7qJ0F0jD)1MF0Q* literal 0 HcmV?d00001 diff --git a/__pycache__/run.cpython-313.pyc b/__pycache__/run.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6222379de9a21bf019dc906467f39dfc1db1e242 GIT binary patch literal 172 zcmey&%ge<81pVvmGwgu$V-N=hn4pZ$azMsZhG2$ZMsEf$#v(=qh8RXA22-eX5koqY zCi6=WPm}Q$S8`EmVo7RzVnKnQCetluAYH@^lv>H~8KmMCw?0rrzo;}%ub}c4hfQvN oN@-52T@eRRjuD891%bo|W=2NFuWSsA(hu1LnsXYNi`amo0DGGv!vFvP literal 0 HcmV?d00001 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..8661e40 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,27 @@ +# app/__init__.py + +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_login import LoginManager + +db = SQLAlchemy() +migrate = Migrate() +login_manager = LoginManager() + +def create_app(config_class='config.Config'): + app = Flask(__name__) + app.config.from_object(config_class) + + db.init_app(app) + migrate.init_app(app, db) + login_manager.init_app(app) + login_manager.login_view = 'auth.login' # IMPORTANT: now uses auth Blueprint + + from app.main import main + from app.auth import auth + + app.register_blueprint(main) + app.register_blueprint(auth) + + return app diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1912c1bb3d23a9bbeb0e3987362ebdaeb82d57de GIT binary patch literal 1147 zcmaJ=-Afcv6u)=o&d1K~v|0<5DXS&fyeJX9Ns&SienHraOE9cE2tIZ0%+3x%bl{$Qe&?QZ?!D)C?^j*d5XhI$ z4`&Aignn}n3E)6ZD==*$3t8AfF3w@hZNU*-F((GL=nz-RNr6oq*;R5%U`vjgQw3y; zTg_6r1R$w}G(buVB`w90S8*=2BpK>4&oD5P9y@vOIT+gHld(rmVb*q+04d|8BF+0Y zn5nU9u~Zq)SMo)hP84u+D?Z6EL>Q+}!Mln4)|wyP4Q(}j%eaQigeP5;vT_k~RBL5n zO+&d{oBR;SB$!DoUs&D2t*~@>G$gTKz(?RC~G4tBfF$yhje@xY>+!gL|z+R z8Ql;YBy&KLyCkziG8>a0Z#GER0cmeW-fNKTUiS9tc!PBQOr+js-(6GC$p_3hPuScSD*0?5XcQ@&5wCE|) zf(I``EA-G?52b&O5KspLmbQo93c-8d>?S5i+ktuS{ocIKc{5#2QxU8$m8Z4O0z%(8 z=r6$W>ehfAAq!d9L&-_Zw4R!#@OJ0>aO&7$>V!i5xOHSxi zU0T7tT$~ZuBJA!<&>o@HtFzU9Q)sTI3vmgraQka(MZTs0Si15mpjD-RzLM@s175=Y z_+Xjo>;G;!h&0!$4t%oz_ZdXD@W9r;s>u>8(Ht?;y;=p#0OqZz%-1gizOaa3p&qR5@3FHas2!}VQ zTlJ!*OYJp(%WY6EZ0dw%lGJtu9#zCdh!8u+E)gGp0T#>8!a=-kJ1)sCHg=d=p)R~Q zULPF?Q$EjV8%PVCtGN%#!E9^cl*s$Wo^e7Zj>*Kw`8K)#gQWJ&J@Zg(ll(bJpOE}9 z$sbyu?zYL)IT`C&K5CQc)9DBAmfB?UCzR`udjdKB3b9`A>=-0{Jasj4x9Nk9W|$4!YApGvB1#+j2`dQ^yYUmV7ok P`99myE=dmO;&6Weapmlp literal 0 HcmV?d00001 diff --git a/app/__pycache__/auth.cpython-313.pyc b/app/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0728d4b96b5f9c168726f0608716b0dfa3d180f5 GIT binary patch literal 2886 zcmbtW%Wo4$7@u9Qy?$n$*o4F(uq{ugs@;S~l@I~tQNV!&Wl1jyvWz`RtoYSgyAWSU zt4a;(0S-{5DpGSyPJ~{dr~V7J9LZ?qmR6;Q-ax5Tsi%J5IO`1%)I&$wnQvx(GxNd$DGU=b1_%U&D^}rHsXnSnK$NRK3cFif-I<# zDdAnB$x}Ntr{)^8vCgT^@u=yRRcda{qj{sE<{K2Xwh%eG*~Plvf>*ZS{af(eZ^5@8 zg`H!Xa%&wHzO_zhCJ+w(iGZ-faGlPSXo013MMGgUm!d3Qq}f6yQKW{G(NvnzWYLgI zEEAv3Gs8ZeN$9hN%;={jsu#mH!=1^`q;v6-PMP5WJ6|d`D{ajVM8YJguxND7&>Uqj zbG(qy^&5GXiqGO~f#fVr&b`_g77Q_U9dc;`W`@E3aXORMi!eN$&wZR{S;KV+SLUXa zC>3YR_9zxG#*OY47@bbvEW4tN4+QsY=g(a=q$^|ERlvwuTAacP- z@=R_9lC%yfM<&QPB3?^oa1U*^Cms?qX*WSI#-yd$1m(DMOJ&nEk1i)nddyxJ?KN$y z+-@`=+bo1>ZHp8%VbGQnC;cr{hiO^m_zuf9tqIZeQ6>K|dZ#(YD$O{-*lx2`6E)jW zu~~!m9m9E0LM5|&4Skxt}dE|H~%on{m1jNyBQZsk26 zC6oD5uBav%1u~}IQLkiZLZ>PaR!!$3hOC#8Nvi8*H}WaMd0h6HG>Xg?vzb*OCg>p> z_A`v2(eYU#lTIPE^SQWQx}HrJ86KM9G}ksH=uR*N8d(=+xPOMn1?$0Lz-q&mNTm!% zGM~laGWH{d7;=Y6kcDjwV`3;M8=7h<#*lE!I`UFgRl^@m6bj7`{V&X;l2JUAv4eo| z!KZ%%*#dd)>8W{o7tTJFJ#~3^Ro-2f)vByELLaOJANM}$t@od)_MfSR&MqnqC9rh$ z`)l7`tM^8$z0tMe{V7ni8ovBMnbz>Ev3l>N!@Cj%|Qvr0j_DxW6J#j0TtI zz5grEWsoOEl&5XQ=`wq*66D3-9jgVc?ZgS<62|eUUrQ$abHaqsx++G&mv$34A&hS$ zL|8d!BCK+|l}#bFW(S!}wuCq#m;wT!wFGEMPHTzV#DUN(8QMUImQdd!K!G#+A2&!Z z7fHj5a@lu>KVN5Ra2!=Qy~$Mzj^uj!;M-fG%r~qlQP^39Dqhhf@MhMr)@p$&f-^zR^hVG{)5ZwC>s2z4^{jGU~Jlh4S-KS;;V zq~{sw|4sJa?Of_wnW@Uh7NlPV`&XgQLyIHJ`&T-b_bh}eLjOzAC49JHBjUgdv|e-) v(ZN9%mP*UhOL+uE5Of4VQ3M^aKo@I5=Tkv>DT=~-*zd<|=SvBy-1&b2<3c>N literal 0 HcmV?d00001 diff --git a/app/__pycache__/forms.cpython-312.pyc b/app/__pycache__/forms.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5936510843f131fe7224b061b856e34da4107021 GIT binary patch literal 2147 zcmdT_O>Epm6dtebU3=FXZ&O4ANd9`OZ35OALG6254NzGO(gY{*w2=n6WXoR0*9=V{3GyznlQ)T-a)ezmvTtO$Q~{~lCGDxF3zG#xsGSE?E6*_T(3|&T4t`)uKI43FA5UVW~{1p+1aSM z)Ulad^exw8sp3Y>@;<0A{X@%hZL8{5){2x?=p!LIO_~oJx*Xju!c=?F`V}VI#36=Y z6C;rzj%W*j(r#O!EZk1)CZdHQ^L?>ov=F8Ujmy8ZlF$0ad^_ zQ(~GobJLZz6_@(?Si+R`b_kdf83pp%6!4p2obhRW1F%gno1(GWtrg4Stc#W4eM4AH zb}ie}+d}N#s(NxaCi(NkjReh9GqHx9h`;($sFQ*?2ic|6+MaLr#~kD>k2;oJ&pV$1 zOTlO(Ni{IRr7p7y{bdat92$p0$EH3N`Or)Cdg1JaJ&`zJboad?Eu}+=ktG` zx?ruZ^JfJp4@TGP>{Od-$L!o<@afo$p3upH!Qdjm7P+sV{&w^0%>#XCPapd6<2#Fu z#ZVt=>SG7`r9J&pQ=k4yI?U+zd(VY~lg%=$}co86`pS}bO*3%&~vNrJVY;%QPwD2sdNA8?&oaa=(b)b*! z>0?cO;tACg&ECmSo8(l}JlL2kZ=3uoN@zSU5A?l9ZGbEcUc-YWemiSlGCEy`2TE%5ARcse7l_tpZ4 z{!?pB+Q(Y!v3A*Rr*W+tYF#cQnL1l3yKC6(s1B#m90JCd$$V$7kfy^Z8$oy(;i+gm zi2)KwUqL7!ma~4R*Q|%EDymPM6XBC}(zs z_QBMc7=4nMkn~Tm;eimIj8Bb;F+R;^qog+=4?gvc*z)cgnOA0DM-Vze+G02t%lm@4y|5_ zW#SuTl970HyllB2kJq4*k$K#xgHtVyJ9fFqljkkhy;*~z{~0$L*D6k3d}LIfIBV6d zDf>pl0lUc6Y{hcQJd(Z9u*%alu3xaqPSL76wQ3fi20VPuu9oW8Hzfa5CGXlW=SS(w zX!~RHXPn$&1qPDAAY@1&7Zki&sG#<-;kHpBj2fA)e=IZ4wWI#6?Mp`9|W`G+FNAv6sJ)*_?RHork}hg}ZzY-*=fTRRn0>b4bQd1;D*-!ly9 zm^2k6aK~DZu4*VBdZVqk;*4F!#~(&00MC|6EF({1MQV*UFiSlFL}{xGwpCnA+qV!& zcW^ZfNp!%~8x0#4d34?>*KIJbEpi2m&f+S$o zYhgh=^OzQQaAA>mwlTNOAPU~MH-CeFv7unx<0zKdBYof3OJ6NL)cc!y|F;+KU0J>2 z>HVHQ@=!nB)K9ncGhZsd#q~#B2fV%$t*%ioG5Sgg1UmI=KMxAgZ2c$0AW`!D!fjHZ<67Qrs>v^C+1A6 zj!b;5QMX+@>}@ijfCXHj(~JapaX&@z1zXp9SB~F5@nCH2=ueiHpJ|@C=pDY))MuAJ zSWk4XL{*tR%Y)P9$a0!;GO@_o4M4?&U!~KH}wK;=sxHj zdVj6(!_{WK;9V)T^4GnyPP6+XPkCWI*|XByN*-Ju|3i`0v1c(^J?V!=0nOpFJp!WG zETZbaFnT~2$gZ+rbmVVC*~!8~l34LsTn(rL;zk$}rAS8O(Q~zuQxzoJji>r4d5LB{ zD7Y$?1)h_cBn;BR5Q%q4yi4MFVu@q-rjzaj?~yn{Vozd@l8ywO?H)#fiTPLby(4Fv zdKMu^Si4)v1D}ukV3L!`ObVHe{?6vj`aBS6@d(a{s{AlTnNdi*tE~^HiGCTPI>jZZ z2n896V&!2ha=Yvb3iXRg1it$+Tsz_S!WfL=cKY$T1l3bnlBCCM{}VRSWFt@5^e=4s cG3(hn#Vv-ftv*@myB&ST@FlMECpyjyl>h($ literal 0 HcmV?d00001 diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c26382c47c415f51c0a4bf6650b06c2c9eb88be7 GIT binary patch literal 447 zcmYLFy-UMT5P$FGrRfKyb&--qDGtqG>(Z@==vJYFaWfF2ecED@hHDDyRB_PJrOr~_9jIaHzkOx_tF#(?sp&W?ss=DDVK``<>z#7@Qn6d1uNm1lN_TnBQ~+=A?Z?} zT?TBEIJ8b|=4frUtOM@wqtdGR-`ZN6E+dACj4&l1C3D_clo~I*0R_zTEJNz&zBj%EwL!`$^R63u zfpA^WF(-8xO=6KUL0%Q9Mo?cZ^POPSpZH=IDj2H0$2$mL8m06%N9gvOQGHyW*A_;7 a%3rB|Z{C^@2X`Caj8ZGn2wO|Nl<^NLR#-a# literal 0 HcmV?d00001 diff --git a/app/__pycache__/models.cpython-312.pyc b/app/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c78f5b21d7760ecd6c702b334cac9473e8055ea GIT binary patch literal 1245 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 literal 0 HcmV?d00001 diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..7a56e31 --- /dev/null +++ b/app/auth.py @@ -0,0 +1,41 @@ +# app/auth.py + +from flask import Blueprint, render_template, redirect, url_for, flash, request +from flask_login import login_user, logout_user, login_required +from werkzeug.security import generate_password_hash, check_password_hash + +from app import db +from app.models import User +from app.forms import RegistrationForm, LoginForm + +auth = Blueprint('auth', __name__, url_prefix='/auth') + +@auth.route('/register', methods=['GET', 'POST']) +def register(): + form = RegistrationForm() + if form.validate_on_submit(): + hashed_password = generate_password_hash(form.password.data) + user = User(username=form.username.data, email=form.email.data, password_hash=hashed_password) + db.session.add(user) + db.session.commit() + flash('Account created! Please log in.', 'success') + return redirect(url_for('auth.login')) + return render_template('register.html', form=form) + +@auth.route('/login', methods=['GET', 'POST']) +def login(): + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter_by(email=form.email.data).first() + if user and check_password_hash(user.password_hash, form.password.data): + login_user(user) + return redirect(url_for('main.index')) + else: + flash('Login failed. Check username/password.', 'danger') + return render_template('login.html', form=form) + +@auth.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('main.index')) diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..56c8752 --- /dev/null +++ b/app/forms.py @@ -0,0 +1,26 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, SubmitField +from wtforms.validators import DataRequired, Email, EqualTo, ValidationError, Length +from .models import User + +class RegistrationForm(FlaskForm): + username = StringField('Username', validators=[DataRequired(), Length(min=3, max=64)]) + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired(), Length(min=8, max=64)]) + confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) + submit = SubmitField('Register') + + def validate_username(self, username): + user = User.query.filter_by(username=username.data).first() + if user: + raise ValidationError('Username already exists.') + + def validate_email(self, email): + user = User.query.filter_by(email=email.data).first() + if user: + raise ValidationError('Email already registered.') + +class LoginForm(FlaskForm): + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Login') diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..20b186b --- /dev/null +++ b/app/main.py @@ -0,0 +1,9 @@ +# app/main.py + +from flask import Blueprint, render_template + +main = Blueprint('main', __name__) + +@main.route('/') +def index(): + return render_template('index.html') diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..1f93c8f --- /dev/null +++ b/app/models.py @@ -0,0 +1,13 @@ +from . import db +from flask_login import UserMixin +from . import login_manager + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(256), nullable=False) + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) diff --git a/app/static/style.css b/app/static/style.css new file mode 100644 index 0000000..cacac2b --- /dev/null +++ b/app/static/style.css @@ -0,0 +1,4 @@ +.error { + color: red; + font-size: 0.9em; +} diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..0af6771 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,8 @@ +{% extends "layout.html" %} + +{% block title %}Home{% endblock %} + +{% block content %} +

Welcome to the Flask Template App!

+

This is the index page.

+{% endblock %} diff --git a/app/templates/layout.html b/app/templates/layout.html new file mode 100644 index 0000000..9484b47 --- /dev/null +++ b/app/templates/layout.html @@ -0,0 +1,37 @@ + + + + + Flask Template App + + + + +
+ {% if current_user.is_authenticated %} + Hello {{ current_user.username }} | + Logout + {% else %} + Login | + Register + {% endif %} +
+ + +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} +{% endwith %} + +
+ + +{% block content %}{% endblock %} + + + diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..aeb5083 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,10 @@ +{% extends "layout.html" %} +{% block content %} +

Login

+
+ {{ form.hidden_tag() }} + {{ form.email(size=32, placeholder=form.email.label.text) }}
+ {{ form.password(size=32, placeholder=form.password.label.text) }}
+ {{ form.submit() }} +
+{% endblock %} diff --git a/app/templates/register.html b/app/templates/register.html new file mode 100644 index 0000000..373e693 --- /dev/null +++ b/app/templates/register.html @@ -0,0 +1,39 @@ +{% extends "layout.html" %} +{% block content %} +

Register

+
+ {{ form.hidden_tag() }} + +

+ {{ form.username(size=32, placeholder=form.username.label.text) }} + {% for error in form.username.errors %} + {{ error }} + {% endfor %} +

+ +

+ {{ form.email(size=32, placeholder=form.email.label.text) }} + {% for error in form.email.errors %} + {{ error }} + {% endfor %} +

+ +

+ {{ form.password(size=32, placeholder=form.password.label.text) }} + {% for error in form.password.errors %} + {{ error }} + {% endfor %} +

+ +

+ {{ form.confirm_password(size=32, placeholder=form.confirm_password.label.text) }} + {% for error in form.confirm_password.errors %} + {{ error }} + {% endfor %} +

+ +

+ {{ form.submit() }} +

+
+{% endblock %} diff --git a/config.py b/config.py new file mode 100644 index 0000000..25e70e2 --- /dev/null +++ b/config.py @@ -0,0 +1,15 @@ +import os + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY', 'you-will-never-guess') + + DB_USER = os.environ.get('DB_USER', 'postgres') + DB_PASS = os.environ.get('DB_PASS', 'postgres') + DB_HOST = os.environ.get('DB_HOST', 'db') + DB_PORT = os.environ.get('DB_PORT', '5432') + DB_NAME = os.environ.get('DB_NAME', 'postgres') + + SQLALCHEMY_DATABASE_URI = ( + f'postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}' + ) + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..398bfe8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +services: + db: + image: postgres + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: flask_app + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + + web: + build: . + ports: + - "5000:5000" + depends_on: + - db + environment: + FLASK_ENV: development + SECRET_KEY: your-secret-key + DB_USER: postgres + DB_PASS: postgres + DB_HOST: db + DB_PORT: 5432 + DB_NAME: flask_app + volumes: + - .:/app + +volumes: + pgdata: diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..4259383 --- /dev/null +++ b/dockerfile @@ -0,0 +1,31 @@ +# Dockerfile +FROM python:slim + +# Set work directory +WORKDIR /app + +# Install PostgreSQL client +RUN apt-get update && \ + apt-get install -y postgresql-client && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Copy project files +COPY . . + +# Install dependencies +RUN pip install --upgrade pip && pip install -r requirements.txt + +# Copy and prepare entrypoint script +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Set environment variables +ENV FLASK_APP=run.py +ENV FLASK_RUN_HOST=0.0.0.0 + +# Expose port +EXPOSE 5000 + +# Use entrypoint script +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..fc4fc6a --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Wait for the database to be ready (timeout after 3 minutes) +MAX_WAIT=180 # seconds +WAIT_INTERVAL=2 +WAITED=0 + +until pg_isready -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" > /dev/null 2>&1; do + if [ $WAITED -ge $MAX_WAIT ]; then + echo "Timeout: Could not connect to the database at $DB_HOST:$DB_PORT within $MAX_WAIT seconds." + exit 1 + fi + echo "Waiting for database at $DB_HOST:$DB_PORT... ($WAITED/$MAX_WAIT seconds)" + sleep $WAIT_INTERVAL + WAITED=$((WAITED + WAIT_INTERVAL)) +done + +echo "Database is ready. Applying migrations..." +flask db upgrade + +echo "Starting Flask app..." +exec flask run --host=0.0.0.0 --port=5000 \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/__pycache__/env.cpython-312.pyc b/migrations/__pycache__/env.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ad0ffed74425609f647fbf8d93accaf8e30ae20 GIT binary patch literal 4468 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 literal 0 HcmV?d00001 diff --git a/migrations/__pycache__/env.cpython-313.pyc b/migrations/__pycache__/env.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cadcbf3393db89648ca3e9317615f301ec3b6a9a GIT binary patch literal 4508 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 Lebax$iG_ayX-Jps literal 0 HcmV?d00001 diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/77b60347ee39_initial_migration.py b/migrations/versions/77b60347ee39_initial_migration.py new file mode 100644 index 0000000..83f2e85 --- /dev/null +++ b/migrations/versions/77b60347ee39_initial_migration.py @@ -0,0 +1,36 @@ +"""initial migration + +Revision ID: 77b60347ee39 +Revises: +Create Date: 2025-06-04 15:29:32.328762 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '77b60347ee39' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=64), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('password_hash', sa.String(length=128), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('username') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user') + # ### end Alembic commands ### diff --git a/migrations/versions/79c2e82697ce_update_user_password_hash_length_to_256.py b/migrations/versions/79c2e82697ce_update_user_password_hash_length_to_256.py new file mode 100644 index 0000000..db530b7 --- /dev/null +++ b/migrations/versions/79c2e82697ce_update_user_password_hash_length_to_256.py @@ -0,0 +1,38 @@ +"""update user password hash length to 256 + +Revision ID: 79c2e82697ce +Revises: 77b60347ee39 +Create Date: 2025-06-04 16:18:20.867161 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '79c2e82697ce' +down_revision = '77b60347ee39' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.alter_column('password_hash', + existing_type=sa.VARCHAR(length=128), + type_=sa.String(length=256), + existing_nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.alter_column('password_hash', + existing_type=sa.String(length=256), + type_=sa.VARCHAR(length=128), + existing_nullable=False) + + # ### end Alembic commands ### diff --git a/migrations/versions/__pycache__/77b60347ee39_initial_migration.cpython-313.pyc b/migrations/versions/__pycache__/77b60347ee39_initial_migration.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd29531321a48ae8e2fbad55b0086ec49ec9ed6b GIT binary patch literal 1519 zcmb7EyKmD#7(d$=$2AELQ4|OuOc6+tpiR@Jr4cXDg4C84REr5D5l&y?&os;={gW&|dB)p?faMJskf2#+LCh!M5E3Ot@G8@7cg z(hUtl0}$U894Qv$i0huGxEi}iSfb%~JLC)_CO@d?QL zrqGmsqV@d;n)*+)Eq_Hz{2l|Bc;@m@1HO7tx3)FC6g-tpd&VNqe5V!~Dxpkbp5dBL zC=+e#JT!^o)*Q!NbO=+p+BGX={S4d*CfOlwDJZiTshGC2ehaiLSv7tCrbjKKZ2IN( z2KQ7bZzGl{h7D>2A)4r$OrG?dTE%7YX*VDxxKq{xYP%)Ye~H=^lh$WRebRINfSR@& zu*t(g(QC#hrRIUrH()pihWB{yTfFzl)EYh*N~yOv z^(4NAwXUSD;X^;SDKVMuhWGt8s64yjpk{&D%%Dbv4+}2NQ$Af8v0WwAqF%LKBbowz zcpcpX9P6{Zn{GQ<`$0Yk+viQ#PS)JY+`S|3@SgC4%oY|m49hDT28){xsVv&X4!*zS zm`<@wDs{SpqbjuXZ(~W0Z*;{)YP!X;0o_L&pQ#q964&w#&kbuu`5XRxqH?0*Sv7~8 zqKCoCXU5L~`6da1@LiMz{BtW3QXf(3n}QPQhVo&1eoNS?@G0?`@G^Jd=rzS%*c zU;;ir-TS!pb`Ib-(HTGT$QW#*@dzBC;7E>qOZrTvvL$TgmO^t*j^>>_RUL(D*mOr- z1Zp@MqS;hR`q$rdlfX-f)n$ZQiO1N3mX>8dW71OMf`)$^aBkR)} zTUNE^+ho(O)T%zoMhHW1wNWXrU#k+bUNf&#A_Ct)*s|=hy>X>nxl+DntyQ+xHn;5Z z)y+zEt+HmCCnA-S#N{}-&8H_LV{am&KT3J{F%T7OgP#!|!QG)#Kz&@9R~~@3L_NTf zYqAUxd;#_R#Nfzul)LKKTi3_d*%xEvoP151m^EbQY=*FKmMUgBnxofp$iwj67@f(2 zCR~K~VP&)^kO20Ot!1$AZNi7jGVDnu<2KI;J`N?WGQUMS9xsfkaRoIeJq#d5Gm1_C z${c!7M?W@$TWy6JvGcwV5(XJ2`vZWwxvkd#y^YR6|AUh218FH8w_Q!&D)vyLB! z-A=@1=5g)I?fUgkw(DGVQi`#0Up-qFCk!_Ri{DxGl4P~hZc;Brg|OB8gbJy!)k!wp zOlq!3&HYC{ZkC6e#x6?jYO>F@Zi2xALf^m*&SPVD5PI;tF?D1VpBcr+dk3FCGwdH) z$NJ2XUhM0|$MN&x^7p$>b`My;xc+=@;k%uOJ5T-o+`GpY-#OL5Sb719vGS(^=K7Ig z|6WZH5JxhhqW<)PfH?sR z0_Fw0j*v-!E~3eeAbt>Kye(m0wExQ6&Xb)3zhATkZ@Uk7pR)ekd;iKCeG?;>rnu(1 zLF~IO*SwH)8f`z5waM;mk!$SScMmSX@NqQlxQtstBkQRQ0g;O1-7km2g%u=s# XJaYk|FxxZ#P*rJVFpoel