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( private readPersistedMetadata(
restoredSession: AuthSession | null restoredSession: AuthSession | null
): PersistedSessionMetadata | null { ): PersistedSessionMetadata | null {
if (typeof sessionStorage === 'undefined') { if (typeof localStorage === 'undefined') {
return null; return null;
} }
try { try {
const raw = sessionStorage.getItem(SESSION_STORAGE_KEY); const raw = localStorage.getItem(SESSION_STORAGE_KEY);
if (!raw) { if (!raw) {
if (!restoredSession) { if (!restoredSession) {
return null; return null;
@@ -165,7 +165,7 @@ export class AuthSessionStore {
typeof parsed.issuedAtEpochMs !== 'number' || typeof parsed.issuedAtEpochMs !== 'number' ||
typeof parsed.dpopKeyThumbprint !== 'string' typeof parsed.dpopKeyThumbprint !== 'string'
) { ) {
sessionStorage.removeItem(SESSION_STORAGE_KEY); localStorage.removeItem(SESSION_STORAGE_KEY);
return restoredSession ? this.toMetadata(restoredSession) : null; return restoredSession ? this.toMetadata(restoredSession) : null;
} }
const tenantId = const tenantId =
@@ -180,37 +180,48 @@ export class AuthSessionStore {
tenantId, tenantId,
}; };
} catch { } catch {
sessionStorage.removeItem(SESSION_STORAGE_KEY); localStorage.removeItem(SESSION_STORAGE_KEY);
return restoredSession ? this.toMetadata(restoredSession) : null; return restoredSession ? this.toMetadata(restoredSession) : null;
} }
} }
private readPersistedSession(): AuthSession | 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; return null;
} }
try { try {
const raw = sessionStorage.getItem(FULL_SESSION_STORAGE_KEY); const raw = store.getItem(FULL_SESSION_STORAGE_KEY);
if (!raw) { if (!raw) {
return null; return null;
} }
const parsed = JSON.parse(raw) as AuthSession; const parsed = JSON.parse(raw) as AuthSession;
if (!this.isValidSession(parsed)) { if (!this.isValidSession(parsed)) {
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY); store.removeItem(FULL_SESSION_STORAGE_KEY);
return null; return null;
} }
if (parsed.tokens.expiresAtEpochMs <= Date.now()) { if (parsed.tokens.expiresAtEpochMs <= Date.now()) {
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY); store.removeItem(FULL_SESSION_STORAGE_KEY);
this.restoredSessionExpired = true; this.restoredSessionExpired = true;
return null; return null;
} }
return parsed; return parsed;
} catch { } catch {
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY); store.removeItem(FULL_SESSION_STORAGE_KEY);
return null; return null;
} }
} }
@@ -262,31 +273,28 @@ export class AuthSessionStore {
} }
private persistMetadata(metadata: PersistedSessionMetadata): void { private persistMetadata(metadata: PersistedSessionMetadata): void {
if (typeof sessionStorage === 'undefined') { if (typeof localStorage === 'undefined') {
return; return;
} }
sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(metadata)); localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(metadata));
} }
private persistSession(session: AuthSession): void { private persistSession(session: AuthSession): void {
if (typeof sessionStorage === 'undefined') { const json = JSON.stringify(session);
return; try { sessionStorage.setItem(FULL_SESSION_STORAGE_KEY, json); } catch { /* SSR / quota */ }
} try { localStorage.setItem(FULL_SESSION_STORAGE_KEY, json); } catch { /* SSR / quota */ }
sessionStorage.setItem(FULL_SESSION_STORAGE_KEY, JSON.stringify(session));
} }
private clearPersistedMetadata(): void { private clearPersistedMetadata(): void {
if (typeof sessionStorage === 'undefined') { if (typeof localStorage === 'undefined') {
return; return;
} }
sessionStorage.removeItem(SESSION_STORAGE_KEY); localStorage.removeItem(SESSION_STORAGE_KEY);
} }
private clearPersistedSession(): void { private clearPersistedSession(): void {
if (typeof sessionStorage === 'undefined') { try { sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY); } catch { /* SSR */ }
return; try { localStorage.removeItem(FULL_SESSION_STORAGE_KEY); } catch { /* SSR */ }
}
sessionStorage.removeItem(FULL_SESSION_STORAGE_KEY);
} }
getActiveTenantId(): string | null { getActiveTenantId(): string | null {