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
+11 -18
View File
@@ -3,16 +3,16 @@ import { useGameStore } from '../store/gameStore';
import { emit } from '../lib/socket';
interface Props {
mode: 'create' | 'join';
gid?: string;
onClose: () => void;
}
export default function NameModal({ mode, gid, onClose }: Props) {
const [name, setName] = useState(localStorage.getItem('bridzik_name') ?? '');
/** Prompt for a new game's name. Identity is taken from the logged-in session,
* so no player name is asked here. */
export default function NameModal({ onClose }: Props) {
const [name, setName] = useState('');
const myPlayer = useGameStore((s) => s.myPlayer);
// Close modal once we have a player identity
// Close once we've joined the just-created game (create → register chain done).
useEffect(() => {
if (myPlayer) onClose();
}, [myPlayer, onClose]);
@@ -21,29 +21,22 @@ export default function NameModal({ mode, gid, onClose }: Props) {
e.preventDefault();
const trimmed = name.trim();
if (!trimmed) return;
localStorage.setItem('bridzik_name', trimmed);
if (mode === 'join' && gid) {
// emit.registerPlayer sets _pendingGid so the listener can resolve gid
emit.registerPlayer(gid, trimmed);
} else {
// emit.createGame stores the name; the socket listener auto-chains registerPlayer
emit.createGame(trimmed);
}
// Stores the name; the socket listener auto-chains register_player.
emit.createGame(trimmed);
};
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-40 p-4">
<div className="bg-slate-800 rounded-2xl p-6 w-full max-w-sm shadow-xl">
<h2 className="text-lg font-bold mb-4 text-white">Zadaj svoje meno</h2>
<h2 className="text-lg font-bold mb-4 text-white">Nazov hry</h2>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<input
autoFocus
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
maxLength={20}
placeholder="Tvoje meno"
maxLength={30}
placeholder="Napr. Vecerna partia"
className="bg-slate-700 text-white rounded-lg px-4 py-2 outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="flex gap-2 justify-end">
@@ -59,7 +52,7 @@ export default function NameModal({ mode, gid, onClose }: Props) {
disabled={!name.trim()}
className="px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-500 text-white font-semibold disabled:opacity-40"
>
Potvrdit
Vytvorit
</button>
</div>
</form>