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:
+22
-6
@@ -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 />} />
|
||||
|
||||
Reference in New Issue
Block a user