From d728bda7be3194e0515e12a455632e636c6605b5 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Mon, 9 Oct 2023 15:00:33 -0400 Subject: [PATCH] User accounts added - Adds user login with flask-login. - Adds basic Toast flash messages using toastify.js --- NOTES.md | 5 + README.md | 1 + htmx_contact/__init__.py | 13 + htmx_contact/main.py | 2 + htmx_contact/models.py | 13 + htmx_contact/static/css/main.css | 19 + htmx_contact/static/favicon/about.txt | 6 + .../static/favicon/android-chrome-192x192.png | Bin 0 -> 12671 bytes .../static/favicon/android-chrome-512x512.png | Bin 0 -> 35226 bytes .../static/favicon/apple-touch-icon.png | Bin 0 -> 11254 bytes htmx_contact/static/favicon/favicon-16x16.png | Bin 0 -> 673 bytes htmx_contact/static/favicon/favicon-32x32.png | Bin 0 -> 1480 bytes htmx_contact/static/favicon/favicon.ico | Bin 0 -> 15406 bytes htmx_contact/static/favicon/site.webmanifest | 1 + htmx_contact/static/js/main.js | 23 + .../static/js/vendor/toastify.1.12.0.js | 445 ++++++++++++++++++ htmx_contact/templates/base.html | 16 + htmx_contact/templates/login.html | 11 + htmx_contact/templates/sign-up.html | 14 + htmx_contact/user.py | 66 ++- tasks.py | 2 +- 21 files changed, 632 insertions(+), 5 deletions(-) create mode 100644 NOTES.md create mode 100644 htmx_contact/static/css/main.css create mode 100644 htmx_contact/static/favicon/about.txt create mode 100644 htmx_contact/static/favicon/android-chrome-192x192.png create mode 100644 htmx_contact/static/favicon/android-chrome-512x512.png create mode 100644 htmx_contact/static/favicon/apple-touch-icon.png create mode 100644 htmx_contact/static/favicon/favicon-16x16.png create mode 100644 htmx_contact/static/favicon/favicon-32x32.png create mode 100644 htmx_contact/static/favicon/favicon.ico create mode 100644 htmx_contact/static/favicon/site.webmanifest create mode 100644 htmx_contact/static/js/main.js create mode 100644 htmx_contact/static/js/vendor/toastify.1.12.0.js create mode 100644 htmx_contact/templates/login.html create mode 100644 htmx_contact/templates/sign-up.html diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..48905c7 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,5 @@ +# Notes + +## Toasts using Toastify.js + +[In depth example of a Toast](https://www.cssscript.com/simple-vanilla-javascript-toast-notification-library-toastify/) diff --git a/README.md b/README.md index 8831b6a..2721c01 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,4 @@ For a list of current utilities. - [x] Unit Tests - [ ] Integration Tests - [ ] Continuous Delivery pipeline with Drone.io +- [ ] CRSF Protection via [TBD](https://testdriven.io/blog/csrf-flask/) Looks like it will probably still be flask-wtf but only for the CSRF Protect. diff --git a/htmx_contact/__init__.py b/htmx_contact/__init__.py index f5ff2e1..9b138a1 100644 --- a/htmx_contact/__init__.py +++ b/htmx_contact/__init__.py @@ -1,7 +1,14 @@ from flask import Flask from flask_login import LoginManager +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker from .config import ContactSettings +from .models import User + +# Database +engine = create_engine(ContactSettings().DATABASE_URI) +Session = sessionmaker(engine) # Configure Authentication login_manager = LoginManager() @@ -9,6 +16,12 @@ login_manager.session_protection = "strong" login_manager.login_view = "user.user_login" +@login_manager.user_loader +def load_user(userid): + with Session() as session: + return session.get(User, userid) + + def create_app(config: ContactSettings = None): app = Flask("htmx_contact") diff --git a/htmx_contact/main.py b/htmx_contact/main.py index fbb76b2..172c55a 100644 --- a/htmx_contact/main.py +++ b/htmx_contact/main.py @@ -1,6 +1,7 @@ from flask import Blueprint from flask import redirect from flask import render_template +from flask_login import login_required bp = Blueprint("main", __name__, url_prefix="/") @@ -11,5 +12,6 @@ def index(): @bp.route("/contacts", methods=["GET"]) +@login_required def contacts(): return render_template("contacts.html", message="Hello HTMX") diff --git a/htmx_contact/models.py b/htmx_contact/models.py index 330f343..f86b714 100644 --- a/htmx_contact/models.py +++ b/htmx_contact/models.py @@ -48,6 +48,19 @@ class User(Base, UserMixin): def check_password(self, password): return ph.verify(self.password_hash, password) + # Flask-Login methods + def get_id(self): + return self.id + + def is_authenticated(): + return True + + def is_active(self): + return True + + def is_anonymous(self): + return False + class Contact(Base): __tablename__ = "contact" diff --git a/htmx_contact/static/css/main.css b/htmx_contact/static/css/main.css new file mode 100644 index 0000000..00cff52 --- /dev/null +++ b/htmx_contact/static/css/main.css @@ -0,0 +1,19 @@ +.toast-basic { + padding: 12px 20px; + color: #ffffff; + display: inline-block; + box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.12), 0 10px 36px -4px rgba(77, 96, 232, 0.3); + background: -webkit-linear-gradient(315deg, #73a5ff, #5477f5); + background: linear-gradient(135deg, #73a5ff, #5477f5); + position: fixed; + top: -150px; + right: 15px; + opacity: 0; + transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1); + border-radius: 2px; + cursor: pointer; +} + +.toastify.on { + opacity: 1; +} diff --git a/htmx_contact/static/favicon/about.txt b/htmx_contact/static/favicon/about.txt new file mode 100644 index 0000000..e694967 --- /dev/null +++ b/htmx_contact/static/favicon/about.txt @@ -0,0 +1,6 @@ +This favicon was generated using the following font: + +- Font Title: Leckerli One +- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli" +- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf +- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL)) diff --git a/htmx_contact/static/favicon/android-chrome-192x192.png b/htmx_contact/static/favicon/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..68116ef4d16281ed7a781052b1a026d93ad9a4b6 GIT binary patch literal 12671 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 literal 0 HcmV?d00001 diff --git a/htmx_contact/static/favicon/android-chrome-512x512.png b/htmx_contact/static/favicon/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..072257cb2774d0bd1e170b9d2ba225e77a4db12a GIT binary patch literal 35226 zcmXtfby$<{_y0Bq3?xPgNR4g*=^i32DIuVO(jeWrjgXKM0g>*IR5}NUfJiq=cS?8c zH{YM{@4C1y_~YT+&wcWB&UrnNI$A2kgbxS-006O?>JvQx0EGP$1c2aSf1UV@UjYCx zfZ7uUeVU!P+hX8$nd?})0tD_EV-<` z8M2Q^E~T$XD!%MDiXC{lxsSy}3i8k2dTk&$+VePO-1h(SqmM6lIZ?CR8`9P1nbPj- z^a@ZY0Qj9^)1ZpKM4@Rka8-?yHiP45+FuWevHX()3Q0XxKVelJ5jAyYX7Gn-PV$km zl#dtzHSS(a8dv0to6;-9-E}65!)27;U9*wu7k$Ks;4cli%6GLWc~R+#WUCh+uDl7R zOgTdHx^|9}3Eh$*pZLgL@sv}>u>W!YEj9rJ0-=Hrfp$*g;xWH1V;27}_p8lNh0f-0U>$i-`oJ^+eSSPkM<{QD? z4(FAZG>X(g`QFRocBjftDt6{`;zA+BdZTztP(*yZgJp=vy~_IGK!axVE(BL91l|B? z2SBLpYAk#~FA2X$OBLr(%de8I8C(Ews;PO)&l-C?4 zQ03<$3YiWE5CN{EMMX|SkZyhNW259=sBU~6<gya5JWvCs*LuLVP8VtMfm}n;qKEr zqSd&Utq1&~bts6nSz@k#+N`tuy`rgW8I;sOhYdK$t3EBZwOWCF` z*J8&k1EQ7`@*qn|$l^4(vz)_dUw-?!_dPNGF( z^@T$OYJy6kc3b%O!dSOOvhqiyWlpV{haHrNVO1ZIiKdsu8%7DMGT|qD57VcE?oA_+ zGuRUauOYu1FtSAkHFhpOT5I@s5}CcbUG=Q%vS6?Wht5y;J>hVd{xat><4#_jKb}sc z6CuclSQTvsQzdOHW$#p5*|BJ4cKTnfTsrPr&W~wNmhz9^0|i03HXM@O1dFLde>HZ= zGMD$qJ!$}hi4?Hj#{3SLVH*jrw`1oXW!GB2y&(vgGrxn?JS>bRBXB8vaw#ND-UfTH zRe<6m33Y__;%Xe-yL3qDF-o2Gw0Raj71M1u3ntn!kLa5rh=>i=5M z^LEKZiU4T<9~gmj^;w_mZ*{_5Ub6i37P8p1wxHCR3j$9b4tQ6bG+8%ta$@Y|PjkMK zVZ&9hlWV$|v~}ZW*O#N^oK+XCrPe^ISn+PS4tD0;mylbLIdi&bYI6I8uy2L0il(pjuqZu9_eP4tE9# z0!}#I9$z(Yd^$N)Z;JX@5&fD{qRr;`5^#FU3{$@Zcl?se3b4M-kDj1VDBb;cnKCzxHkvs7z^VVd#C}a0l!mH5~RU zS5oqI%!8j(d&ViIj3oG_3?q41PA9^?ppSzT$0bI8#y8Qe3OY4fxIC=^M$Wd zPu;09r^t5b6y;uRb8eR#|0srl;pPIMw~(^+_{T%>8vRPsub;;1VBHgvlpXWSJJt9D z*~DUFH2pH3*Y1ubR=o`1P7Qc(`CFo!qOWyc>xHG#?&As&2v`e%2Vd-eYxcPHP&zN5 zaZ~}(6S8nN1_ZFEBAplM0@%%!|fKm^E(iw9m!m71ICwGR1mGp#kFnnNN2@n|SK zfZ$Z-s$_IgTvxpJS!#M+QZ^XGkM%+1

|DO`tE-P5Y8&>3BZ0OD2U2X3hh0$d4zT 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 Hu0mjf+&wqR literal 0 HcmV?d00001 diff --git a/htmx_contact/static/favicon/favicon-32x32.png b/htmx_contact/static/favicon/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..9d37a36573d2f0e6c4d957e9d81e7d934235ab16 GIT binary patch literal 1480 zcmV;(1vmPMP)ywBW3T9(Wqmk0)!$kE*}tb7+?Yz=$nQ9_Rv2a^sCuADm0n5++v-IX|ka_vi4hM zt2n+I9IK$k39;6T^8(<6M>y>EsNd}0Wcp2MPgDR|yZdx1W8zL)u$2sGrgE^%8W(|i z*KKvY(!XuysGJR109kd{b5(-6ficYcn+Jz%69nNWH*>G-uS&lZZZ85rX3cQEh7*p{ z!nA+m@E{w5E)QFFWkYgDu-y;<;?31ipNV#Oik4wPh8nX>9P}Cv6CMGBP7i)IyU}^W z4c-$I@*ofXfnX;503@Fq63@xeerF1Y7YcMJnyG<`lMkDbF%J&^GLH8;$6*saA7x1H zusI8TG2yqCT|2akG4^8kzAgD$Y`$9yb(Dj{*Y{f8SpT&hSM1U0Ab7iat5UX$dnEwk zQ&X$m7s8|crTcW)G+#^93TW8x`x~^k9#1HmGlG-59esGAy11qRe+h+0T&)` zv@7|;#q+AW#3w54%B~x#XJAhT@0gy*aALIyO}|fIU&k2!uz7Eko5bPr2muIMynL1h z`-=1-2R_nZMHj_JR97%o-&>hn#YBZOB(Q5!5t%m@CSdK{cocnZLANE8;+6^%G7>re zdC#A;fcu#OS^wM2iF(n>X5?Br@AG7T#7y@0#}BXERW?kq7dEr@~gMk~$_M0bIB zo*=L~0x*1@63D$u0LpIHV9$eIANSkbSoCRVp2<&Bq2W=Z--MZ-ZB0=vVI%+zx@Rc-`ngfz3`v;*lnjFQ1XkI}vvzZ}Kq# zY|M*CbzvY27f}E{HaiZ_=IcTX4iOW(+D1tYW8d7j(F-7k3%*!xK*2OGK?4r*?O_X* z2EZzM6+0H_V2bxXy;6Gj7Zu*UoQu6KPzb`Lc!t*XCa5Oq_xayvtY%3fmS)FEKliqe z;+-GIl%HML>|_u|e}=``8th*ZP`I~Sol>&#jqUen@yw(v+OP9ib<7q4U{11EIVn_R zVnC=<6ryU%1sf?B_{M8zo%rBF06_U2ad>NySDeIrQE+gCGGXAHg)`KsU#iENuWaZX zl+grHh?J!GqhdW4X9cRUZ~)=~2TKh86E`>8(b(-pAkNj&;aW;kqH^|5<`RDZ`lML= zkX=K`hzm_d;`bAzez%!aQ5ne6GyJe7$v@vVz@wtch7}Z`uT$QLr^B23CQy0YAwxeU zqo*hti8ImCaT^6-iZ+m7c9IJ|403_Ex%~-~|0RgF#>m;)NnIGAYH7t$EBJ=tZdy>wj66Pq_Hvt52u52lJxPn5d{R z3AiJY%&C95sm8=-_J3{7g6}3Zr)p|Wag)s{s5#~TL30{Tu)jTLtcd2M_MD>5t3nJT iQhN?+&xM<7&;J7B)m&dHal@nl0000JG*J);2v~y<5Ta30wm_^c z@7>pymKJJDSDZg`IrrYzR|pA{+;`5I znSYs?fByNGd6CE=k#|N8J1m0d#K<{^Mj|IfB9Vy`o$q50k3?pnZt~=2`!SKoV@F0J z$D$8F0O8c*(NRhDkDHtrh)z$$W83j7H-Fn%$F?9OyRM(`o@9TlFEJ3SCkG}fzn`?B z5Br*U_??JH*CgkBTxFJi*MjX;srjEnU9Vd|+#h|h~GmwWUI#Zq{By4$z(!V0OY zWnNalhqnuU6t+%R1wWu=7z9(%Qi=Hbz z){zI!_Q0p___}--x6Tuo+?uJX`0N~)2R+Xkp5tsZ8mhYQDe05F@7Dr9^1yFhnB*-V zbKmtEX?Q-XL(i&4t*p{_d_z?e&j~!~ntk-UUSIvqXMX)Z9hiqNRmIJ-RC(7b`7Ug{ zu?2^g4HG^!|9H$(N#!?O>-3-V3E`(vuc+kQPkC?%|Bx#ap6`VxuE#vhcKg@OLU0~~2L(kkb*N-xw<%c+PYp+oC zLPB8lpIYV>uk4_omLI~q^2>VuW*seZoW1*Wm0t8U)a{pD;Ty7U%CE*joO&#RN8@Wg zgZ2;Mb1p`PjPaA8gJuGE-f!;x>tcDr5LV;q_DMOwn6AY*EB-W^GG)nv z*;inJ!~JhGpL z&n!Dl6*kSl9zWfqUv#_soHa@Ib!O?Q*yED|`x1B1JaCQdIj_9FPOBPKd2Ovq;@oj* zHrGA9;7f{nNSAsktqM>5%cIjw7}D^Q2b`VPnf>@)FAQq5^31LJT?_rvj%A)c`lFV9 z2|DhSmi|HmGHI@Z>@goXuNS@gHvjmaEquL-<24JDtXclF#DwzeA&}p-TQ<}&OBP>9{K~$wvx1!cRlEVapB{!0nPz+Rgi9>y?t9YHrg5ehe^b^(f16M2 z1vyL1I`f@&LZeZ0X_Q~Q%R{65^8IdG;mMmkZ6=<;_vSek*p4}V;vId*srtcz`~X^E-@wdchR2 zeKo<{+RIg8<4q#htP>g6jd;=?55s4+34PiQ0rIEoO+Bn-AbtB69UBg|Yc;XW;6SO| zV^?*=1LxndTm1K*ra!X!LKlB6ySoK_4|3?ABWLRiQyY zg#MV@)|@-ardcv~{rCBO_H-yr*&tLN3 zXMd3I!V@Z^<>5{8zcdeg%H9rr{fcj$jll)DcKi2H8~KyIvJ0y#JTevekpB- z@g4Qs@w-?0^!1#U=^u9tVbxWUceEfr$Q7%viX2Lx9sEtjZ#nFhS8biv{yVmZu!+9t zB||s&9IqB4X#NJ2;TaRggYG>1_QAdwbqq(34|r%R4-#jpKiYe+hA=YygC78msgQw; zrak7*cr1&xyo|Icn`1SW#K5={VMp8p{>x*|vt^TlMwPUZeG^U^3;q$aJ_|U9@oOAx z-Zg&Gf(@KV_D3%m?HE*FKjX1NqQB?&J7-7U-R(G%@t$+R`)~`- z$WJ8UXSdW_axiA?Tzib68~zcuqRbRLJj^^w@-wxO*HmTCI+cCkobJW|e>QvnS@7jA z!}B-D%}nkbp1gD@OWZe*z1KXvV{S9cf2eF#ljHH`OFXbxEBZmT{63FqUPnBz z^xOg`e>m%ywqud~0N%p%QnK&x_lutK$!X?RUxd8ih=&LI%;lHvR;jt44x~??){NtN z5a6SZaWQ|%{FP_imOOG#lQ-(tAXysj@3{#5s(yHKkN(MxU79>_P5k_Bjf3$ojx(&# zf2!<$2xItsyD?OGKKSvUoc5tu*x8hS>`H(BOBv)o={sxx*Zb5tQJa7A+-TH2)s{ zjF?0iPvn{YL+9}T}%9OOl?2-2?NosHtn$Q^3MIaB|Q#rTow195F^gxPK=!cq3sriyX!W@ z1Q6p%bOT3Q%(e%Zn@!@Z;=UYt@I37EO}^(=am!rXr#cO?ebktYW7vstbmfE7&a&l# z>oBtFe8iWhD6Iqa+L%x3ALhgIqJzA4MmKY)`^q^Gk9NgB?=hKmf zheqCc56kY$`qB%_JJ6BgV+8+<5q2d%l#%@Z{@P;8@oZzBIeX8UxC?fRjIp$1iQG@) z{WyCJ99Qv~IPOEV=B)JmwtG^3V2^HT*UxTGBPUBg%q+CYM7DVj;I4q&J(GJKy-nX| zkJ(+`xo@)fhubpRcc=EVFJ8=baZ{h0=af4DbG>(6OgQRYzrt~u{W^Kq_A^|VL-0No zHZ<;TTI`1T1cYztRaUw-59_c-FBLCzc=G8HfXV zu=M<5oSg|(fXdy4yfZR4?K&$uP6R_8y+xsb)>%C^qvc3P_D|cY1Kd3Ki z>76)x`kecN#{F#H@x#^`L9DKp-DR^a-`qc``6qNW|CoD^|4u*H*~iheFArQdtbysj zh%JNI3F9NY_dS9;fSDCP5+5qP=&N#O_Lm|(0 z8S0Cjhp~nD@x79ZXB@(hAKwWMsN~tvtqnxpDEBts`a3Ls=&lRoo_9zpudg5V7$`Hv z%`+A6D!F*wz3Hz*z<1mDA;-a73(kYx;dtSrAm&Nih4*vpI9kAzt2*Le^a0X~5U;cL ze>gpfSq+mnJ=bARoXKZm44vh1dahQL*{|Pc(&V^zrdQc<}cc;v+yt*@$e8kB9Ko%+~9$EAe+B--=b5U9872H z;79ua>o9)>&LeYK?f4C9Z_3hc_&I_9=hW4Fb}%N=Vh)9Qpdh}wsedcyPxUydTd}8+ z(+fG{I+)wgwUXR;&xNF=>5lbN&S~Td;Jd+I8l}x=j1w}>GwBt{KOl!P*8KY6>d0(0^#I$rSNL|A&+qM|3Op;|lf} Z>KlIwPRe literal 0 HcmV?d00001 diff --git a/htmx_contact/static/favicon/site.webmanifest b/htmx_contact/static/favicon/site.webmanifest new file mode 100644 index 0000000..1dd9112 --- /dev/null +++ b/htmx_contact/static/favicon/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} diff --git a/htmx_contact/static/js/main.js b/htmx_contact/static/js/main.js new file mode 100644 index 0000000..6120d02 --- /dev/null +++ b/htmx_contact/static/js/main.js @@ -0,0 +1,23 @@ +function getFlashMessages() { + let elements = document.querySelectorAll('[x-flash-message]'); + let contentArray = Array.from(elements).map(el => el.innerHTML) + return contentArray; +} + + +function showFlashMessages(messages) { + messages.forEach(message => { + Toastify({ + text: message, + className: "toast-basic", + duration: 3000, + newWindow: true, + close: false, // Show toast close icon + gravity: "top", // `top` or `bottom` + position: "center", // `left`, `center` or `right` + stopOnFocus: true, // Prevents dismissing of toast on hover + }).showToast(); + }) +} + +showFlashMessages(getFlashMessages()) diff --git a/htmx_contact/static/js/vendor/toastify.1.12.0.js b/htmx_contact/static/js/vendor/toastify.1.12.0.js new file mode 100644 index 0000000..7b2e47e --- /dev/null +++ b/htmx_contact/static/js/vendor/toastify.1.12.0.js @@ -0,0 +1,445 @@ +/*! + * Toastify js 1.12.0 + * https://github.com/apvarun/toastify-js + * @license MIT licensed + * + * Copyright (C) 2018 Varun A P + */ +(function (root, factory) { + if (typeof module === "object" && module.exports) { + module.exports = factory(); + } else { + root.Toastify = factory(); + } +})(this, function (global) { + // Object initialization + var Toastify = function (options) { + // Returning a new init object + return new Toastify.lib.init(options); + }, + // Library version + version = "1.12.0"; + + // Set the default global options + Toastify.defaults = { + oldestFirst: true, + text: "Toastify is awesome!", + node: undefined, + duration: 3000, + selector: undefined, + callback: function () { + }, + destination: undefined, + newWindow: false, + close: false, + gravity: "toastify-top", + positionLeft: false, + position: '', + backgroundColor: '', + avatar: "", + className: "", + stopOnFocus: true, + onClick: function () { + }, + offset: { x: 0, y: 0 }, + escapeMarkup: true, + ariaLive: 'polite', + style: { background: '' } + }; + + // Defining the prototype of the object + Toastify.lib = Toastify.prototype = { + toastify: version, + + constructor: Toastify, + + // Initializing the object with required parameters + init: function (options) { + // Verifying and validating the input object + if (!options) { + options = {}; + } + + // Creating the options object + this.options = {}; + + this.toastElement = null; + + // Validating the options + this.options.text = options.text || Toastify.defaults.text; // Display message + this.options.node = options.node || Toastify.defaults.node; // Display content as node + this.options.duration = options.duration === 0 ? 0 : options.duration || Toastify.defaults.duration; // Display duration + this.options.selector = options.selector || Toastify.defaults.selector; // Parent selector + this.options.callback = options.callback || Toastify.defaults.callback; // Callback after display + this.options.destination = options.destination || Toastify.defaults.destination; // On-click destination + this.options.newWindow = options.newWindow || Toastify.defaults.newWindow; // Open destination in new window + this.options.close = options.close || Toastify.defaults.close; // Show toast close icon + this.options.gravity = options.gravity === "bottom" ? "toastify-bottom" : Toastify.defaults.gravity; // toast position - top or bottom + this.options.positionLeft = options.positionLeft || Toastify.defaults.positionLeft; // toast position - left or right + this.options.position = options.position || Toastify.defaults.position; // toast position - left or right + this.options.backgroundColor = options.backgroundColor || Toastify.defaults.backgroundColor; // toast background color + this.options.avatar = options.avatar || Toastify.defaults.avatar; // img element src - url or a path + this.options.className = options.className || Toastify.defaults.className; // additional class names for the toast + this.options.stopOnFocus = options.stopOnFocus === undefined ? Toastify.defaults.stopOnFocus : options.stopOnFocus; // stop timeout on focus + this.options.onClick = options.onClick || Toastify.defaults.onClick; // Callback after click + this.options.offset = options.offset || Toastify.defaults.offset; // toast offset + this.options.escapeMarkup = options.escapeMarkup !== undefined ? options.escapeMarkup : Toastify.defaults.escapeMarkup; + this.options.ariaLive = options.ariaLive || Toastify.defaults.ariaLive; + this.options.style = options.style || Toastify.defaults.style; + if (options.backgroundColor) { + this.options.style.background = options.backgroundColor; + } + + // Returning the current object for chaining functions + return this; + }, + + // Building the DOM element + buildToast: function () { + // Validating if the options are defined + if (!this.options) { + throw "Toastify is not initialized"; + } + + // Creating the DOM object + var divElement = document.createElement("div"); + divElement.className = "toastify on " + this.options.className; + + // Positioning toast to left or right or center + if (!!this.options.position) { + divElement.className += " toastify-" + this.options.position; + } else { + // To be depreciated in further versions + if (this.options.positionLeft === true) { + divElement.className += " toastify-left"; + console.warn('Property `positionLeft` will be depreciated in further versions. Please use `position` instead.') + } else { + // Default position + divElement.className += " toastify-right"; + } + } + + // Assigning gravity of element + divElement.className += " " + this.options.gravity; + + if (this.options.backgroundColor) { + // This is being deprecated in favor of using the style HTML DOM property + console.warn('DEPRECATION NOTICE: "backgroundColor" is being deprecated. Please use the "style.background" property.'); + } + + // Loop through our style object and apply styles to divElement + for (var property in this.options.style) { + divElement.style[property] = this.options.style[property]; + } + + // Announce the toast to screen readers + if (this.options.ariaLive) { + divElement.setAttribute('aria-live', this.options.ariaLive) + } + + // Adding the toast message/node + if (this.options.node && this.options.node.nodeType === Node.ELEMENT_NODE) { + // If we have a valid node, we insert it + divElement.appendChild(this.options.node) + } else { + if (this.options.escapeMarkup) { + divElement.innerText = this.options.text; + } else { + divElement.innerHTML = this.options.text; + } + + if (this.options.avatar !== "") { + var avatarElement = document.createElement("img"); + avatarElement.src = this.options.avatar; + + avatarElement.className = "toastify-avatar"; + + if (this.options.position == "left" || this.options.positionLeft === true) { + // Adding close icon on the left of content + divElement.appendChild(avatarElement); + } else { + // Adding close icon on the right of content + divElement.insertAdjacentElement("afterbegin", avatarElement); + } + } + } + + // Adding a close icon to the toast + if (this.options.close === true) { + // Create a span for close element + var closeElement = document.createElement("button"); + closeElement.type = "button"; + closeElement.setAttribute("aria-label", "Close"); + closeElement.className = "toast-close"; + closeElement.innerHTML = "✖"; + + // Triggering the removal of toast from DOM on close click + closeElement.addEventListener( + "click", + function (event) { + event.stopPropagation(); + this.removeElement(this.toastElement); + window.clearTimeout(this.toastElement.timeOutValue); + }.bind(this) + ); + + //Calculating screen width + var width = window.innerWidth > 0 ? window.innerWidth : screen.width; + + // Adding the close icon to the toast element + // Display on the right if screen width is less than or equal to 360px + if ((this.options.position == "left" || this.options.positionLeft === true) && width > 360) { + // Adding close icon on the left of content + divElement.insertAdjacentElement("afterbegin", closeElement); + } else { + // Adding close icon on the right of content + divElement.appendChild(closeElement); + } + } + + // Clear timeout while toast is focused + if (this.options.stopOnFocus && this.options.duration > 0) { + var self = this; + // stop countdown + divElement.addEventListener( + "mouseover", + function (event) { + window.clearTimeout(divElement.timeOutValue); + } + ) + // add back the timeout + divElement.addEventListener( + "mouseleave", + function () { + divElement.timeOutValue = window.setTimeout( + function () { + // Remove the toast from DOM + self.removeElement(divElement); + }, + self.options.duration + ) + } + ) + } + + // Adding an on-click destination path + if (typeof this.options.destination !== "undefined") { + divElement.addEventListener( + "click", + function (event) { + event.stopPropagation(); + if (this.options.newWindow === true) { + window.open(this.options.destination, "_blank"); + } else { + window.location = this.options.destination; + } + }.bind(this) + ); + } + + if (typeof this.options.onClick === "function" && typeof this.options.destination === "undefined") { + divElement.addEventListener( + "click", + function (event) { + event.stopPropagation(); + this.options.onClick(); + }.bind(this) + ); + } + + // Adding offset + if (typeof this.options.offset === "object") { + + var x = getAxisOffsetAValue("x", this.options); + var y = getAxisOffsetAValue("y", this.options); + + var xOffset = this.options.position == "left" ? x : "-" + x; + var yOffset = this.options.gravity == "toastify-top" ? y : "-" + y; + + divElement.style.transform = "translate(" + xOffset + "," + yOffset + ")"; + + } + + // Returning the generated element + return divElement; + }, + + // Displaying the toast + showToast: function () { + // Creating the DOM object for the toast + this.toastElement = this.buildToast(); + + // Getting the root element to with the toast needs to be added + var rootElement; + if (typeof this.options.selector === "string") { + rootElement = document.getElementById(this.options.selector); + } else if (this.options.selector instanceof HTMLElement || (typeof ShadowRoot !== 'undefined' && this.options.selector instanceof ShadowRoot)) { + rootElement = this.options.selector; + } else { + rootElement = document.body; + } + + // Validating if root element is present in DOM + if (!rootElement) { + throw "Root element is not defined"; + } + + // Adding the DOM element + var elementToInsert = Toastify.defaults.oldestFirst ? rootElement.firstChild : rootElement.lastChild; + rootElement.insertBefore(this.toastElement, elementToInsert); + + // Repositioning the toasts in case multiple toasts are present + Toastify.reposition(); + + if (this.options.duration > 0) { + this.toastElement.timeOutValue = window.setTimeout( + function () { + // Remove the toast from DOM + this.removeElement(this.toastElement); + }.bind(this), + this.options.duration + ); // Binding `this` for function invocation + } + + // Supporting function chaining + return this; + }, + + hideToast: function () { + if (this.toastElement.timeOutValue) { + clearTimeout(this.toastElement.timeOutValue); + } + this.removeElement(this.toastElement); + }, + + // Removing the element from the DOM + removeElement: function (toastElement) { + // Hiding the element + // toastElement.classList.remove("on"); + toastElement.className = toastElement.className.replace(" on", ""); + + // Removing the element from DOM after transition end + window.setTimeout( + function () { + // remove options node if any + if (this.options.node && this.options.node.parentNode) { + this.options.node.parentNode.removeChild(this.options.node); + } + + // Remove the element from the DOM, only when the parent node was not removed before. + if (toastElement.parentNode) { + toastElement.parentNode.removeChild(toastElement); + } + + // Calling the callback function + this.options.callback.call(toastElement); + + // Repositioning the toasts again + Toastify.reposition(); + }.bind(this), + 400 + ); // Binding `this` for function invocation + }, + }; + + // Positioning the toasts on the DOM + Toastify.reposition = function () { + + // Top margins with gravity + var topLeftOffsetSize = { + top: 15, + bottom: 15, + }; + var topRightOffsetSize = { + top: 15, + bottom: 15, + }; + var offsetSize = { + top: 15, + bottom: 15, + }; + + // Get all toast messages on the DOM + var allToasts = document.getElementsByClassName("toastify"); + + var classUsed; + + // Modifying the position of each toast element + for (var i = 0; i < allToasts.length; i++) { + // Getting the applied gravity + if (containsClass(allToasts[i], "toastify-top") === true) { + classUsed = "toastify-top"; + } else { + classUsed = "toastify-bottom"; + } + + var height = allToasts[i].offsetHeight; + classUsed = classUsed.substr(9, classUsed.length - 1) + // Spacing between toasts + var offset = 15; + + var width = window.innerWidth > 0 ? window.innerWidth : screen.width; + + // Show toast in center if screen with less than or equal to 360px + if (width <= 360) { + // Setting the position + allToasts[i].style[classUsed] = offsetSize[classUsed] + "px"; + + offsetSize[classUsed] += height + offset; + } else { + if (containsClass(allToasts[i], "toastify-left") === true) { + // Setting the position + allToasts[i].style[classUsed] = topLeftOffsetSize[classUsed] + "px"; + + topLeftOffsetSize[classUsed] += height + offset; + } else { + // Setting the position + allToasts[i].style[classUsed] = topRightOffsetSize[classUsed] + "px"; + + topRightOffsetSize[classUsed] += height + offset; + } + } + } + + // Supporting function chaining + return this; + }; + + // Helper function to get offset. + function getAxisOffsetAValue(axis, options) { + + if (options.offset[axis]) { + if (isNaN(options.offset[axis])) { + return options.offset[axis]; + } + else { + return options.offset[axis] + 'px'; + } + } + + return '0px'; + + } + + function containsClass(elem, yourClass) { + if (!elem || typeof yourClass !== "string") { + return false; + } else if ( + elem.className && + elem.className + .trim() + .split(/\s+/gi) + .indexOf(yourClass) > -1 + ) { + return true; + } else { + return false; + } + } + + // Setting up the prototype for the init object + Toastify.lib.init.prototype = Toastify.lib; + + // Returning the Toastify function to be assigned to the window object/module + return Toastify; +}); diff --git a/htmx_contact/templates/base.html b/htmx_contact/templates/base.html index 1a50a49..30392d9 100644 --- a/htmx_contact/templates/base.html +++ b/htmx_contact/templates/base.html @@ -4,11 +4,27 @@ {% block title %}{% endblock %} + + + + + + +{% with messages = get_flashed_messages() %} +{% if messages %} +

+{% endif %} +{% endwith %} {% block content %} {% endblock %} {% block scripts %} + {% endblock %} diff --git a/htmx_contact/templates/login.html b/htmx_contact/templates/login.html new file mode 100644 index 0000000..c5b02a3 --- /dev/null +++ b/htmx_contact/templates/login.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block content %} +
+ + + + + +
+{% endblock %} diff --git a/htmx_contact/templates/sign-up.html b/htmx_contact/templates/sign-up.html new file mode 100644 index 0000000..d2c1f36 --- /dev/null +++ b/htmx_contact/templates/sign-up.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Sign Up

+ + + + + + + +
+{% endblock %} diff --git a/htmx_contact/user.py b/htmx_contact/user.py index 9c3851d..70f01d0 100644 --- a/htmx_contact/user.py +++ b/htmx_contact/user.py @@ -1,18 +1,76 @@ +import logging + from flask import Blueprint +from flask import abort +from flask import flash +from flask import redirect +from flask import render_template +from flask import request +from flask import url_for +from flask_login import login_user as fl_login_user +from flask_login import logout_user as fl_logout_user +from pydantic import BaseModel +from pydantic import ValidationError +from sqlalchemy import select + +from htmx_contact import Session +from htmx_contact.models import User bp = Blueprint("user", __name__, url_prefix="/user") +logger = logging.getLogger(__name__) + + +class LoginValidator(BaseModel): + """Used to validate user login""" + + email: str + password: str + @bp.route("/login", methods=["GET", "POST"]) def user_login(): - pass + if request.method == "POST": + try: + data = LoginValidator.model_validate(dict(request.form)) + except ValidationError: + logger.warning("User input failed validation") + abort(422) + + logger.info(f"Received login request from {data.email}") + data = LoginValidator.model_validate(dict(request.form)) + + with Session() as session: + select_user_stmt = select(User).where(User.primary_email == data.email) + user = session.scalar(select_user_stmt) + if user is not None and user.check_password(data.password): + # Login and redirect from where they came from + fl_login_user(user=user) + flash("Welcome back {}.".format(user.username)) + return redirect(request.args.get("next") or url_for("main.contacts")) + # User was None or password was incorrect + flash("Incorrect username or password") + return render_template("login.html") + else: + return render_template("login.html") @bp.route("/logout", methods=["GET"]) def user_logout(): - pass + fl_logout_user() + """Logs a user out of thier session""" + return redirect(url_for('main.contacts')) -@bp.route("/sign-up") +@bp.route("/sign-up", methods=["GET"]) def user_sign_up(): - pass + """Renders the signup form to the user""" + return render_template("sign-up.html") + + +@bp.route("/", methods=["POST"]) +def create_user(): + """Creates a new user""" + # create the user. + # Add message flash + return redirect(url_for("user.user_login")) diff --git a/tasks.py b/tasks.py index b93ee66..1ed4984 100644 --- a/tasks.py +++ b/tasks.py @@ -19,7 +19,7 @@ def install_deps(c): @task def serve_dev(c, debugger=True, reload=True, threads=True, port=8888, host="0.0.0.0"): """Serves the htmx_contact.app locally""" - cmd = "flask --app=./ ./htmx_contact run" + cmd = "flask --app=./htmx_contact:create_app run" cmd += " --debug" if debugger else "" cmd += " --reload" if reload else "" cmd += " --with-threads" if threads else ""