From 2c896e711ac017d3b8c55dcd55091f3da7289ea5 Mon Sep 17 00:00:00 2001 From: RochesterX Date: Mon, 9 Mar 2026 16:31:40 -0400 Subject: [PATCH] First working prototype --- project.db | Bin 23982080 -> 23982080 bytes project.db-shm | Bin 32768 -> 32768 bytes project.db-wal | Bin 201912 -> 20632 bytes .../assets/css/{oldstyle.css => styleold.css} | 0 public/home.html | 81 +--- public/home.js | 343 +++++++-------- public/login.js | 118 ++++- public/paydirt.html | 74 ++++ public/register.js | 69 ++- server.js | 408 ++++-------------- 10 files changed, 534 insertions(+), 559 deletions(-) rename public/assets/css/{oldstyle.css => styleold.css} (100%) create mode 100644 public/paydirt.html diff --git a/project.db b/project.db index 5fb1d39c63129ce8a31152eeb50a4b861a690d50..780a58c29843da9fe697dcd8cd95cb3001ef430a 100644 GIT binary patch delta 28796 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 A^Z)<= delta 16538 zcmaib2YeL8`~J48DSIiGOCcdZ=y2R#zXaJyLP(GnAXF(yNFYccA%rd*Q4z7w0<0o% zK?D(yDndZGfS^MT2DzJL|xHq+^L zDncD6+=$iacV_5j^0h0C`r7piEw!tRy4s?diM72a8fzbYDx$V-W^C=gnJKlW^v2q6 zXGR&Jg!*i))-p({ogl>fyB1X!O)9O!>HnV7;k)p&Jvm#@S(204 zxqYgOipxv0D<)J|6^*J^S(Du_C$k_YwIDOAXHIH!^>p*pHpbM{)W_80?p=qccJEz~ zlbcgJPE4r%QIKk9i*dCZM3cXuqPnPjU{U$xQuUI49mu^yN{gx{{p(!jlrj0GRrr^G z{g6AksH$i}b!lnIzkcXHcFcsa6UJm!Oql%d#Ngqps(b!NVmT8^{^M*x)#TFu$luuZ z$xL2nu~^uri*_~`eQa$-zz?27X8Qhc@` zXK;bBtp=)}7jNNKh!X!0e-Ur(j;dRdui2eiw6Ls=-vph=SZeS?F=psmnM`%*r0VK4dvTg%OB*}7WJrJS5Xq67 zl|5wKfNrC*yzVj9?5Tr%qoz*p|M+0r`0N>Fz8MbJgtEzw9{xPX6t|5W5)CSz}?9+*fDzInC_*c2jz`z%+8M=o#ZDu(_KER&nek_j>eMs za7k=lToSLtBRT9fI*rJf|FYiYMp zLwk*yJjFY%*N~~%c3LW?4~)1A(AyVvS*I~-0gEsDLP zO)9WU&^YNf$?EbspdD%~yvQ`G(U=oL&5&7DF{%t&qO@vSs6Z9PRbz&Zt?WB>($ro( zhUSl*(5JLVnYGurp5@b~@BI_1Xn%5sk$mt!sHHpx1Aa>eKwy~dBNZODlAlWKvo zK&CdE)$MUe9v76w4F%~ac?%>@y3>x!;&$4+URO;kPGnltXp)kcW~57G_8L2`v{z}# zOP+`1zlLolTGs`mjjmgdIKc(N~%xPt#N~d+5GCeD|{LyYxax;579<3;JmlG-9b8tA&*t@csdN9hQ-^*R!|_45xCeX2btSx^DE^<0ZtAMh^onhHr$56) zm)uNd7}MIXdVsHAxe&oH5zS7fFicqCy9S0a1nLJdjPA>=62oYd*NY4zB(LnlFkEtN zcd$*K({qrAH@x3YUE%Mt31`cRcfrQJCH^Vi{b#ONWR|6B%}H@PYg};2j^U6S6Dy8o zK#YDCzZb8GU)CD8rTp_QQ`Zeb?bzTHNy8gBW06JxGAz;XK-%>h4v1MPu)KjCj|Zk$ zaUf3ZC-GbH%1%>57jbdEzINd0`Ay+YonYWt!xDk8c0Es+S;=Vx1N+p&)AJslW&s?4 z2M7QSKnu_T^Z*0E2nYj&10n#C01;pUL;<1!F@RWr84w4s0OA1&fJ8tNpa~!ukOF85 zNCh+lGzYW*qybt2S^-)E+5p-D+5y@FIsiHXtbk4c8$bfs0St#Z?NH_HlFC>m1{$8x>G064 zHv|r*j$$oJopDZ>;&-JdEswdQ3p6D9So88$+MRLOVgZQcYeh(wByj}fj58SVA$Lf{OhNnAH zIfvWoRZHh|!`N-L1w(haoK|OGleL6RQ3}VtV+?%0doXKcy~@O=Z^kJj7pylb&yGls z@c%)o_z}Ag@>cJ(+SID|La9S%Avu&fjugla((iGic-lUa+X*8l>O!h$n9~O-p}tS! z8i@76fm2Nj9?z*Zr5$S6hTGGRvzH|3b58m79`C*Wsi=7ML1I0AnISC{Y6YSo5w zGIia-@`|6Hd1uX5Bp(9D@1C|OcrE1mm)~zP@%<)MxR+f^%J|kj^<41V(~EohlQ#bz zyg`#`y!J+3#ZTu(J`w!Fl5@6tcs#ifUw3`hJj0XIeofLuWamb=V0JQlA7@|3a$w^4 zq!zrOkHd2C*h&03g$9HZW?$l$$`?bmpEenL=C3HXHE|m*c3em1x^}9T<@-ebN#r0$ zm4n^PHP)=kK`qDu{{!zfY> f1uuttow=UqJk%|hYv&MP^Y%Jp-28o`bsSJa1HMH z-_3O@+pk(;{OzA3{R%6A4Ru(Je`)G~J`FC?lN4BTL(fMjhuq|GLvNRFgghH7f%+YL z0ZVfvO7IAtX*91WqJV-*EceAccKbkrH~nxHdy65giD%p&hE(MjKKK0%-Gjm8h-{ zWBkj#&kT9=mpk&fPx~f(pc^S4rtU27T64VNsptxLI=jzhl_a;-?T}Y))=RB!PDdgw z#^1hAy%&WVOKlAl9@2);GpNOfM6m)mptU#eh+O5Kmc4fQoiqdAV{QRjKl*{f%%t|N0kr0x&np2wuUim+jrBml+A~xCo1>XOt&be`_o%0bKV)zHgKkM0;^knql7gC#Hkim zt2B(NHY)pymly+=*0*D|{umvr)9H2i1Il+pC&}O9@n**5bc4oVff&_bLaDfizPti) zqsa$`EDv@IJDv+zJzj7tht`53saFO+pzsC*K{fjp5-4|{NbzGAWb$ec!cOh z13mU%&I^9_0RR6yc=f=Pqi^{J%g0D|FE!o}I&a~0NnOLT*-p$VE?U%DIIevbo6KpA z92(FK7ibY7GdC0~2d{sYpfosVMlAns zpIJFmRT!_BcDIZUoVjab(jeXUpzT)dc1BZeLYcF3 zM4RP%W@?o)gY-fG@yXzn4|2m`+H$Im7UB<>tiZ=&HdDuBfv{t0k$jW%fbaW+abflp zj?;v>jT#6TZ9aHds4)mreGvayZ5q2#-nISOZ~ZFI1fTu=;KG&h4Kkf-Ao(Jla{Lv% zNWC<8iL!iea3pYbspl**79dbI9OtYq$Qn)Tz7Y>c3#)_Um-GSC@ywclp z*yu-XNh7R~Ahxu*P$KQ}FcWgQBZz1-NAr^3Y6`#OCO`M`e%={g?2)#WkB^|TBqh3XT^9YETPN2#F` zp+c&~gZu3;VX{NTK?Kq0YTIdNMa`&~q3i}yFM|9B^*tjp(=XJwCT%EsGH~cbIylkr zeI@aMGf$PU;mWp;CTf-9IjvhNbq)JW3Yxi6uS~A}AWoU1eI_~3Fu4h9R3^MXJxM7{ zYNAsfu9EH?e*$bBR*r0#==eZM;4^InT|7`r*pN-!g{1^xOmTtk%?$n;!H`^ZEKjO2?=YME` zDSm=Y=)_!~!bL}DW4Kf+^D*(g{GHfWoG6p}3C!LLnB{TIEBsk5 zCZf!(F{i=gri$scg6}|luOgdVjEZ=|Lbi5>I`913_g3S!gE5e%>%jv^C@%?zvCRX2 zl|9NPjE9A8=%~m;i(w}{@)LT1$Gvb`oJJ3%hju(j91Gn7UHE633Gu{8`fw5qjgXIe z6?_FBQMN9Fp0Rqt@aVu_Lu6KOaX~_;IlTjqJkV3yJ8ka3S=T_;tlXX!XH;(AYANXN z(|kRFUL)Ezx?(%pJ0(1DyHFBsU2r>CuXMrssH>|N!u!Uj#y%LTWH%O@P({0-%eWeS z?!%?sCe?iWxqM~^9{HY=58gWQr+HVk{6R{eDI+g#kuOo@kXLo@>wof%+HhH!6n{TI z&jRIkI-nRHtK>7t2WY`iG8x<3TGd)aB<@)IF$$GjEi?8{&;?in2lxID|I`o%McL1h zZjVePiko-ytLcClfSG_N0J8wI0e-+7Kn>tYz*B&y0doP*0OkRn1DDRBEVw65I{IigMX1!UjJhrLc zX)^0$bQZl?2Uox^33|m8{<&EZ4^N5k8}uw|jxm$SofsS?Xxhu);NU%XOFGZBXc^g# z6FBZ6Y7rVcQ)o}`NF2r{!H9ytV&+Y~Pz)8m;NFe+vq9eR!u>}bcWl8UcSf4;jEoIl z!0~)pPkHC|jF|2I3{yww-1z?Bn;&H-4ScXj-W;jpYZ@-exA3aw)dj(ou*5O({Z#q( zrAlv;NPXk)o1=acW*tmT2?TUmXxHi#;<1MBGj6xQK@#&I|rVrW`+yHYgiuUMsUOTA7x zeY9Q|*!N`&jIoPW*T?z=`WQ}F#lq^V=4gleodA?Zmn7}50^;+brgTiV!&)O8g@QfU z*n{lzU%)AylI)<7HW&-w-cZ{)Tp*rm14=lsg35S{E+F{jgW~U1xj8=hgX_}0x<%ID ztm`Yzb?6+BuhUwQug;iO-n`5R)epJA{-Rx-0wGfF7Hd?-KN&OxPHeQWQA*v=bd6HCafK#u+lU3iw5brzng)HX4J^3R z5vByUU#So|9jk1A?IZ*%yXu>zv&7ZG@j(QLWx?@T3kU=j2O<*W7yKLk*LbB)iF&5K zSrlveTQk@MU6NjrwY}q%ZRc2xp9ReTtsv3?6O6_WmJY!tUni~*+s(*n-L8ljj^lXk z&m2ml^?;ITJ#@5NCE^G${^9Q)JQ~Zt(H>&zt}Ya-(|Gv^or=h-Q~W9LVGvKpx!_s4 z28v|_nkIN!#GmlsyF+@Fx|W`kAJEHC^c?afc=f@Eq_tlh4SqDEqSwv+-q3Xy<&&oI zPaj{YPviwpl2$Aj(v|sI%NaS&D1IBxp{}9A%TRbffLX~#AqP#eHz!YJjtTr;{sKQQ z;*I_iv!S&lgk)p|bM$_WDkF1uVFUo2-XsaWgHH(1J@&RBXW@gq9BJO%7McinAJh#^ml6T6C7Nki4j#mp~;KO z9XNQPFB_*JXO)d*^{v}C{zjZJlHLUYf!jmnA&#E0gX=4O7u3fqg7H0j(NHt9``_%Ss?8%{0sgmIFxFNLqTNCacQucguD?&DfV){m57=oW|1?qF0 zw6^?PXbfq;l*kxr`3F{$nN%2r14`eV60`Exz=%|3&Sa~koQbM1EArQuwMt%5{fNNw zxg*#p9k?hssZKdiRNs`-S(M=DUI_})&eQTSICEejS) zn{Mx(gIL;z@)qo{DXDmGR?5_? zE9JxVy5R1x^XtSVg^4`xjuH$hX&&aRMz1scW<*`6xFZkca^?hbZlV&~pDaSoCS=VzPKV3;3ChxsR8r^9@8q(%OkM#;$0 zlZ;EKpgV2CIDAR&776Aj(9!SGbsxN7&4a7`hj$ts_+&WjQRdd+D@Ept&eCy zeOgvM(iZA3`H9pc?}1*-yWy_~>;>!tyaU(|H~UnxEx?a}+kiWOp8!7tegWJC{0jJO_YMEO_&K3> zBn~jfTHyeGM`8!OBhmMqHEmL#!`DThmP@bXJ6S^pYvB?g(7@9Yn4qvEE+L@@ z#3XPA#7hT}i4Niu%=#EE30oap$tpE3XkwMEckbz5+2(`^_wBtNgR=FB`I^9)eS=^# zW5V0-n_9t2Oh#gIf@owe&E$=G9mkwS4!qI~+m$L0{xq;~ZhO}3SK~<3jEa)#Dor@G zr!e25(4uefkEVH!{zO-EX)s{^RIM9J(e;;UXl4)V&-c|ey`7`<%Uuw_&E`Y8Nn~+ zUtBf+;_Bd~P!7Sr@aJCe(>O!)i8=Q|OKyu21U_X#>~wBC>d22}>jYC8r!#YUjgT7? zt`#`cZw-+tm6@Z=ajGnS*lo?C%c`hqF1d(1Uds1s%H3tJvtpdqI5UTB#>U1JF^eN3 zh4wKKf|2JFewdw?YBK1~k_I2p>KD}s^-uk9ql zZ+_4k7|QJ~)U_SC#17J8Z(N}C;6NaY{vvtdEf5!SQT>$cuzM~-Jmhh)EhHxd_GHbp zXaHF=#zCgXrHV7mQsKtXdjlRj9N=yDaJ;`T`tPMs$SI_#c2FRf4)y@>-h5+O;44H_ zsvyH8#DjBGCEJPf3+XD1nD|ol5SDWT2j>r9<2thAAsoX`LB$RwK*hl(0)$8|0f%rL z4u?Y{3T$$>{UuI0Hesn*x%cxuliy2+OZ?z&^pAu5jbspUfbhaJiWUOiTZDWXOADJW zP?5Bu@qVvMZEc8_@Na4`rmo@2Z}ngBbFata@DpCaTWcnNvHbdreZI;l87lACmUJds&UPr7F|%o1GE^2c{fD$sv_~ zb4SO>fkFZ@VYj{eE^NPaA$5hC;LZkna${yuhi;KYE< zsX5<2LHYE~8e?E~IAV25kGH>9-MWL$o?K(DN>O4KS}&M1O)nZx%4rSe>P8^zXRywDw{TQPXbMb=wD%0RNH zz{P-cfs`3Q#<0+_z-0PG2R1!_sh}Fk z2XUA;rj1{|3`@+g&Mbf5nS5k5-u|lLzSwZzBwt&GKg%0=TB$)RgLa%lBx1MPvyG_880N z6OV2ufy}^$5>;)y5DXDDEdQ%d2g_#dB-s77A@3Y=4J`3s50&T<1d`yp)p_(M2Ua2M z+Qs?g1ibKT^*1I3^EDR_Qij#Y?usODxm+UGF|f4EgO=HJcvlqCpbEx=bP)yO$XPs1 zq>_kJ!1=&`b{}lfs&h@70R#%l6PoUklE$cWLtJb~2q&TR-LxP{`SI3qjq=CO_aX~2 zNF(Ht7ewsnpGSS`ND&1o0VAB?xcG$GXrUNz|v9V&)2^$+W z5;k^h9N0Lqabe@e#={10h+a0nGK{{myNxkU7L=&edx71lrmk8DNi%Ow4;1~;hiw(8 zTjz%T)YJ=8BFyF(i%!QHbR1rt!u<>Oza`~P7#|xL{+)$Q3P>i>jVIc8jUBJi1TsfkS*!n= zsja%$+BKXP(oxiGUdMy@WKiiuUdq&?@CzRB4K{*d9v4zzluV~(vGN|O(%KZ z=HFuv?mZLSP4@EgOrj`Aq&dI~t5JVTDw&P{=}?*pcN3J9)$iy>*R)QrQ&xqBh zS9;}LNerM3DKHypeL#$i$Dt@$sZ&?<$mSmuG|1mIbCOSq>ZT+FOeJ2{;t!|U;c2)- z9V%dN+(VWE)TMQ@EFh66s2|>*lRT&s?ML{5;V6q%kK2ZCMfn|YqNm70;NQiB?E#zG zP|!Qz09wmWE_6dc2#bX8=Xif~URWcam|)$0*nV98wBP>D?kK1H&H7#V1pxVK@!MNw z4IB@n2UL^?UOvWqny(vaoF7L+ipIA9Ewt`Xd(ya!TK}jOEjXf&GrFJpB-m5nbwvlP z#z|gzy%3LO8JX#dOk=8Xbbvwai)g0Iy2zY&9SENQ+U|)!r-zDnFZclIfDAxqz$1Vz02zP)nSd-nHlQmY2ha_W z3&;a>2RsVs0q6{~kg=P3{Mdjh(K@oe)z$HH-}EzqNK9J@Jib=x@h zFZx$6yHd3!Z1A0<D$8%iRBg_TDotg85GnjLP)0vM5`w` z=k(FcZgL`ud7H3_*+p1n;MGFND3aMpID*+hIGovzSvQQ?M))yiE8(Hc7Q#cAHwh1B zHWMDiY$806*+_T*^9E+^o6H6s?}sm&nb*mQP0V`28<};4-(X%Nyn$Iu_;u!0!t0qe zgtbf^VGR=?EHJAH^UNy3@JCC8S>_eO46_2W<}c=D!ef}1bbfqdG^3CcrOa}|CCoCy zqnM?Higv3u5LGmYHV=cVXrdeuR0NaA)Qz!WqnygwvTC%=}(vj+WtsS(stLV0iO}XMLYpQM%BUoiUO~t=fsl7R zAx}9W_c%hX#|b&dV&YDg5jru3(DBiPj+JVQ@Q+7J$l?2=2)$QK=tvQv(n3NdBMFTf zL8y2*p`u}g3Lhghawwq@LkMXH6VeO{d~|9&`-Fd3SA5JqmQ84A7NH@Tgc1;;c$tu; z3!%722$?$*ip?MtlTIkwhl%;rOXw31p^I)pAG-)$a1uK2Aau@eC;X8_XswOVtDOj~ zu@b85NGQ;O(CYStR<$FvvaN<^+3GgfPHK&9RV!>Kw#2qF4cm$q*iLAU?f7Qc_DaRJ zXH&3#wnqv+cr+Q??oF`GOTsoc5!-GF*jnSU?P$TaLmam4&Dge!#kOq>wr!%ZZ5@TJ z#e{8~h^;vi+t>(fW5Th888X-?Beous0h3;jtxkun7T$zlS&ar;0oM0e7B-2%Lcit> z@E3;+(=xLALS6ni7EC!C)a8$7A5`!U^zQ_*ygv{-#ER4LA0qxD{wUrQuZW+C=fqRu zdty-BBW@QrifhG{;xci8I8U4-PXG5GJnj)lQ-r5P;tDnvMNraUGStQrS|VW5pNzo- zf6N%z_!W#9%>1p^_ffhAA*{{W$LlMVm? diff --git a/project.db-shm b/project.db-shm index cff3ff4cfdf2e0d17a5b7fac2900da0a6f33cddd..e78583a72501c8e2dfc029f37fd31fd990970da0 100644 GIT binary patch delta 191 zcmZo@U}|V!s+V}A%K!q*K+MR%AixTw?~5@o6tOx_%8(RQyW}r4_o%YVBjW={bGPhK zjUiP%$ZQY*nfo6JfCU*Cq@Xkd0}GJF1jKAGdUGISvMVza0}GJxla-MXNd0DIW@cao HGC1`B)UhrU literal 32768 zcmeI*T`pun7zW_iUmG-fW@t_On=v!Sf7^jQxE3y~!Jb^Zp4kZ!i*QRFL|hOgB%Sp0 zC3WhYO4V0iJqxJQy4p$=wbJ^M#%}2^o5_Cj-}OYNwt0MdzF0pz8td$z{BNHf?6uxG zzsg-jmG|aJ+zpn;#>2~C>EF`oW!`vc7p2Ck&X4}CYIrT@OLdl|f&c*m1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!Cur0)6SvK)G>isNDQDoG%&4XjZbBt!!r}oxA(Go?U2= z009C72oNAZfB*pk1PBlyK!5-N0t5&U=o*2sOy+xL(#}$T#@Gnd8u% zf+0RsD+sQ6*uE&NZ=!wBg7u+gDN+z&AL^Tkh($s1$rnM3;ADGoyHy1D%pCaUJItB? z_y2uoKKo$e(gv>c{jt|D=k3E7=DA7 zAN$gB`09~^)*5bpWqf_O^~U#RF!ssSKdxCEqnz!%;jtrcjsnNQBzB(*+PxpNvNCz)Jb1jUhK({P`B7+ z+CguZmTMeDp^{Q4$eoHO0oo;|5UYV)G$_@g1{3rYWv<;s4M;)GNzqP9JPs*01>G#* z8BGAHNDBMjU%{HiFP@?noo_C?+tF&uy zB&fg3BFfFehg0;(I9Wj)WL2F}d88suW>gJ9o>deE&?-mqgJV_)c60|tI@U4~j^;gv zY(7`<#~amnv`#0NRR5~qR}^N${k&N4u?v|_C_6hJ?-?$ZmAbsECbX5PZ$aAS8DLC# zZkE}h5QuIDxFIjliohu%qFB{*j|T{_8sup}BaWqLO&E+@9N5$jh1j;j{yljst#nG@ zQ8*{YlE9_P)MB&JN%TTSK4O?n7ik8gL7xj5TrXV6immigYQs|V4}aJ`*d&ILUyv8cFnLAZ_|@JI61GD)a;95IHi$GQ3X0Dk zSxhzSP487jXlCgZuKY{ydxxIA-nLBk`svh!NWQwK>9_MW!dj^Z(A z4k^uY3n42Epg^>;6lg`F3IaSlUkwFZS}crWrKW7YpYoO)Wuu?-W@li)(3UH67TfgS z8(58EzMLs0vW0~esTWD9t{FK8jBKLn>w1?K=jK2=7_ITegb-a-3=v8kZkxjW&QUj6 z)AacyUlEF}ZnbI5v?NMQKs7EELZC2L)T)WlYP^%F<&ALm*`=uI)Y#7dp!VXXpS+WO z;efe7kXsnJMgBy7P5yRe22Z=*Ce%COfdLf)y zJiD+uUvT+?D6e#yN+vhc5))iPFW0?B14gB=x*YNE4QqeTT=$06*fZBhvA!eO8mFe7 aDvhw($Qu}WV;9)enQgFlJ-7W2$bSLaSs5(= literal 201912 zcmeI53v?sZndi$7$+p_|ZQI>6Y!A?!bON(8F4wJhX?B;9Wl6SWN!G)XEHWV_sbtC4 z%aZjR4ik|%!;oY+156TzZ8jtfb}z5gryiTNgK{MAb-YB!}X$p>D#V(|Kp z?t0_t2Wx*j_>*hMpL!|kl2_LMer@B;KfdilkAG^2Y#8eQHiZ(yUmrd&^qHZd!Mg{$ zcjYXvTA~Bb3@~I9EIW-Uu{ZL z@@io|BDsW|>{@b*aztD9&PAqpR#+|;e4@J62$l2Ya1)nYwpejxu|hsxVi{H(XYe@7 z9>9_nvsQs`?HfkaRV~xr%gAy1DavMBD{0k2DW^8q4<}C)V)OG;PR1QdN2e=Ewt(Xm zU#6LH@>4OT=(j18QCq$2iE%D_#UV{sq6;?GLp~oe_S8ABFpk-Ah7DoS$|_cdr4@-| z6-+|fwne5Hh2a&kwQC4bmv;ylk?DKNtGV^K3d_wZ4zS+jsxM7vg7JiFy2AUs1<@72 zky*jdxtcS0*1njWiTgCQI+>6r+1w7%$W~saIe`^LZl`EuSrTO2>KjDV-p$d>7Sj1n zfjDPvb7iH{D7b}bB@|hhm|OG~d^2n)9!@Ap-dWFvDpJ6^I*}~R^2vEt#Dd(|ITC?p zF((MhyCT67EijzI;#U7IMD5!g$(mMP6FQ*;la*#NCtslWB>!omG#p&^T@@xLE>Qj$hZ+I&& zSviJQ7_w{$s=Y;^v7nHRtxNk6wZCP$a^KM!O@bmzCoGRtixqipZqlD;lFMAcl_=yJ zbeS!)&ZMfkrek?=vYMThJo(7Htu{3k45!1cSuK^)O65X6RLH62jiMvxk`iJVD=S(# zo@N*hGo32EX$y~OUd9TqX zE=QT1y_r^;oRCjfIQKMH6*>3%SxrtMay&W2GK7$oWod>NBt~qR%^Qt~aSFu?@eT%; zCSPr)kg1T((|y)LCi5<(U*y zD-Js3EYqnGb%R=iz-H7@? z%d~$#byd41_GDHxkCv=tvuYX`#v2F~y~!4^ z-W~{~;S!YAzv(h+6*=oWfs#FS0&IGen!{JbOkie4O*gnWkL4u2Cd{fXF+Pu%u&6os z)M|yT+auX#q#VU_w0nac364Z2k7H$fa0DT5C2b%lD>AE}{RTxwGANSr6=+Fe$To7R z+@cGl8h>!N=@&l?450+k1sRw__}lhx&Y_`pbLO5K-zp_p*+(Sn*4Q?AVADnGw=55A zx}K5Xw&()QJH~GxaSR>Ob%Ei2-*I#SvJn;_00e*l5C8%|00+@Qz{oU~fR%|PCB{k`ol!Gg3 z3^TcACLF2QGqg9jv=&T;a@frz9YTo~rb-SR_NX(W%eAFHy>Nj1cTo3CmOvMH*Q@}b z3&4B?oih#QBiL;H5AzY|{VQNT0+^3LpCaPIn~#9pEuBBQ!0p|OAHMD0&)M6$z>yQ_ z0!PlvTR{jK2mk>f00b^J0yl0G0k|_M0v!FX{kkrIG3WxI3&1>uFi)XKW(0$I3O6hf zH$DBsJcZ=Cu`Y-%rIzrcw3NY}CFDb&Xi zn2*4$1Q*+_HRKlv00AIyUJ?LZfE1fyGWgr|7f6mFYGiY#jC7{+KjM%W#-32vWmC9# zdU2wesxOeapT*#ObjhP8SL%)+pCFTvr(?01K-D?BY|k$W>(h<5rz0>_7lF0%`0UI_ z0OJT)#Sc>H=$@`1S+$|ISZMpbMOr z_q`A_5C8%|00`_%;9}DS+Rx0Am1WPazrY2m3vA#8e*y3p0Dl4S7XW_&@E16L{RK9z zXXN&2P``yrKJd~NgV%p_*JZbT!+-bK-@iPB{(%}p84A6P-ax-VFQX&qyXaf!N%S@J z5V{}TgYHDPp<7T3HP9-`kT=2t1b_e#00KY&2mk>f00e*l5C8%|00QHZ2 z&p=m>Y#P#oUe^Ww;=srL%e7aI-eN(o5?vsy>jJ+*|A<~h-$T!!r-&->Wpo(bjs6OK z5`7#si6)Rn*P}3bBP>7w2mk>f00e*l5C8%|00;m9AOHk_K!-qoZ&!C05drqJU-UmW z)_!58M%ypCKQdyxAVR?IzTPff2|&glh!il~eqm9E+An&a9c;hod2Cnb#Uqx^3wgkJ zK_2t_joozT`r7Gkb6@WOi9t1ykovm@ZtpdIHy`dXQtD8*sdu1jfan63ysQVkt_wVO z-SdBR@aaE(+=4z$bb*Gh3%rSbiGGe=ML$B{N8dr;L{AWP;4A2h=w5UP-HL8TA0vGR z8l8+o0tA2n5C8%|00;m9AOHk_01yBIKmZ7w3;}abcNftB^n@q??S%Z7zq^a53FPyA z)}(Fu&?5sQ(?1A0Of2R$KrgPst*pq+G^NC$#kUvCf5 z0`!+xBYnNwOmu-)^q+NI;JV93zxpTYzeX+Sb)pO0N}+equSt)A*N86gWAp+UOYjVO z8a+;wfrrt9r02jr=q}QC;8yZRSbzW!00KY&2mk>f00e*l5C8%|00;nq_ens1R=4ai z)?>zc)L4(`Yx8bnZ9LES4C}9ZhK%)~vEF5@Eyj94Uw8K#>po*`?j?f;^sWP@Uj0>% zz9PdB1_sEVw)hL&_#eb4-pV}sknS%qs{0F^l)C`<3%t*QhmQjRAOHk_01yBIKmZ5; z0U!VbfWU=L;B5K}ptpDM7a+jR@Aen?;7@-4H*5b}O6&dtqq@JqNx2JvzrclFeIb89 z00;m9AOHk_01yBIKmZ5;0U&TH1dKud=ZL=m5e0Vi7a;jDzuRA6=vUWYuXTMv)cplU z*Zl=f=q>>M0;f_t;JrWq2mk>f00e*l5C8%|00;m9AOHj?ec1o8`~}c2&y>Fa0dG34 zzrY_nU;4qb(Rcn?_ZJw|{RK|ST>$(Aphg1$AOHk_01yBIKmZ5;0U!VbfB+D9{{(a| zfHUYXfZo~SFF@k$B|lsI1>U?2d;jyG_>%4~Fw*uHco`ibeuLjaPZGDmhtU1RYw%8V z8@dIx$UFqADAWFW|9OW!00AHX1b_e#00KY&2mk>f00e*l5V(K|oMnH3;qHOkO~h!h z+kDv6O%m!5VZDRDz@1Mqv;X$5ue(#3%rQFM`j^-il_o#CNmJ+P39l?B>FgN z5=|hDu1Dea*9Ba1kToCx1b_e#00KY&2mk>f00e*l5C8(FNZ=gv7uelJWf z00e*l5C8%|00;nqi<`i4{RQsb>@T3Z4VVx2=pQgsqk7W&?4X|XJhsb796kLLKUFC*ArGg(c!NM%w-Yuc24bkI@TcEWtDA zY4kYhH}Eiekn|k52i-;b4&2)Qy0{Ayat{Q601yBIKmZ5;0U!VbfB+Bx0zlw&5IFPx z0+!z1o`J3&eWmvpc$dF`WwrVF8}NOjy1&5a$@vSst~(5zj@-jOfB+Bx0zd!=00AHX z1b_e#00KY&2wboP&a%HizwR&4yO+oTx#Ri^?0%^KsnyH>^a|ZyU{v=P_$Tx$bP^5& z7i_sf#()4200KY&2mk>f00e*l5C8%|00^8ifpg4XV8HMfF!v6Sp#*!6?=NuV$ix>u zasA~tSVC00;m9 zAOHk_01yBIKmZ5;0U&TO6VQkK4_+~>Cl+<6otX4w;Ep|p7r@?L{kQ(-#(t~*0=G?$ zeQIUmnkRIBfzfq;fnS~2Vc=peO2|7900KY&2mk>f00e*l5C8%|00;nq(?j6Q`wN&1 ze}TQn^cQeE9%Ao%df;D&(96VkFhij?&=KM|_$~A#=_T+Gx}UfW-idBQx1bi8hhUXB z3%pLgz+d3>6hG_*2mk>f00e*l5C8%|00;m9AOHj|TmomsUjV&Lp>B&xl9Tlp=UEqZN z0v{v228^#$B?RvW0zd!=00AHX1b_e#00KY&2mk>fa8VP`%>y?41&GUEA9;p9`~Cu# zy!F#}CMrGi7W5j?1#YF#ugQD_uc9BL7szOWXVBB=anf(#Ve}yBIdBiUi}W41wf%Kb zmnCE#2mk>f00e*l5C8%|00;m9AOHk_z-b|H=KKZFJCwylRwwco@XTEO((Zr1bg%9& zFnV(S0zU(Pfzwj>uoEBv1b_e#00KY&2mk>f00e*l5V(*DoGpKWe%)WdbV<)ZxA}1I z-kyQ3oT;1qLLDNo&0RUN9@4QVA9(4C!RtS|>vxj>aZvkR@jl&OV07JI;3w#W4g>l= zU;zR^00;m9AOHk_01yBIKmZ5;0U!VbbOPsyzrcXuFVNRJ(A{M|e2KYtpsOpl*Z7q> z)J?)P4-j48l9%;}*L8tw@A&MWKm6?9f73!nHjEzMU*KkRh>RiVI1K3ffCUHu0U!Vb zfB+Bx0zd!=00AHX1b_e#I7tFq{RQ;j^hbA+4BW9tA5M_l+pGW9|J)e*CDms>++)0j z8r75DX9xA9=doQz@`%MqX@)bdB!T6Y5ZZJLxv|*++si<@A?VBYnNwOmu-) z^q+NIfPd|#-~GL-KRBWL3yiM&3%q_}hk=vKHM|W700AHX1b_e#00KY&2mk>f00e*l z5I80Q{aM{&$O4uzp^3^%UD~C^?<(a?l;zb#@eh~ z3z&MzFoH|;)n0w2_ZaAH|7_?2ub{o3AGmJ6FoY7+i1|ntS|0xT@PVPv3=Iw5J=nb~ zXL;2U9e8Gd>3_5@()YaiNWbrxnSl*J00;m9AOHl;6#_SoBI?SPNe^c)`Qf;{R%?GB zWa9@hb5OCZ%$3xnV^R*Ts4>jsnwfB8sQsId zTrT9aLVo?|&Z+R4?6E~05z(hglIqH=+4u@uoh;dfl@hzOLMLRe5-qwXs!}mv_ve>v7t-VXXpxG=t>O9lWi5!-%@7W!ifgsagFL zWwWi7v}&Q0Q=99E<%vRUetycyxI^jabS23aaJ=HnG&4?qDy9_uHf1tutCu}7&SkGS zr0Gg@!Nz*X=R?MxI(ZhxF+0w%AuL*1#mca>B5|yONl4qa$TXucydt)C4I%3C4tpaq zeNTBcw;oqvxmm>l)|*`QrRhvCo^VZ9c%Qc*x&k;dEBHBAa|X}a7n3t_pQctP6VfD` z+aVg+%F8q-u%gKA6pbuPf{a^zgNWL@Ihxr*I^U@a&RN@BS*bJ%ZedypMHVLJ7QF@E z3>%7v6H1bI*0Z6C6!5N2Bule=a-J2jAUAf7M4(yB34(qmY~_L_T42Z-f?NH&5Vdb} zBx_oEP3VLYOjer7oP@PTI-78KU1htZaCENdlh{OlLTwatc~3?3XKQq_IOWX} zddaQFl}RNRgYqKgT$A?271DG}%Hk4juSJwZtr1*~GC6xQtu#3ypRRE3X|5`A?)4L$ zR26bOIm9xAkd<#S8Hc2A3!YSh<-AE~ovxJ32QRkI*)4CS|it zyXBxa=8q-a_Gn0)kfK~_b!kRRMW!=bGHx6W^HzaW6dp55=j<^uP6AbmBLOxtvM4hPoh$lS3HX~dv2{*Xf7?3Fo%Uo zI5`v9771xpc;3p&v?OvIFCRU7aEQgEWULIP1%)Gp(Aw9tt-UK15@}KlTB+WNrw}hK z#WF>Itz7fCWAmB3Pjjbf&&pi3UUk%2X~N~16jLh>I^-n2 z9WO_A*om|0QECof5i^0A88zMD;yjj<^qMfMy2SWAUc#d0;8Uv=wr-DPn~`!9&(ZD; zgd|21pgfM1?J*LPyp=SCoUF*~PPYprxIyxBbR5CLH)!t+hRF1WV<^*w-atpl zOa$LTPm*~E9zyq%SqSb#x1n25i_A~BN*o7YM?XU^6N5rnfB+Bx0zd!=00AHX1b_e# z00KY&2mpa&6F4irgQk7NVsPO0Q}7+ETr;(&_*Lgs7DE>ZccFhG^A!FBy?7$$!DD9} zwg3Sj00e*l5C8%|00;m9AOHk_01&v?2%HPPgNDE0!0r3cDftd2KQQ?E+*k3xvk+H< zu@n0a-n;HRc(Ii*=Cl+-`Pmbj~ zsBa_B_r_b!xbNUKe|ghQ|1fKnUm_foH~dP&XRfx3lly7Jq@mUpx50H-GqhAJ+W^_MrI5`U}LN_<#Tq z00KY&2mk>f00e*l5C8%|00;nqod}pNC1d=l7tRxZfj|70KVSHNtDn0^_ZQfMylsC0 z?};4-cFH;Y4F~`MAOHk_01yBIKmZ5;0U!VbfWXB?z!=I;y>RLI>n{)*{@a<|FNptL z_ZQfMN?qt}^d|Z_`ZO6w@FP@0-$&m;-$YN4xe304zKCu{hfv31;NmK0$Sn{60zd!= z00AHX1b_e#00KY&2mpciNYJ(J%|pIc?<7Ce?^Q4U;zR^00;m9AOHk_01yBIKmZ5; z0U!Vb-a7$(?Ej@_#b2P8qAqzQ`M^t83|{}yU4Q-E4`2FcmYcsa)EA*fOpkP-^zhe* z4-9=~XlU^6!R}o-%d3{?z%v7k*=c&D?|Ji)e&2h~GJFyU00AHX1Wpry*6?mbjkQes z_ET5wCHv&KN?AL|#t&lVU~)cckIvO>srqs(Yg61VFYSqH`GvB_;Sbnj_Ju%vZaT&_ z#Zuhmk2vMfY+CSn)TY)9m9%Q1lwlc09%qzstdO0SScVnH89dIi2e4$toRw3K{z*8% zh*r#41-Uhhjv0`zHl-jsnwfBB5|yO3G;1RWSUVJ zUJ+Znh7fgmhrJP*zNfsJTaT--+^pgN>rJlu(sU*mPq?Nlyw6(@T>%`K75tp5IfG~I zi^-X|PgARt32Bne?GTM@nW*iby2P?Ef}o()x`fOmBwS(@dO^Q?#kxv_I30?lGh5cEcAD;F%$ z0z(=r-0I(jsC}CwS<}jELMN19veHcEB&;>k*@VOED%&N6qjN={#3u3+YNMFTdn%$o zTceZ3DQ`x|6^3*~0QPuW%feTcZ{uluea~gu-`1nQ#^7BTPJN z=K@+nTb#%*t23Gw@#jTGarwP;UCU#Cy%cV&I6Zz(e$5|Ned@964R7ToE630ZLzXQ; zwYLZ~78J6vb!orRVqCfJXpJU8k);!sN2nN)HyC@*5pHECa5Ax+1mEH2UZT0}|I8o}i#le0I|N|O`v z=?dqb=Bgs+UT;uHRUyZdLo7oGSy`55ctK*smf37v#*Aj$IECVccn5<^lmo2X%mkOy ze%>9On~XLfO*{0oc&>Qo|l5Tr6Bu+?CF15NeqopF#nJpPN4u^TGKq?B48Kraf z7#SylFtQ$q%t@GITYaW&XHP^eCp(eUvW>;!M66tlHPy6~ZrVy=r=QlGiI^u*smd#! z#hN|0RxC7^7D|}ILM5D>iEN96v?@GrWo23tIgXc)o;^6kVp1|z2GfGVkwR$g>)F=c zl?sV8DF&@n@5EDxmzH9gqQ6$IdEBx2Ox~xt)3j%0E?ciU>Z~;3@=S`U6$c%1mg!Wb zkZO8znVssbQ6zeMhph}lixP(=fkbB9e_%O8?mzU)Ev7}Dk!7Lvp>9Ndpk>;x-vk+j z<;kpQ9xYkPX4Ot;@^pDY$lB7jz>>=rtOhE!dOD%ir>c!fS9aD}b=kb!Y(Xtl)@E#h zV0Nw+3M+JH7oJQat!i0vJH(IccSLv#LMgI@ zM#@n+E~GC zVZq~Mq7FL22mC^AZ8^nzWxM7nE-Wq4ZAOHl;6#_SGd(h*~+=Je_V$Qao{q%oOgD!9$=mMMj3xF%Cb)kY4&ZTh#9BXBT zbD+Nf=mKPb3ydRxaRi<57BG$g#u4bf_hB3Xj3a%B+;49gNAQUsw95bK zyHDJu>jGn@lj|?gw|t)5wL_Rd00;nqGe-b)0ni0b;4d(?Z5)BNcVp}-shAu^dLwtn z5!ka+^U0*(40&A2lC>Cz5J#OW*f( ziH3u51n$xBkZ^!QVGC z!$ZU&>7;dm$eFv*hCKrTAOHl;69S+MY|;gI@f_8xPTqI|#mmL>J)2{|~<~8^Ztq diff --git a/public/assets/css/oldstyle.css b/public/assets/css/styleold.css similarity index 100% rename from public/assets/css/oldstyle.css rename to public/assets/css/styleold.css diff --git a/public/home.html b/public/home.html index 71ce6e6..2c438aa 100644 --- a/public/home.html +++ b/public/home.html @@ -1,74 +1,39 @@ - Home + "Password Manager" -
-
-
-
- -

NFL Stats

-
- Search Players -
+
+
+

Personal Information

+
+
+

Password Name

+
-
- Welcome, % - - +
+

Password:

+
+
+

Set password?

+ +
+ + Log Out + +
+

Error Message

-
-

Watched Players

- - - - - -
-
-
-

Highest Contracts

- - - - - -
-
-
-
-

Highest PayDirt Score

- A player's PayDirt score is calculated by weighting their offense score by their annual salary. It is used to evaluate the worth of their contract. -
- - - - - -
-
-
-
-

Highest Offense Score

- A player's offense score is calculated by combining several of their offense stats into a weighted score. It is indicative of their overall performance. -
- - - - - -
+
+

Password successfully retrieved

+
- - - diff --git a/public/home.js b/public/home.js index a3974cd..60f31cf 100644 --- a/public/home.js +++ b/public/home.js @@ -1,212 +1,185 @@ import { postData, verifyLogin } from "./client.js"; -import { formatSalary } from "./utils.js"; +verifyLogin(); const token = window.localStorage.getItem("token"); -if (!token) { - window.location.href = "/login"; +const infoForm = document.getElementById("infoForm"); + +const error = document.querySelector(".error"); +const errorMessage = document.querySelector(".error p"); +const success = document.querySelector(".success"); +const successMessage = document.querySelector(".success p"); + +openKeyDB(); +const encryptionKey = await getEncryptionKey() + +//console.log(`Encryption Key:`); +//console.log(encryptionKey); + +async function handlePasswordSubmission(e) { + console.log(await verifyLogin()); + e.preventDefault(); + const formData = { + name: document.getElementById("name").value, + password: document.getElementById("password").value, + } + const set = document.getElementById("set").checked; + + console.log(set); + + if (set) { + //console.log("formdata SET so SETTING"); + setPassword(formData.name, formData.password); + } else { + //console.log("formdata GET so GETTING"); + getPassword(formData.name); + } } -const highest = document.querySelector("#highest"); +infoForm.onsubmit = handlePasswordSubmission; -updateHighest(); -updateFavorites(); -updatePaydirt(); -updateOffense(); +async function setPassword(name, password) { + const encoder = new TextEncoder(); + const encodedData = encoder.encode(password); -window.addEventListener("pageshow", e => { - if (e.persisted) { - updateFavorites(); - } -}) + const iv = window.crypto.getRandomValues(new Uint8Array(12)); -async function updateOffense() { - if (!verifyLogin()) return; + const cyphertextBuffer = await window.crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv + }, + encryptionKey, + encodedData + ); - - const offenseHeader = document.querySelector("#offense thead"); - const offenseBody = document.querySelector("#offense tbody"); - - - let resultObject = await postData("/getHighestOffense", { amount: 100 }, token); - console.log(resultObject); - - if (resultObject.matches.length === 0) { - alert("Error loading scores"); - return; + const data = { + name: name, + iv: bufferToBase64(iv), + password: bufferToBase64(cyphertextBuffer) } - offenseHeader.innerHTML = ""; - const headerRow = document.createElement("tr"); - //Object.keys(resultObject.matches[0]).forEach(attribute => { - // headerRow.innerHTML += `${attribute}`; - //}); - headerRow.innerHTML = ` - Rank - Position - Name - Team - Offense Score - `; - offenseHeader.appendChild(headerRow); + var resultObject = await postData("/setPassword", data, token); - offenseBody.innerHTML = ""; - let i = 0; - resultObject.matches.forEach(player => { - i++; - const row = document.createElement("tr"); - - row.innerHTML = ` - ${i} - ${player.Position} - ${player.PlayerName} - ${player.Team} - ${Math.floor(player.OffenseScore)} - `; - offenseBody.appendChild(row); - }); -}; - -async function updatePaydirt() { - if (!verifyLogin()) return; - - - const paydirtHeader = document.querySelector("#paydirt thead"); - const paydirtBody = document.querySelector("#paydirt tbody"); - - - let resultObject = await postData("/getHighestPaydirt", { amount: 100 }, token); - console.log(resultObject); - - if (resultObject.matches.length === 0) { - alert("Error loading scores"); - return; + if (resultObject.message.includes("UNIQUE")) { + resultObject = await postData("/updatePassword", data, token); } - paydirtHeader.innerHTML = ""; - const headerRow = document.createElement("tr"); - //Object.keys(resultObject.matches[0]).forEach(attribute => { - // headerRow.innerHTML += `${attribute}`; - //}); - headerRow.innerHTML = ` - Rank - Position - Name - Team - Paydirt Score - `; - paydirtHeader.appendChild(headerRow); + if (resultObject.success) { + error.style.display = "none"; + success.style.display = "flex"; + successMessage.innerHTML = `Password for "${name}" successfully set to "${password}"` + } else { + errorMessage.innerHTML = resultObject.message; + success.style.display = "none"; + error.style.display = "flex"; + } +} - paydirtBody.innerHTML = ""; - let i = 0; - resultObject.matches.forEach(player => { - i++; - const row = document.createElement("tr"); +async function getPassword(name) { + const data = { + name: name + } + const resultObject = await postData("/getPassword", data, token); - row.innerHTML = ` - ${i} - ${player.Position} - ${player.PlayerName} - ${player.Team} - ${Math.floor(player.PaydirtScore)} - `; - paydirtBody.appendChild(row); - }); -}; - -async function updateHighest() { - if (!verifyLogin()) return; - - const tableHeader = document.querySelector("#highest thead"); - const tableBody = document.querySelector("#highest tbody"); - - let resultObject = await postData("/getHighest", { amount: 100 }, token); - console.log(resultObject); - - if (resultObject.matches.length === 0) { - alert("Error loading highest"); - return; + if (resultObject.success) { + error.style.display = "none"; + success.style.display = "flex"; + } else { + errorMessage.innerHTML = resultObject.message; + success.style.display = "none"; + error.style.display = "flex"; + return; } - tableHeader.innerHTML = ""; - const headerRow = document.createElement("tr"); - //Object.keys(resultObject.matches[0]).forEach(attribute => { - // headerRow.innerHTML += `${attribute}`; - //}); - headerRow.innerHTML = ` - Rank - Position - Name - Team - Contract - Annual Salary - `; - tableHeader.appendChild(headerRow); + const ivBuffer = base64ToBuffer(resultObject.iv); + const cyphertextBuffer = base64ToBuffer(resultObject.password); - tableBody.innerHTML = ""; - let i = 0; - resultObject.matches.forEach(player => { - i++; - const row = document.createElement("tr"); - - //for (attribute in player) { - // row.innerHTML += `${player}l`; - //} - row.innerHTML = ` - ${i} - ${player.Position} - ${player.PlayerName} - ${player.Team} - ${formatSalary(player.TrueAvgPerYear * player.Years)} - ${formatSalary(player.TrueAvgPerYear)} - `; - tableBody.appendChild(row); - }); -}; - -async function updateFavorites() { - if (!verifyLogin()) return; - - const tableHeader = document.querySelector("#favorites thead"); - const tableBody = document.querySelector("#favorites tbody"); + const decryptedBuffer = await window.crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: ivBuffer + }, + encryptionKey, + cyphertextBuffer + ); - let resultObject = await postData("/getWatchlist", {}, token); + const decoder = new TextDecoder(); + const pw = decoder.decode(decryptedBuffer); + document.getElementById("password").value = pw; + //console.log("Password: " + pw); - tableHeader.innerHTML = ""; - const headerRow = document.createElement("tr"); - //Object.keys(resultObject.matches[0]).forEach(attribute => { - // headerRow.innerHTML += `${attribute}`; - //}); - headerRow.innerHTML = ` - Rank - Position - Name - Team - Remove - `; - tableHeader.appendChild(headerRow); + //console.log(resultObject); - tableBody.innerHTML = ""; - console.log(resultObject.watchlist); - let i = 0; - resultObject.watchlist.forEach(player => { - i++; - const row = document.createElement("tr"); + successMessage.innerHTML = `Password for "${name}" is "${pw}"` +} - row.innerHTML = ` - ${i} - ${player.Position} - ${player.PlayerName} - ${player.Team} - Stop Watching - `; - tableBody.appendChild(row); + + + +function openKeyDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open("PasswordManagerVault", 1); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains("cryptoKeys")) { + db.createObjectStore("cryptoKeys"); + } + }; + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); }); +} - document.querySelectorAll(".remove").forEach(element => { - element.addEventListener("click", async e => { - postData("/toggleWatched", { id: element.dataset.id }, token); - element.closest("tr").remove(); - }) +async function saveEncryptionKey(cryptoKeyObject) { + const db = await openKeyDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(["cryptoKeys"], "readwrite"); + const store = transaction.objectStore("cryptoKeys"); + + // We store the object under the label "masterEncryptionKey" + const request = store.put(cryptoKeyObject, "masterEncryptionKey"); + + request.onsuccess = () => resolve(true); + request.onerror = () => reject(request.error); }); -}; +} + +async function getEncryptionKey() { + const db = await openKeyDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(["cryptoKeys"], "readonly"); + const store = transaction.objectStore("cryptoKeys"); + + const request = store.get("masterEncryptionKey"); + + request.onsuccess = () => { + if (request.result) { + resolve(request.result); + } else { + resolve(null); + } + }; + request.onerror = () => reject(request.error); + }); +} + +function bufferToBase64(buffer) { + const bytes = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); +} + +function base64ToBuffer(base64) { + const binaryString = window.atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; +} diff --git a/public/login.js b/public/login.js index 7f36992..205aa49 100644 --- a/public/login.js +++ b/public/login.js @@ -6,11 +6,28 @@ const error = document.querySelector(".error"); const errorMessage = document.querySelector(".error p"); const success = document.querySelector(".success"); +openKeyDB(); + if (loginForm) loginForm.onsubmit = async e => { e.preventDefault(); + + let saltObject = await postData("/getSalt", { + username: document.getElementById("logUser").value + }); + + if (saltObject.message == "User not found") { + errorMessage.innerHTML = saltObject.message; + error.style.display = "flex"; + return; + } + + const { authenticationKeyString, encryptionKeyObject } = await generateKeys(document.getElementById("logPass").value, hexToUint8Array(saltObject.salt)); + + saveEncryptionKey(encryptionKeyObject); + let resultObject = await postData("/login", { username: document.getElementById("logUser").value, - password: document.getElementById("logPass").value + password: authenticationKeyString }); if (resultObject.message.includes("success")) { error.style.display = "none"; @@ -24,3 +41,102 @@ if (loginForm) loginForm.onsubmit = async e => { localStorage.setItem("token", resultObject.token); } }; + +async function generateKeys(password, salt) { + const encoder = new TextEncoder(); + + const base = await window.crypto.subtle.importKey( + "raw", + encoder.encode(password), + "PBKDF2", + false, + ["deriveBits", "deriveKey"] + ); + + const authSalt = new Uint8Array([...salt, ...encoder.encode("auth")]); + const authKeyBits = await window.crypto.subtle.deriveBits( + { + name: "PBKDF2", + salt: authSalt, + iterations: 100000, + hash: "SHA-256" + }, + base, + 256 + ); + + const encryptionSalt = new Uint8Array([...salt, ...encoder.encode("enc")]); + const encryptionKey = await window.crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: encryptionSalt, + iterations: 100000, + hash: "SHA-256" + }, + base, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"] + ); + + console.log(convertBufferToHex(authKeyBits)); + console.log(encryptionKey); + + return { + authenticationKeyString: convertBufferToHex(authKeyBits), + encryptionKeyObject: encryptionKey + }; +} + +function openKeyDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open("PasswordManagerVault", 1); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains("cryptoKeys")) { + db.createObjectStore("cryptoKeys"); + } + }; + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +async function saveEncryptionKey(cryptoKeyObject) { + const db = await openKeyDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(["cryptoKeys"], "readwrite"); + const store = transaction.objectStore("cryptoKeys"); + + // We store the object under the label "masterEncryptionKey" + const request = store.put(cryptoKeyObject, "masterEncryptionKey"); + + request.onsuccess = () => resolve(true); + request.onerror = () => reject(request.error); + }); +} + +function convertBufferToHex(buffer) { + const bytes = new Uint8Array(buffer); + + return Array.from(bytes) + .map(byte => byte.toString(16).padStart(2, "0")) + .join(""); +} + +function hexToUint8Array(hexString) { + if (hexString.length % 2 !== 0) { + throw new Error("Invalid hex string"); + } + + const array = new Uint8Array(hexString.length / 2); + + for (let i = 0; i < hexString.length; i += 2) { + array[i / 2] = parseInt(hexString.substring(i, i + 2), 16); + } + + return array; +} + diff --git a/public/paydirt.html b/public/paydirt.html new file mode 100644 index 0000000..71ce6e6 --- /dev/null +++ b/public/paydirt.html @@ -0,0 +1,74 @@ + + + + Home + + + +
+
+
+
+ +

NFL Stats

+
+ Search Players +
+
+
+ Welcome, % + + +
+ +
+
+

Watched Players

+ + + + + +
+
+
+

Highest Contracts

+ + + + + +
+
+
+
+

Highest PayDirt Score

+ A player's PayDirt score is calculated by weighting their offense score by their annual salary. It is used to evaluate the worth of their contract. +
+ + + + + +
+
+
+
+

Highest Offense Score

+ A player's offense score is calculated by combining several of their offense stats into a weighted score. It is indicative of their overall performance. +
+ + + + + +
+
+
+ + + + + + + diff --git a/public/register.js b/public/register.js index f96cb2e..25408df 100644 --- a/public/register.js +++ b/public/register.js @@ -8,16 +8,27 @@ const success = document.querySelector(".success"); if (registerForm) registerForm.onsubmit = async e => { e.preventDefault(); + if (registerForm.regPass.value !== registerForm.regVerify.value) { error.style.display = "flex"; errorMessage.innerHTML = "Passwords must match"; return; } + const salt = window.crypto.getRandomValues(new Uint8Array(16)); + + + const { authenticationKeyString, encryptionKeyObject } = await generateKeys(registerForm.regPass.value, salt); + + console.log(authenticationKeyString); + console.log(encryptionKeyObject); + + let resultObject = await postData("/register", { username: registerForm.regUser.value, - password: registerForm.regPass.value, - role: registerForm.regRole.value + password: authenticationKeyString, + role: registerForm.regRole.value, + salt: convertBufferToHex(salt) }); if (resultObject.message.includes("User registered")) { error.style.display = "none"; @@ -30,3 +41,57 @@ if (registerForm) registerForm.onsubmit = async e => { window.location.href = "/login"; } }; + +async function generateKeys(password, salt) { + const encoder = new TextEncoder(); + + const base = await window.crypto.subtle.importKey( + "raw", + encoder.encode(password), + "PBKDF2", + false, + ["deriveBits", "deriveKey"] + ); + + const authSalt = new Uint8Array([...salt, ...encoder.encode("auth")]); + const authKeyBits = await window.crypto.subtle.deriveBits( + { + name: "PBKDF2", + salt: authSalt, + iterations: 100000, + hash: "SHA-256" + }, + base, + 256 + ); + + const encryptionSalt = new Uint8Array([...salt, ...encoder.encode("enc")]); + const encryptionKey = await window.crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: encryptionSalt, + iterations: 100000, + hash: "SHA-256" + }, + base, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"] + ); + + console.log(convertBufferToHex(authKeyBits)); + console.log(encryptionKey); + + return { + authenticationKeyString: convertBufferToHex(authKeyBits), + encryptionKeyObject: encryptionKey + }; +} + +function convertBufferToHex(buffer) { + const bytes = new Uint8Array(buffer); + return Array.from(bytes) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); +} + diff --git a/server.js b/server.js index 2812eb0..98bafde 100644 --- a/server.js +++ b/server.js @@ -38,23 +38,25 @@ if (!JWT_SECRET) { } app.post("/register", async (req, res) => { - const { username, password, role } = req.body; + const { username, password, role, salt } = req.body; try { const hash = await bcrypt.hash(password, 10); const inputs = { username: username, hash: hash, - role: role + role: role, + salt: salt } - const query = db.prepare("INSERT INTO Users (Username, PasswordHash, Role, CreatedAt) VALUES (@username, @hash, @role, datetime('now'))"); + const query = db.prepare("INSERT INTO Users (Username, PasswordHash, Role, CreatedAt, Salt) VALUES (@username, @hash, @role, datetime('now'), @salt)"); query.run(inputs); res.send({ success: true, message: "User registered" }) } catch (err) { - if (err.message.includes("Violation of UNIQUE KEY constraint")) { + if (err.message.includes("UNIQUE")) { res.status(500).send({ success: false, message: `Username "${username}" is already taken.` }); + return; } res.status(500).send({ success: false, message: err.message }); } @@ -62,7 +64,7 @@ app.post("/register", async (req, res) => { app.post("/login", async (req, res) => { const { username, password } = req.body; - try { + //try { const query = db.prepare("SELECT * FROM Users WHERE Username = @username"); const results = query.all({ username: username }); @@ -82,7 +84,8 @@ app.post("/login", async (req, res) => { res.send({ success: true, message: "Login successful", - token + token, + salt: results[0].Salt }); const update = db.prepare("UPDATE Users SET LastLogin = datetime('now') WHERE Username = @username"); update.run({ username: username }); @@ -90,11 +93,97 @@ app.post("/login", async (req, res) => { console.log("Issued token: " + JSON.stringify(token)) } else res.status(401).send({ success: false, message: "Invalid credentials" }); + //} catch (err) { + // res.status(500).send({ success: false, message: err.message }); + //} +}); + +app.post("/getSalt", async (req, res) => { + const { username } = req.body; + try { + const query = db.prepare("SELECT * FROM Users WHERE Username = @username"); + const results = query.all({ username: username }); + + if (results.length == 0) return res.status(400).send({ message: "User not found" }); + + const isDeleted = results[0].IsDeleted; + + if (isDeleted === true) { + return res.status(400).json({ message: "User not found (deleted)" }) + } + + res.send({ + salt: results[0].Salt + }); } catch (err) { res.status(500).send({ success: false, message: err.message }); } }); +app.post("/setPassword", authenticate, async (req, res) => { + try { + const { name, iv, password } = req.body; + + const inputs = { + name: name, + userID: req.user.Id, + iv: iv, + password: password + } + + const query = db.prepare("INSERT INTO Passwords (Name, UserID, Password, IV) VALUES (@name, @userID, @password, @iv)"); + query.run(inputs); + + res.status(200).json({ success: true, message: "Success" }); + } catch (err) { + res.status(500).json({ success: false, message: err.message }); + } +}); + +app.post("/updatePassword", authenticate, async (req, res) => { + try { + const { name, iv, password } = req.body; + + const inputs = { + name: name, + userID: req.user.Id, + iv: iv, + password: password + } + + const query = db.prepare("UPDATE Passwords SET Password = @password, IV = @iv WHERE UserID = @userID AND Name = @name"); + query.run(inputs); + + res.status(200).json({ success: true, message: "Success" }); + } catch (err) { + res.status(500).json({ success: false, message: err.message }); + } +}); + +app.post("/getPassword", authenticate, async (req, res) => { + try { + const { name } = req.body; + + const query = db.prepare("SELECT * FROM Passwords WHERE UserID = @userID AND Name = @name"); + const results = query.all({ userID: req.user.Id, name: name}); + + if (results.length == 0) { + res.status(500).json({ success: false, message: `No password with name ${name}` }); + return; + } + + if (results.length > 1) { + res.status(500).json({ success: false, message: `More than one password with name ${name}` }); + return; + } + + res.status(200).json({ password: results[0].Password, iv: results[0].IV, success: true, message: "Success" }); + } catch (err) { + res.status(500).json({ success: false, message: err.message }); + } +}); + + app.post("/getWatchlist", authenticate, async (req, res) => { const { id } = req.body; @@ -108,319 +197,12 @@ app.post("/getWatchlist", authenticate, async (req, res) => { res.status(200).json({ watchlist: watchlist }); }); -app.post("/isWatched", authenticate, async (req, res) => { - const { id } = req.body; - - const query = db.prepare(`SELECT PlayerID FROM Watch WHERE UserID = @userID`); - const watchlist = query.all({ userID: req.user.Id, id: id}); - - const isWatched = watchlist.some(row => row.PlayerID === parseInt(id)); - - res.status(200).json({ isWatched: isWatched }); -}); - -app.post("/toggleWatched", authenticate, async (req, res) => { - const { id } = req.body; - - const query = db.prepare(`SELECT PlayerID FROM Watch WHERE UserID = @userID`); - const watchlist = query.all({ userID: req.user.Id, id: id }); - - const isWatched = watchlist.some(row => row.PlayerID === parseInt(id)); - - if (isWatched) { - const query = db.prepare(`DELETE FROM Watch WHERE UserID = @userID AND PlayerID = @id`); - query.run({ userID: req.user.Id, id: id }); - - res.status(200).json({ message: "No longer watching player" }); - return; - } - - // Otherwise, watch the player - const watchQuery = db.prepare(`INSERT INTO Watch (UserID, PlayerID) VALUES (@userID, @id)`); - watchQuery.run({ userID: req.user.Id, id: id }); - - res.status(200).json({ message: "Watching Player" }); -}); - -app.post("/getPlayers", authenticate, async (req, res) => { - const { player, positions } = req.body; - - const query = db.prepare(`SELECT p.PlayerID, p.PlayerName, c.TotalValue, p.Team, p.Position FROM Player AS p JOIN Contract AS c ON p.PlayerID = c.PlayerID WHERE p.PlayerName LIKE '%' || @query || '%' AND p.Position IN (@one, @two, @three, @four) ORDER BY p.PlayerName;`); - const matches = query.all({ - query: player, - one: positions[0], - two: positions[1], - three: positions[2], - four: positions[3] - }); - - res.status(200).json({ query: player, matches: matches }); -}); - -app.post("/getHighest", authenticate, async (req, res) => { - const { amount } = req.body; - - const query = db.prepare(` - SELECT p.PlayerID, p.PlayerName, p.[Position], p.Team, TotalValue, TrueAvgPerYear, Years - FROM Player AS p JOIN Contract AS c ON p.PlayerID = c.PlayerID - ORDER BY TotalValue DESC - LIMIT @amount; - `); - const matches = query.all({ amount: amount }); - - res.status(200).json({ matches: matches }); -}); - -app.post("/getHighestOffense", authenticate, async (req, res) => { - const { amount } = req.body; - - const query = db.prepare(` -SELECT Player.PlayerID, Player.[Position], Player.PlayerName, Player.Team, -SUM(total_yards) AS TotalYards, -SUM(passing_yards) AS PassingYards, -SUM(rushing_yards) AS RushingYards, -SUM(receiving_yards) AS RecievingYards, -CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown) AS AmendedTotalTDs, -CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END AS PassTDs, -SUM(receiving_touchdown) AS ReceivingTDs, -SUM(rush_touchdown) AS RushTDs, -SUM(offense_snaps) * 1.0 / SUM(team_offense_snaps) AS SnapPercentage, -SUM(interception) + sum(fumble_lost) AS Turnovers, -SUM(tackled_for_loss) AS TackledForLoss, - CASE - WHEN Player.Position = 'QB' - THEN SUM(qb_dropback) - SUM(pass_attempts) - SUM(qb_scramble) - ELSE 0 - END AS Sacks, -SUM(safety) AS Safties, - -SUM(total_yards) -+ ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) -+ (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) -- ((SUM(interception) + sum(fumble_lost)) * 75) -- ( - SUM(tackled_for_loss)) - -- (SUM(safety) * 100.0) -AS OffenseScore, - -AvgPerYear, -(SUM(total_yards) -+ ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) -+ (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) -- ((SUM(interception) + sum(fumble_lost)) * 75) -- ( -SUM(tackled_for_loss) -) -- (SUM(safety) * 100.0)) / AvgPerYear AS PaydirtScore - -FROM Player JOIN DatasetPlayerStats ON Player.PlayerID = DatasetPlayerStats.PlayerID JOIN Contract ON Player.PlayerID = Contract.PlayerID -WHERE season = 2024 AND SeasonType = 'REG' -GROUP BY Player.PlayerID, Player.PlayerName, Player.Team, Player.[Position], Contract.AvgPerYear -ORDER BY OffenseScore DESC -LIMIT @amount; - `); - const matches = query.all({ amount: amount }); - - res.status(200).json({ matches: matches }); -}); - - -app.post("/getPlayerStats", authenticate, async (req, res) => { - const { playerID } = req.body; - - const query = db.prepare(` -SELECT player.playerid, player.playername, season, seasontype, week, pass_attempts, complete_pass, total_yards, total_tds, interception, receptions, receiving_yards, receiving_touchdown, rush_attempts, rushing_yards, rush_touchdown, fumble -fROM Player JOIN DatasetPlayerStats ON Player.PlayerID = DatasetPlayerStats.PlayerID -WHERE Player.PlayerID = @playerID -ORDER BY Season, week; - `); - const matches = query.all({ playerID: playerID }) - - res.status(200).json({ matches: matches }); -}); - -app.post("/getPlayerScores", authenticate, async (req, res) => { - const { playerID } = req.body; - - const query = db.prepare(` -SELECT Player.PlayerID, Player.[Position], Player.PlayerName, Player.Team, -c.TotalValue, c.TrueAvgPerYear, c.Years, c.StartYear, c.EndYear, Player.Height, Player.Weight, -SUM(total_yards) AS TotalYards, -SUM(passing_yards) AS PassingYards, -SUM(rushing_yards) AS RushingYards, -SUM(receiving_yards) AS RecievingYards, -CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown) AS AmendedTotalTDs, -CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END AS PassTDs, -SUM(receiving_touchdown) AS ReceivingTDs, -SUM(rush_touchdown) AS RushTDs, -SUM(offense_snaps) * 1.0 / SUM(team_offense_snaps) AS SnapPercentage, -SUM(interception) + sum(fumble_lost) AS Turnovers, -SUM(tackled_for_loss) AS TackledForLoss, - CASE - WHEN Player.Position = 'QB' - THEN SUM(qb_dropback) - SUM(pass_attempts) - SUM(qb_scramble) - ELSE 0 - END AS Sacks, -SUM(safety) AS Safties, - -SUM(total_yards) -+ ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) -+ (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) -- ((SUM(interception) + sum(fumble_lost)) * 75) -- ( - SUM(tackled_for_loss) - ) - -- (SUM(safety) * 100.0) -AS OffenseScore, - -AvgPerYear, -(SUM(total_yards) -+ ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) -+ (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) -- ((SUM(interception) + sum(fumble_lost)) * 75) -- ( - SUM(tackled_for_loss) -) -- (SUM(safety) * 100.0)) / AvgPerYear * 1000000 AS PaydirtScore - - -FROM Player JOIN DatasetPlayerStats ON Player.PlayerID = DatasetPlayerStats.PlayerID JOIN Contract AS c ON Player.PlayerID = c.PlayerID -WHERE season = 2024 AND SeasonType = 'REG' AND Player.PlayerID = @playerID -GROUP BY c.TotalValue, c.TrueAvgPerYear, c.Years, c.StartYear, Player.Height, Player.Weight, c.EndYear, Player.PlayerID, Player.PlayerName, Player.Team, Player.[Position], c.AvgPerYear -ORDER BY PaydirtScore DESC; - `); - const matches = query.all({ playerID: playerID }) - - res.status(200).json({ match: matches[0] }); -}); - - - -app.post("/getHighestPaydirt", authenticate, async (req, res) => { - const { amount } = req.body; - - const query = db.prepare(` -SELECT Player.PlayerID, Player.[Position], Player.PlayerName, Player.Team, -SUM(total_yards) AS TotalYards, -SUM(passing_yards) AS PassingYards, -SUM(rushing_yards) AS RushingYards, -SUM(receiving_yards) AS RecievingYards, -CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown) AS AmendedTotalTDs, -CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END AS PassTDs, -SUM(receiving_touchdown) AS ReceivingTDs, -SUM(rush_touchdown) AS RushTDs, -SUM(offense_snaps) * 1.0 / SUM(team_offense_snaps) AS SnapPercentage, -SUM(interception) + sum(fumble_lost) AS Turnovers, -SUM(tackled_for_loss) AS TackledForLoss, - CASE - WHEN Player.Position = 'QB' - THEN SUM(qb_dropback) - SUM(pass_attempts) - SUM(qb_scramble) - ELSE 0 - END AS Sacks, -SUM(safety) AS Safties, - -SUM(total_yards) -+ ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) -+ (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) -- ((SUM(interception) + sum(fumble_lost)) * 75) -- ( - SUM(tackled_for_loss)) - -- (SUM(safety) * 100.0) -AS OffenseScore, - -AvgPerYear, -(SUM(total_yards) -+ ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) -+ (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) -- ((SUM(interception) + sum(fumble_lost)) * 75) -- ( -SUM(tackled_for_loss) -) - -- (SUM(safety) * 100.0)) / AvgPerYear * 1000000 AS PaydirtScore - - -FROM Player JOIN DatasetPlayerStats ON Player.PlayerID = DatasetPlayerStats.PlayerID JOIN Contract ON Player.PlayerID = Contract.PlayerID -WHERE season = 2024 AND SeasonType = 'REG' -GROUP BY Player.PlayerID, Player.PlayerName, Player.Team, Player.[Position], Contract.AvgPerYear -ORDER BY PaydirtScore DESC -LIMIT @amount; - `); - const matches = query.all({ amount: amount }); - - res.status(200).json({ matches: matches }); -}); - -app.post("/getPlayer", authenticate, async (req, res) => { - const { id } = req.body; - - const query = db.prepare(`SELECT p.PlayerName, p.PlayerID, c.TotalValue, c.TrueAvgPerYear, c.Years, c.StartYear, c.EndYear, p.Team, p.Position FROM Player AS p JOIN Contract AS c ON p.PlayerID = c.PlayerID WHERE p.PlayerID = @query`); - const matches = query.run({ query: id }); - - if (matches.length !== 1) { - res.status(400).json({ success: false }) - return; - } - res.status(200).json({ success: true, match: matches[0] }); -}); - app.post("/getInfo", authenticate, async (req, res) => { const userData = req.user; res.status(200).json(userData); }); -app.post("/getCourses", authenticate, async (req, res) => { - const query = db.prepare("SELECT * FROM Courses"); - const courses = query.all(); - - res.status(200).json(courses); -}); - -app.post("/setInfo", authenticate, async (req, res) => { - const { firstName, lastName, dob } = req.body; - - try { - const query = db.prepare(` - UPDATE Users - SET FirstName = @firstName, - LastName = @lastName, - DOB = @dob - WHERE Username = @username - `); - query.run({ - username: req.user.Username, - firstName: firstName, - lastName: lastName, - dob: dob || null - }); - } catch (error) { - console.log(error); - if (error.message.includes("failed for parameter 'dob'.")) { - res.status(500).json({ message: "Must input date of birth" }) - return; - } - res.status(500).json({ message: "Update request failed" }) - return; - } - - var updatedUser = req.user; - updatedUser.FirstName = firstName; - updatedUser.LastName = lastName; - updatedUser.DOB = dob; - - const token = jwt.sign(updatedUser, JWT_SECRET); - console.log("Issued token: " + JSON.stringify(token)) - res.status(200).send({ - success: true, - message: "Information updated successfully", - token - }); -}); - app.post("/delete", authenticate, async (req, res) => { let { username, actor } = req.body;