fix(auth): persist session to localStorage for cross-tab support

Session metadata and full session now written to both sessionStorage and
localStorage so that new tabs and window.open() inherit the auth state
without requiring a fresh login.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-07 15:33:04 +03:00
parent e0c537c427
commit 4bbbc52380

View File

@@ -144,12 +144,12 @@ export class AuthSessionStore {
private readPersistedMetadata(
restoredSession: AuthSession | null
): PersistedSessionMetadata | null {
if (typeof sessionStorage === 'undefined') {
if (typeof localStorage === 'undefined') {
return null;
}
try {
const raw = sessionStorage.getItem(SESSION_STORAGE_KEY);
const raw = localStorage.getItem(SESSION_STORAGE_KEY);
if (!raw) {
if (!restoredSession) {
return null;
@@ -165,7 +165,7 @@ export class AuthSessionStore {
typeof parsed.issuedAtEpochMs !== 'number' ||
typeof parsed.dpopKeyThumbprint !== 'string'
) {
sessionStorage.removeItem(SESSION_STORAGE_KEY);
localStorage.removeItem(SESSION_STORAGE_KEY);
return restoredSession ? this.toMetadata(restoredSession) : null;
}
const tenantId =
@@ -180,37 +180,48 @@ export class AuthSessionStore {
tenantId,
};
} catch {
sessionStorage.removeItem(SESSION_STORAGE_KEY);
localStorage.removeItem(SESSION_STORAGE_KEY);
return restoredSession ? this.toMetadata(restoredSession) : null;
}
}
private readPersistedSession(): AuthSession | null {
if (typeof sessionStorage === 'undefined') {
// Try sessionStorage first (same-tab, survives F5), then fall back
// to localStorage (cross-tab, survives new-tab / window.open).
const fromSession = this.readSessionFrom('session');
if (fromSession) {
return fromSession;
}
return this.readSessionFrom('local');
}
private readSessionFrom(storage: 'session' | 'local'): AuthSession | null {
const store = storage === 'session' ? sessionStorage : localStorage;
if (typeof store === 'undefined') {
return null;
}
try {
const raw = sessionStorage.getItem(FULL_SESSION_STORAGE_KEY);
const raw = store.getItem(FULL_SESSION_STORAGE_KEY);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw) as AuthSession;
if (!this.isValidSession(parsed)) {
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
store.removeItem(FULL_SESSION_STORAGE_KEY);
return null;
}
if (parsed.tokens.expiresAtEpochMs <= Date.now()) {
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
store.removeItem(FULL_SESSION_STORAGE_KEY);
this.restoredSessionExpired = true;
return null;
}
return parsed;
} catch {
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
store.removeItem(FULL_SESSION_STORAGE_KEY);
return null;
}
}
@@ -262,31 +273,28 @@ export class AuthSessionStore {
}
private persistMetadata(metadata: PersistedSessionMetadata): void {
if (typeof sessionStorage === 'undefined') {
if (typeof localStorage === 'undefined') {
return;
}
sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(metadata));
localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(metadata));
}
private persistSession(session: AuthSession): void {
if (typeof sessionStorage === 'undefined') {
return;
}
sessionStorage.setItem(FULL_SESSION_STORAGE_KEY, JSON.stringify(session));
const json = JSON.stringify(session);
try { sessionStorage.setItem(FULL_SESSION_STORAGE_KEY, json); } catch { /* SSR / quota */ }
try { localStorage.setItem(FULL_SESSION_STORAGE_KEY, json); } catch { /* SSR / quota */ }
}
private clearPersistedMetadata(): void {
if (typeof sessionStorage === 'undefined') {
if (typeof localStorage === 'undefined') {
return;
}
sessionStorage.removeItem(SESSION_STORAGE_KEY);
localStorage.removeItem(SESSION_STORAGE_KEY);
}
private clearPersistedSession(): void {
if (typeof sessionStorage === 'undefined') {
return;
}
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
try { sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY); } catch { /* SSR */ }
try { localStorage.removeItem(FULL_SESSION_STORAGE_KEY); } catch { /* SSR */ }
}
getActiveTenantId(): string | null {