summaryrefslogtreecommitdiff
path: root/routers/auth.py
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-06 15:25:36 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-06 15:25:36 -0700
commit1bdf4ca8c0f51718124ffe5247ac133973d4f251 (patch)
tree2904145bdbc5d2a2164cd3cb6c95346e48afd4a5 /routers/auth.py
parent051775337251b0c7036959901eacb58471100862 (diff)
Add authentication, public profile, and infinite scroll
- Implement session-based auth with argon2 password hashing - Add login form and logout button in nav - Create public /tyler profile page with curated stats - Implement infinite scroll for film lists (load 20 at a time) - Add lazy loading for poster images - Fix profile page CSS to use dark theme variables - Use consistent star character (✦) across all pages - Add /films/partial endpoint for pagination Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Diffstat (limited to 'routers/auth.py')
-rw-r--r--routers/auth.py58
1 files changed, 58 insertions, 0 deletions
diff --git a/routers/auth.py b/routers/auth.py
new file mode 100644
index 0000000..d3fb963
--- /dev/null
+++ b/routers/auth.py
@@ -0,0 +1,58 @@
+import os
+from fastapi import APIRouter, Request
+from fastapi.responses import RedirectResponse
+from fastapi.templating import Jinja2Templates
+from argon2 import PasswordHasher
+from argon2.exceptions import VerifyMismatchError
+
+templates = Jinja2Templates(directory="templates")
+
+router = APIRouter()
+
+OWNER_PASSWORD_HASH = os.getenv("OWNER_PASSWORD_HASH", "")
+ph = PasswordHasher()
+
+
+@router.get("/login")
+async def login_form(request: Request):
+ if request.session.get("authenticated"):
+ return RedirectResponse("/", status_code=303)
+ return templates.TemplateResponse(request=request, name="login.html", context={"request": request})
+
+
+@router.post("/login")
+async def login(request: Request):
+ if request.session.get("authenticated"):
+ return RedirectResponse("/", status_code=303)
+
+ form = await request.form()
+ password = form.get("password", "")
+
+ if not OWNER_PASSWORD_HASH:
+ error = "Server not configured: OWNER_PASSWORD_HASH not set"
+ return templates.TemplateResponse(
+ request=request,
+ name="login.html",
+ context={"request": request, "error": error},
+ status_code=500
+ )
+
+ try:
+ ph.verify(OWNER_PASSWORD_HASH, password)
+ request.session["authenticated"] = True
+ return RedirectResponse("/", status_code=303)
+ except VerifyMismatchError:
+ pass
+
+ return templates.TemplateResponse(
+ request=request,
+ name="login.html",
+ context={"request": request, "error": "Invalid password"},
+ status_code=401
+ )
+
+
+@router.post("/logout")
+async def logout(request: Request):
+ request.session.clear()
+ return RedirectResponse("/login", status_code=303)