194 lines
9.7 KiB
JavaScript
194 lines
9.7 KiB
JavaScript
import { installGlobalDiagnostics, logInfo } from "./logger.js";
|
|
installGlobalDiagnostics();
|
|
|
|
import { bindIdentityDialog, handleResetToken, logoutCurrentSession, validateCurrentSession } from "./auth-ui.js";
|
|
import { getAuthToken } from "@rustpad/session";
|
|
import { api } from "@rustpad/api";
|
|
|
|
function slugify(value, fallback) {
|
|
return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback;
|
|
}
|
|
|
|
function bindPreview(inputId, previewId, countId, prefix, fallback) {
|
|
const input = document.querySelector(inputId);
|
|
const preview = document.querySelector(previewId);
|
|
const count = document.querySelector(countId);
|
|
const update = () => {
|
|
count.textContent = `${input.value.length}/80`;
|
|
preview.textContent = `${prefix}${slugify(input.value, fallback)}`;
|
|
};
|
|
input.addEventListener("input", update);
|
|
update();
|
|
}
|
|
|
|
function setBusy(button, busy, idleText, busyText) {
|
|
button.disabled = busy;
|
|
button.textContent = busy ? busyText : idleText;
|
|
}
|
|
|
|
bindPreview("#pad-name", "#pad-slug-preview", "#pad-name-count", "/p/", "note");
|
|
bindPreview("#workspace-name", "#workspace-slug-preview", "#workspace-name-count", "/w/", "workspace");
|
|
|
|
document.querySelectorAll(".password-toggle").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const input = document.getElementById(button.dataset.target);
|
|
const show = input.type === "password";
|
|
input.type = show ? "text" : "password";
|
|
button.textContent = show ? "Hide" : "Show";
|
|
});
|
|
});
|
|
|
|
document.querySelector("#pad-form").addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
const name = document.querySelector("#pad-name");
|
|
const password = document.querySelector("#pad-password");
|
|
const button = document.querySelector("#pad-button");
|
|
const error = document.querySelector("#pad-error");
|
|
error.textContent = "";
|
|
setBusy(button, true, "Create note", "Creating…");
|
|
try {
|
|
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) sessionStorage.setItem(`rustpad:pad:${result.slug}:password`, password.value);
|
|
window.location.assign(`${result.url}?view=split&mode=markdown`);
|
|
} catch (requestError) {
|
|
error.textContent = requestError.message;
|
|
} finally {
|
|
setBusy(button, false, "Create note", "Creating…");
|
|
}
|
|
});
|
|
|
|
document.querySelector("#workspace-form").addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
const name = document.querySelector("#workspace-name");
|
|
const password = document.querySelector("#workspace-password");
|
|
const button = document.querySelector("#workspace-button");
|
|
const error = document.querySelector("#workspace-error");
|
|
error.textContent = "";
|
|
setBusy(button, true, "Create workspace", "Creating…");
|
|
try {
|
|
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) sessionStorage.setItem(`rustpad:workspace:${result.slug}:password`, password.value);
|
|
window.location.assign(result.url);
|
|
} catch (requestError) {
|
|
error.textContent = requestError.message;
|
|
} finally {
|
|
setBusy(button, false, "Create workspace", "Creating…");
|
|
}
|
|
});
|
|
|
|
handleResetToken();
|
|
|
|
const identityDialog = document.querySelector("#identity-dialog");
|
|
const guestAccount = document.querySelector("#footer-account-guest");
|
|
const userAccount = document.querySelector("#footer-account-user");
|
|
const userLabel = document.querySelector("#footer-user-label");
|
|
const registerLink = document.querySelector("#footer-register");
|
|
const registrationEnabled = document.body.dataset.registrationEnabled === "true";
|
|
const resourcesDialog = document.querySelector("#resources-dialog");
|
|
const resourcesList = document.querySelector("#resources-list");
|
|
const resourcesError = document.querySelector("#resources-error");
|
|
|
|
function authHeaders() { const token = getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
|
|
async function loadResources() {
|
|
resourcesError.textContent = ""; resourcesList.innerHTML = "<p>Loading…</p>";
|
|
try {
|
|
const data = await api("/api/auth/resources", { headers: authHeaders() });
|
|
const items = [...data.workspaces.map(item => ({...item, kind:"workspace", url:`/w/${item.slug}`})), ...data.pads.map(item => ({...item, kind:"pad", url:`/p/${item.slug}`}))];
|
|
resourcesList.innerHTML = items.length ? "" : "<p>No assigned items yet.</p>";
|
|
for (const item of items) {
|
|
const row = document.createElement("article");
|
|
row.className = "resource-row";
|
|
row.innerHTML = `<div class="resource-main"><div><a href="${item.url}">${item.title}</a><small>${item.kind === "workspace" ? "Workspace" : "Note"}${item.protected ? " · password protected" : ""}</small></div><div class="resource-actions"><button type="button" data-password>Change password</button><button type="button" data-delete>Delete</button></div></div><div class="resource-inline" data-inline hidden></div>`;
|
|
|
|
const inline = row.querySelector("[data-inline]");
|
|
const closeInline = () => { inline.hidden = true; inline.innerHTML = ""; };
|
|
const setInlineMessage = (text, type = "") => {
|
|
const message = inline.querySelector("[data-inline-message]");
|
|
if (!message) return;
|
|
message.className = `form-message resource-inline-message ${type}`.trim();
|
|
message.textContent = text;
|
|
};
|
|
|
|
row.querySelector("[data-password]").addEventListener("click", () => {
|
|
inline.hidden = false;
|
|
inline.innerHTML = `<form class="resource-password-form" autocomplete="off"><label>New password<input name="resource-password" type="password" minlength="8" maxlength="128" autocomplete="new-password" data-bwignore="true" placeholder="Minimum 8 characters"></label><p class="resource-inline-help">Leave empty to remove password protection.</p><p class="form-message resource-inline-message" data-inline-message role="status"></p><div class="resource-inline-actions"><button class="primary-button" type="submit">Save</button><button class="secondary-button" type="button" data-cancel>Cancel</button></div></form>`;
|
|
const form = inline.querySelector("form");
|
|
const input = form.querySelector("input");
|
|
form.querySelector("[data-cancel]").addEventListener("click", closeInline);
|
|
form.addEventListener("submit", async event => {
|
|
event.preventDefault();
|
|
const password = input.value;
|
|
if (password && password.length < 8) { setInlineMessage("Password must contain at least 8 characters.", "error"); return; }
|
|
const submit = form.querySelector('[type="submit"]');
|
|
submit.disabled = true;
|
|
setInlineMessage("");
|
|
try {
|
|
await api("/api/auth/resources", { method:"PUT", headers:authHeaders(), body:JSON.stringify({kind:item.kind, slug:item.slug, password}) });
|
|
await loadResources();
|
|
} catch(e) {
|
|
setInlineMessage(e.message, "error");
|
|
submit.disabled = false;
|
|
}
|
|
});
|
|
input.focus();
|
|
});
|
|
|
|
row.querySelector("[data-delete]").addEventListener("click", () => {
|
|
inline.hidden = false;
|
|
inline.innerHTML = `<div class="resource-delete-confirm"><p>Delete “${item.title}” permanently?</p><p class="form-message resource-inline-message" data-inline-message role="status"></p><div class="resource-inline-actions"><button class="danger-button" type="button" data-confirm-delete>Delete</button><button class="secondary-button" type="button" data-cancel>Cancel</button></div></div>`;
|
|
inline.querySelector("[data-cancel]").addEventListener("click", closeInline);
|
|
inline.querySelector("[data-confirm-delete]").addEventListener("click", async event => {
|
|
event.currentTarget.disabled = true;
|
|
setInlineMessage("");
|
|
try {
|
|
await api("/api/auth/resources", { method:"DELETE", headers:authHeaders(), body:JSON.stringify({kind:item.kind, slug:item.slug}) });
|
|
await loadResources();
|
|
} catch(e) {
|
|
setInlineMessage(e.message, "error");
|
|
event.currentTarget.disabled = false;
|
|
}
|
|
});
|
|
});
|
|
resourcesList.append(row);
|
|
}
|
|
} catch(e) { resourcesList.innerHTML=""; resourcesError.textContent=e.message; }
|
|
}
|
|
|
|
|
|
function renderAccount(session) {
|
|
guestAccount.hidden = Boolean(session);
|
|
userAccount.hidden = !session;
|
|
if (session) userLabel.textContent = `Signed in as ${session.nickname}`;
|
|
registerLink.hidden = !registrationEnabled;
|
|
}
|
|
|
|
if (identityDialog) {
|
|
const authDialog = bindIdentityDialog({
|
|
dialog: identityDialog,
|
|
onIdentity: async (_nickname, session) => renderAccount(session),
|
|
});
|
|
|
|
document.querySelector("#footer-login")?.addEventListener("click", () => {
|
|
authDialog.setMode("login");
|
|
identityDialog.showModal();
|
|
});
|
|
registerLink?.addEventListener("click", () => {
|
|
authDialog.setMode("register");
|
|
identityDialog.showModal();
|
|
});
|
|
document.querySelector("#footer-resources")?.addEventListener("click", async () => { resourcesDialog.showModal(); await loadResources(); });
|
|
document.querySelector("#close-resources")?.addEventListener("click", () => resourcesDialog.close());
|
|
resourcesDialog?.addEventListener("click", (event) => { if (event.target === resourcesDialog) resourcesDialog.close(); });
|
|
document.querySelector("#footer-logout")?.addEventListener("click", async () => {
|
|
await logoutCurrentSession();
|
|
renderAccount(null);
|
|
});
|
|
|
|
renderAccount(null);
|
|
validateCurrentSession().then(renderAccount);
|
|
}
|