diff --git a/public/app.js b/public/app.js index ab9cf26..62f2b97 100644 --- a/public/app.js +++ b/public/app.js @@ -471,10 +471,21 @@ function connectSocket(onReady) { if (!socket) { socket = io({ auth: { token: authToken } }); initSocketHandlers(); + } else { + // Reuse the socket but make sure it carries the current auth token + socket.auth = { token: authToken }; } socket.once('connect', onReady); } +// Re-authenticate an existing socket after login/register so the server sees +// the user on this same connection (auth is only read at (re)connect time). +function reauthSocket() { + if (!socket) return; + socket.auth = { token: authToken }; + if (socket.connected) socket.disconnect().connect(); +} + function initSocketHandlers() { // Persistent reconnect handler — auto-rejoin after any network drop mid-game socket.on('connect', () => { @@ -555,20 +566,23 @@ function initSocketHandlers() { } // ─── Session persistence (localStorage so it survives tab/app close) ────────── +// NOTE: the game-session (seat) token uses its OWN key. It must never share a +// key with the auth JWT ('shelem_token'), or joining a game would overwrite the +// login token and drop the player to an unauthenticated guest. function saveSession() { - localStorage.setItem('shelem_room', myRoomId); - localStorage.setItem('shelem_seat', mySeat); - localStorage.setItem('shelem_token', myToken); + localStorage.setItem('shelem_room', myRoomId); + localStorage.setItem('shelem_seat', mySeat); + localStorage.setItem('shelem_seat_token', myToken); } function clearSession() { localStorage.removeItem('shelem_room'); localStorage.removeItem('shelem_seat'); - localStorage.removeItem('shelem_token'); + localStorage.removeItem('shelem_seat_token'); } function tryRejoin() { const room = localStorage.getItem('shelem_room'); const seat = localStorage.getItem('shelem_seat'); - const token = localStorage.getItem('shelem_token'); + const token = localStorage.getItem('shelem_seat_token'); if (!room || seat === null || !token) return; // Set inside the callback so the persistent 'connect' handler doesn't // also emit rejoin on the very first connection (would be a duplicate) @@ -1235,7 +1249,7 @@ async function doLogin() { localStorage.setItem('shelem_user', authUser); hide('overlay-auth'); updateAuthBar(); - if (socket) socket.auth = { token: authToken }; + reauthSocket(); } catch { $('auth-login-error').textContent = 'Network error'; } } @@ -1257,6 +1271,7 @@ async function doRegister() { localStorage.setItem('shelem_user', authUser); hide('overlay-auth'); updateAuthBar(); + reauthSocket(); } catch { $('auth-reg-error').textContent = 'Network error'; } } @@ -1666,12 +1681,34 @@ function wireGameEvents() { }); } +// Validate the stored auth token before trusting it. Catches tokens that are +// expired, or corrupted by the old localStorage key collision, so we don't run +// around as a "logged in" ghost whose socket is actually an unauthenticated guest. +async function validateAuth() { + if (!authToken) return; + try { + const r = await fetch('/api/me', { headers: { Authorization: `Bearer ${authToken}` } }); + if (!r.ok) throw new Error('invalid'); + const d = await r.json(); + authUser = d.username; + localStorage.setItem('shelem_user', authUser); + } catch { + authToken = null; authUser = null; + localStorage.removeItem('shelem_token'); + localStorage.removeItem('shelem_user'); + } + updateAuthBar(); +} + // ─── Boot ───────────────────────────────────────────────────── -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener('DOMContentLoaded', async () => { initLobby(); wireGameEvents(); applyBarBottom(); + // Drop any stale/corrupted auth token before we use it to connect + await validateAuth(); + // Pre-fill name from auth if (authUser) $('input-name').value = authUser; diff --git a/server.js b/server.js index 6a18819..f573897 100644 --- a/server.js +++ b/server.js @@ -136,6 +136,12 @@ app.get('/api/config', (_req, res) => { res.json({ signupsOpen: config.signupsOpen }); }); +// Validate the stored auth token; the client uses this at startup to detect a +// stale/corrupted token and drop the fake "logged in" state. +app.get('/api/me', requireAuth, (req, res) => { + res.json({ id: req.user.id, username: req.user.username }); +}); + app.post('/api/register', (req, res) => { if (!config.signupsOpen) return res.status(403).json({ error: 'New registrations are currently closed.' });