guest password protect
This commit is contained in:
+172
-2
@@ -2509,6 +2509,66 @@ dialog::backdrop {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.workspace-password-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px 14px;
|
||||
margin-top: 14px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-card);
|
||||
}
|
||||
|
||||
.workspace-password-card[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workspace-password-card__copy {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.workspace-password-card__copy strong {
|
||||
font-size: .82rem;
|
||||
}
|
||||
|
||||
.workspace-password-card__copy small {
|
||||
color: var(--muted);
|
||||
font-size: .72rem;
|
||||
}
|
||||
|
||||
.workspace-password-card__controls {
|
||||
display: grid;
|
||||
grid-template-columns: 106px auto;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.workspace-password-card__controls input {
|
||||
width: 7vh;
|
||||
height: 5vh;
|
||||
min-width: 11vh;
|
||||
padding: 0 1vh;
|
||||
}
|
||||
|
||||
.workspace-password-card__controls button {
|
||||
min-width: 7vh;
|
||||
min-height: 4vh;
|
||||
padding: 0 1vh;
|
||||
}
|
||||
|
||||
.workspace-password-card>.form-message {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0;
|
||||
font-size: .72rem;
|
||||
}
|
||||
|
||||
.workspace-password-card>.form-message:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.notes-view-switch {
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
@@ -2639,6 +2699,14 @@ dialog::backdrop {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.workspace-password-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.workspace-password-card__controls {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.notes-view-switch button {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -6577,6 +6645,7 @@ dialog::backdrop {
|
||||
font-size: .68rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Search, pagination and theme-aware scrollbars. */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
@@ -6810,7 +6879,7 @@ dialog::backdrop {
|
||||
grid-template-columns: 12px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.revision > div,
|
||||
.revision>div,
|
||||
.revision__meta,
|
||||
.revision__meta strong,
|
||||
.revision time,
|
||||
@@ -6842,7 +6911,7 @@ dialog::backdrop {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.history-header > div,
|
||||
.history-header>div,
|
||||
.history-header h2,
|
||||
.history-header p,
|
||||
.history-help,
|
||||
@@ -6853,3 +6922,104 @@ dialog::backdrop {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.page-password-inline {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 5px 0 0;
|
||||
padding: 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--border) 78%, transparent);
|
||||
border-radius: 7px;
|
||||
background: color-mix(in srgb, var(--surface-card) 76%, var(--surface-inset));
|
||||
}
|
||||
|
||||
.page-password-inline[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-password-requirement {
|
||||
margin: 6px 0 0;
|
||||
|
||||
color: var(--muted);
|
||||
font-size: .69rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.page-password-requirement[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-password-inline__heading {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.page-password-inline__heading label {
|
||||
color: var(--text-tertiary);
|
||||
font-size: .76rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.page-password-inline__heading small {
|
||||
color: var(--muted);
|
||||
font-size: .68rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.page-password-inline__controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.page-password-inline__controls input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 50%;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-inset);
|
||||
color: var(--text);
|
||||
font-size: .74rem;
|
||||
}
|
||||
|
||||
.page-password-inline__controls input::placeholder {
|
||||
color: var(--muted-2);
|
||||
}
|
||||
|
||||
.page-password-inline__controls input:focus {
|
||||
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
.page-password-inline__save {
|
||||
min-width: 1vh;
|
||||
height: 3vh;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-card);
|
||||
color: var(--text-tertiary);
|
||||
font-size: .72rem;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-password-inline__save:hover {
|
||||
background: var(--surface-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.page-password-inline .error:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-password-inline .error {
|
||||
margin: 0;
|
||||
font-size: .68rem;
|
||||
}
|
||||
+29
-15
@@ -54,6 +54,19 @@
|
||||
class="public-task-toggle"
|
||||
title="Allow the published page to open without the resource password or private access"><input
|
||||
id="unprotect-public-page" type="checkbox"> Unprotect Page</label>
|
||||
<p id="page-password-requirement" class="page-password-requirement" hidden>Access to page
|
||||
options requires a password-protected note.</p>
|
||||
<form id="set-page-password-form" class="page-password-inline" hidden>
|
||||
<div class="page-password-inline__heading">
|
||||
<label id="set-page-password-label" for="set-page-password">Set password</label>
|
||||
<small id="set-page-password-help">Minimum 8 characters.</small>
|
||||
</div>
|
||||
<div class="page-password-inline__controls"><input id="set-page-password" type="password"
|
||||
minlength="8" maxlength="128" autocomplete="new-password" placeholder="Min. 8 chars"
|
||||
aria-describedby="set-page-password-help" required><button type="submit"
|
||||
class="page-password-inline__save">Set</button></div>
|
||||
<small id="set-page-password-error" class="error"></small>
|
||||
</form>
|
||||
</div>
|
||||
</details><button id="files-button" class="secondary-button">Files</button><button id="delete-note"
|
||||
class="secondary-button danger-button" hidden>Delete</button><button id="history-button"
|
||||
@@ -123,15 +136,16 @@
|
||||
Compact</label><label class="line-toggle"><input id="line-links-toggle" type="checkbox">
|
||||
Line links</label>
|
||||
<div class="toolbar-fill"></div><button id="mode-toggle" class="toolbar-action active"
|
||||
aria-pressed="true" aria-label="Markdown" title="Markdown"><span class="control-label-full">Markdown</span><span
|
||||
class="control-label-short" aria-hidden="true">M</span></button>
|
||||
<div class="view-switch" aria-label="Editor view"><button data-view="edit" aria-label="Edit" title="Edit"><span
|
||||
class="control-label-full">Edit</span><span class="control-label-short"
|
||||
aria-hidden="true">E</span></button><button data-view="split" class="active" aria-label="Split"
|
||||
title="Split"><span class="control-label-full">Split</span><span class="control-label-short"
|
||||
aria-hidden="true">S</span></button><button data-view="preview" aria-label="Preview" title="Preview"><span
|
||||
class="control-label-full">Preview</span><span class="control-label-short"
|
||||
aria-hidden="true">P</span></button></div>
|
||||
aria-pressed="true" aria-label="Markdown" title="Markdown"><span
|
||||
class="control-label-full">Markdown</span><span class="control-label-short"
|
||||
aria-hidden="true">M</span></button>
|
||||
<div class="view-switch" aria-label="Editor view"><button data-view="edit" aria-label="Edit"
|
||||
title="Edit"><span class="control-label-full">Edit</span><span class="control-label-short"
|
||||
aria-hidden="true">E</span></button><button data-view="split" class="active"
|
||||
aria-label="Split" title="Split"><span class="control-label-full">Split</span><span
|
||||
class="control-label-short" aria-hidden="true">S</span></button><button data-view="preview"
|
||||
aria-label="Preview" title="Preview"><span class="control-label-full">Preview</span><span
|
||||
class="control-label-short" aria-hidden="true">P</span></button></div>
|
||||
</div>
|
||||
<div id="connection-notice" class="connection-notice" role="status" aria-live="polite" hidden>
|
||||
<span class="connection-notice__signal"
|
||||
@@ -283,12 +297,12 @@
|
||||
guest</button><button id="show-register" class="text-button" type="button">Register</button><button
|
||||
id="show-login" class="text-button" type="button">Log in</button></div>
|
||||
<section id="auth-panel" class="auth-panel" hidden>
|
||||
<h3 id="auth-mode-title">Log in</h3><label id="auth-email-field"><span id="auth-email-label">E-mail</span><input
|
||||
id="auth-email" name="username" type="email" maxlength="320" autocomplete="username"
|
||||
placeholder="you@example.com"></label><label>Password<input
|
||||
id="auth-password" name="password" type="password" minlength="8" maxlength="128"
|
||||
autocomplete="current-password"></label><button id="auth-submit" class="primary-button"
|
||||
type="submit">Log in and continue</button>
|
||||
<h3 id="auth-mode-title">Log in</h3><label id="auth-email-field"><span
|
||||
id="auth-email-label">E-mail</span><input id="auth-email" name="username" type="email"
|
||||
maxlength="320" autocomplete="username"
|
||||
placeholder="you@example.com"></label><label>Password<input id="auth-password" name="password"
|
||||
type="password" minlength="8" maxlength="128" autocomplete="current-password"></label><button
|
||||
id="auth-submit" class="primary-button" type="submit">Log in and continue</button>
|
||||
<div class="identity-links"><button id="show-reset" class="text-button" type="button">Forgot
|
||||
password?</button><button id="auth-back" class="text-button" type="button">Back to
|
||||
nickname</button><button id="logout-account" class="text-button" type="button">Log out saved
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
<p>Select a note or create a new one.</p>
|
||||
</div><button id="new-note-button" class="primary-button inline-button">New note</button>
|
||||
</section>
|
||||
<form id="workspace-password-form" class="workspace-password-card" hidden>
|
||||
<div class="workspace-password-card__copy"><strong>Protect workspace</strong><small>One password secures the workspace and all its notes.</small></div>
|
||||
<div class="workspace-password-card__controls"><input id="workspace-set-password" type="password" minlength="8" maxlength="128" autocomplete="new-password" placeholder="Min. 8 chars" required><button class="secondary-button" type="submit">Set</button></div>
|
||||
<small id="workspace-password-error" class="form-message error" role="alert"></small>
|
||||
</form>
|
||||
<div class="notes-toolbar">
|
||||
<label class="list-search"><span class="sr-only">Search notes</span><input id="notes-search" type="search" placeholder="Search notes…" autocomplete="off"></label>
|
||||
<label class="page-size-label">Per page<select id="notes-per-page"><option value="25">25</option><option value="50">50</option><option value="100">100</option></select></label>
|
||||
|
||||
Reference in New Issue
Block a user