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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user