This commit is contained in:
Mateusz Gruszczyński committed 2026-10-26 15:47:16 +01:00
1 parent f508b7c882
commit 25d04daaee
12 files changed
+323 -90

No files matched your search

+20 -23
View File
@@ -8,8 +8,8 @@
*/
import { logDebug, logError, logWarn } from "@rustpad/logger";
import { formatNumber, restoreGuestLanguage, translateApiMessage } from "@rustpad/i18n";
import { restoreGuestTheme } from "@rustpad/theme";
import { formatNumber, translateApiMessage } from "@rustpad/i18n";
import { expireAuthSession, getAuthToken } from "@rustpad/session";
const DEFAULT_ERRORS = {
400: "Invalid request.",
@@ -65,29 +65,26 @@ function formatBytes(bytes) {
return `${formatNumber(bytes)} B`;
}
function clearExpiredSession() {
localStorage.removeItem("rustpad:auth-state");
localStorage.removeItem("rustpad:auth-token");
sessionStorage.removeItem("rustpad:auth-token");
localStorage.removeItem("rustpad:nickname");
sessionStorage.removeItem("rustpad:nickname");
document.cookie = "rustpad_nickname=; Path=/; SameSite=Lax; Max-Age=0";
restoreGuestTheme();
void restoreGuestLanguage();
window.dispatchEvent(new CustomEvent("rustpad:session-expired"));
window.dispatchEvent(new CustomEvent("rustpad:session-change", { detail: { session: null } }));
}
// Multiple simultaneous 401s must not start multiple session checks.
let sessionCheckPromise = null;
async function clearSessionIfInvalid() {
if (!localStorage.getItem("rustpad:auth-state")) return;
try {
const response = await fetch("/api/auth/me", {
signal: AbortSignal.timeout(5000),
});
if (response.status === 401) clearExpiredSession();
} catch {
// A network failure does not prove that the session is invalid.
if (!getAuthToken()) return;
if (!sessionCheckPromise) {
sessionCheckPromise = (async () => {
const response = await fetch("/api/auth/session", {
credentials: "same-origin",
cache: "no-store",
signal: AbortSignal.timeout(5000),
});
// A failed check is not proof that the user is logged out.
if (!response.ok) return;
const data = await response.json();
if (data.authenticated === false && data.session === null) await expireAuthSession();
})().catch(() => {
// Network and parsing errors must not erase the saved session.
}).finally(() => { sessionCheckPromise = null; });
}
await sessionCheckPromise;
}
function validateUploadSize(body, configuredMaxBytes) {
+9 -8
View File
@@ -12,7 +12,7 @@ import * as sessionStore from "@rustpad/session";
import { askConfirm, askInput, showMessage } from "@rustpad/modal";
import { toast } from "@rustpad/toast";
const { getAuthToken, setAuthSession, setNickname } = sessionStore;
const { getAuthToken, setAuthSession, setNickname, expireAuthSession } = sessionStore;
const clearAuthSession = sessionStore.clearAuthSession || (() => {
localStorage.removeItem("rustpad:auth-state");
localStorage.removeItem("rustpad:auth-token");
@@ -343,15 +343,16 @@ function bindLegacyIdentityDialog({ dialog, onIdentity }) {
}
export async function validateCurrentSession() {
const expectedSession = Boolean(getAuthToken());
try {
const session = await api("/api/auth/me");
await setAuthSession(session);
return session;
} catch {
if (expectedSession) await clearAuthSession();
const result = await api("/api/auth/session", { cache: "no-store" });
if (result.authenticated === false && result.session === null) {
await expireAuthSession();
return null;
}
if (result.authenticated !== true || !result.session || typeof result.session.nickname !== "string") {
throw new Error("Invalid authentication session response.");
}
await setAuthSession(result.session);
return result.session;
}
export async function logoutCurrentSession() {
+7 -1
View File
@@ -618,6 +618,12 @@ if (identityDialog) {
})
.catch(error => {
console.warn("Could not validate the saved session", error);
if (accountStateRevision === lookupRevision) renderAccount(null);
if (accountStateRevision !== lookupRevision) return;
if (accountSlot) {
accountSlot.dataset.state = "error";
accountSlot.setAttribute("aria-busy", "false");
}
const retry = document.querySelector("#footer-account-retry");
if (retry) retry.hidden = false;
});
}
+10 -1
View File
@@ -93,6 +93,11 @@ export async function setAuthSession(session) {
await setLanguage(session?.language || "en", { scope: "account" });
window.dispatchEvent(new CustomEvent("rustpad:session-change", { detail: { session } }));
}
export async function expireAuthSession() {
if (!getAuthToken()) return;
await clearAuthSession();
window.dispatchEvent(new CustomEvent("rustpad:session-expired"));
}
export async function clearAuthSession() {
localStorage.removeItem(AUTH_STATE_KEY);
localStorage.removeItem("rustpad:auth-token");
@@ -101,6 +106,10 @@ export async function clearAuthSession() {
sessionStorage.removeItem(NICKNAME_KEY);
setNicknameCookie("");
restoreGuestTheme();
await restoreGuestLanguage();
try {
await restoreGuestLanguage();
} catch {
// A language bundle failure must not block logout or session-expiry events.
}
window.dispatchEvent(new CustomEvent("rustpad:session-change", { detail: { session: null } }));
}
+11 -5
View File
@@ -266,11 +266,17 @@ async function toggleNoteFavorite(button) {
{ title: t("favorites.title", {}, "Favorites") },
);
} catch (error) {
if (error.status === 401 || error.status === 403) {
const session = await validateCurrentSession();
if (!session) {
accountSession = null;
renderNotes();
// The API client already rechecks the session after a 401.
// Only a 403 needs an additional check for an unrelated access restriction.
if (error.status === 403) {
try {
const session = await validateCurrentSession();
if (!session) {
accountSession = null;
renderNotes();
}
} catch {
// A failed recheck is not evidence that the account session expired.
}
}
toast.danger(error.message, { title: t("favorites.updateFailed", {}, "Could not update favorites") });