Files
SkinsPins/app/routers/auth.py
T
Rolf c51c872c33 Add Skins format, GolfCourseAPI import, and UX improvements
- Add Skins game format with optional carryover, mirroring BBB scoring pattern
- Add GolfCourseAPI integration for course search and import (httpx)
- Unwrap {"course": {}} API response envelope on import
- Add searchable course combobox and player filter to new game form
- Redirect to summary page after finishing a game
- Fix auth to allow registered users without a pending invitation
- Fix hardcoded BBB format in create_game; store carryover flag in DB
- Add game summary/scorecard view for completed games
- Add live games social stream to dashboard
- Add course name + subtitle to recent game cards
- Reorder nav: Home → Games → Admin → Profile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 09:18:04 +01:00

109 lines
3.5 KiB
Python

import os
import secrets
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
import aiosqlite
from app.database import get_db
from app.auth import create_magic_token, verify_magic_token, create_session, delete_session
from app.templates_config import templates
router = APIRouter()
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "")
SESSION_COOKIE = "session"
SESSION_TTL_DAYS = 30
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
if request.cookies.get(SESSION_COOKIE):
return RedirectResponse("/", status_code=302)
return templates.TemplateResponse("auth/login.html", {"request": request})
@router.post("/login", response_class=HTMLResponse)
async def login_submit(
request: Request,
email: str = Form(...),
db: aiosqlite.Connection = Depends(get_db),
):
email = email.strip().lower()
# Check email is admin, already registered, or has a pending invitation
if email != ADMIN_EMAIL:
async with db.execute("SELECT id FROM users WHERE email = ?", (email,)) as cursor:
existing_user = await cursor.fetchone()
if existing_user is None:
async with db.execute(
"SELECT id FROM invitations WHERE email = ?", (email,)
) as cursor:
invitation = await cursor.fetchone()
if invitation is None:
return templates.TemplateResponse(
"auth/login.html",
{"request": request, "error": "This email address has not been invited."},
)
await create_magic_token(email, db)
return templates.TemplateResponse("auth/check_email.html", {"request": request, "email": email})
@router.get("/auth/verify", response_class=HTMLResponse)
async def auth_verify(
request: Request,
token: str,
db: aiosqlite.Connection = Depends(get_db),
):
email = await verify_magic_token(token, db)
if email is None:
return templates.TemplateResponse(
"auth/login.html",
{"request": request, "error": "This link is invalid or has expired. Please request a new one."},
)
# Get or create user
async with db.execute("SELECT id, is_onboarded FROM users WHERE email = ?", (email,)) as cursor:
user = await cursor.fetchone()
if user is None:
user_id = secrets.token_urlsafe(16)
now = datetime.now(timezone.utc).isoformat()
await db.execute(
"INSERT INTO users (id, email, created_at) VALUES (?, ?, ?)",
(user_id, email, now),
)
await db.execute("DELETE FROM invitations WHERE email = ?", (email,))
await db.commit()
is_onboarded = False
else:
user_id = user["id"]
is_onboarded = bool(user["is_onboarded"])
session_token = await create_session(user_id, db)
redirect_to = "/" if is_onboarded else "/onboarding"
response = RedirectResponse(redirect_to, status_code=302)
response.set_cookie(
SESSION_COOKIE,
session_token,
max_age=SESSION_TTL_DAYS * 86400,
httponly=True,
samesite="lax",
)
return response
@router.get("/logout")
async def logout(request: Request, db: aiosqlite.Connection = Depends(get_db)):
token = request.cookies.get(SESSION_COOKIE)
if token:
await delete_session(token, db)
response = RedirectResponse("/login", status_code=302)
response.delete_cookie(SESSION_COOKIE)
return response