// ============================================================ // 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; // Cross-tab sync: when one tab logs out, all other open tabs drop // their UI within milliseconds instead of waiting for their next // failed fetch. Uses the same BroadcastChannel name across tabs. var bc = null; try { bc = new BroadcastChannel('pedscribe-auth'); } catch (e) { bc = null; } window.__authBroadcast = bc; if (bc) { bc.onmessage = function(ev) { if (ev && ev.data && ev.data.type === 'logout') { handleAuthFailure(null, 'You signed out in another tab.'); } }; } 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; }); }; })();