diff --git a/components/Landing.tsx b/components/Landing.tsx index 41edd05..d6ba50a 100644 --- a/components/Landing.tsx +++ b/components/Landing.tsx @@ -568,8 +568,8 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on type="text" placeholder="Game PIN" value={pin} - onChange={(e) => setPin(e.target.value)} - className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none text-center" + onChange={(e) => setPin(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6))} + className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none text-center uppercase tracking-widest" /> {gameInfo?.randomNamesEnabled ? (
diff --git a/hooks/useGame.ts b/hooks/useGame.ts index f6c23c0..7d06ebb 100644 --- a/hooks/useGame.ts +++ b/hooks/useGame.ts @@ -180,7 +180,14 @@ export const useGame = () => { } }, [auth.isLoading, auth.isAuthenticated, location.pathname]); - const generateGamePin = () => Math.floor(Math.random() * 900000) + 100000 + ""; + const generateGamePin = () => { + const chars = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; + let pin = ''; + for (let i = 0; i < 6; i++) { + pin += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return pin; + }; const generateRandomName = (): string => { return uniqueNamesGenerator({ @@ -214,30 +221,43 @@ export const useGame = () => { } }, []); - const createGameSession = async (pin: string, peerId: string, quizData: Quiz, config: GameConfig): Promise => { - try { - const response = await fetch(`${BACKEND_URL}/api/games`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - pin, - hostPeerId: peerId, - quiz: quizData, - gameConfig: config, - }), - }); - - if (!response.ok) { - console.error('Failed to create game session'); + const createGameSession = async (pin: string, peerId: string, quizData: Quiz, config: GameConfig): Promise<{ hostSecret: string; pin: string } | null> => { + const maxRetries = 5; + let currentPin = pin; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const response = await fetch(`${BACKEND_URL}/api/games`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + pin: currentPin, + hostPeerId: peerId, + quiz: quizData, + gameConfig: config, + }), + }); + + if (response.status === 409) { + currentPin = generateGamePin(); + continue; + } + + if (!response.ok) { + console.error('Failed to create game session'); + return null; + } + + const data = await response.json(); + return { hostSecret: data.hostSecret, pin: currentPin }; + } catch (err) { + console.error('Error creating game session:', err); return null; } - - const data = await response.json(); - return data.hostSecret; - } catch (err) { - console.error('Error creating game session:', err); - return null; } + + console.error('Failed to create game after max retries'); + return null; }; const updateHostPeerId = async (pin: string, secret: string, newPeerId: string) => { @@ -340,18 +360,23 @@ export const useGame = () => { const initializeHostGame = async (newQuiz: Quiz, hostParticipates: boolean = true) => { setQuiz(newQuiz); - const pin = generateGamePin(); - setGamePin(pin); + const initialPin = generateGamePin(); + setGamePin(initialPin); - setupHostPeer(pin, async (peerId) => { - const secret = await createGameSession(pin, peerId, newQuiz, gameConfigRef.current); + setupHostPeer(initialPin, async (peerId) => { + const result = await createGameSession(initialPin, peerId, newQuiz, gameConfigRef.current); - if (!secret) { + if (!result) { setError("Failed to create game. Please try again."); setGameState('LANDING'); return; } + const { hostSecret: secret, pin } = result; + if (pin !== initialPin) { + setGamePin(pin); + } + setHostSecret(secret); storeSession({ pin, role: 'HOST', hostSecret: secret }); @@ -498,8 +523,8 @@ export const useGame = () => { return; } - const hostMatch = path.match(/^\/host\/(\d+)$/); - const playMatch = path.match(/^\/play\/(\d+)$/); + const hostMatch = path.match(/^\/host\/([A-Z0-9]+)$/i); + const playMatch = path.match(/^\/play\/([A-Z0-9]+)$/i); const session = getStoredSession(); const pinFromUrl = hostMatch ? hostMatch[1] : (playMatch ? playMatch[1] : null); diff --git a/server/src/routes/games.ts b/server/src/routes/games.ts index c82b251..eaf0b7d 100644 --- a/server/src/routes/games.ts +++ b/server/src/routes/games.ts @@ -14,6 +14,14 @@ const gameCreationLimiter = rateLimit({ message: { error: 'Too many game creations, please try again later.' }, }); +const gameLookupLimiter = rateLimit({ + windowMs: 60 * 1000, + max: isDev ? 500 : 100, + standardHeaders: true, + legacyHeaders: false, + message: { error: 'Too many requests, please try again later.' }, +}); + const SESSION_TTL_MINUTES = parseInt(process.env.GAME_SESSION_TTL_MINUTES || '5', 10); interface GameSession { @@ -71,7 +79,7 @@ router.post('/', gameCreationLimiter, (req: Request, res: Response) => { } }); -router.get('/:pin', (req: Request, res: Response) => { +router.get('/:pin', gameLookupLimiter, (req: Request, res: Response) => { try { const { pin } = req.params;