From 4722aca065f6caf0a749d2abad0f54f0b08e7a89 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 24 Mar 2026 10:46:44 -0700 Subject: [PATCH] fix(auth): wire cross-session logout in rstack-identity + encryptid profile rstack-identity is the actual sign-out component used in production. clearSession() now calls /api/session/logout, and connectedCallback validates the session with the server to detect revocation. Also updated the auth.rspace.online profile page handleLogout(). Co-Authored-By: Claude Opus 4.6 --- shared/components/rstack-identity.ts | 36 ++++++++++++++++++++++++++++ src/encryptid/server.ts | 8 +++++++ 2 files changed, 44 insertions(+) diff --git a/shared/components/rstack-identity.ts b/shared/components/rstack-identity.ts index 3ee548d..bf2c630 100644 --- a/shared/components/rstack-identity.ts +++ b/shared/components/rstack-identity.ts @@ -157,6 +157,20 @@ export function getSession(): SessionState | null { } export 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("rspace-username"); _removeSessionCookie(); @@ -352,6 +366,9 @@ export class RStackIdentity extends HTMLElement { autoProvisionSpace(session.accessToken); } + // Validate session with server — detects logout from another browser session + this.#validateSessionWithServer(); + // Propagate login/logout across tabs via storage events window.addEventListener("storage", this.#onStorageChange); } @@ -360,6 +377,25 @@ export class RStackIdentity extends HTMLElement { window.removeEventListener("storage", this.#onStorageChange); } + async #validateSessionWithServer() { + const session = getSession(); + if (!session?.accessToken) return; + try { + const res = await fetch(`${ENCRYPTID_URL}/api/session/verify`, { + headers: { Authorization: `Bearer ${session.accessToken}` }, + }); + if (!res.ok) { + // Session revoked — clear locally and re-render + localStorage.removeItem(SESSION_KEY); + localStorage.removeItem("rspace-username"); + _removeSessionCookie(); + resetDocBridge(); + this.#render(); + this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true })); + } + } catch { /* network error — let token expire naturally */ } + } + #onStorageChange = (e: StorageEvent) => { if (e.key === "encryptid_session" || e.key === PERSONAS_KEY) { this.#render(); diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index 9fb35a9..06a977e 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -7958,6 +7958,14 @@ app.get('/', (c) => { } window.handleLogout = () => { + // Notify server so other browser sessions are revoked + const token = localStorage.getItem(TOKEN_KEY); + if (token) { + fetch('/api/session/logout', { + method: 'POST', + headers: { Authorization: 'Bearer ' + token }, + }).catch(() => {}); + } localStorage.removeItem(TOKEN_KEY); document.getElementById('auth-form').style.display = 'block'; document.getElementById('profile').style.display = 'none';