From 2729ba49f2f1f75b97307510691a9394b4731198 Mon Sep 17 00:00:00 2001 From: Alex Muszynski Date: Sat, 14 Jun 2025 10:02:02 -0400 Subject: [PATCH] feat: front end for login --- .gitignore | 27 ++ backend/alembic/README | 1 - backend/alembic/README.md | 25 + .../a3ac646e53a8_init.cpython-313.pyc | Bin 2215 -> 0 bytes .../alembic/versions/b9dcd098debd_1_1_0.py | 58 +++ backend/app/__pycache__/crud.cpython-313.pyc | Bin 2030 -> 0 bytes .../app/__pycache__/database.cpython-313.pyc | Bin 552 -> 0 bytes backend/app/__pycache__/main.cpython-313.pyc | Bin 2659 -> 0 bytes .../app/__pycache__/models.cpython-313.pyc | Bin 714 -> 0 bytes .../app/__pycache__/schemas.cpython-313.pyc | Bin 929 -> 0 bytes backend/app/crud.py | 10 +- backend/app/main.py | 10 + backend/app/schemas.py | 4 +- backend/app/utils.py | 6 +- backend/pyproject.toml | 1 - frontend/package-lock.json | 17 + frontend/package.json | 1 + frontend/src/App.tsx | 439 +++++++++++++++--- 18 files changed, 531 insertions(+), 68 deletions(-) delete mode 100644 backend/alembic/README create mode 100644 backend/alembic/README.md delete mode 100644 backend/alembic/versions/__pycache__/a3ac646e53a8_init.cpython-313.pyc create mode 100644 backend/alembic/versions/b9dcd098debd_1_1_0.py delete mode 100644 backend/app/__pycache__/crud.cpython-313.pyc delete mode 100644 backend/app/__pycache__/database.cpython-313.pyc delete mode 100644 backend/app/__pycache__/main.cpython-313.pyc delete mode 100644 backend/app/__pycache__/models.cpython-313.pyc delete mode 100644 backend/app/__pycache__/schemas.cpython-313.pyc diff --git a/.gitignore b/.gitignore index bf9a613..620c125 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,28 @@ /backups + +# Python +/__pycache__/* +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/backend/alembic/README b/backend/alembic/README deleted file mode 100644 index 98e4f9c..0000000 --- a/backend/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/README.md b/backend/alembic/README.md new file mode 100644 index 0000000..8498672 --- /dev/null +++ b/backend/alembic/README.md @@ -0,0 +1,25 @@ +# Alembic + +Alembic allows for our database migrations to be tracked in a version control system. + +To create a new migration run: + +```bash +alembic revision --autogenerate -m 'Describe change here' +``` + +It's best practice to review the script post revision creation: `alembic/versions` + +To apply the migration: + +```bash +alembic upgrade head +``` + +Now you can re-check using `alembic check` + +If we need to rollback use: + +```bash +alembic downgrade -i +``` diff --git a/backend/alembic/versions/__pycache__/a3ac646e53a8_init.cpython-313.pyc b/backend/alembic/versions/__pycache__/a3ac646e53a8_init.cpython-313.pyc deleted file mode 100644 index cebecc62508b6131e79d399c79f4412a4789731a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2215 zcmb_dL2MI86rEj<*Y?IvNSc5|RIoxw>Q;%tiBn=BRGOqA37EB#|6CGB$K%G@3Y?8W~F^62pmOBgYaWvivI?5XD3mRLHdwv2uj@&szW< z6PQ>irdikBTxw2NAf#2w9C zw1;en9G};6i|{aU%_RAGBNx9;C@&>D?tNwmX*k2hG7FTpuR`}iupapA6EH2KEeZJp zAD+2==5F8p{?-1fky~{e&ft`h$AcV0C&OA8)8DgWu!*v>PCH6h#;mZcz-t zll^^BD8i*yF=SryBp#0-pzj)eDA~{`J_RfOaqRJbF{0%0a2v}`NDm?g%zuIy_z#FY zW4i_~L=THd*Dxq;P$!iS84$|z|)_Lb0G<-3x+`<(UDK0=x+s5(#fE)JU ziqCNuBC+aQUWNe_8msD3k%_j(q=|xA%3CZj zlRh^!J(XdB3nw!Zr%q;A_q)`{YqUH^$`imdhiZo9&`y5RCX7pP`U;QAxj{M6Scvym z_j+^y)(7CTzk#VqzO%Q_R;D?xW^T`{=y%_~Kf5}6=X4c6F4gcx1JpoZ1Tc{nA#L zvmF9Ll*!e}2M6ojv9&j=7pkY8oL|RtJlJ#NtM%@|wSCpm>cEqpI-YiK1g{}c;*_YB zj_qLqRn-eQRb`S>E*fx0&V9-$%>s?K)M$V9HB&Qr6D`vy_X%(xALNjAsH$dJ1xMr0 zXR9jZET%FWyEv<8po3wp^UC+INv@t=on!}QE22*PioB;Y^Vk#Oi2wD(7} z?^zIa9$XHt9IB%O8+`-Ik(E+KdC*^Zuhwy}js`Z4CL5~P9*ul5wATH!XQm5 diff --git a/backend/alembic/versions/b9dcd098debd_1_1_0.py b/backend/alembic/versions/b9dcd098debd_1_1_0.py new file mode 100644 index 0000000..e42395e --- /dev/null +++ b/backend/alembic/versions/b9dcd098debd_1_1_0.py @@ -0,0 +1,58 @@ +"""1.1.0 + +Revision ID: b9dcd098debd +Revises: a3ac646e53a8 +Create Date: 2025-06-14 09:22:14.878105 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'b9dcd098debd' +down_revision: Union[str, None] = 'a3ac646e53a8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_index(op.f('ix_users_id'), table_name='users') + op.drop_index(op.f('ix_users_username'), table_name='users') + op.drop_table('users') + op.drop_index(op.f('ix_items_id'), table_name='items') + op.drop_index(op.f('ix_items_name'), table_name='items') + op.drop_table('items') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('items', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('body', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('items_pkey')) + ) + op.create_index(op.f('ix_items_name'), 'items', ['name'], unique=False) + op.create_index(op.f('ix_items_id'), 'items', ['id'], unique=False) + op.create_table('users', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('username', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('hashed_password', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('permissions', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), + sa.Column('subscriber', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('users_pkey')) + ) + op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True) + op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + # ### end Alembic commands ### diff --git a/backend/app/__pycache__/crud.cpython-313.pyc b/backend/app/__pycache__/crud.cpython-313.pyc deleted file mode 100644 index 79c89c30d84abdda5d430cdb1c4ae859620ddb7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2030 zcmcIl&1)M+6rWk`hop&QC2nyE4c(>&XAf};&KJSBc5xhA0-H+c6oXN;t9WC{Qf6g} zZ!#E~lz?5RX?lutY7K#yp87ZR6rqB}kT!>$a#L`8&wI04*-{*z(g*wI&71dTXMex< zX7^*UC;|EVV0`hZLdYwe1Y)#}{dF+5NQPu+iX+q)(sMG6wkdOc?!lDL14)GOwl+!HW(@$dAhW$dMhH^hEERvf{5X)S|-3 zIG*R0mn}u;rLyf5Jz;qHMW>YWgks+ohFf(?3$C4qZ|aB&RtTQ`T~J#jOVYHn)-i5% zGSV2|BbVD5=>VG}>nCTX!$(5fW|m%`CZw|`dq3iG+cijb=&Yp+?cR#RS2>6li+CnS1+^bp&*I}LG5k34G(-euXbCrsG@_fY}uB?ji9jCg0C~>4g!`LgJ zYNW|x8`mCO`(^6a)X%8~Gxyj~oee$JpRpbGEWQ)}Yp8x>}5X!MJM&Wsq3P><%K0RqTTtl|8&*uaqi)v26=IUoJrv z34=QY?s$s<=$}D~Fznm1a=U?NdG6$@j$hLz#Kqw8z6DhyFXEqUEd8`pj}JeZd_4Et zT;ubLVApOoRsF}QwW<48*XPijS({l;p^;om-v8krb)d=4)Mh0MUHId{CH^X02Oqzl z|IdpASXM5;@lkSL5@Zx{>X9zSNE!>fM~-j~jQYJ3?&%$V3X(W(&*Pwk4)?SJchxzV z1cYM2ab&fJOh42o^D9{X6hpoT^`SmG;ex77{%5deIJyye5ZN4j*w;26vL@@Rr6j$x zaA{)iyvFXgn23A#in${GP*#V_yu^`hsLV>mFIsFR5ptKS^7538E(@8Pu)y)W{46T$ zU;kPNk3xyB6rC}S=;L3+b3wI2l+u66`IqF2mQHDkZVt2vj@u(|<@hE{=>**zLhl6K r?)S&t{sTF0g*Dp0d9y`e*uMJv=#$a6Xtoj>P5AD_cK!fd33-%v!s0|( diff --git a/backend/app/__pycache__/database.cpython-313.pyc b/backend/app/__pycache__/database.cpython-313.pyc deleted file mode 100644 index 58007e11adddb8b2219559edd85bd9dca64fca3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 552 zcmYjN&r2IY6rS1HAKA^uR1vilM1nmm6nn}+kYc2jl9e7@+tWfAcc;nH-JSS$BLsSg z^wx{LwDjoF{s$iWFO*z#iWKRow?h5_XG}_cFf-r#=9~AvH2M8(>IY_szBi)*1?}25QbqKqWNx7iP)5 zqk#e=Fq_(x0w=@`6go}mLS7j7{1y~iV(3Vla<{e~t|fyCld+V9`3n3hycvC$uj%2vkX zLCT)NiWI4@D_+X&FTogJqxD~C^Cz;e(dvyk{~;TZ%liE3@Z<37>iNRB{$%9d*h`<@ hkL~r5c~yUOHn=dp@BXO!BlpT$9$ODjbXbzbZUOf^kxKvo diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc deleted file mode 100644 index 50919a13b879cb37e45bf7ebfe580faaf9f39bd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2659 zcmcgu&2JM&6rb5$dmXRsm;@5Xp}{2N!wDq;l0p!m77;$2C`DO9D`Cmn*qda*U!C2w zkW$rY4^XN$v?^+jsd{K44j@&PTJ_RD!6^sAhzhFql1pWp9(w4Z@2%~axS*mQy3)+N zdGp?H-q$?sy4UL_@Ll@t<>^KTA-~|J{JBcR`ZmROcWC4cbZu zN=#O(%0a^X(b}=LvRq|VT?4eZQ)_}1ojQ>2tVoq9)g6_`y2`uCsw#s*RA?npe;KWx zs-9=adZUhhN%aNE1?d9auyWfo>~lvOo{j^7CwMPJv$pDPQT0a~)h#gaE~i>obywgS zchx`rrl)$hwS+FI+p5r;0KM#1B}7F1Z|DtG=*?4&;lLW=hD9!)&`j%XEO=F@RS5bGI1@*oqBr8 zNb6iWrkOgZOA|&q5r4uBBEbd=Y2C7k5ES|OQVEfb0!ot>GEUwkH)#mgF%d+ozH)>@ zWQy$kSD8t2MBG6p=>c()F4NJl!0-ifM?9Ia%@h(7CK?gq{$4GcwMR-b(tEO(xpPXl zVu=Y>59b~jCKm7-$&m*&+dgc`9V-Z;qkh%rzwUbEa?iUyaDC+d^O{g3{xY|M@O#QZ z`mxkTKHYMVepxplej^^Fv)*0^Wc=cb)4G2H)dbL0>DB`Odm`+^2oC_-_Vg)I`dK|YXsJ; z+-q2R%It-d`Iv5IGig(gr7{UU$t65tTRE$2ulol| z{9T0A0vmEr0}EH;OxG+uhPMC@JC%KziQ=JH9{rZxf}dT6eU?J9H4B)h54h`k1n(>4&7C{^HO&)8U9i--bVPCk$8v8C2CU! zHhdWY#OLs3BLit+T!=%Q&Le{TVTq~YprF%nVHa7JMvc$lIn4S_g8`v*#tKblX3`0G z{)%a7){Gg8LuqhlLbo&{3I5xebd@8*5Er;@u-F0Na$=$wflZOA*@z7522m8Z9~b1- zS2v%!o`TH!x^gF1W#8Pp4}8r_V!r?Qif<_I8d_EAa-(*%{-2Pr_M%J0l7(lXmrC{@ zzS0{}H9MBs5E#fOc&6Jf`3Q_A%~CLHh8JyGw6SOD4Gp{m<@o zT!ehuor6o+jA?NRtHFMSTuK_i5RJ5Dm$aQeEC&or(Oqmuw(Z4swFY8NA=U_fE$xFhrvDH_QSqi(mvS zsCuX%09Ejz3&FWy-qW)5*6l0HS8~BTJ$!%n{yQgby^5;-g?d!=FLt7;A61V$235Zu G^uGbH?fe-4 diff --git a/backend/app/__pycache__/models.cpython-313.pyc b/backend/app/__pycache__/models.cpython-313.pyc deleted file mode 100644 index 41f01e8c0e36387522a68ce04967367e79e658e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 714 zcmYLH&x_MQ6rLoLG;OxKT6SgW3NFj~BNz8l7Yee-x(J(M1!s?iA=`}IV3KxUQm`ir zf_U7^g2x{9=D$+BxC0{MsRu!*-hGp_I$>VE@4b01-+PmJwOS##UjKY_wyzQLLy_e& z7hryX;F2)Hv=;F+&^#S<&j7=tK(`6gnb9&m3#@IjNp=XMb;8V!^}s0dq13b7(o7MT zO6sv^ZD~F3N5eRgrQ;+MJps~U8H7o1h8l>gNqXp~qFdw_)>@GHa|D;9LqKB$bf$s9 zbf6t`hjc7v;6s}RlyYt(Gn+b;|1k?$C1(yRF~wGx4b@)Rtw?&DiFl@(mS%`BJ#lsE z3?Phs81a{4gzh8|uUrjQ@er!ClVKG3r;&K_k+j`aX>*?VvEW=*IFI|mFj9Dh^Yft} zEh1~2zX&1CqA(FjAH!A7GgYIA1I5yii_pUWRM(QKkaPl(b|D;OVLt&iT1ij)!ANd! zo@Ra)cDbM73{QudNI8dVcw&y5$l9Ji7~~rzi73547=gO<+^8?5O*C)Gm-Vgj)9>ZA zi_+xe)B3c$KRu3y}l(5Z8Kd~{XYe1GG^?zFZyZYlrnWMk^weRBj`%UW&MmX-88 z@}usVh)21||NRub*nyAT;^l(9Ov|MfRc@kB+7FA-9l$N5>W4_{Xnq-*rhOxg&!q8{ R)PGx+cJE!|55ZTS`xmQ+v*iE) diff --git a/backend/app/__pycache__/schemas.cpython-313.pyc b/backend/app/__pycache__/schemas.cpython-313.pyc deleted file mode 100644 index 6127c00b693fa12bb422ebf0a89b5778d711fc79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 929 zcmZ`%&ubGw6n?WiyIJ$2QA42yNlHLu53Lsq9!e2CNG;~F9s|QR*`y1}ZhX5r%-D2QT`%_K!ltRF>j>T|?IDRem7rcw!0edVVqhR3rsV2!Js(FeNm$e&L0FX^8I z6F=FV&XG%<1K&7l61;;&xpsQEXm#EmT{JGz56#cAL)6(J-n}wkt0AA{kv%5(e+}~< znbae>XXJiB+q0;0)Fmysa!bCeHp0QR@S-q7w)IbV&p>JlS|jONy22rLpU2TK94%|) zeVzY%*R`=iJFGG=j~fKKKE!zPN2%}*OMHXkHydoSi*R_4HheUp{=*q8nyvTs!gimC zbsp!&JZ2cBc$;0h^@c3C$tnY9PuM{{a4M%SeoA4rS3poCV^7zW+L_J=enhQCe!oQz v^B{j+-`OwT)d9B2$AwI`3BH>`h%eAu*{0YypRa(XRaJ`i`I8?&Q=aS>xIe#= diff --git a/backend/app/crud.py b/backend/app/crud.py index 5f19cf7..45b8ba8 100644 --- a/backend/app/crud.py +++ b/backend/app/crud.py @@ -31,10 +31,10 @@ def delete_item(db: Session, item_id: int): def authenticate_user(db: Session, username: str, password: str): - user = get_user_by_username(db, username) + user = get_user_by_username(db, username) or get_user_by_email(db, username) if not user: return None - if not verify_password(password, str(user.hashed_password)): + if not verify_password(password, user.hashed_password): return None return user @@ -50,7 +50,11 @@ def get_user_by_email(db: Session, email: str): def create_user(db: Session, user: schemas.UserCreate): hashed_pw = hash_password(user.password) db_user = models.User( - username=user.username, email=user.email, hashed_password=hashed_pw + username=user.username, + email=user.email, + hashed_password=hashed_pw, + permissions=user.permissions, + subscriber=user.subscriber, ) db.add(db_user) db.commit() diff --git a/backend/app/main.py b/backend/app/main.py index 687bda0..a50819a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI, Depends, HTTPException, status +from fastapi.middleware.cors import CORSMiddleware from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session @@ -10,6 +11,14 @@ Base.metadata.create_all(bind=engine) app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000", "https://localhost:3000"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + # Dependency def get_db(): @@ -81,4 +90,5 @@ def register_user(user: schemas.UserCreate, db: Session = Depends(get_db)): status_code=status.HTTP_400_BAD_REQUEST, detail="Account with that email already registered", ) + # Default Cases return crud.create_user(db, user) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 6d46a78..1c0eaa1 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -24,13 +24,15 @@ class UserBase(BaseModel): class UserCreate(UserBase): password: str + permissions: dict = {} + subscriber: bool = False class UserOut(UserBase): id: int class Config: - orm_mode = True + from_attributes = True # Other Schemas diff --git a/backend/app/utils.py b/backend/app/utils.py index 07249e0..a107133 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -1,4 +1,3 @@ -import logging import os from typing import Any, Mapping from passlib.context import CryptContext @@ -6,7 +5,8 @@ from datetime import UTC, datetime, timedelta from jose import JWTError, jwt from app.logger_config import Logger -pwd_context = CryptContext(schemas=["bcrypt"], deprecated="auto") +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +_logger = Logger().logger def hash_password(password: str) -> str: @@ -37,5 +37,5 @@ def decode_access_token(token: str) -> Mapping[Any, Any] | None: try: return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) except JWTError: - logging.exception(msg="Failed to Decode JWT", extra={"TOKEN": token}) + _logger.exception(msg="Failed to Decode JWT", extra={"TOKEN": token}) return None diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 5654703..362f2fa 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -10,7 +10,6 @@ dependencies = [ "fastapi[standard]>=0.115.12", "passlib[bcrypt]>=1.7.4", "psycopg2-binary>=2.9.10", - "pyrefly>=0.18.1", "python-jose[cryptography]>=3.5.0", "sqlalchemy>=2.0.41", "uvicorn[standard]>=0.34.2", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 60c40ce..54ec7b5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "fs": "^0.0.1-security", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-hook-form": "^7.57.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.6.0" }, @@ -4392,6 +4393,22 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz", + "integrity": "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-markdown": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a0f28e4..ac62763 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "fs": "^0.0.1-security", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-hook-form": "^7.57.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.6.0" }, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index df1b5cc..34b6ab1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,78 +3,191 @@ import { Routes, Route, Link, - useParams, + useNavigate, } from "react-router-dom"; -import { useState, useEffect } from "react"; -import ReactMarkdown from "react-markdown"; +import { useState, useContext, useEffect, createContext, useRef } from "react"; +import { useForm } from "react-hook-form"; import { BlogViewer } from "./utils/BlogViewer"; import { BlogList } from "./utils/BlogList"; import { RequireAdmin } from "./utils/RouteGuard"; import { AdminPage } from "./utils/AdminPage"; import Unauthorized from "./utils/UnauthorizedPage"; +// Use Vite environment variable for API base URL +const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000"; + +// Auth Context +interface AuthContextProps { + isAuthenticated: boolean; + login: (token: string) => void; + logout: () => void; +} +const AuthContext = createContext({ + isAuthenticated: false, + login: () => {}, + logout: () => {}, +}); +export const useAuth = () => useContext(AuthContext); + +const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const navigate = useNavigate(); + + useEffect(() => { + const token = localStorage.getItem("token"); + setIsAuthenticated(!!token); + }, []); + + const login = (token: string) => { + localStorage.setItem("token", token); + setIsAuthenticated(true); + }; + + const logout = () => { + localStorage.removeItem("token"); + setIsAuthenticated(false); + navigate("/signin"); + }; + + return ( + + {children} + + ); +}; + function App() { const [darkMode, setDarkMode] = useState(true); return ( -
- -
- setDarkMode(!darkMode)} /> -
- - } /> - } /> - } /> - } /> - } /> - - - - } - /> - } /> - + + +
+
+ setDarkMode(!darkMode)} /> +
+ + } /> + } /> + } /> + } /> + } /> + + + + } + /> + } /> + } /> + } /> + } /> + +
-
-
+ + ); } function AppBar({ toggleDarkMode }: { toggleDarkMode: () => void }) { + const { isAuthenticated, logout } = useAuth(); + const [menuOpen, setMenuOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + menuOpen && + menuRef.current && + !menuRef.current.contains(event.target as Node) + ) { + setMenuOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [menuOpen]); + return ( -