feat: implement cross-tab coordination for token refresh and enhance logout handling
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m57s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m57s
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { logoutUser } from '@/stores/auth'
|
||||
|
||||
const hasLocalStorage =
|
||||
typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function'
|
||||
const CROSS_TAB_REFRESH_KEY = 'lastRefreshAt'
|
||||
const CROSS_TAB_REFRESH_MIN_INTERVAL = 8_000 // 8s — prevents concurrent multi-tab refresh
|
||||
|
||||
let unauthorizedInterceptorInstalled = false
|
||||
let unauthorizedRedirectHandler: (() => void) | null = null
|
||||
let unauthorizedHandlingInProgress = false
|
||||
@@ -15,6 +20,9 @@ export function setUnauthorizedRedirectHandlerForTests(handler: (() => void) | n
|
||||
export function resetInterceptorStateForTests(): void {
|
||||
unauthorizedHandlingInProgress = false
|
||||
refreshPromise = null
|
||||
if (hasLocalStorage && typeof localStorage.removeItem === 'function') {
|
||||
localStorage.removeItem(CROSS_TAB_REFRESH_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnauthorizedResponse(): void {
|
||||
@@ -35,13 +43,27 @@ function handleUnauthorizedResponse(): void {
|
||||
* Attempt to refresh the access token by calling the refresh endpoint.
|
||||
* Returns true if refresh succeeded, false otherwise.
|
||||
* Uses a mutex so concurrent 401s only trigger one refresh call.
|
||||
* Also uses a localStorage timestamp to coordinate across tabs, preventing
|
||||
* concurrent refreshes that would trigger false-positive theft detection.
|
||||
*/
|
||||
async function attemptTokenRefresh(originalFetch: typeof fetch): Promise<boolean> {
|
||||
// Cross-tab coordination: if another tab refreshed recently, the new cookie
|
||||
// is already set. Skip the refresh call and let the original request retry.
|
||||
if (hasLocalStorage && typeof localStorage.getItem === 'function') {
|
||||
const lastRefresh = localStorage.getItem(CROSS_TAB_REFRESH_KEY)
|
||||
if (lastRefresh && Date.now() - Number(lastRefresh) < CROSS_TAB_REFRESH_MIN_INTERVAL) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (refreshPromise) return refreshPromise
|
||||
|
||||
refreshPromise = (async () => {
|
||||
try {
|
||||
const res = await originalFetch('/api/auth/refresh', { method: 'POST' })
|
||||
if (res.ok && hasLocalStorage && typeof localStorage.setItem === 'function') {
|
||||
localStorage.setItem(CROSS_TAB_REFRESH_KEY, String(Date.now()))
|
||||
}
|
||||
return res.ok
|
||||
} catch {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user