Change pins to alphanumeric

This commit is contained in:
Joey Yakimowich-Payne 2026-01-15 21:01:04 -07:00
commit bfcc33cc50
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
3 changed files with 65 additions and 32 deletions

View file

@ -568,8 +568,8 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
type="text" type="text"
placeholder="Game PIN" placeholder="Game PIN"
value={pin} value={pin}
onChange={(e) => setPin(e.target.value)} 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" 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 ? ( {gameInfo?.randomNamesEnabled ? (
<div className="p-4 bg-theme-primary/10 rounded-2xl border-2 border-theme-primary/20"> <div className="p-4 bg-theme-primary/10 rounded-2xl border-2 border-theme-primary/20">

View file

@ -180,7 +180,14 @@ export const useGame = () => {
} }
}, [auth.isLoading, auth.isAuthenticated, location.pathname]); }, [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 => { const generateRandomName = (): string => {
return uniqueNamesGenerator({ return uniqueNamesGenerator({
@ -214,30 +221,43 @@ export const useGame = () => {
} }
}, []); }, []);
const createGameSession = async (pin: string, peerId: string, quizData: Quiz, config: GameConfig): Promise<string | null> => { const createGameSession = async (pin: string, peerId: string, quizData: Quiz, config: GameConfig): Promise<{ hostSecret: string; pin: string } | null> => {
try { const maxRetries = 5;
const response = await fetch(`${BACKEND_URL}/api/games`, { let currentPin = pin;
method: 'POST',
headers: { 'Content-Type': 'application/json' }, for (let attempt = 0; attempt < maxRetries; attempt++) {
body: JSON.stringify({ try {
pin, const response = await fetch(`${BACKEND_URL}/api/games`, {
hostPeerId: peerId, method: 'POST',
quiz: quizData, headers: { 'Content-Type': 'application/json' },
gameConfig: config, body: JSON.stringify({
}), pin: currentPin,
}); hostPeerId: peerId,
quiz: quizData,
if (!response.ok) { gameConfig: config,
console.error('Failed to create game session'); }),
});
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; 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) => { const updateHostPeerId = async (pin: string, secret: string, newPeerId: string) => {
@ -340,18 +360,23 @@ export const useGame = () => {
const initializeHostGame = async (newQuiz: Quiz, hostParticipates: boolean = true) => { const initializeHostGame = async (newQuiz: Quiz, hostParticipates: boolean = true) => {
setQuiz(newQuiz); setQuiz(newQuiz);
const pin = generateGamePin(); const initialPin = generateGamePin();
setGamePin(pin); setGamePin(initialPin);
setupHostPeer(pin, async (peerId) => { setupHostPeer(initialPin, async (peerId) => {
const secret = await createGameSession(pin, peerId, newQuiz, gameConfigRef.current); const result = await createGameSession(initialPin, peerId, newQuiz, gameConfigRef.current);
if (!secret) { if (!result) {
setError("Failed to create game. Please try again."); setError("Failed to create game. Please try again.");
setGameState('LANDING'); setGameState('LANDING');
return; return;
} }
const { hostSecret: secret, pin } = result;
if (pin !== initialPin) {
setGamePin(pin);
}
setHostSecret(secret); setHostSecret(secret);
storeSession({ pin, role: 'HOST', hostSecret: secret }); storeSession({ pin, role: 'HOST', hostSecret: secret });
@ -498,8 +523,8 @@ export const useGame = () => {
return; return;
} }
const hostMatch = path.match(/^\/host\/(\d+)$/); const hostMatch = path.match(/^\/host\/([A-Z0-9]+)$/i);
const playMatch = path.match(/^\/play\/(\d+)$/); const playMatch = path.match(/^\/play\/([A-Z0-9]+)$/i);
const session = getStoredSession(); const session = getStoredSession();
const pinFromUrl = hostMatch ? hostMatch[1] : (playMatch ? playMatch[1] : null); const pinFromUrl = hostMatch ? hostMatch[1] : (playMatch ? playMatch[1] : null);

View file

@ -14,6 +14,14 @@ const gameCreationLimiter = rateLimit({
message: { error: 'Too many game creations, please try again later.' }, 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); const SESSION_TTL_MINUTES = parseInt(process.env.GAME_SESSION_TTL_MINUTES || '5', 10);
interface GameSession { 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 { try {
const { pin } = req.params; const { pin } = req.params;