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() {
|
||||
try {
|
||||
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) {
|
||||
const data = await resp.json();
|
||||
this.mailboxes = data.mailboxes || [];
|
||||
|
|
|
|||
|
|
@ -394,18 +394,28 @@ export class RStackIdentity extends HTMLElement {
|
|||
async #validateSessionWithServer() {
|
||||
const session = getSession();
|
||||
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 {
|
||||
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(VALIDATE_KEY);
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
localStorage.removeItem("rspace-username");
|
||||
_removeSessionCookie();
|
||||
resetDocBridge();
|
||||
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 */ }
|
||||
}
|
||||
|
|
@ -548,7 +558,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
if (e.key === "encryptid_session" || e.key === PERSONAS_KEY) {
|
||||
this.#render();
|
||||
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
|
||||
if (!e.newValue) window.location.reload();
|
||||
}
|
||||
|
|
@ -595,7 +605,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
const payload = parseJWT(newToken);
|
||||
storeSession(newToken, (payload.username as string) || refreshData.username || username, (payload.did as string) || did);
|
||||
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 */ }
|
||||
}
|
||||
|
|
@ -684,9 +694,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
if (action === "signout") {
|
||||
clearSession();
|
||||
resetDocBridge();
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
||||
// Reload so the server re-renders the current rApp in logged-out mode
|
||||
window.location.reload();
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "signout" } }));
|
||||
return;
|
||||
} else if (action === "my-account") {
|
||||
this.showAccountModal();
|
||||
|
|
@ -715,7 +723,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
clearSession();
|
||||
resetDocBridge();
|
||||
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();
|
||||
} else if (action === "remove-persona") {
|
||||
const targetDid = (el as HTMLElement).dataset.did || "";
|
||||
|
|
@ -879,7 +887,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
storeSession(data.token, data.username || "", data.did || "");
|
||||
close();
|
||||
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?.();
|
||||
// Auto-redirect to personal space
|
||||
autoResolveSpace(data.token, data.username || "");
|
||||
|
|
@ -956,7 +964,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
storeSession(data.token, username, data.did || "");
|
||||
close();
|
||||
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?.();
|
||||
// Show post-signup prompt recommending second device before redirecting
|
||||
this.#showPostSignupPrompt(data.token, username);
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ app.use('*', cors({
|
|||
}
|
||||
return undefined;
|
||||
},
|
||||
allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -117,13 +117,18 @@ if (spaceSlug) {
|
|||
}
|
||||
|
||||
// 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;
|
||||
spaceSwitcher?.reload?.();
|
||||
|
||||
// If signed out, redirect to homepage
|
||||
const session = localStorage.getItem("encryptid_session");
|
||||
if (!session) {
|
||||
// Only redirect to homepage on genuine sign-out or server revocation
|
||||
if (reason === "signout" || reason === "revoked") {
|
||||
window.location.href = "/";
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue