security upgrade

This commit is contained in:
Mateusz Gruszczyński
2026-07-30 09:57:43 +02:00
parent 2274cf57c9
commit f6b8e016a8
24 changed files with 1100 additions and 272 deletions
+3 -5
View File
@@ -33,6 +33,7 @@ function formatBytes(bytes) {
}
function clearExpiredSession() {
localStorage.removeItem("rustpad:auth-state");
localStorage.removeItem("rustpad:auth-token");
sessionStorage.removeItem("rustpad:auth-token");
localStorage.removeItem("rustpad:nickname");
@@ -42,11 +43,9 @@ function clearExpiredSession() {
}
async function clearSessionIfInvalid() {
const token = localStorage.getItem("rustpad:auth-token") || sessionStorage.getItem("rustpad:auth-token");
if (!token) return;
if (!localStorage.getItem("rustpad:auth-state")) return;
try {
const response = await fetch("/api/auth/me", {
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(5000),
});
if (response.status === 401) clearExpiredSession();
@@ -70,8 +69,7 @@ function validateUploadSize(body) {
function requestHeaders(options, body) {
const headers = new Headers(options.headers || {});
const authToken = localStorage.getItem("rustpad:auth-token") || sessionStorage.getItem("rustpad:auth-token");
if (authToken && !headers.has("authorization")) headers.set("authorization", `Bearer ${authToken}`);
headers.delete("x-rustpad-user-token");
if (!(body instanceof FormData) && !headers.has("content-type")) headers.set("content-type", "application/json");
return headers;
}
+9 -11
View File
@@ -13,8 +13,10 @@ import { askConfirm, askInput, showMessage } from "@rustpad/modal";
const { getAuthToken, setAuthSession, setNickname } = sessionStore;
const clearAuthSession = sessionStore.clearAuthSession || (() => {
localStorage.removeItem("rustpad:auth-state");
localStorage.removeItem("rustpad:auth-token");
sessionStorage.removeItem("rustpad:auth-token");
localStorage.removeItem("rustpad:nickname");
sessionStorage.removeItem("rustpad:nickname");
});
@@ -287,7 +289,7 @@ function bindLegacyIdentityDialog({ dialog, onIdentity }) {
return;
}
nickname.disabled = false;
const result = await api("/api/auth/identity", { method: "POST", body: JSON.stringify({ nickname: name, session_token: getAuthToken() || null }) });
const result = await api("/api/auth/identity", { method: "POST", body: JSON.stringify({ nickname: name }) });
setNickname(result.nickname);
await onIdentity(result.nickname, null);
dialog.close();
@@ -305,25 +307,21 @@ function bindLegacyIdentityDialog({ dialog, onIdentity }) {
}
export async function validateCurrentSession() {
const token = getAuthToken();
if (!token) return null;
const expectedSession = Boolean(getAuthToken());
try {
const session = await api("/api/auth/me", { headers: { Authorization: `Bearer ${token}` } });
const session = await api("/api/auth/me");
setAuthSession(session);
return session;
} catch {
clearAuthSession();
if (expectedSession) clearAuthSession();
return null;
}
}
export async function logoutCurrentSession() {
const token = getAuthToken();
if (token) {
try {
await api("/api/auth/logout", { method: "POST", headers: { Authorization: `Bearer ${token}` } });
} catch { }
}
try {
await api("/api/auth/logout", { method: "POST" });
} catch { }
clearAuthSession();
}
+3 -3
View File
@@ -62,7 +62,7 @@ document.querySelector("#pad-form").addEventListener("submit", async (event) =>
const payload = { name: name.value.trim() };
if (password.value) payload.password = password.value;
const result = await api("/api/pads", { method: "POST", headers: authHeaders(), body: JSON.stringify(payload) });
if (password.value) { const grant = await api("/api/access-token", { method: "POST", body: JSON.stringify({ kind: "pad", slug: result.slug, password: password.value }) }); setAccessToken("pad", result.slug, grant.access_token); }
if (password.value) { const grant = await api("/api/access-token", { method: "POST", body: JSON.stringify({ kind: "pad", slug: result.slug, password: password.value }) }); setAccessToken("pad", result.slug, grant.granted); }
window.location.assign(safeAppUrl(`${result.url}?view=split&mode=markdown`));
} catch (requestError) {
error.textContent = requestError.message;
@@ -83,7 +83,7 @@ document.querySelector("#workspace-form").addEventListener("submit", async (even
const payload = { name: name.value.trim() };
if (password.value) payload.password = password.value;
const result = await api("/api/workspaces", { method: "POST", headers: authHeaders(), body: JSON.stringify(payload) });
if (password.value) { const grant = await api("/api/access-token", { method: "POST", body: JSON.stringify({ kind: "workspace", slug: result.slug, password: password.value }) }); setAccessToken("workspace", result.slug, grant.access_token); }
if (password.value) { const grant = await api("/api/access-token", { method: "POST", body: JSON.stringify({ kind: "workspace", slug: result.slug, password: password.value }) }); setAccessToken("workspace", result.slug, grant.granted); }
window.location.assign(safeAppUrl(result.url));
} catch (requestError) {
error.textContent = requestError.message;
@@ -110,7 +110,7 @@ const profileDialog = document.querySelector("#profile-dialog");
const profileForm = document.querySelector("#profile-form");
let currentSession = null;
function authHeaders() { const token = getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
function authHeaders() { return {}; }
function escapeHtml(value) { const node = document.createElement("div"); node.textContent = String(value ?? ""); return node.innerHTML; }
function shareExpiry(hours, forever) { if (forever) return null; const value = Number(hours); if (!Number.isFinite(value) || value <= 0 || value > 87600) throw new Error("Enter a validity between 1 and 87600 hours."); return new Date(Date.now() + value * 3600000).toISOString(); }
function formatShareExpiry(value) { if (!value) return "Never expires"; const date = new Date(value); return Number.isNaN(date.getTime()) ? value : `Expires ${date.toLocaleString()}`; }
+12 -16
View File
@@ -29,7 +29,7 @@ export function startNoteEditor(adapter) {
let unreadChat = 0;
const compactToggle = document.querySelector("#compact-toggle"), lineLinksToggle = document.querySelector("#line-links-toggle"), authorshipColorsToggle = document.querySelector("#authorship-colors-toggle"), authorshipColorsLabel = document.querySelector("#authorship-colors-label"), publicPageEnabled = document.querySelector("#public-page-enabled"), publicTaskUpdates = document.querySelector("#public-task-updates"), unprotectPublicPage = document.querySelector("#unprotect-public-page"), participantBadges = document.querySelector("#participant-badges"), fontFamily = document.querySelector("#font-family"), fontSize = document.querySelector("#font-size"), currentUser = document.querySelector("#current-user"), userColorPicker = document.querySelector("#user-color-picker"), mobileColorPicker = document.querySelector("#mobile-color-picker"), useGlobalColorButton = document.querySelector("#use-global-color");
const mobileFontFamily = document.querySelector("#mobile-font-family"), mobileFontSize = document.querySelector("#mobile-font-size"), mobileLineToggle = document.querySelector("#mobile-line-numbers-toggle"), mobilePreviewLineToggle = document.querySelector("#mobile-preview-line-numbers-toggle"), mobileCompactToggle = document.querySelector("#mobile-compact-toggle"), mobileLineLinksToggle = document.querySelector("#mobile-line-links-toggle");
const shareToken = new URLSearchParams(location.search).get("share"); if (shareToken) setAccessToken(adapter.access.kind, adapter.access.key, shareToken);
const shareToken = new URLSearchParams(location.search).get("share");
const notePreferenceKey = name => `rustpad:${name}:${location.pathname}`;
let accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = "";
let editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false;
@@ -113,13 +113,11 @@ export function startNoteEditor(adapter) {
}
function updateCurrentUser() { const color = currentUserColor() || defaultColorFor(nickname); const pickerColor = /^#[0-9a-f]{6}$/i.test(color) ? color : "#7c6cff"; const overridden = Boolean(noteUserColor()); currentUser.querySelector(".user-chip__name").textContent = nickname; currentUser.style.setProperty("--owner", color); currentUser.title = overridden ? "Note color override" : "Global profile color"; userColorPicker.value = pickerColor; if (mobileColorPicker) mobileColorPicker.value = pickerColor; useGlobalColorButton.hidden = !overridden; document.querySelector(".mobile-editor-bubble")?.style.setProperty("--owner", color); }
function sessionHeaders() {
const userToken = getAuthToken();
const resourceToken = accessToken || userToken;
const headers = resourceToken ? { Authorization: `Bearer ${resourceToken}` } : {};
if (userToken && userToken !== resourceToken) headers["X-RustPad-User-Token"] = userToken;
return headers;
return accessToken && accessToken !== "cookie"
? { Authorization: `Bearer ${accessToken}` }
: {};
}
function accountHeaders() { const token = getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
function accountHeaders() { return {}; }
async function loadNoteInfo() {
info = await adapter.loadInfo(sessionHeaders());
globalColor = info.global_color || ""; noteColor = info.note_color || "";
@@ -619,8 +617,8 @@ export function startNoteEditor(adapter) {
onFilesChanged: files => updateMarkdownFiles(files, { rerender: true }),
});
refreshFilesForAliases = () => loadFiles();
function connect() { socket?.stop(); socket = adapter.createSocket({ password, accessToken, nickname, color: currentUserColor() || null, sessionToken: getAuthToken(), guestId: getGuestId(), onStatus: s => setStatus(s === "online" ? "online" : s === "offline" ? "offline" : null, s === "online" ? "Connected" : s === "offline" ? "Reconnecting…" : "Connecting…"), onAuthenticated: m => { resourceUnlocked = true; if (passwordDialog.open) passwordDialog.close(); const readOnly = m.access_level === "read_only"; editor.readOnly = readOnly; accessLevel.textContent = readOnly ? "Access: read only" : "Access: full"; applyRemote(m.content, m.owner_map); if (!readOnly) editor.focus(); }, onDocument: m => { applyRemote(m.content, m.owner_map); document.querySelector("#save-state").textContent = `${m.author ? `${m.author} · ` : ""}${new Date(m.updated_at).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`; }, onPresence: updatePresence, onLatency: updateLatency, onChat: appendChatMessage, onError: m => { const friendly = /read-only access/i.test(m) ? "This note is read only. Enter the password or ask the owner to grant write access." : m; document.querySelector("#password-error").textContent = friendly; if (/read-only access/i.test(m)) { toast(friendly); accessLevel.textContent = "Access: read only"; editor.readOnly = true; return; } if (/nickname|session|account/i.test(m)) { if (!identityDialog.open) identityDialog.showModal(); } else if (info?.protected && !passwordDialog.open) passwordDialog.showModal(); } }); socket.connect(); }
bindIdentityDialog({ dialog: identityDialog, onIdentity: async value => { nickname = value; accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key) || getAuthToken(); identityDialog.close(); updateCurrentUser(); await loadNoteInfo(); if (info.protected && !accessToken) passwordDialog.showModal(); else { loadFiles(); connect(); } } });
function connect() { socket?.stop(); socket = adapter.createSocket({ password, accessToken, nickname, color: currentUserColor() || null, sessionToken: null, guestId: getGuestId(), onStatus: s => setStatus(s === "online" ? "online" : s === "offline" ? "offline" : null, s === "online" ? "Connected" : s === "offline" ? "Reconnecting…" : "Connecting…"), onAuthenticated: m => { resourceUnlocked = true; if (passwordDialog.open) passwordDialog.close(); const readOnly = m.access_level === "read_only"; editor.readOnly = readOnly; accessLevel.textContent = readOnly ? "Access: read only" : "Access: full"; applyRemote(m.content, m.owner_map); if (!readOnly) editor.focus(); }, onDocument: m => { applyRemote(m.content, m.owner_map); document.querySelector("#save-state").textContent = `${m.author ? `${m.author} · ` : ""}${new Date(m.updated_at).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`; }, onPresence: updatePresence, onLatency: updateLatency, onChat: appendChatMessage, onError: m => { const friendly = /read-only access/i.test(m) ? "This note is read only. Enter the password or ask the owner to grant write access." : m; document.querySelector("#password-error").textContent = friendly; if (/read-only access/i.test(m)) { toast(friendly); accessLevel.textContent = "Access: read only"; editor.readOnly = true; return; } if (/nickname|session|account/i.test(m)) { if (!identityDialog.open) identityDialog.showModal(); } else if (info?.protected && !passwordDialog.open) passwordDialog.showModal(); } }); socket.connect(); }
bindIdentityDialog({ dialog: identityDialog, onIdentity: async value => { nickname = value; accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key); identityDialog.close(); updateCurrentUser(); await loadNoteInfo(); if (info.protected && !accessToken && !getAuthToken()) passwordDialog.showModal(); else { loadFiles(); connect(); } } });
identityDialog.addEventListener("close", () => { if (!nickname) queueMicrotask(() => { if (!identityDialog.open) identityDialog.showModal(); }); });
async function showSystemNotFound() {
try {
@@ -639,23 +637,21 @@ export function startNoteEditor(adapter) {
async function initialize() {
try {
if (getAuthToken()) {
const session = await validateCurrentSession();
nickname = session?.nickname || getNickname();
}
const session = await validateCurrentSession();
nickname = session?.nickname || getNickname();
if (!nickname) {
if (!identityDialog.open) identityDialog.showModal();
return;
}
accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key) || getAuthToken();
accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key);
await loadNoteInfo();
document.title = adapter.title(info);
publicPageEnabled.checked = Boolean(info.public_page_enabled); publicTaskUpdates.checked = Boolean(info.allow_public_task_updates); unprotectPublicPage.checked = Boolean(info.public_page_unprotected); updatePageControls();
adapter.configureView?.(info);
applyUi({ write: true, replace: true });
updateCurrentUser();
if (info.protected && !accessToken) passwordDialog.showModal();
if (info.protected && !accessToken && !getAuthToken()) passwordDialog.showModal();
else { loadFiles(); connect(); }
} catch (e) {
if (e.status === 403 || e.status === 404) {
@@ -1145,7 +1141,7 @@ export function startNoteEditor(adapter) {
document.querySelector("#open-password")?.focus();
}
});
document.querySelector("#password-form").addEventListener("submit", async e => { e.preventDefault(); try { password = document.querySelector("#open-password").value; const result = await adapter.requestAccess(password); accessToken = result.access_token; setAccessToken(adapter.access.kind, adapter.access.key, accessToken); password = ""; document.querySelector("#open-password").value = ""; document.querySelector("#password-error").textContent = ""; loadFiles(); connect(); } catch (error) { document.querySelector("#password-error").textContent = error.message; } });
document.querySelector("#password-form").addEventListener("submit", async e => { e.preventDefault(); try { password = document.querySelector("#open-password").value; const result = await adapter.requestAccess(password); setAccessToken(adapter.access.kind, adapter.access.key, result.granted); accessToken = getAccessToken(adapter.access.kind, adapter.access.key); password = ""; document.querySelector("#open-password").value = ""; document.querySelector("#password-error").textContent = ""; loadFiles(); connect(); } catch (error) { document.querySelector("#password-error").textContent = error.message; } });
const historyPanel = document.querySelector("#history-panel"); document.querySelector("#history-button").addEventListener("click", async () => { if (info?.protected && !resourceUnlocked) { if (!passwordDialog.open) passwordDialog.showModal(); document.querySelector("#open-password")?.focus(); return; } historyPanel.classList.add("open"); historyPanel.setAttribute("aria-hidden", "false"); document.body.classList.add("history-open"); const list = document.querySelector("#history-list"); list.innerHTML = '<p class="empty">Loading…</p>'; try { const revisions = await adapter.loadHistory(accessToken); list.innerHTML = revisions.length ? revisions.map((r, i) => { const snippet = escapeHtml(r.content.trim().split("\n").slice(0, 3).join(" · ").slice(0, 150) || "Empty note"); const author = r.author || "Unknown author"; return `<article class="revision"><span class="revision__marker" style="--owner:${colorFor(author)}"></span><div><div class="revision__meta"><strong>${escapeHtml(author)}</strong><time>${formatDate(r.created_at)}</time></div><p class="revision__snippet">${snippet}</p><button data-preview="${r.id}">Preview</button><button data-revision="${r.id}">Restore</button><div class="revision__preview" id="preview-${r.id}" hidden></div></div></article>`; }).join("") : '<p class="empty">No history yet.</p>'; for (const r of revisions) { list.querySelector(`[data-preview="${r.id}"]`)?.addEventListener("click", () => { const el = list.querySelector(`#preview-${r.id}`); el.hidden = !el.hidden; el.textContent = r.content; }); list.querySelector(`[data-revision="${r.id}"]`)?.addEventListener("click", async () => { await adapter.restoreRevision(r.id, accessToken); toast("Version restored"); }); } } catch (e) { list.innerHTML = `<p class="error">${escapeHtml(e.message)}</p>`; } }); document.querySelector("#close-history").addEventListener("click", () => { historyPanel.classList.remove("open"); historyPanel.setAttribute("aria-hidden", "true"); document.body.classList.remove("history-open"); });
const deleteNoteButton = document.querySelector("#delete-note"); if (deleteNoteButton && adapter.deleteNote) deleteNoteButton.addEventListener("click", async () => { try { await adapter.deleteNote(info, accessToken); } catch (error) { toast(error.message); } });
+5 -2
View File
@@ -74,7 +74,10 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
}
}
document.querySelector("#upload-button").addEventListener("click", () => input.click());
document.querySelector("#upload-button").addEventListener("click", () => {
if (!getAuthToken()) { toast("Log in to upload files."); return; }
input.click();
});
input.addEventListener("change", async event => {
let file = event.target.files[0];
if (!file) return;
@@ -154,7 +157,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
if (!deleteButton) return;
if (!await askConfirm(`Delete file "${deleteButton.dataset.fileName}" permanently?`, { title: "Delete file", confirmText: "Delete", danger: true })) return;
try {
await api(endpoints.remove(deleteButton.dataset.deleteFile), { method: "DELETE", headers: getAuthToken() ? { Authorization: `Bearer ${getAuthToken()}` } : {}, body: JSON.stringify({ access_token: getAccessToken() || null }) });
await api(endpoints.remove(deleteButton.dataset.deleteFile), { method: "DELETE", headers: {}, body: JSON.stringify({ access_token: getAccessToken() || null }) });
toast("File deleted");
await loadFiles();
} catch (error) { toast(error.message); }
+2 -3
View File
@@ -19,8 +19,7 @@ const token = location.pathname.split("/").filter(Boolean)[1];
const content = document.querySelector("#public-content");
const lineNumbersToggle = document.querySelector("#public-line-numbers-toggle");
const passwordDialog = document.querySelector("#public-password-dialog"), passwordForm = document.querySelector("#public-password-form"), passwordInput = document.querySelector("#public-password"), passwordError = document.querySelector("#public-password-error");
const passwordKey = `rustpad:public-page-password:${token}`;
let pagePassword = sessionStorage.getItem(passwordKey) || "";
let pagePassword = "";
function pageHeaders() { return pagePassword ? { "X-RustPad-Page-Password": pagePassword } : {}; }
async function renderMermaid() { const nodes = content.querySelectorAll(".mermaid"); if (!nodes.length) return; try { const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"); mermaid.initialize({ startOnLoad: false, theme: "dark", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", '<p class="error">Failed to load Mermaid.</p>')); } }
async function renderCodeHighlight() { const blocks = content.querySelectorAll('pre code[class^="language-"]'); if (!blocks.length) return; try { const hljs = await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm"); blocks.forEach(block => { const lines = block.querySelectorAll(".code-line"); if (!lines.length) { hljs.default.highlightElement(block); return; } const language = [...block.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.default.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.default.highlightAuto(line.textContent).value; } }); block.classList.add("hljs"); }); } catch { } }
@@ -51,7 +50,7 @@ content.addEventListener("click", event => {
window.addEventListener("hashchange", () => scrollToPublicAnchor(location.hash, "smooth"));
content.addEventListener("change", async event => { const box = event.target.closest(".task-checkbox"); if (!box || box.disabled) return; const previous = !box.checked; box.disabled = true; try { const page = await api(`/api/public/${encodeURIComponent(token)}/tasks`, { method: "POST", headers: pageHeaders(), body: JSON.stringify({ source_line: Number(box.dataset.sourceLine), checked: box.checked }) }); document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")} · tasks can be updated`; toast("Task saved"); } catch (error) { box.checked = previous; toast(error.message); } finally { box.disabled = false; } });
lineNumbersToggle.addEventListener("change", () => { document.body.classList.toggle("hide-preview-line-numbers", !lineNumbersToggle.checked); });
passwordForm.addEventListener("submit", async event => { event.preventDefault(); pagePassword = passwordInput.value; sessionStorage.setItem(passwordKey, pagePassword); await initialize(); });
passwordForm.addEventListener("submit", async event => { event.preventDefault(); pagePassword = passwordInput.value; await initialize(); });
passwordDialog.addEventListener("cancel", event => event.preventDefault());
document.querySelector("#copy-public-link").addEventListener("click", async () => { try { await copyText(location.href); toast("Link copied"); } catch (error) { toast(error.message); } });
initialize();
+24 -18
View File
@@ -7,13 +7,23 @@
* See LICENSE file in repository root for details.
*/
const ACCESS_STORAGE_VERSION_KEY = "rustpad:access-storage-version";
const ACCESS_STORAGE_VERSION = "http-only-cookie-v1";
export function accessTokenKey(resourceKind, resourceSlug) { return `rustpad:access:${resourceKind}:${resourceSlug}`; }
export function getAccessToken(resourceKind, resourceSlug) {
return localStorage.getItem(accessTokenKey(resourceKind, resourceSlug)) || "";
if (localStorage.getItem(ACCESS_STORAGE_VERSION_KEY) !== ACCESS_STORAGE_VERSION) {
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
if (key?.startsWith("rustpad:access:")) localStorage.removeItem(key);
}
localStorage.setItem(ACCESS_STORAGE_VERSION_KEY, ACCESS_STORAGE_VERSION);
}
export function setAccessToken(resourceKind, resourceSlug, token) {
export function getAccessToken(resourceKind, resourceSlug) {
return localStorage.getItem(accessTokenKey(resourceKind, resourceSlug)) ? "cookie" : "";
}
export function setAccessToken(resourceKind, resourceSlug, granted) {
const key = accessTokenKey(resourceKind, resourceSlug);
if (token) localStorage.setItem(key, token);
if (granted) localStorage.setItem(key, "1");
else localStorage.removeItem(key);
}
// Remove plaintext passwords saved by the previous frontend version.
@@ -59,28 +69,24 @@ export function getGuestId() {
return guestId;
}
const AUTH_TOKEN_KEY = "rustpad:auth-token";
// Account sessions must be shared by every tab on this origin. Keep a
// compatibility fallback for sessions created by older frontend versions.
const legacySessionToken = sessionStorage.getItem(AUTH_TOKEN_KEY);
if (legacySessionToken && !localStorage.getItem(AUTH_TOKEN_KEY)) {
localStorage.setItem(AUTH_TOKEN_KEY, legacySessionToken);
}
sessionStorage.removeItem(AUTH_TOKEN_KEY);
export function getAuthToken() { return localStorage.getItem(AUTH_TOKEN_KEY) || ""; }
const AUTH_STATE_KEY = "rustpad:auth-state";
// Remove browser-readable account tokens created by older frontend versions.
localStorage.removeItem("rustpad:auth-token");
sessionStorage.removeItem("rustpad:auth-token");
export function getAuthToken() { return localStorage.getItem(AUTH_STATE_KEY) ? "cookie" : ""; }
export function setAuthSession(session) {
localStorage.setItem(AUTH_TOKEN_KEY, session.token);
sessionStorage.removeItem(AUTH_TOKEN_KEY);
localStorage.setItem(AUTH_STATE_KEY, "1");
setNickname(session.nickname);
}
export function clearAuthSession() {
localStorage.removeItem(AUTH_TOKEN_KEY);
sessionStorage.removeItem(AUTH_TOKEN_KEY);
localStorage.removeItem(AUTH_STATE_KEY);
localStorage.removeItem("rustpad:auth-token");
sessionStorage.removeItem("rustpad:auth-token");
localStorage.removeItem(NICKNAME_KEY);
sessionStorage.removeItem(NICKNAME_KEY);
setNicknameCookie("");
}
export async function resolveIdentity(api, nickname) {
const result = await api("/api/auth/identity", { method: "POST", body: JSON.stringify({ nickname, session_token: getAuthToken() || null }) });
const result = await api("/api/auth/identity", { method: "POST", body: JSON.stringify({ nickname }) });
setNickname(result.nickname); return result;
}
+1 -1
View File
@@ -28,7 +28,7 @@ class RoomSocket {
this.socket = new WebSocket(this.url);
this.socket.addEventListener("open", () => {
logInfo("websocket.open", { kind: this.kind });
this.send({ type: "authenticate", password: this.password || null, access_token: this.accessToken || null, nickname: this.nickname || null, session_token: this.sessionToken || null, guest_id: this.guestId || null, color: this.color || null });
this.send({ type: "authenticate", password: this.password || null, access_token: this.accessToken || null, nickname: this.nickname || null, guest_id: this.guestId || null, color: this.color || null });
});
this.socket.addEventListener("message", event => {
let message;
+8 -12
View File
@@ -24,7 +24,6 @@ let info;
const shareToken = new URLSearchParams(location.search).get("share");
let accessToken = shareToken || getAccessToken("workspace", slug);
let nickname = getNickname();
if (shareToken) setAccessToken("workspace", slug, shareToken);
const dialog = document.querySelector("#password-dialog");
const identityDialog = document.querySelector("#identity-dialog");
const workspaceContent = document.querySelector("#workspace-content");
@@ -136,11 +135,11 @@ async function openWorkspace() {
}
async function init() {
try {
const headers = accessToken ? { Authorization: `Bearer ${accessToken}` } : {};
const headers = accessToken && accessToken !== "cookie" ? { Authorization: `Bearer ${accessToken}` } : {};
info = await api(`/api/workspaces/${encodeURIComponent(slug)}`, { headers });
document.querySelector("#workspace-title").textContent = info.title;
document.querySelector("#workspace-url").textContent = location.pathname;
if (info.protected && !accessToken) dialog.showModal(); else openWorkspace();
if (info.protected && !accessToken && !getAuthToken()) dialog.showModal(); else openWorkspace();
} catch (e) {
if (e.status === 403 || e.status === 404) await showSystemNotFound();
else document.querySelector("#workspace-error").textContent = e.message;
@@ -151,8 +150,8 @@ document.querySelector("#password-form").addEventListener("submit", async e => {
try {
const password = document.querySelector("#open-password").value;
const result = await api("/api/access-token", { method: "POST", body: JSON.stringify({ kind: "workspace", slug, password }) });
accessToken = result.access_token;
setAccessToken("workspace", slug, accessToken);
setAccessToken("workspace", slug, result.granted);
accessToken = getAccessToken("workspace", slug);
document.querySelector("#open-password").value = "";
document.querySelector("#password-error").textContent = "";
openWorkspace();
@@ -195,11 +194,8 @@ document.querySelector("#copy-workspace-link").addEventListener("click", async (
catch (e) { toast(e.message); }
});
async function startAuthorizedWorkspace() {
const hadAccountToken = Boolean(getAuthToken());
if (hadAccountToken) {
const session = await validateCurrentSession();
nickname = session?.nickname || getNickname();
}
const session = await validateCurrentSession();
nickname = session?.nickname || getNickname();
if (!nickname) {
workspaceContent.hidden = true;
@@ -207,7 +203,7 @@ async function startAuthorizedWorkspace() {
return;
}
accessToken = shareToken || getAuthToken() || getAccessToken("workspace", slug);
accessToken = shareToken || getAccessToken("workspace", slug);
workspaceContent.hidden = false;
await init();
}
@@ -216,7 +212,7 @@ bindIdentityDialog({
dialog: identityDialog,
onIdentity: async value => {
nickname = value;
accessToken = shareToken || getAuthToken() || getAccessToken("workspace", slug);
accessToken = shareToken || getAccessToken("workspace", slug);
workspaceContent.hidden = false;
await init();
},