import { installGlobalDiagnostics, logInfo } from "@rustpad/logger"; installGlobalDiagnostics(); import { bindIdentityDialog, handleAccountActionToken, handleAccountConfirmationToken, handleResetToken, handleShareInvitationToken, 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"; import { toast } from "@rustpad/toast"; 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…"); } }); await handleAccountConfirmationToken() || await handleResetToken() || await handleAccountActionToken() || await handleShareInvitationToken(); 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"); const profileDialog = document.querySelector("#profile-dialog"); const profileForm = document.querySelector("#profile-form"); let currentSession = null; 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)}.

`; 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)}${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 { 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"); } }); dialog.showModal(); try { await refresh(); } catch (err) { setDialogMessage(err.message, "error"); } }); row.querySelector("[data-password]")?.addEventListener("click", () => { inline.hidden = false; inline.innerHTML = `

Leave empty to remove password protection.

`; 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) { currentSession = 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); if (session) toast(`Logged in as ${session.nickname}.`); }, }); document.querySelector("#footer-login")?.addEventListener("click", () => { authDialog.setMode("login"); identityDialog.showModal(); }); registerLink?.addEventListener("click", () => { authDialog.setMode("register"); identityDialog.showModal(); }); document.querySelector("#footer-profile")?.addEventListener("click", () => { document.querySelector("#profile-nickname").value = currentSession?.nickname || ""; const profileColor = document.querySelector("#profile-color"); profileColor.value = currentSession?.editor_color || "#7c6cff"; document.querySelector("#profile-current-email").value = currentSession?.email || ""; document.querySelector("#profile-email").value = ""; document.querySelector("#profile-new-password").value = ""; document.querySelector("#profile-password").value = ""; const profileMessage = document.querySelector("#profile-message"); profileMessage.textContent = ""; profileMessage.classList.remove("success", "error"); const directoryManaged = Boolean(currentSession?.directory_managed); document.querySelector("#profile-copy").textContent = directoryManaged ? "Directory account details are read-only. You can change only the displayed nickname." : "Manage your local RustPad account."; document.querySelectorAll("[data-local-profile-field]").forEach(element => { element.hidden = directoryManaged; }); document.querySelectorAll("[data-directory-profile-field]").forEach(element => { element.hidden = !directoryManaged; }); document.querySelector("#profile-directory-name").value = currentSession?.directory_display_name || ""; document.querySelector("#profile-directory-organization").value = currentSession?.directory_organization || ""; const suggestion = currentSession?.suggested_nickname; const suggestionElement = document.querySelector("#profile-nickname-suggestion"); suggestionElement.textContent = suggestion ? `Suggested nickname: ${suggestion}` : "No automatic nickname suggestion is available."; document.querySelector("#profile-password").required = false; profileDialog.showModal(); }); document.querySelector("#close-profile")?.addEventListener("click", () => profileDialog.close()); profileDialog?.addEventListener("click", event => { if (event.target === profileDialog) profileDialog.close(); }); profileForm?.addEventListener("submit", async event => { event.preventDefault(); const message = document.querySelector("#profile-message"); message.textContent = ""; message.classList.remove("success", "error"); try { const selectedColor = document.querySelector("#profile-color").value; const newEmail = currentSession?.directory_managed ? null : (document.querySelector("#profile-email").value.trim() || null); const newPassword = currentSession?.directory_managed ? null : (document.querySelector("#profile-new-password").value || null); const password = currentSession?.directory_managed ? "" : document.querySelector("#profile-password").value; if ((newEmail || newPassword) && !password) throw new Error("Enter the current password to change e-mail or password."); const result = await api("/api/auth/profile", { method: "POST", headers: authHeaders(), body: JSON.stringify({ nickname: document.querySelector("#profile-nickname").value.trim(), editor_color: selectedColor, new_email: newEmail, new_password: newPassword, password }) }); message.textContent = result.message; message.classList.add("success"); currentSession.nickname = result.nickname; currentSession.editor_color = result.editor_color; renderAccount(currentSession); } catch (e) { message.textContent = e.message; message.classList.add("error"); } }); document.querySelector("#profile-delete")?.addEventListener("click", async () => { const message = document.querySelector("#profile-message"); const password = document.querySelector("#profile-password").value; message.classList.remove("success", "error"); if (!password) { message.textContent = "Enter the current password first."; message.classList.add("error"); return; } if (!confirm("Send an e-mail link to permanently delete this account?")) return; try { const result = await api("/api/auth/account/delete", { method: "POST", headers: authHeaders(), body: JSON.stringify({ password }) }); message.textContent = result.message; message.classList.add("success"); } catch (e) { message.textContent = e.message; message.classList.add("error"); } }); 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); toast("Logged out."); }); window.addEventListener("rustpad:session-expired", () => renderAccount(null)); renderAccount(null); validateCurrentSession().then(renderAccount); }