Add playing handicap, 6-digit login code, and game UX improvements
- Playing handicap: computed at game creation (WHS formula), stored on game_players alongside per-hole strokes allocation; displayed on game summary with HC card and / markers on scorecard (only when SI defined); live game shows +N per player card when SI exists, HC total otherwise; new game confirmation previews computed HC per player - Login: 6-digit code sent alongside magic link in every auth email; check_email page has auto-advancing digit inputs with paste support; new POST /auth/verify-code route handles code verification - Game view: Score/Card toggle fixed with position:fixed to always show above nav bar on mobile; swipe on score view changes holes, swipe on scorecard switches tabs; dirty tracking prevents saving unmodified holes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+49
-1
@@ -7,7 +7,7 @@ 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.auth import create_magic_token, verify_magic_token, verify_magic_code, create_session, delete_session
|
||||
from app.templates_config import templates
|
||||
|
||||
router = APIRouter()
|
||||
@@ -52,6 +52,54 @@ async def login_submit(
|
||||
return templates.TemplateResponse("auth/check_email.html", {"request": request, "email": email})
|
||||
|
||||
|
||||
@router.post("/auth/verify-code", response_class=HTMLResponse)
|
||||
async def verify_code(
|
||||
request: Request,
|
||||
email: str = Form(...),
|
||||
code: str = Form(...),
|
||||
db: aiosqlite.Connection = Depends(get_db),
|
||||
):
|
||||
code = code.strip()
|
||||
verified_email = await verify_magic_code(email, code, db)
|
||||
|
||||
if verified_email is None:
|
||||
return templates.TemplateResponse(
|
||||
"auth/check_email.html",
|
||||
{"request": request, "email": email, "error": "Invalid or expired code. Try again."},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
async with db.execute("SELECT id, is_onboarded FROM users WHERE email = ?", (verified_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, verified_email, now),
|
||||
)
|
||||
await db.execute("DELETE FROM invitations WHERE email = ?", (verified_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("/auth/verify", response_class=HTMLResponse)
|
||||
async def auth_verify(
|
||||
request: Request,
|
||||
|
||||
Reference in New Issue
Block a user