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:
Rolf
2026-03-23 20:37:08 +01:00
parent c658c970ac
commit 8601584099
10 changed files with 335 additions and 123 deletions
+25 -3
View File
@@ -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(