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.
85 lines
3.6 KiB
JavaScript
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;
|
|
});
|
|
};
|
|
})();
|