guest password protect

This commit is contained in:
Mateusz Gruszczyński
2026-08-04 16:41:45 +02:00
parent 8d58549d11
commit 6fc408ddf7
18 changed files with 527 additions and 43 deletions
+7
View File
@@ -21,6 +21,7 @@ export function createPadAdapter() {
return {
access: { kind: "pad", key: slug },
passwordScope: "note",
addressSelector: "#document-url",
title: info => `${info.title} · RustPad`,
loadInfo: headers => api(base, { headers }),
@@ -37,6 +38,7 @@ export function createPadAdapter() {
method: "POST",
body: JSON.stringify({ kind: "pad", slug, password }),
}),
setPassword: password => api(`${base}/password`, { method: "POST", body: JSON.stringify({ password }) }),
publish: (accessToken, allowTaskUpdates, unprotectPage, enabled = true) => api(`${base}/publish`, {
method: "POST",
body: JSON.stringify({ access_token: accessToken || null, allow_task_updates: allowTaskUpdates, unprotect_page: unprotectPage, enabled }),
@@ -62,6 +64,7 @@ export function createWorkspaceNoteAdapter() {
return {
access: { kind: "workspace", key: workspaceSlug },
passwordScope: "workspace",
addressSelector: "#document-url",
title: info => `${info.title} · ${info.workspace_title}`,
loadInfo: headers => api(base, { headers }),
@@ -78,6 +81,10 @@ export function createWorkspaceNoteAdapter() {
method: "POST",
body: JSON.stringify({ kind: "workspace", slug: workspaceSlug, password }),
}),
setPassword: password => api(`/api/workspaces/${encode(workspaceSlug)}/password`, {
method: "POST",
body: JSON.stringify({ password }),
}),
publish: (accessToken, allowTaskUpdates, unprotectPage, enabled = true) => api(`${base}/publish`, {
method: "POST",
body: JSON.stringify({ access_token: accessToken || null, allow_task_updates: allowTaskUpdates, unprotect_page: unprotectPage, enabled }),
+70 -2
View File
@@ -1851,13 +1851,50 @@ export function startNoteEditor(adapter) {
});
const publishPageButton = document.querySelector("#publish-page");
const pageSettings = document.querySelector(".page-settings");
const setPagePasswordForm = document.querySelector("#set-page-password-form");
const setPagePasswordInput = document.querySelector("#set-page-password");
const setPagePasswordError = document.querySelector("#set-page-password-error");
const setPagePasswordLabel = document.querySelector("#set-page-password-label");
const setPagePasswordHelp = document.querySelector("#set-page-password-help");
const pagePasswordRequirement = document.querySelector("#page-password-requirement");
function updatePageControls() {
const enabled = publicPageEnabled.checked;
const passwordProtected = Boolean(info?.protected);
const workspacePassword = adapter.passwordScope === "workspace";
if (!passwordProtected) {
publicPageEnabled.checked = false;
unprotectPublicPage.checked = false;
}
const requirementText = workspacePassword
? "Access to page options requires a password-protected workspace."
: "Access to page options requires a password-protected note.";
if (pagePasswordRequirement) {
pagePasswordRequirement.textContent = requirementText;
pagePasswordRequirement.hidden = passwordProtected;
}
if (setPagePasswordLabel) setPagePasswordLabel.textContent = workspacePassword ? "Set workspace password" : "Set password";
if (setPagePasswordHelp) setPagePasswordHelp.textContent = workspacePassword ? "Protects the entire workspace. Minimum 8 characters." : "Minimum 8 characters.";
const canSetPassword = !passwordProtected && Boolean(adapter.setPassword) && Boolean(info?.can_set_password);
setPagePasswordForm.hidden = !canSetPassword;
const enabled = passwordProtected && publicPageEnabled.checked;
publicPageEnabled.disabled = !passwordProtected;
publishPageButton.disabled = !enabled;
publicTaskUpdates.disabled = !enabled;
unprotectPublicPage.disabled = !enabled;
pageSettings?.querySelector("summary")?.setAttribute(
"aria-disabled",
"false",
);
pageSettings?.classList.toggle("is-enabled", enabled);
pageSettings?.querySelector("summary")?.setAttribute("title", enabled ? "Published page enabled" : "Published page disabled");
pageSettings?.querySelector("summary")?.setAttribute(
"title",
!passwordProtected
? canSetPassword
? "Set a password before enabling the published page"
: requirementText
: enabled
? "Published page enabled"
: "Published page disabled",
);
}
document.addEventListener("pointerdown", event => {
const target = event.target instanceof Element ? event.target : null;
@@ -1868,6 +1905,37 @@ export function startNoteEditor(adapter) {
if (event.key === "Escape" && pageSettings?.open) pageSettings.open = false;
if (event.key === "Escape" && mobileConnectionDetails?.open) mobileConnectionDetails.open = false;
});
setPagePasswordForm?.addEventListener("submit", async event => {
event.preventDefault();
if (!adapter.setPassword) return;
const passwordValue = setPagePasswordInput.value;
setPagePasswordError.textContent = "";
if (passwordValue.length < 8) {
setPagePasswordError.textContent = "Password must contain at least 8 characters.";
return;
}
const submit = setPagePasswordForm.querySelector('button[type="submit"]');
submit.disabled = true;
try {
await adapter.setPassword(passwordValue);
const access = await adapter.requestAccess(passwordValue);
setAccessToken(adapter.access.kind, adapter.access.key, access.granted);
accessToken = getAccessToken(adapter.access.kind, adapter.access.key) || shareToken;
password = "";
setPagePasswordInput.value = "";
await loadNoteInfo();
resourceUnlocked = false;
socket?.stop();
loadFiles();
connect();
toast("Password set. Page options are now available.");
} catch (error) {
setPagePasswordError.textContent = error.message;
} finally {
submit.disabled = false;
updatePageControls();
}
});
const savePublicOptions = async () => adapter.publish(accessToken, publicTaskUpdates.checked, unprotectPublicPage.checked, publicPageEnabled.checked);
publicPageEnabled.addEventListener("change", async () => {
const previous = !publicPageEnabled.checked;
+39
View File
@@ -32,12 +32,19 @@ const notesList = document.querySelector("#notes-list");
const notesSearch = document.querySelector("#notes-search");
const notesPerPage = document.querySelector("#notes-per-page");
const notesPagination = document.querySelector("#notes-pagination");
const workspacePasswordForm = document.querySelector("#workspace-password-form");
const workspacePasswordInput = document.querySelector("#workspace-set-password");
const workspacePasswordError = document.querySelector("#workspace-password-error");
let notesPage = 1;
let notesSearchTimer;
const notesViewKey = `rustpad:workspace:${slug}:notes-view`;
let notesView = localStorage.getItem(notesViewKey) === "table" ? "table" : "grid";
let notesCache = [];
function updateWorkspacePasswordControl() {
workspacePasswordForm.hidden = Boolean(info?.protected || !info?.can_set_password);
}
function escapeHtml(v) { const e = document.createElement("div"); e.textContent = v; return e.innerHTML; }
function formatBytes(value) {
const bytes = Math.max(0, Number(value) || 0);
@@ -135,6 +142,7 @@ async function openWorkspace() {
notesCache = data.notes;
renderNotes();
renderNotesPagination(data.pagination);
updateWorkspacePasswordControl();
if (dialog.open) dialog.close();
} catch (e) {
if (info?.protected || e.message.toLowerCase().includes("password")) {
@@ -151,6 +159,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;
updateWorkspacePasswordControl();
if (info.protected && info.access_level === "none") dialog.showModal(); else openWorkspace();
} catch (e) {
if (e.status === 403 || e.status === 404) await showSystemNotFound();
@@ -169,6 +178,36 @@ document.querySelector("#password-form").addEventListener("submit", async e => {
openWorkspace();
} catch (error) { document.querySelector("#password-error").textContent = error.message; }
});
workspacePasswordForm.addEventListener("submit", async event => {
event.preventDefault();
const password = workspacePasswordInput.value;
workspacePasswordError.textContent = "";
if (password.length < 8) {
workspacePasswordError.textContent = "Password must contain at least 8 characters.";
return;
}
const submit = workspacePasswordForm.querySelector('button[type="submit"]');
submit.disabled = true;
try {
await api(`/api/workspaces/${encodeURIComponent(slug)}/password`, {
method: "POST",
body: JSON.stringify({ password }),
});
const result = await api("/api/access-token", {
method: "POST",
body: JSON.stringify({ kind: "workspace", slug, password }),
});
setAccessToken("workspace", slug, result.granted);
accessToken = getAccessToken("workspace", slug);
workspacePasswordInput.value = "";
toast("Workspace password set");
await openWorkspace();
} catch (error) {
workspacePasswordError.textContent = error.message;
} finally {
submit.disabled = false;
}
});
document.querySelector("#new-note-button").addEventListener("click", () => document.querySelector("#note-dialog").showModal());
document.querySelector("#cancel-note").addEventListener("click", () => document.querySelector("#note-dialog").close());
document.querySelector("#note-form").addEventListener("submit", async e => {