fix(frontend): keep auth tokens in sync across browser tabs
deploy / deploy (push) Successful in 1m4s

Refresh tokens rotate on every use and each refresh deletes the old
session server-side, so when one tab refreshed, other open tabs were
left holding a dead access token and a rotated-away refresh token —
their next request 401'd and bounced them to the login screen.

Sync the auth store across tabs via the storage event (propagating
logins, refreshes, and logouts), and make refresh race-resilient: if a
refresh fails but a newer token has meanwhile synced in from another
tab, adopt it and retry instead of ending a still-valid session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:13:13 +03:00
parent da867406e2
commit a371045b41
2 changed files with 35 additions and 5 deletions
+27 -2
View File
@@ -25,10 +25,35 @@ function loadStored(): AuthState {
export const authStore = writable<AuthState>(loadStored());
// Persist on change. Compare first so a value that just arrived from another tab
// (applied by the storage listener below) isn't written straight back, which
// would risk a storage-event echo between tabs.
authStore.subscribe((state) => {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('auth', JSON.stringify(state));
if (typeof localStorage === 'undefined') return;
const serialized = JSON.stringify(state);
if (localStorage.getItem('auth') !== serialized) {
localStorage.setItem('auth', serialized);
}
});
// Keep tabs in sync. Refresh tokens rotate on every use (each refresh deletes the
// old session server-side), so when one tab logs in, refreshes, or logs out, the
// others must pick up the new tokens — or the cleared session — immediately.
// Otherwise a second tab would later refresh with a token that's already been
// rotated away and get bounced to the login screen.
if (typeof window !== 'undefined') {
window.addEventListener('storage', (e) => {
if (e.key !== 'auth') return;
let next: AuthState = initial;
if (e.newValue) {
try {
next = (JSON.parse(e.newValue) as AuthState) ?? initial;
} catch {
next = initial;
}
}
authStore.set(next);
});
}
export const isAuthenticated = derived(authStore, ($auth) => !!$auth.accessToken);