Fix V2 race conditions, auth gaps, serialization bugs, and async stats

Phase 1 - Critical Fixes:
- Add game_lock (asyncio.Lock) to Room class for serializing mutations
- Wrap all game action handlers in lock to prevent race conditions
- Split Card.to_dict into to_dict (full data) and to_client_dict (hidden)
- Fix CardState.from_dict to handle missing rank/suit gracefully
- Fix GameOptions reconstruction in recovery_service (dict -> object)
- Extend state cache TTL from 4h to 24h, add touch_game method

Phase 2 - Security:
- Add optional WebSocket authentication via token query param
- Use authenticated user ID/name when available
- Add auth support to spectator WebSocket endpoint

Phase 3 - Performance:
- Make stats processing async (fire-and-forget) to avoid blocking
  game completion notifications

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-27 16:27:30 -05:00
parent 1dbfb3f14b
commit f27020f21b
8 changed files with 446 additions and 231 deletions

View File

@@ -420,9 +420,19 @@ async def spectate_game(websocket: WebSocket, room_code: str):
WebSocket endpoint for spectating live games.
Spectators receive real-time game state updates but cannot interact.
Supports optional authentication via token query parameter.
"""
await websocket.accept()
# Optional authentication for spectators
token = websocket.query_params.get("token")
spectator_user = None
if token and _auth_service:
try:
spectator_user = await _auth_service.get_user_from_token(token)
except Exception:
pass # Anonymous spectator
if not _spectator_manager or not _room_manager:
await websocket.close(code=4003, reason="Spectator service unavailable")
return
@@ -449,6 +459,7 @@ async def spectate_game(websocket: WebSocket, room_code: str):
"game_state": game_state,
"spectator_count": _spectator_manager.get_spectator_count(game_id),
"players": room.player_list(),
"authenticated": spectator_user is not None,
})
# Keep connection alive