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

This commit is contained in:
2026-03-27 23:02:29 -04:00
parent 89097a390e
commit a4e23aad11
6 changed files with 54 additions and 11 deletions

View File

@@ -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