mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-15 13:55:19 +00:00
feat: add popup for re-authentication on Entra proxy timeout
This commit is contained in:
parent
2f04cc8a64
commit
f440e9bfbe
2 changed files with 402 additions and 9 deletions
276
src/lib/utils/fetchWithTokenRefresh.ts
Normal file
276
src/lib/utils/fetchWithTokenRefresh.ts
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { user } from '$lib/stores';
|
||||||
|
import { userSignOut } from '$lib/apis/auths';
|
||||||
|
|
||||||
|
// Track redirecting state to prevent multiple redirects
|
||||||
|
let isRedirecting = false;
|
||||||
|
|
||||||
|
// Allow custom handler to be set from layout.svelte
|
||||||
|
let sessionExpiryHandler: ((message?: string) => void) | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the handler for session expiry
|
||||||
|
* @param handler The function to call when a session expiry is detected
|
||||||
|
*/
|
||||||
|
export function setSessionExpiryHandler(handler: (message?: string) => void) {
|
||||||
|
sessionExpiryHandler = handler;
|
||||||
|
console.info('[Cursor] Session expiry handler set');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle session expiry - either use the custom handler or fallback to forceLogoutAndRedirect
|
||||||
|
*/
|
||||||
|
function handleSessionExpiry(message?: string) {
|
||||||
|
if (sessionExpiryHandler) {
|
||||||
|
console.info('[Cursor] Using custom session expiry handler');
|
||||||
|
sessionExpiryHandler(message);
|
||||||
|
} else {
|
||||||
|
console.info('[Cursor] No custom handler, using forceLogoutAndRedirect');
|
||||||
|
forceLogoutAndRedirect(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the global fetch interceptor to catch 401 Unauthorized responses
|
||||||
|
* and redirect to login page when token expires
|
||||||
|
*/
|
||||||
|
export function setupFetchInterceptor() {
|
||||||
|
// Store the original fetch function
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
|
||||||
|
// Get Microsoft Proxy domain if present
|
||||||
|
const msProxyDomain = window.location.hostname.indexOf('msappproxy.net') >= 0 ?
|
||||||
|
window.location.hostname : null;
|
||||||
|
|
||||||
|
// Replace the global fetch with our interceptor
|
||||||
|
window.fetch = async function(input, init) {
|
||||||
|
// Modify request to prevent automatic redirect following
|
||||||
|
// This allows us to detect 302 redirects from Microsoft Entra
|
||||||
|
const modifiedInit = {
|
||||||
|
...init,
|
||||||
|
// Don't follow redirects automatically so we can detect them
|
||||||
|
redirect: 'manual' as RequestRedirect
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call the original fetch function with modified init
|
||||||
|
const response = await originalFetch.apply(this, [input, modifiedInit]);
|
||||||
|
|
||||||
|
// Get the request URL for logging
|
||||||
|
const requestUrl = typeof input === 'string' ? input : input instanceof URL ? input.toString() : 'unknown';
|
||||||
|
|
||||||
|
// Clone the response so we can check it without disturbing the original
|
||||||
|
const responseClone = response.clone();
|
||||||
|
|
||||||
|
console.info('setupFetchInterceptor responseClone', {
|
||||||
|
status: responseClone.status,
|
||||||
|
type: responseClone.type,
|
||||||
|
url: responseClone.url,
|
||||||
|
ok: responseClone.ok,
|
||||||
|
requestUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the URL itself looks like an auth endpoint
|
||||||
|
// This helps catch cases where the URL has already changed to an auth endpoint
|
||||||
|
if (responseClone.url && (
|
||||||
|
responseClone.url.indexOf('/oauth2/') >= 0 ||
|
||||||
|
responseClone.url.indexOf('/auth/') >= 0 ||
|
||||||
|
responseClone.url.indexOf('login.microsoftonline.com') >= 0 ||
|
||||||
|
responseClone.url.indexOf('login.microsoft.com') >= 0
|
||||||
|
)) {
|
||||||
|
console.info('AUTH URL DETECTED IN RESPONSE - FORCING LOGOUT', {
|
||||||
|
requestUrl,
|
||||||
|
responseUrl: responseClone.url
|
||||||
|
});
|
||||||
|
handleSessionExpiry('Your session has expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check status without waiting for json parsing
|
||||||
|
if (responseClone.status === 401) {
|
||||||
|
console.info('401 UNAUTHORIZED DETECTED - FORCING LOGOUT', {
|
||||||
|
url: typeof input === 'string' ? input : input instanceof URL ? input.toString() : 'unknown'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force logout and redirect immediately
|
||||||
|
handleSessionExpiry('Your session has expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for 302 redirects from proxy - now we can catch these!
|
||||||
|
// 'opaqueredirect' is the response type for cross-origin redirects when using manual redirect mode
|
||||||
|
if (responseClone.status === 302 || responseClone.status === 307 || responseClone.type === 'opaqueredirect') {
|
||||||
|
const location = responseClone.headers.get('location');
|
||||||
|
|
||||||
|
console.info('REDIRECT DETECTED', {
|
||||||
|
requestUrl,
|
||||||
|
responseType: responseClone.type,
|
||||||
|
status: responseClone.status,
|
||||||
|
location: location || 'No location header (opaque redirect)'
|
||||||
|
});
|
||||||
|
|
||||||
|
// For opaque redirects, we won't have a location header to check
|
||||||
|
// So we need to make an educated guess based on circumstances
|
||||||
|
if (responseClone.type === 'opaqueredirect') {
|
||||||
|
// If we're on Microsoft Proxy, it's very likely this is an auth redirect
|
||||||
|
if (msProxyDomain) {
|
||||||
|
console.info('OPAQUE REDIRECT ON MICROSOFT PROXY - FORCING LOGOUT');
|
||||||
|
handleSessionExpiry('Your proxy session has expired');
|
||||||
|
// Still return response to allow normal error handling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For normal redirects, we can check the location
|
||||||
|
else if (location &&
|
||||||
|
(location.indexOf('login.microsoftonline.com') >= 0 ||
|
||||||
|
location.indexOf('login.microsoft.com') >= 0)) {
|
||||||
|
console.info('MICROSOFT ENTRA REDIRECT DETECTED - FORCING LOGOUT');
|
||||||
|
handleSessionExpiry('Your proxy session has expired');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the original response to preserve normal fetch behavior
|
||||||
|
// The API methods will still get their original response
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
// Check if this is a CORS error from a redirect to Microsoft login
|
||||||
|
const err = error as Error & { status?: number; redirectUrl?: string; detail?: string };
|
||||||
|
|
||||||
|
// Log the error for debugging
|
||||||
|
console.info('FETCH ERROR:', err);
|
||||||
|
|
||||||
|
// Some endpoints return serialized errors with redirectUrl that point to auth endpoints
|
||||||
|
if (err && typeof err === 'object') {
|
||||||
|
// Check common auth-related fields in error responses
|
||||||
|
if (
|
||||||
|
(err.redirectUrl && (
|
||||||
|
err.redirectUrl.indexOf('login.microsoft') >= 0 ||
|
||||||
|
err.redirectUrl.indexOf('/oauth2/') >= 0
|
||||||
|
)) ||
|
||||||
|
(err.message && (
|
||||||
|
err.message.indexOf('authentication') >= 0 ||
|
||||||
|
err.message.indexOf('unauthorized') >= 0 ||
|
||||||
|
err.message.indexOf('login') >= 0
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
console.info('AUTH-RELATED ERROR FIELDS DETECTED - FORCING LOGOUT', err);
|
||||||
|
handleSessionExpiry('Your session has expired');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is an auth-related error
|
||||||
|
if (err?.status === 401 || (typeof err?.message === 'string' && err.message.indexOf('unauthorized') >= 0)) {
|
||||||
|
console.info('AUTH ERROR DETECTED - FORCING LOGOUT', err);
|
||||||
|
handleSessionExpiry();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rethrow the error to preserve original behavior
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also set up a global AJAX error handler for APIs that might not use fetch
|
||||||
|
window.addEventListener('unhandledrejection', function(event) {
|
||||||
|
const reason = event?.reason as Error & {
|
||||||
|
status?: number;
|
||||||
|
detail?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look for authentication errors in unhandled promise rejections
|
||||||
|
if (reason?.status === 401 ||
|
||||||
|
(reason?.detail && typeof reason.detail === 'string' &&
|
||||||
|
(reason.detail.indexOf('not authenticated') >= 0 || reason.detail.indexOf('token') >= 0 || reason.detail.indexOf('auth') >= 0))) {
|
||||||
|
console.info('AUTH ERROR IN UNHANDLED REJECTION - FORCING LOGOUT', reason);
|
||||||
|
handleSessionExpiry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for CORS errors which might indicate Microsoft redirect
|
||||||
|
if (reason instanceof TypeError &&
|
||||||
|
reason.message &&
|
||||||
|
(reason.message.indexOf('CORS') >= 0 || reason.message.indexOf('Failed to fetch') >= 0)) {
|
||||||
|
console.info('CORS ERROR IN UNHANDLED REJECTION - POSSIBLY MS REDIRECT', reason);
|
||||||
|
|
||||||
|
// If we're on Microsoft proxy domain, assume this is an auth error
|
||||||
|
if (window.location.hostname.indexOf('msappproxy.net') >= 0) {
|
||||||
|
handleSessionExpiry('Your proxy session has expired');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for specific error messages in the reason object at any level
|
||||||
|
const reasonStr = JSON.stringify(reason);
|
||||||
|
if (reasonStr.indexOf('unauthorized') >= 0 || reasonStr.indexOf('authentication') >= 0 || reasonStr.indexOf('not logged in') >= 0) {
|
||||||
|
console.info('AUTH-RELATED TEXT IN UNHANDLED REJECTION - FORCING LOGOUT', reason);
|
||||||
|
handleSessionExpiry();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info('Token expiration interceptor installed successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces a logout and redirects the user to the login page.
|
||||||
|
* This is used when a token is detected as expired.
|
||||||
|
* Also calls userSignOut to properly expire the JWT token on the backend
|
||||||
|
* and get the correct redirect URL.
|
||||||
|
*
|
||||||
|
* @param customMessage Optional custom message to display during logout
|
||||||
|
*/
|
||||||
|
export async function forceLogoutAndRedirect(customMessage?: string) {
|
||||||
|
// Prevent multiple redirects
|
||||||
|
if (isRedirecting) return;
|
||||||
|
isRedirecting = true;
|
||||||
|
|
||||||
|
console.info('🔐 SESSION EXPIRED! FORCING LOGOUT AND PAGE RELOAD 🔐');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clear user data locally
|
||||||
|
user.set(undefined);
|
||||||
|
|
||||||
|
// Remove token from localStorage
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
|
||||||
|
// Try to call userSignOut to properly expire the server-side session
|
||||||
|
// and get the redirect URL from the backend
|
||||||
|
let redirectUrl: string | undefined;
|
||||||
|
try {
|
||||||
|
const res = await userSignOut();
|
||||||
|
// Handle the custom response type which includes redirect_url
|
||||||
|
const customRes = res as unknown as { redirect_url?: string };
|
||||||
|
if (customRes?.redirect_url) {
|
||||||
|
redirectUrl = customRes.redirect_url;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error signing out from server:', error);
|
||||||
|
// Continue with client-side logout even if server logout fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
toast.error(customMessage || 'Your session has expired. Logging you out...', {
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use redirect URL from server if available, otherwise default to auth page
|
||||||
|
if (redirectUrl) {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
} else {
|
||||||
|
// Force navigation to login page with hard reload
|
||||||
|
// const currentUrl = encodeURIComponent(window.location.pathname + window.location.search);
|
||||||
|
// window.location.href = `/auth?redirect=${currentUrl}&expired=true&t=${Date.now()}`;
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// As a backup, force reload after a short delay if redirect doesn't happen
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error during forced logout:', e);
|
||||||
|
// Last resort: force reload
|
||||||
|
window.location.reload();
|
||||||
|
} finally {
|
||||||
|
// Reset redirecting flag after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
isRedirecting = false;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,17 @@
|
||||||
import { spring } from 'svelte/motion';
|
import { spring } from 'svelte/motion';
|
||||||
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
||||||
import { Toaster, toast } from 'svelte-sonner';
|
import { Toaster, toast } from 'svelte-sonner';
|
||||||
|
import {
|
||||||
|
setupFetchInterceptor,
|
||||||
|
forceLogoutAndRedirect,
|
||||||
|
setSessionExpiryHandler
|
||||||
|
} from '$lib/utils/fetchWithTokenRefresh';
|
||||||
|
|
||||||
let loadingProgress = spring(0, {
|
let loadingProgress = spring(0, {
|
||||||
stiffness: 0.05
|
stiffness: 0.05
|
||||||
});
|
});
|
||||||
|
|
||||||
import { onMount, tick, setContext, onDestroy } from 'svelte';
|
import { onMount, tick, setContext } from 'svelte';
|
||||||
import {
|
import {
|
||||||
config,
|
config,
|
||||||
user,
|
user,
|
||||||
|
|
@ -73,6 +78,8 @@
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
|
|
||||||
// handle frontend updates (https://svelte.dev/docs/kit/configuration#version)
|
// handle frontend updates (https://svelte.dev/docs/kit/configuration#version)
|
||||||
beforeNavigate(async ({ willUnload, to }) => {
|
beforeNavigate(async ({ willUnload, to }) => {
|
||||||
if (updated.current && !willUnload && to?.url) {
|
if (updated.current && !willUnload && to?.url) {
|
||||||
|
|
@ -84,6 +91,7 @@
|
||||||
setContext('i18n', i18n);
|
setContext('i18n', i18n);
|
||||||
|
|
||||||
const bc = new BroadcastChannel('active-tab-channel');
|
const bc = new BroadcastChannel('active-tab-channel');
|
||||||
|
const sessionBC = new BroadcastChannel('session-refresh-channel');
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
let tokenTimer = null;
|
let tokenTimer = null;
|
||||||
|
|
@ -94,6 +102,8 @@
|
||||||
|
|
||||||
const BREAKPOINT = 768;
|
const BREAKPOINT = 768;
|
||||||
|
|
||||||
|
let showReauthDialog = false;
|
||||||
|
|
||||||
const setupSocket = async (enableWebsocket) => {
|
const setupSocket = async (enableWebsocket) => {
|
||||||
const _socket = io(`${WEBUI_BASE_URL}` || undefined, {
|
const _socket = io(`${WEBUI_BASE_URL}` || undefined, {
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
|
|
@ -578,15 +588,58 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (now >= exp - TOKEN_EXPIRY_BUFFER) {
|
// Check if token is expiring soon with the fixed 30-second buffer
|
||||||
const res = await userSignOut();
|
if (now >= (exp - TOKEN_EXPIRY_BUFFER)) {
|
||||||
user.set(null);
|
handleSessionExpiryWithCoordination();
|
||||||
localStorage.removeItem('token');
|
|
||||||
|
|
||||||
location.href = res?.redirect_url ?? '/auth';
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add a function to refresh the user session without reloading
|
||||||
|
async function refreshSessionUser() {
|
||||||
|
console.info('[Cursor] Refreshing session user data without reload');
|
||||||
|
if (localStorage.token) {
|
||||||
|
try {
|
||||||
|
const sessionUser = await getSessionUser(localStorage.token);
|
||||||
|
if (sessionUser) {
|
||||||
|
console.info('[Cursor] Got valid session user:', sessionUser.username || sessionUser.email);
|
||||||
|
await user.set(sessionUser);
|
||||||
|
console.info('[Cursor] Updated user store');
|
||||||
|
await config.set(await getBackendConfig());
|
||||||
|
|
||||||
|
// Re-establish socket connection with new token
|
||||||
|
if ($socket) {
|
||||||
|
console.info('[Cursor] Emitting user-join with new token');
|
||||||
|
$socket.emit('user-join', { auth: { token: sessionUser.token } });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('[Cursor] Session refreshed successfully');
|
||||||
|
// Show a toast notification that the session was refreshed
|
||||||
|
toast.success('Your session has been refreshed', {
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide the reauth dialog if it's showing
|
||||||
|
showReauthDialog = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.warn('[Cursor] Got null session user');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Cursor] Failed to refresh session user:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[Cursor] No token available for refresh');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the forceLogoutAndRedirect function to broadcast when re-authentication is complete
|
||||||
|
function handleSessionExpiryWithCoordination() {
|
||||||
|
console.info('[Cursor] Session expiry detected, showing dialog');
|
||||||
|
showReauthDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
let touchstartY = 0;
|
let touchstartY = 0;
|
||||||
|
|
||||||
|
|
@ -617,6 +670,12 @@
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Set up the session expiry handler first, before setting up the fetch interceptor
|
||||||
|
console.info('[Cursor] Setting up session expiry handler...');
|
||||||
|
setSessionExpiryHandler(handleSessionExpiryWithCoordination);
|
||||||
|
|
||||||
|
console.info('[Cursor] Setting up fetch interceptor...');
|
||||||
|
setupFetchInterceptor();
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
if (window.applyTheme) {
|
if (window.applyTheme) {
|
||||||
|
|
@ -744,17 +803,25 @@
|
||||||
if (localStorage.token) {
|
if (localStorage.token) {
|
||||||
// Get Session User Info
|
// Get Session User Info
|
||||||
const sessionUser = await getSessionUser(localStorage.token).catch((error) => {
|
const sessionUser = await getSessionUser(localStorage.token).catch((error) => {
|
||||||
toast.error(`${error}`);
|
console.error('Session user fetch failed:', error);
|
||||||
|
toast.error(`Authentication error: ${error}`);
|
||||||
|
|
||||||
|
// Clear token on auth error
|
||||||
|
localStorage.removeItem('token');
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sessionUser) {
|
if (sessionUser) {
|
||||||
await user.set(sessionUser);
|
await user.set(sessionUser);
|
||||||
await config.set(await getBackendConfig());
|
await config.set(await getBackendConfig());
|
||||||
|
|
||||||
|
// Run check immediately
|
||||||
|
checkTokenExpiry();
|
||||||
} else {
|
} else {
|
||||||
// Redirect Invalid Session User to /auth Page
|
// Redirect Invalid Session User to /auth Page
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
await goto(`/auth?redirect=${encodedUrl}`);
|
// Use window.location for a complete page reload
|
||||||
|
window.location.href = `/?redirect=${encodedUrl}&invalid=true`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Don't redirect if we're already on the auth page
|
// Don't redirect if we're already on the auth page
|
||||||
|
|
@ -801,6 +868,43 @@
|
||||||
loaded = true;
|
loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for session refresh broadcasts from other tabs
|
||||||
|
sessionBC.onmessage = async (event) => {
|
||||||
|
if (event.data === 'session-refreshed') {
|
||||||
|
await refreshSessionUser();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the storage event listener to handle token changes
|
||||||
|
window.addEventListener('storage', async function(event) {
|
||||||
|
if (event.key === 'token') {
|
||||||
|
if (event.newValue === null) {
|
||||||
|
// Token was removed in another tab
|
||||||
|
console.log('[Cursor] TOKEN REMOVED IN ANOTHER TAB');
|
||||||
|
// Instead of reloading, show the reauth dialog
|
||||||
|
handleSessionExpiryWithCoordination();
|
||||||
|
} else if (event.oldValue === null && event.newValue) {
|
||||||
|
// Token was added in another tab (user logged in again)
|
||||||
|
console.log('[Cursor] TOKEN ADDED IN ANOTHER TAB - REFRESHING SESSION');
|
||||||
|
|
||||||
|
// Broadcast to other tabs that they should refresh too
|
||||||
|
// This helps ensure all tabs get the message
|
||||||
|
const sessionBC = new BroadcastChannel('session-refresh-channel');
|
||||||
|
sessionBC.postMessage('session-refreshed');
|
||||||
|
sessionBC.close();
|
||||||
|
} else if (event.oldValue !== event.newValue) {
|
||||||
|
// Token was changed in another tab
|
||||||
|
console.log('[Cursor] TOKEN CHANGED IN ANOTHER TAB - REFRESHING SESSION');
|
||||||
|
|
||||||
|
// Broadcast to other tabs that they should refresh too
|
||||||
|
// This helps ensure all tabs get the message
|
||||||
|
const sessionBC = new BroadcastChannel('session-refresh-channel');
|
||||||
|
sessionBC.postMessage('session-refreshed');
|
||||||
|
sessionBC.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', onResize);
|
window.removeEventListener('resize', onResize);
|
||||||
};
|
};
|
||||||
|
|
@ -854,3 +958,16 @@
|
||||||
position="top-right"
|
position="top-right"
|
||||||
closeButton
|
closeButton
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
title="Session Expired"
|
||||||
|
message="Your session has expired. Click 'Re-authenticate' to reload the page and sign in again, or click 'Cancel' to close this dialog and save your unsaved work before manually reloading."
|
||||||
|
show={showReauthDialog}
|
||||||
|
cancelLabel="Cancel"
|
||||||
|
confirmLabel="Re-authenticate"
|
||||||
|
on:cancel={() => (showReauthDialog = false)}
|
||||||
|
on:confirm={() => {
|
||||||
|
forceLogoutAndRedirect('Reloading for re-authentication...');
|
||||||
|
showReauthDialog = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue