fix(auth): wire cross-session logout through rspace-header

The actual logout UI is in rspace-header.ts (not the encryptid login
button component). clearSession() now calls /api/session/logout, and
on page load the header validates the session with the server to detect
revocation from another browser session.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-24 10:40:23 -07:00
parent aec42ef976
commit bfbf1d14cd
1 changed files with 33 additions and 0 deletions

View File

@ -47,7 +47,22 @@ function getSession(): SessionState | null {
} }
} }
function clearSession(): void { function clearSession(): void {
// Notify server so all other browser sessions get revoked
try {
const stored = localStorage.getItem(SESSION_KEY);
if (stored) {
const session = JSON.parse(stored) as SessionState;
if (session.accessToken) {
fetch(`${ENCRYPTID_URL}/api/session/logout`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${session.accessToken}` },
}).catch(() => { /* best-effort */ });
}
}
} catch { /* ignore */ }
localStorage.removeItem(SESSION_KEY); localStorage.removeItem(SESSION_KEY);
localStorage.removeItem('rspace-username'); localStorage.removeItem('rspace-username');
} }
@ -921,6 +936,24 @@ export function mountHeader(options: HeaderOptions): void {
} }
} catch { /* silently ignore */ } } catch { /* silently ignore */ }
})(); })();
// Validate session with server — detects logout from another browser session
(async () => {
const session = getSession();
if (!session) return;
try {
const res = await fetch(`${ENCRYPTID_URL}/api/session/verify`, {
headers: { 'Authorization': `Bearer ${session.accessToken}` },
});
if (!res.ok) {
// Session revoked or invalid — clear locally and re-render
localStorage.removeItem(SESSION_KEY);
localStorage.removeItem('rspace-username');
renderHeader();
onAuthChange?.();
}
} catch { /* network error — let token expire naturally */ }
})();
} }
/** /**