380 lines
29 KiB
JavaScript
380 lines
29 KiB
JavaScript
/*
|
||
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
||
* Source-Available Code / Dual-Licensed.
|
||
*
|
||
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
||
* Commercial or production use requires a valid paid license.
|
||
* See LICENSE file in repository root for details.
|
||
*/
|
||
|
||
import { installGlobalDiagnostics, logInfo } from "@rustpad/logger";
|
||
installGlobalDiagnostics();
|
||
|
||
import { bindIdentityDialog, handleAccountActionToken, handleAccountConfirmationToken, handleResetToken, handleShareInvitationToken, logoutCurrentSession, validateCurrentSession } from "@rustpad/auth-ui";
|
||
import { getAuthToken, getGuestId, setAccessToken } from "@rustpad/session";
|
||
import { applyTheme, getTheme } from "@rustpad/theme";
|
||
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;
|
||
}
|
||
|
||
getGuestId();
|
||
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.granted); }
|
||
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.granted); }
|
||
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 userLabelPrimary = userLabel?.querySelector("[data-account-primary]");
|
||
const userLabelSecondary = userLabel?.querySelector("[data-account-secondary]");
|
||
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 resourcesSearch = document.querySelector("#resources-search");
|
||
const resourcesPerPage = document.querySelector("#resources-per-page");
|
||
const resourcesPagination = document.querySelector("#resources-pagination");
|
||
let resourcesPage = 1;
|
||
let resourcesSearchTimer;
|
||
const profileDialog = document.querySelector("#profile-dialog");
|
||
|
||
const profileForm = document.querySelector("#profile-form");
|
||
let currentSession = null;
|
||
|
||
function authHeaders() { return {}; }
|
||
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()}`; }
|
||
function renderResourcesPagination(meta) {
|
||
resourcesPagination.innerHTML = meta.total ? `<button type="button" data-page="${meta.page - 1}" ${meta.page <= 1 ? "disabled" : ""}>Previous</button><span>Page ${meta.page} of ${meta.total_pages} · ${meta.total} items</span><button type="button" data-page="${meta.page + 1}" ${meta.page >= meta.total_pages ? "disabled" : ""}>Next</button>` : "";
|
||
}
|
||
async function loadResources() {
|
||
resourcesError.textContent = ""; resourcesList.innerHTML = "<p>Loading…</p>";
|
||
try {
|
||
const params = new URLSearchParams({ q: resourcesSearch.value.trim(), page: String(resourcesPage), per_page: resourcesPerPage.value });
|
||
const data = await api(`/api/auth/resources?${params}`, { headers: authHeaders() });
|
||
resourcesPage = data.pagination.page;
|
||
const items = data.items.map(item => ({ ...item, url: item.kind === "workspace" ? `/w/${item.slug}` : `/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";
|
||
const sharedLabel = !item.owned ? `<span class="resource-shared-badge">Shared by ${escapeHtml(item.shared_by || "another user")}</span>` : "";
|
||
const permissionLabel = item.permission === "rw" ? "Read and write" : "Read only";
|
||
row.classList.toggle("resource-row--shared", !Boolean(item.owned));
|
||
row.innerHTML = `<div class="resource-main"><div class="resource-copy"><div class="resource-title-line"><a href="${escapeHtml(safeAppUrl(item.url))}">${escapeHtml(item.title)}</a>${sharedLabel}</div><small>${item.kind === "workspace" ? "Workspace" : "Note"}${item.private ? " · private" : ""}${!item.owned ? ` · ${permissionLabel}` : item.protected ? " · password protected" : ""}</small></div><div class="resource-actions">${item.owned ? `<button class="action-button action-button--secondary compact-button" type="button" data-privacy>${item.private ? "Make public" : "Make private"}</button><button class="action-button action-button--primary compact-button" type="button" data-share>Share</button><button class="action-button action-button--secondary compact-button" type="button" data-password>Change password</button><button class="action-button action-button--danger compact-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-privacy]")?.addEventListener("click", async event => {
|
||
const button = event.currentTarget;
|
||
const nextPrivate = !Boolean(item.private);
|
||
button.disabled = true;
|
||
try {
|
||
await api("/api/auth/resources/privacy", { method: "POST", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, private: nextPrivate }) });
|
||
item.private = nextPrivate;
|
||
button.textContent = nextPrivate ? "Make public" : "Make private";
|
||
const meta = row.querySelector(".resource-copy small");
|
||
meta.textContent = `${item.kind === "workspace" ? "Workspace" : "Note"}${item.private ? " · private" : ""}${!item.owned ? ` · ${permissionLabel}` : item.protected ? " · password protected" : ""}`;
|
||
} catch (e) {
|
||
resourcesError.textContent = e.message;
|
||
} finally {
|
||
button.disabled = false;
|
||
}
|
||
});
|
||
row.querySelector("[data-share]")?.addEventListener("click", async () => {
|
||
const dialog = document.createElement("dialog");
|
||
dialog.className = "app-dialog share-dialog";
|
||
dialog.innerHTML = `<div class="share-panel">
|
||
<div class="share-panel-head"><div><h3>Share access</h3><p>Manage registered users and direct access links for <strong>${escapeHtml(item.title)}</strong>.</p></div><button class="dialog-close" type="button" data-cancel aria-label="Close share dialog">×</button></div>
|
||
<div class="share-dialog-body">
|
||
<section class="share-section"><div class="share-section-head"><div><h4>People with access</h4><p>Invite existing RustPad users by e-mail or account/directory username.</p></div></div><form data-user-share-form class="share-form-grid">
|
||
<label class="share-emails"><span>E-mail or username</span><input name="recipients" type="text" placeholder="alice@example.com, jsmith" autocomplete="off"></label>
|
||
<label><span>Permission</span><select name="permission"><option value="ro">Read only</option><option value="rw">Read and write</option></select></label>
|
||
<button class="primary-button" type="submit">Grant access</button>
|
||
</form><div data-user-list class="share-list"></div></section>
|
||
<section class="share-section"><div class="share-section-head"><div><h4>Direct links</h4><p>Create links for people without an account or manage existing links.</p></div></div><form data-link-form class="share-form-grid share-link-form">
|
||
<label><span>Permission</span><select name="permission"><option value="ro">Read only</option><option value="rw">Read and write</option></select></label>
|
||
<label><span>Valid for</span><div class="share-hours-field"><input name="hours" type="number" min="1" max="87600" value="24" inputmode="numeric"><span>hours</span></div></label>
|
||
<label class="share-forever"><input name="forever" type="checkbox"><span>Never expires</span></label>
|
||
<button class="primary-button" type="submit">Create link</button>
|
||
</form><div class="share-link-list-wrap"><h5>Individual links</h5><div data-link-list class="share-list share-link-list"></div></div></section>
|
||
</div>
|
||
<div class="share-dialog-footer"><p class="form-message resource-inline-message" data-inline-message role="status"></p><button class="secondary-button" type="button" data-done>Done</button></div>
|
||
</div>`;
|
||
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 => `<div class="share-list-row"><div class="share-list-identity"><span class="share-avatar">${escapeHtml((u.nickname || u.email || "?").slice(0, 1).toUpperCase())}</span><div><strong>${escapeHtml(u.nickname)}</strong><small>${escapeHtml(u.email)}</small></div></div><span class="share-role">${u.permission === "rw" ? "Read and write" : "Read only"}</span><button class="secondary-button compact-button" type="button" data-remove-user="${escapeHtml(u.email)}">Remove</button></div>`).join("") : '<p class="share-empty">No users have access.</p>';
|
||
linkList.innerHTML = d.links.length ? d.links.map(link => {
|
||
const linkPreview = '<small class="share-link-legacy">For security, link values are shown only when created. Create a new link to copy it again.</small>';
|
||
return `<form class="share-list-row share-link-row" data-link-token="${escapeHtml(link.token_hash)}"><div class="share-link-info"><strong>Individual link</strong><small>${escapeHtml(formatShareExpiry(link.expires_at))}</small>${linkPreview}</div><label><span class="sr-only">Permission</span><select name="permission" aria-label="Link permission"><option value="ro" ${link.permission === "ro" ? "selected" : ""}>Read only</option><option value="rw" ${link.permission === "rw" ? "selected" : ""}>Read and write</option></select></label><label><span class="sr-only">Validity in hours</span><div class="share-hours-field"><input name="hours" type="number" min="1" max="87600" value="24" aria-label="New validity in hours"><span>h</span></div></label><label class="share-forever"><input name="forever" type="checkbox" ${link.expires_at ? "" : "checked"}><span>Never</span></label><div class="share-row-actions"><button class="secondary-button compact-button" type="submit">Update</button><button class="danger-button compact-button" type="button" data-revoke-link>Revoke</button></div></form>`;
|
||
}).join("") : '<p class="share-empty">No active links.</p>';
|
||
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.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. For security, it is shown only once.", "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 = `<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 “${escapeHtml(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);
|
||
}
|
||
renderResourcesPagination(data.pagination);
|
||
} catch (e) { resourcesList.innerHTML = ""; resourcesPagination.innerHTML = ""; resourcesError.textContent = e.message; }
|
||
}
|
||
|
||
resourcesSearch?.addEventListener("input", () => { clearTimeout(resourcesSearchTimer); resourcesSearchTimer = setTimeout(() => { resourcesPage = 1; loadResources(); }, 250); });
|
||
resourcesPerPage?.addEventListener("change", () => { resourcesPage = 1; loadResources(); });
|
||
resourcesPagination?.addEventListener("click", event => { const button = event.target.closest("[data-page]"); if (!button || button.disabled) return; resourcesPage = Number(button.dataset.page) || 1; loadResources(); });
|
||
|
||
|
||
function renderAccount(session) {
|
||
currentSession = session;
|
||
guestAccount.hidden = Boolean(session);
|
||
userAccount.hidden = !session;
|
||
if (session) {
|
||
const nickname = String(session.nickname || "").trim();
|
||
let primaryIdentity = String(session.email || nickname).trim();
|
||
let secondaryIdentity = nickname;
|
||
|
||
if (session.directory_managed) {
|
||
const organization = String(session.directory_organization || "").trim();
|
||
const displayName = String(session.directory_display_name || session.email || nickname).trim();
|
||
primaryIdentity = organization ? `${organization}\\${displayName}` : displayName;
|
||
const suggestedNickname = String(session.suggested_nickname || "").trim();
|
||
secondaryIdentity = nickname && suggestedNickname
|
||
&& nickname.toLowerCase() !== suggestedNickname.toLowerCase()
|
||
? nickname
|
||
: "";
|
||
}
|
||
|
||
userLabelPrimary.textContent = `Signed as ${primaryIdentity}`;
|
||
userLabelPrimary.title = `Signed as ${primaryIdentity}`;
|
||
userLabelSecondary.textContent = secondaryIdentity;
|
||
userLabelSecondary.title = secondaryIdentity;
|
||
userLabelSecondary.hidden = !secondaryIdentity;
|
||
} else if (userLabelPrimary && userLabelSecondary) {
|
||
userLabelPrimary.textContent = "";
|
||
userLabelPrimary.removeAttribute("title");
|
||
userLabelSecondary.textContent = "";
|
||
userLabelSecondary.removeAttribute("title");
|
||
userLabelSecondary.hidden = true;
|
||
}
|
||
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";
|
||
const selectedTheme = currentSession?.theme || getTheme();
|
||
profileForm.querySelectorAll(`input[name="profile-theme"]`).forEach(input => { input.checked = input.value === selectedTheme; });
|
||
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 the nickname, editor color, and interface theme."
|
||
: "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 selectedTheme = profileForm.querySelector(`input[name="profile-theme"]:checked`)?.value || "dark"; 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, theme: selectedTheme, 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; currentSession.theme = result.theme; applyTheme(result.theme); 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);
|
||
}
|