fix(auth): throttle session validation, typed auth-change events, misc fixes
- rstack-identity.ts: throttle server session validation to every 5min, add reason detail to all auth-change events (signin/signout/revoked/ refresh/persona-switch), remove redundant location.reload on signout - shell.ts: skip UI side-effects on token refresh, only redirect home on genuine signout/revocation - server.ts: add PUT to CORS allowMethods - folk-inbox-client.ts: pass auth token on mailbox API fetch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
32be9d7b94
commit
8071b620e1
|
|
@ -182,7 +182,10 @@ class FolkInboxClient extends HTMLElement {
|
||||||
private async loadMailboxes() {
|
private async loadMailboxes() {
|
||||||
try {
|
try {
|
||||||
const base = window.location.pathname.replace(/\/$/, "");
|
const base = window.location.pathname.replace(/\/$/, "");
|
||||||
const resp = await fetch(`${base}/api/mailboxes`);
|
const token = getAccessToken();
|
||||||
|
const resp = await fetch(`${base}/api/mailboxes`, {
|
||||||
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
|
});
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
this.mailboxes = data.mailboxes || [];
|
this.mailboxes = data.mailboxes || [];
|
||||||
|
|
|
||||||
|
|
@ -394,18 +394,28 @@ export class RStackIdentity extends HTMLElement {
|
||||||
async #validateSessionWithServer() {
|
async #validateSessionWithServer() {
|
||||||
const session = getSession();
|
const session = getSession();
|
||||||
if (!session?.accessToken) return;
|
if (!session?.accessToken) return;
|
||||||
|
|
||||||
|
// Throttle: skip if validated within the last 5 minutes
|
||||||
|
const VALIDATE_KEY = "eid_last_validated";
|
||||||
|
const VALIDATE_INTERVAL = 5 * 60 * 1000;
|
||||||
|
const lastValidated = parseInt(localStorage.getItem(VALIDATE_KEY) || "0", 10);
|
||||||
|
if (Date.now() - lastValidated < VALIDATE_INTERVAL) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${ENCRYPTID_URL}/api/session/verify`, {
|
const res = await fetch(`${ENCRYPTID_URL}/api/session/verify`, {
|
||||||
headers: { Authorization: `Bearer ${session.accessToken}` },
|
headers: { Authorization: `Bearer ${session.accessToken}` },
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
// Session revoked — clear locally and re-render
|
// Session revoked — clear locally and re-render
|
||||||
|
localStorage.removeItem(VALIDATE_KEY);
|
||||||
localStorage.removeItem(SESSION_KEY);
|
localStorage.removeItem(SESSION_KEY);
|
||||||
localStorage.removeItem("rspace-username");
|
localStorage.removeItem("rspace-username");
|
||||||
_removeSessionCookie();
|
_removeSessionCookie();
|
||||||
resetDocBridge();
|
resetDocBridge();
|
||||||
this.#render();
|
this.#render();
|
||||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "revoked" } }));
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(VALIDATE_KEY, String(Date.now()));
|
||||||
}
|
}
|
||||||
} catch { /* network error — let token expire naturally */ }
|
} catch { /* network error — let token expire naturally */ }
|
||||||
}
|
}
|
||||||
|
|
@ -548,7 +558,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
if (e.key === "encryptid_session" || e.key === PERSONAS_KEY) {
|
if (e.key === "encryptid_session" || e.key === PERSONAS_KEY) {
|
||||||
this.#render();
|
this.#render();
|
||||||
if (e.key === "encryptid_session") {
|
if (e.key === "encryptid_session") {
|
||||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: e.newValue ? "signin" : "signout" } }));
|
||||||
// Session cleared from another tab — reload to show logged-out state
|
// Session cleared from another tab — reload to show logged-out state
|
||||||
if (!e.newValue) window.location.reload();
|
if (!e.newValue) window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
@ -595,7 +605,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
const payload = parseJWT(newToken);
|
const payload = parseJWT(newToken);
|
||||||
storeSession(newToken, (payload.username as string) || refreshData.username || username, (payload.did as string) || did);
|
storeSession(newToken, (payload.username as string) || refreshData.username || username, (payload.did as string) || did);
|
||||||
this.#render();
|
this.#render();
|
||||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "refresh" } }));
|
||||||
}
|
}
|
||||||
} catch { /* offline — keep whatever we have */ }
|
} catch { /* offline — keep whatever we have */ }
|
||||||
}
|
}
|
||||||
|
|
@ -684,9 +694,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
if (action === "signout") {
|
if (action === "signout") {
|
||||||
clearSession();
|
clearSession();
|
||||||
resetDocBridge();
|
resetDocBridge();
|
||||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "signout" } }));
|
||||||
// Reload so the server re-renders the current rApp in logged-out mode
|
|
||||||
window.location.reload();
|
|
||||||
return;
|
return;
|
||||||
} else if (action === "my-account") {
|
} else if (action === "my-account") {
|
||||||
this.showAccountModal();
|
this.showAccountModal();
|
||||||
|
|
@ -715,7 +723,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
clearSession();
|
clearSession();
|
||||||
resetDocBridge();
|
resetDocBridge();
|
||||||
this.#render();
|
this.#render();
|
||||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "persona-switch" } }));
|
||||||
this.showAuthModal();
|
this.showAuthModal();
|
||||||
} else if (action === "remove-persona") {
|
} else if (action === "remove-persona") {
|
||||||
const targetDid = (el as HTMLElement).dataset.did || "";
|
const targetDid = (el as HTMLElement).dataset.did || "";
|
||||||
|
|
@ -879,7 +887,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
storeSession(data.token, data.username || "", data.did || "");
|
storeSession(data.token, data.username || "", data.did || "");
|
||||||
close();
|
close();
|
||||||
this.#render();
|
this.#render();
|
||||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "signin" } }));
|
||||||
callbacks?.onSuccess?.();
|
callbacks?.onSuccess?.();
|
||||||
// Auto-redirect to personal space
|
// Auto-redirect to personal space
|
||||||
autoResolveSpace(data.token, data.username || "");
|
autoResolveSpace(data.token, data.username || "");
|
||||||
|
|
@ -956,7 +964,7 @@ export class RStackIdentity extends HTMLElement {
|
||||||
storeSession(data.token, username, data.did || "");
|
storeSession(data.token, username, data.did || "");
|
||||||
close();
|
close();
|
||||||
this.#render();
|
this.#render();
|
||||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "signin" } }));
|
||||||
callbacks?.onSuccess?.();
|
callbacks?.onSuccess?.();
|
||||||
// Show post-signup prompt recommending second device before redirecting
|
// Show post-signup prompt recommending second device before redirecting
|
||||||
this.#showPostSignupPrompt(data.token, username);
|
this.#showPostSignupPrompt(data.token, username);
|
||||||
|
|
|
||||||
|
|
@ -461,7 +461,7 @@ app.use('*', cors({
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
allowHeaders: ['Content-Type', 'Authorization'],
|
allowHeaders: ['Content-Type', 'Authorization'],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -117,13 +117,18 @@ if (spaceSlug) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload space list when user signs in/out (to show/hide private spaces)
|
// Reload space list when user signs in/out (to show/hide private spaces)
|
||||||
document.addEventListener("auth-change", () => {
|
document.addEventListener("auth-change", (e) => {
|
||||||
|
const reason = (e as CustomEvent).detail?.reason;
|
||||||
|
|
||||||
|
// Token refreshes are invisible — no UI side-effects needed
|
||||||
|
if (reason === "refresh") return;
|
||||||
|
|
||||||
|
// Reload space switcher on state-changing events
|
||||||
const spaceSwitcher = document.querySelector("rstack-space-switcher") as any;
|
const spaceSwitcher = document.querySelector("rstack-space-switcher") as any;
|
||||||
spaceSwitcher?.reload?.();
|
spaceSwitcher?.reload?.();
|
||||||
|
|
||||||
// If signed out, redirect to homepage
|
// Only redirect to homepage on genuine sign-out or server revocation
|
||||||
const session = localStorage.getItem("encryptid_session");
|
if (reason === "signout" || reason === "revoked") {
|
||||||
if (!session) {
|
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue