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:
+25
-3
@@ -37,19 +37,41 @@ async def create_magic_token(email: str, db: aiosqlite.Connection) -> str:
|
||||
|
||||
token_id = secrets.token_urlsafe(16)
|
||||
token = secrets.token_urlsafe(32)
|
||||
code = f"{secrets.randbelow(1000000):06d}"
|
||||
expires_at = _in(minutes=TOKEN_TTL_MINUTES)
|
||||
|
||||
await db.execute(
|
||||
"INSERT INTO magic_link_tokens (id, email, token, expires_at) VALUES (?, ?, ?, ?)",
|
||||
(token_id, email, token, expires_at),
|
||||
"INSERT INTO magic_link_tokens (id, email, token, code, expires_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(token_id, email, token, code, expires_at),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
link = f"{SITE_URL}/auth/verify?token={token}"
|
||||
await send_magic_link(email, link)
|
||||
await send_magic_link(email, link, code)
|
||||
return token
|
||||
|
||||
|
||||
async def verify_magic_code(email: str, code: str, db: aiosqlite.Connection):
|
||||
"""Returns the email if code matches a valid unused token, else None."""
|
||||
async with db.execute(
|
||||
"SELECT expires_at, used_at FROM magic_link_tokens WHERE email = ? AND code = ? AND used_at IS NULL",
|
||||
(email, code),
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
if _expired(row["expires_at"]):
|
||||
return None
|
||||
|
||||
await db.execute(
|
||||
"UPDATE magic_link_tokens SET used_at = ? WHERE email = ? AND code = ?",
|
||||
(_now(), email, code),
|
||||
)
|
||||
await db.commit()
|
||||
return email
|
||||
|
||||
|
||||
async def verify_magic_token(token: str, db: aiosqlite.Connection):
|
||||
"""Returns the email if token is valid and unused, else None."""
|
||||
async with db.execute(
|
||||
|
||||
Reference in New Issue
Block a user