pediatric-ai-scribe-v3/public/js/authFetch.js
Daniel f12bdb0fa4 Fix: revoked sessions now actually log the other device out
The server-side revoke was always working — it deletes user_sessions
rows, and middleware correctly returned 401 on the revoked device's
next /api/* request. The bug was entirely client-side: individual
fetch handlers swallowed the 401 (rendering "no sessions found" or
empty data) and nothing redirected to the login screen. So the
revoked device looked like it stayed signed in.

Added public/js/authFetch.js: a global fetch interceptor that
watches every /api/* response. On 401 from a non-auth endpoint
(i.e. not /login, /register, /logout, /me, etc.), it clears any
cached token/user state and reloads the page. The reload's boot
flow lands on /api/auth/me → 401 → login screen as usual.

Guarded against false positives: only triggers when the app believes
the user is currently logged in (AUTH_TOKEN set or main-app visible)
so a pre-login 401 doesn't accidentally flash the screen.

Loaded before auth.js in index.html.
2026-04-14 05:10:16 +02:00

85 lines
3.6 KiB
JavaScript

// ============================================================
// Global fetch interceptor for auth failures
// ============================================================
// When the server responds 401 to any /api/ request, the user's
// session has been revoked or expired. Previously this would leave
// the app UI on screen silently because each fetch handled its own
// errors. Now we detect it once, clear local state, and bounce to
// the login screen.
// ============================================================
(function() {
if (window.__fetchAuthIntercepted) return;
window.__fetchAuthIntercepted = true;
var _fetch = window.fetch.bind(window);
var handlingLogout = false;
function handleAuthFailure(status, reason) {
if (handlingLogout) return;
handlingLogout = true;
try {
// Clear any cached token + user info
window.AUTH_TOKEN = null;
window.CURRENT_USER = null;
if (window.SecureStorage) {
try { window.SecureStorage.remove('ped_scribe_token'); } catch(e) {}
try { window.SecureStorage.remove('ped_scribe_user'); } catch(e) {}
try { window.SecureStorage.remove('ped_session_id'); } catch(e) {}
}
try {
localStorage.removeItem('ped_scribe_token');
localStorage.removeItem('ped_scribe_user');
localStorage.removeItem('ped_session_id');
} catch(e) {}
// Brief user-visible notice before the reload swap
try {
if (typeof window.showToast === 'function') {
window.showToast(reason || 'Your session has ended. Please sign in again.', 'info');
}
} catch(e) {}
// Reload — the boot flow in auth.js will call /api/auth/me, get a
// fresh 401 (no cookie / no token), and show the login screen.
setTimeout(function() { window.location.reload(); }, 800);
} catch (e) {
// Last resort
window.location.reload();
}
}
window.fetch = function(input, init) {
return _fetch(input, init).then(function(resp) {
// Only act on same-origin /api/ calls; don't interfere with login
// or logout endpoints themselves (they're allowed to return 401).
try {
var url = typeof input === 'string' ? input : (input && input.url) || '';
var isApiCall = url.indexOf('/api/') !== -1;
var isAuthEndpoint = url.indexOf('/api/auth/login') !== -1
|| url.indexOf('/api/auth/register') !== -1
|| url.indexOf('/api/auth/logout') !== -1
|| url.indexOf('/api/auth/forgot-password') !== -1
|| url.indexOf('/api/auth/reset-password') !== -1
|| url.indexOf('/api/auth/verify-email') !== -1
|| url.indexOf('/api/auth/resend-verification') !== -1
|| url.indexOf('/api/auth/oidc') !== -1
|| url.indexOf('/api/auth/me') !== -1
|| url.indexOf('/api/auth/2fa') !== -1;
if (resp.status === 401 && isApiCall && !isAuthEndpoint) {
// Only treat as global session failure if the app thinks the user
// was logged in (AUTH_TOKEN set OR main-app visible). Avoid
// flashing the login screen on public-endpoint 401s during the
// pre-login phase.
var authed = !!window.AUTH_TOKEN
|| document.documentElement.classList.contains('has-session')
|| (document.getElementById('main-app') && document.getElementById('main-app').style.display === 'block');
if (authed) {
handleAuthFailure(resp.status, 'Your session was ended by another device. Please sign in again.');
}
}
} catch(e) {}
return resp;
});
};
})();