fix2 tokens
This commit is contained in:
+2
-4
@@ -206,20 +206,18 @@ async function loadResources() {
|
||||
const d = await api(`/api/auth/resources/sharing?kind=${encodeURIComponent(item.kind)}&slug=${encodeURIComponent(item.slug)}`, { headers: authHeaders() });
|
||||
userList.innerHTML = d.users.length ? d.users.map(u => `<div class="share-list-row"><div class="share-list-identity"><span class="share-avatar">${escapeHtml((u.nickname || u.email || "?").slice(0, 1).toUpperCase())}</span><div><strong>${escapeHtml(u.nickname)}</strong><small>${escapeHtml(u.email)}</small></div></div><span class="share-role">${u.permission === "rw" ? "Read and write" : "Read only"}</span><button class="secondary-button compact-button" type="button" data-remove-user="${escapeHtml(u.email)}">Remove</button></div>`).join("") : '<p class="share-empty">No users have access.</p>';
|
||||
linkList.innerHTML = d.links.length ? d.links.map(link => {
|
||||
const directUrl = link.token ? new URL(`${item.url}?share=${encodeURIComponent(link.token)}`, location.origin).href : "";
|
||||
const linkPreview = link.token ? `<div class="share-link-inline"><input type="text" readonly value="${escapeHtml(directUrl)}" aria-label="Direct access link"><button class="secondary-button compact-button" type="button" data-copy-link>Copy</button></div>` : '<small class="share-link-legacy">Link value unavailable. Recreate this legacy link to display it.</small>';
|
||||
const linkPreview = '<small class="share-link-legacy">For security, link values are shown only when created. Create a new link to copy it again.</small>';
|
||||
return `<form class="share-list-row share-link-row" data-link-token="${escapeHtml(link.token_hash)}"><div class="share-link-info"><strong>Individual link</strong><small>${escapeHtml(formatShareExpiry(link.expires_at))}</small>${linkPreview}</div><label><span class="sr-only">Permission</span><select name="permission" aria-label="Link permission"><option value="ro" ${link.permission === "ro" ? "selected" : ""}>Read only</option><option value="rw" ${link.permission === "rw" ? "selected" : ""}>Read and write</option></select></label><label><span class="sr-only">Validity in hours</span><div class="share-hours-field"><input name="hours" type="number" min="1" max="87600" value="24" aria-label="New validity in hours"><span>h</span></div></label><label class="share-forever"><input name="forever" type="checkbox" ${link.expires_at ? "" : "checked"}><span>Never</span></label><div class="share-row-actions"><button class="secondary-button compact-button" type="submit">Update</button><button class="danger-button compact-button" type="button" data-revoke-link>Revoke</button></div></form>`;
|
||||
}).join("") : '<p class="share-empty">No active links.</p>';
|
||||
userList.querySelectorAll("[data-remove-user]").forEach(button => button.addEventListener("click", async () => { try { button.disabled = true; await api("/api/auth/resources/sharing", { method: "DELETE", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, email: button.dataset.removeUser }) }); setDialogMessage("Access removed.", "success"); await refresh(); } catch (err) { setDialogMessage(err.message, "error"); button.disabled = false; } }));
|
||||
linkList.querySelectorAll("[data-link-token]").forEach(linkRow => {
|
||||
const forever = linkRow.elements.forever, hours = linkRow.elements.hours; const sync = () => { hours.disabled = forever.checked; }; forever.addEventListener("change", sync); sync();
|
||||
linkRow.querySelector("[data-copy-link]")?.addEventListener("click", async () => { try { await copyText(linkRow.querySelector(".share-link-inline input").value); setDialogMessage("Link copied.", "success"); } catch (err) { setDialogMessage(err.message, "error"); } });
|
||||
linkRow.addEventListener("submit", async event => { event.preventDefault(); try { const expires_at = shareExpiry(hours.value, forever.checked); await api("/api/auth/resources/share-links", { method: "PUT", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, token: linkRow.dataset.linkToken, permission: linkRow.elements.permission.value, expires_at }) }); setDialogMessage("Link updated.", "success"); await refresh(); } catch (err) { setDialogMessage(err.message, "error"); } });
|
||||
linkRow.querySelector("[data-revoke-link]").addEventListener("click", async () => { try { await api("/api/auth/resources/share-links", { method: "DELETE", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, token: linkRow.dataset.linkToken }) }); setDialogMessage("Link revoked.", "success"); await refresh(); } catch (err) { setDialogMessage(err.message, "error"); } });
|
||||
});
|
||||
};
|
||||
userForm.addEventListener("submit", async event => { event.preventDefault(); try { const result = await api("/api/auth/resources/sharing", { method: "POST", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, recipients: userForm.recipients.value, permission: userForm.permission.value }) }); userForm.recipients.value = ""; setDialogMessage(result.confirmation_required ? "Invitation sent. Access will appear after the recipient accepts it." : "Access granted.", "success"); await refresh(); } catch (err) { setDialogMessage(err.message, "error"); } });
|
||||
linkForm.addEventListener("submit", async event => { event.preventDefault(); try { const expires_at = shareExpiry(linkForm.hours.value, linkForm.forever.checked); const result = await api("/api/auth/resources/share-links", { method: "POST", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, permission: linkForm.permission.value, expires_at }) }); const absolute = new URL(result.url, location.origin).href; await copyText(absolute); setDialogMessage("Link created and copied. It remains visible below.", "success"); await refresh(); } catch (err) { setDialogMessage(err.message, "error"); } });
|
||||
linkForm.addEventListener("submit", async event => { event.preventDefault(); try { const expires_at = shareExpiry(linkForm.hours.value, linkForm.forever.checked); const result = await api("/api/auth/resources/share-links", { method: "POST", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, permission: linkForm.permission.value, expires_at }) }); const absolute = new URL(result.url, location.origin).href; await copyText(absolute); setDialogMessage("Link created and copied. For security, it is shown only once.", "success"); await refresh(); } catch (err) { setDialogMessage(err.message, "error"); } });
|
||||
dialog.showModal();
|
||||
try { await refresh(); } catch (err) { setDialogMessage(err.message, "error"); }
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
import { api } from "@rustpad/api";
|
||||
import { askConfirm } from "@rustpad/modal";
|
||||
import { NoteSocket, PadSocket } from "@rustpad/socket";
|
||||
import { withShareToken } from "@rustpad/url-state";
|
||||
|
||||
function encode(value) {
|
||||
return encodeURIComponent(value);
|
||||
@@ -60,7 +59,6 @@ export function createWorkspaceNoteAdapter() {
|
||||
const workspaceSlug = parts[1];
|
||||
const noteSlug = parts[3];
|
||||
const base = `/api/workspaces/${encode(workspaceSlug)}/notes/${encode(noteSlug)}`;
|
||||
const shareToken = new URLSearchParams(location.search).get("share");
|
||||
|
||||
return {
|
||||
access: { kind: "workspace", key: workspaceSlug },
|
||||
@@ -106,7 +104,7 @@ export function createWorkspaceNoteAdapter() {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ access_token: accessToken || null }),
|
||||
});
|
||||
location.assign(withShareToken(`/w/${encode(workspaceSlug)}`, shareToken));
|
||||
location.assign(`/w/${encode(workspaceSlug)}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles, unresolvedMa
|
||||
import { getNickname, getGuestId, getAuthToken, getAccessToken, setAccessToken } from "@rustpad/session";
|
||||
import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
|
||||
import { bindNoteFiles } from "@rustpad/note-files";
|
||||
import { currentShareUrl, readEditorState, withShareToken, writeEditorState } from "@rustpad/url-state";
|
||||
import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url-state";
|
||||
import { toast } from "@rustpad/toast";
|
||||
import { getTheme } from "@rustpad/theme";
|
||||
|
||||
@@ -31,8 +31,6 @@ export function startNoteEditor(adapter) {
|
||||
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");
|
||||
const parentLink = document.querySelector("#resource-parent-link");
|
||||
if (parentLink && shareToken) parentLink.href = withShareToken(parentLink.getAttribute("href") || "/", shareToken);
|
||||
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, applyingHistory = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = "";
|
||||
let editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false, connectionNoticeTimer = 0, connectionWasInterrupted = false;
|
||||
@@ -843,7 +841,7 @@ export function startNoteEditor(adapter) {
|
||||
});
|
||||
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(); } } });
|
||||
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 && info.access_level === "none") passwordDialog.showModal(); else { loadFiles(); connect(); } } });
|
||||
identityDialog.addEventListener("close", () => { if (!nickname) queueMicrotask(() => { if (!identityDialog.open) identityDialog.showModal(); }); });
|
||||
async function showSystemNotFound() {
|
||||
try {
|
||||
@@ -876,7 +874,7 @@ export function startNoteEditor(adapter) {
|
||||
adapter.configureView?.(info);
|
||||
applyUi({ write: true, replace: true });
|
||||
updateCurrentUser();
|
||||
if (info.protected && !accessToken && !getAuthToken()) passwordDialog.showModal();
|
||||
if (info.protected && info.access_level === "none") passwordDialog.showModal();
|
||||
else { loadFiles(); connect(); }
|
||||
} catch (e) {
|
||||
if (e.status === 403 || e.status === 404) {
|
||||
|
||||
@@ -9,40 +9,6 @@
|
||||
|
||||
const VIEWS = new Set(["edit", "split", "preview"]);
|
||||
const MODES = new Set(["markdown", "text"]);
|
||||
const APP_URL_BASE = globalThis.location?.origin || "https://rustpad.invalid";
|
||||
|
||||
function appUrl(path) {
|
||||
try {
|
||||
const url = new URL(path, APP_URL_BASE);
|
||||
return url.origin === APP_URL_BASE ? url : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function relativeUrl(url) {
|
||||
return `${url.pathname}${url.search}${url.hash}`;
|
||||
}
|
||||
|
||||
export function withShareToken(path, shareToken) {
|
||||
const url = appUrl(path);
|
||||
if (!url) return "/";
|
||||
const token = typeof shareToken === "string" ? shareToken.trim() : "";
|
||||
if (token) url.searchParams.set("share", token);
|
||||
else url.searchParams.delete("share");
|
||||
return relativeUrl(url);
|
||||
}
|
||||
|
||||
export function editorResourceUrl(path, { shareToken = "", view = "split", mode = "markdown" } = {}) {
|
||||
const url = appUrl(path);
|
||||
if (!url) return "/";
|
||||
if (VIEWS.has(view)) url.searchParams.set("view", view);
|
||||
if (MODES.has(mode)) url.searchParams.set("mode", mode);
|
||||
const token = typeof shareToken === "string" ? shareToken.trim() : "";
|
||||
if (token) url.searchParams.set("share", token);
|
||||
else url.searchParams.delete("share");
|
||||
return relativeUrl(url);
|
||||
}
|
||||
|
||||
export function readEditorState() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -17,7 +17,6 @@ import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
|
||||
import { askConfirm } from "@rustpad/modal";
|
||||
import { safeAppUrl } from "@rustpad/security";
|
||||
import { toast } from "@rustpad/toast";
|
||||
import { editorResourceUrl } from "@rustpad/url-state";
|
||||
|
||||
const parts = location.pathname.split("/").filter(Boolean);
|
||||
const slug = parts[1];
|
||||
@@ -71,9 +70,6 @@ function setNotesView(view) {
|
||||
button.setAttribute("aria-pressed", String(active));
|
||||
});
|
||||
}
|
||||
function noteEditorUrl(path) {
|
||||
return safeAppUrl(editorResourceUrl(safeAppUrl(path), { shareToken }));
|
||||
}
|
||||
function deleteButton(note, inline = false) {
|
||||
const disabled = note.protected;
|
||||
const classes = `note-delete-button${inline ? " note-delete-button--inline" : ""}`;
|
||||
@@ -90,7 +86,7 @@ function renderNotes(notes = notesCache) {
|
||||
if (notesView === "table") {
|
||||
notesList.innerHTML = `<div class="notes-table-scroll"><table><thead><tr><th>Name</th><th>Created by</th><th>Participants</th><th>Files</th><th>Revisions</th><th>Status</th><th>Updated</th><th class="notes-table-actions">Actions</th></tr></thead><tbody>${notes.map(note => `
|
||||
<tr>
|
||||
<td><a class="note-table-link" href="${escapeHtml(noteEditorUrl(note.url))}">${escapeHtml(note.title)}</a></td>
|
||||
<td><a class="note-table-link" href="${escapeHtml(safeAppUrl(`${note.url}?view=split&mode=markdown`))}">${escapeHtml(note.title)}</a></td>
|
||||
<td class="note-author">${escapeHtml(note.created_by || "Unknown")}</td>
|
||||
<td>${Number(note.participant_count) || 0}</td>
|
||||
<td>${Number(note.file_count) || 0} <span class="note-status">(${formatBytes(note.file_size_bytes)})</span></td>
|
||||
@@ -103,7 +99,7 @@ function renderNotes(notes = notesCache) {
|
||||
}
|
||||
notesList.innerHTML = notes.map(note => `
|
||||
<article class="note-card-wrap">
|
||||
<a class="note-card" href="${escapeHtml(noteEditorUrl(note.url))}">
|
||||
<a class="note-card" href="${escapeHtml(safeAppUrl(`${note.url}?view=split&mode=markdown`))}">
|
||||
<div class="note-card-title"><h3>${escapeHtml(note.title)}</h3>${note.protected ? '<span class="protect-badge">Protected</span>' : ''}</div>
|
||||
<div class="note-card-meta"><span>Created by: ${escapeHtml(note.created_by || "Unknown")}</span>${noteStats(note)}<span>Updated: ${formatDate(note.updated_at)}</span></div>
|
||||
</a>
|
||||
@@ -155,7 +151,7 @@ async function init() {
|
||||
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 && !getAuthToken()) dialog.showModal(); else openWorkspace();
|
||||
if (info.protected && info.access_level === "none") dialog.showModal(); else openWorkspace();
|
||||
} catch (e) {
|
||||
if (e.status === 403 || e.status === 404) await showSystemNotFound();
|
||||
else document.querySelector("#workspace-error").textContent = e.message;
|
||||
@@ -183,7 +179,7 @@ document.querySelector("#note-form").addEventListener("submit", async e => {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: document.querySelector("#note-name").value, access_token: accessToken || null, protect: document.querySelector("#note-protect").checked, created_by: nickname || null })
|
||||
});
|
||||
location.assign(noteEditorUrl(note.url));
|
||||
location.assign(safeAppUrl(`${note.url}?view=split&mode=markdown`));
|
||||
} catch (err) { error.textContent = err.message; }
|
||||
});
|
||||
notesList.addEventListener("click", async event => {
|
||||
|
||||
Reference in New Issue
Block a user