Fix auth token being clobbered by game seat token (userId always null)
The login JWT and the per-seat rejoin token shared the localStorage key 'shelem_token', so joining a game overwrote the auth token with the random seat token. The socket then reconnected as an unauthenticated guest, so every seat was recorded with userId=null and no stats or history were attributed. - Seat token now uses its own key 'shelem_seat_token'; 'shelem_token' is auth-only - Add GET /api/me + boot-time validateAuth() to drop stale/corrupted tokens instead of running as a "logged in" ghost - reauthSocket() re-authenticates the live socket on login/register Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+44
-7
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user