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

@@ -140,17 +140,36 @@ class Card:
"""
Convert card to dictionary for JSON serialization.
Always includes full card data (rank/suit/face_up) for server-side
caching and event sourcing. Use to_client_dict() for client views
that should hide face-down cards.
Args:
reveal: If True, show card details even if face-down.
reveal: Ignored for backwards compatibility. Use to_client_dict() instead.
Returns:
Dict with full card info (suit, rank, face_up).
"""
return {
"suit": self.suit.value,
"rank": self.rank.value,
"face_up": self.face_up,
}
def to_client_dict(self) -> dict:
"""
Convert card to dictionary for client display.
Hides card details if face-down to prevent cheating.
Returns:
Dict with card info, or just {face_up: False} if hidden.
"""
if self.face_up or reveal:
if self.face_up:
return {
"suit": self.suit.value,
"rank": self.rank.value,
"face_up": self.face_up,
"face_up": True,
}
return {"face_up": False}
@@ -390,7 +409,9 @@ class Player:
Returns:
List of card dictionaries.
"""
return [card.to_dict(reveal) for card in self.cards]
if reveal:
return [card.to_dict() for card in self.cards]
return [card.to_client_dict() for card in self.cards]
class GamePhase(Enum):