import { installGlobalDiagnostics, logInfo } from "@rustpad/logger";
installGlobalDiagnostics();
import { bindIdentityDialog, handleAccountConfirmationToken, handleResetToken, logoutCurrentSession, validateCurrentSession } from "@rustpad/auth-ui";
import { getAuthToken, setAccessToken } from "@rustpad/session";
import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
import { safeAppUrl } from "@rustpad/security";
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) { 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); }
window.location.assign(safeAppUrl(`${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) { 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); }
window.location.assign(safeAppUrl(result.url));
} catch (requestError) {
error.textContent = requestError.message;
} finally {
setBusy(button, false, "Create workspace", "Creating…");
}
});
handleAccountConfirmationToken();
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}` } : {}; }
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()}`; }
async function loadResources() {
resourcesError.textContent = ""; resourcesList.innerHTML = "
Loading…
";
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 ? "" : "No assigned items yet.
";
for (const item of items) {
const row = document.createElement("article");
row.className = "resource-row";
const sharedLabel = !item.owned ? `Shared by ${escapeHtml(item.shared_by || "another user")}` : "";
const permissionLabel = item.permission === "rw" ? "Read and write" : "Read only";
row.classList.toggle("resource-row--shared", !Boolean(item.owned));
row.innerHTML = `${item.kind === "workspace" ? "Workspace" : "Note"}${item.private ? " · private" : ""}${!item.owned ? ` · ${permissionLabel}` : item.protected ? " · password protected" : ""} ${item.owned ? `` : ""}
`;
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-privacy]")?.addEventListener("click", async () => {
try { await api("/api/auth/resources/privacy", { method: "POST", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, private: !Boolean(item.private) }) }); await loadResources(); }
catch (e) { resourcesError.textContent = e.message; }
});
row.querySelector("[data-share]")?.addEventListener("click", async () => {
const dialog = document.createElement("dialog");
dialog.className = "app-dialog share-dialog";
dialog.innerHTML = `
Share access
Manage registered users and direct access links for ${escapeHtml(item.title)}.
People with access
Invite existing Rustpad users by e-mail.
Direct links
Create links for people without an account or manage existing links.
`;
document.body.append(dialog);
const closeDialog = () => dialog.close();
dialog.addEventListener("close", () => dialog.remove(), { once: true });
dialog.addEventListener("click", event => { if (event.target === dialog) closeDialog(); });
dialog.querySelector("[data-cancel]").addEventListener("click", closeDialog);
dialog.querySelector("[data-done]").addEventListener("click", closeDialog);
const setDialogMessage = (text, type = "") => {
const message = dialog.querySelector("[data-inline-message]");
message.className = `form-message resource-inline-message ${type}`.trim();
message.textContent = text;
};
const userForm = dialog.querySelector("[data-user-share-form]");
const linkForm = dialog.querySelector("[data-link-form]");
const userList = dialog.querySelector("[data-user-list]");
const linkList = dialog.querySelector("[data-link-list]");
const syncForever = () => { linkForm.hours.disabled = linkForm.forever.checked; };
linkForm.forever.addEventListener("change", syncForever); syncForever();
const refresh = async () => {
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 => `${escapeHtml((u.nickname || u.email || "?").slice(0, 1).toUpperCase())}${escapeHtml(u.nickname)}${escapeHtml(u.email)}
${u.permission === "rw" ? "Read and write" : "Read only"} `).join("") : 'No users have access.
';
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 ? `` : 'Link value unavailable. Recreate this legacy link to display it.';
return ``;
}).join("") : 'No active links.
';
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 { await api("/api/auth/resources/sharing", { method: "POST", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, emails: userForm.emails.value, permission: userForm.permission.value }) }); userForm.emails.value = ""; setDialogMessage("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"); } });
dialog.showModal();
try { await refresh(); } catch (err) { setDialogMessage(err.message, "error"); }
});
row.querySelector("[data-password]")?.addEventListener("click", () => {
inline.hidden = false;
inline.innerHTML = ``;
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 = `Delete “${escapeHtml(item.title)}” permanently?
`;
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);
});
window.addEventListener("rustpad:session-expired", () => renderAccount(null));
renderAccount(null);
validateCurrentSession().then(renderAccount);
}