Add persistence layer: TOTP auth, game history, restore

- db/ package: async SQLAlchemy engine + Player/Game/Guess models
- api/auth.py: passwordless TOTP login (pyotp), session token via socket auth
- api/history.py: record guesses/points, DB-backed standings, restore
  unfinished games on startup, host-only end_game
- api/__init__.py: auth-gated handlers, accounts map, rejoin via account
- frontend: Auth (QR + code) and History pages, resume/end-game in lobby/table
- docker-compose: real PostgreSQL service wired via DATABASE_URL
- tests_history.py for the persistence/auth layer; refresh CLAUDE.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tim
2026-06-23 23:09:50 +02:00
co-authored by Claude Opus 4.8
parent beaf142ee4
commit 30c32b7714
24 changed files with 1446 additions and 87 deletions
+22 -6
View File
@@ -1,20 +1,24 @@
import { useEffect } from 'react';
import { BrowserRouter, Routes, Route, Navigate, useNavigate } from 'react-router-dom';
import { BrowserRouter, Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
import { useGameStore } from './store/gameStore';
import { socket, emit } from './lib/socket';
import type { MyPlayer } from './types';
import GameList from './pages/GameList';
import Lobby from './pages/Lobby';
import GameTable from './pages/GameTable';
import Auth from './pages/Auth';
import History from './pages/History';
function AppInner() {
const navigate = useNavigate();
const location = useLocation();
const account = useGameStore((s) => s.account);
const myPlayer = useGameStore((s) => s.myPlayer);
const gameStatus = useGameStore((s) => s.gameStatus);
const error = useGameStore((s) => s.error);
const clearError = useGameStore((s) => s.clearError);
// Reconnect on every socket connect event (handles auto-reconnects after network drops)
// Reconnect into an in-progress game on every socket connect (after network drops).
useEffect(() => {
const doReconnect = () => {
const saved = localStorage.getItem('bridzik_player');
@@ -27,10 +31,20 @@ function AppInner() {
return () => { socket.off('connect', doReconnect); };
}, []);
// Single authoritative navigation: derive target from store state
const targetRoute = myPlayer
? gameStatus ? `/game/${gameStatus.gid}` : `/lobby/${myPlayer.gid}`
: null;
// Single authoritative navigation:
// - not logged in → /auth
// - logged in & in a game → lobby/game
// - logged in, no game, on /auth (just logged in) → leave for the game list
// - logged in, no game, elsewhere → null (let the user navigate: list/history)
const onGameRoute =
location.pathname === '/auth' ||
location.pathname.startsWith('/game') ||
location.pathname.startsWith('/lobby');
const targetRoute = !account
? '/auth'
: myPlayer
? gameStatus ? `/game/${gameStatus.gid}` : `/lobby/${myPlayer.gid}`
: onGameRoute ? '/' : null;
useEffect(() => {
if (!targetRoute) return;
@@ -52,7 +66,9 @@ function AppInner() {
</div>
)}
<Routes>
<Route path="/auth" element={<Auth />} />
<Route path="/" element={<GameList />} />
<Route path="/history" element={<History />} />
<Route path="/lobby/:gid" element={<Lobby />} />
<Route path="/game/:gid" element={<GameTable />} />
<Route path="*" element={<Navigate to="/" replace />} />