diff --git a/public/index.html b/public/index.html index e1104db..7db03c8 100644 --- a/public/index.html +++ b/public/index.html @@ -429,6 +429,7 @@ + diff --git a/public/js/authFetch.js b/public/js/authFetch.js new file mode 100644 index 0000000..2d957e7 --- /dev/null +++ b/public/js/authFetch.js @@ -0,0 +1,85 @@ +// ============================================================ +// 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; + }); + }; +})();