/*
* 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));
} 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 resourceActionLabel(full, short = full) {
return `${escapeHtml(full)} ${escapeHtml(short)} `;
}
function setResourcePrivacyLabel(button, isPrivate) {
if (!button) return;
const full = isPrivate ? "Make public" : "Make private";
const short = isPrivate ? "Public" : "Private";
button.setAttribute("aria-label", full);
button.title = full;
const fullLabel = button.querySelector(".resource-action-label--full");
const shortLabel = button.querySelector(".resource-action-label--short");
if (fullLabel) fullLabel.textContent = full;
if (shortLabel) shortLabel.textContent = short;
}
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 formatShareCreated(value) { const date = new Date(value); return Number.isNaN(date.getTime()) ? value : `Created ${date.toLocaleString()}`; }
function shareLinkId(tokenHash) { return String(tokenHash || "").slice(0, 12); }
function renderResourcesPagination(meta) {
resourcesPagination.innerHTML = meta.total ? `Previous Page ${meta.page} of ${meta.total_pages} · ${meta.total} items = meta.total_pages ? "disabled" : ""}>Next ` : "";
}
function closeResourcePasswordMenus(except = null) {
document.querySelectorAll(".resource-password-menu[open]").forEach(menu => {
if (menu !== except) menu.removeAttribute("open");
});
}
document.addEventListener("click", event => {
const menu = event.target.closest?.(".resource-password-menu");
closeResourcePasswordMenus(menu);
});
document.addEventListener("keydown", event => {
if (event.key === "Escape") closeResourcePasswordMenus();
});
async function loadResources() {
resourcesError.textContent = ""; resourcesList.innerHTML = "
Loading…
";
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 ? "" : "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));
const passwordActions = item.protected
? ``
: ``;
const privacyAction = item.private ? "Make public" : "Make private";
const privacyShort = item.private ? "Public" : "Private";
row.innerHTML = `${item.kind === "workspace" ? "Workspace" : "Note"}${item.private ? " · private" : ""}${!item.owned ? ` · ${permissionLabel}` : item.protected ? " · password protected" : ""} ${item.owned ? `${resourceActionLabel(privacyAction, privacyShort)} ${resourceActionLabel("Share")} ${resourceActionLabel("Delete")} ` : ""}
`;
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;
setResourcePrivacyLabel(button, nextPrivate);
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 initialShareLinksEnabled = Boolean(item.private || item.protected);
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 or account/directory username.
Direct links Create links for people without an account or manage existing links.
Direct links are disabled while this item is public and has no password. Existing links are preserved and become active again after you make it private or add a password.
`;
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 directLinksEnabled = dialog.querySelector("[data-direct-links-enabled]");
const directLinksDisabled = dialog.querySelector("[data-direct-links-disabled]");
const createdLinkValues = new Map();
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() });
const shareLinksEnabled = d.share_links_enabled !== false;
directLinksEnabled.hidden = !shareLinksEnabled;
directLinksDisabled.hidden = shareLinksEnabled;
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"} Remove `).join("") : 'No users have access.
';
linkList.innerHTML = shareLinksEnabled && d.links.length ? d.links.map(link => {
const identifier = shareLinkId(link.token_hash);
const visibleLink = createdLinkValues.get(link.token_hash);
const linkPreview = visibleLink
? `Copy
The full address remains visible until this dialog is closed. `
: `The full address is not stored. Use the label or link ID #${escapeHtml(identifier)} to identify 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-hash]").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 event => {
const button = event.currentTarget;
const input = linkRow.querySelector(".share-link-inline input");
try {
button.disabled = true;
await copyText(input.value);
setDialogMessage("Link copied.", "success");
} catch (err) {
setDialogMessage(err.message, "error");
} finally {
button.disabled = false;
}
});
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_hash: linkRow.dataset.linkTokenHash, label: linkRow.elements.label.value, 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_hash: linkRow.dataset.linkTokenHash }) }); createdLinkValues.delete(linkRow.dataset.linkTokenHash); 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, label: linkForm.label.value, permission: linkForm.permission.value, expires_at }) });
const absolute = new URL(result.url, location.origin).href;
createdLinkValues.set(result.token_hash, absolute);
linkForm.label.value = "";
await refresh();
try {
await copyText(absolute);
setDialogMessage("Link created, displayed below and copied.", "success");
} catch {
setDialogMessage("Link created and displayed below. Automatic copying failed; use the Copy button.", "warning");
}
} catch (err) {
setDialogMessage(err.message, "error");
}
});
dialog.showModal();
try { await refresh(); } catch (err) { setDialogMessage(err.message, "error"); }
});
row.querySelector("[data-password]")?.addEventListener("click", event => {
event.currentTarget.closest(".resource-password-menu")?.removeAttribute("open");
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.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-remove-password]")?.addEventListener("click", event => {
event.currentTarget.closest(".resource-password-menu")?.removeAttribute("open");
inline.hidden = false;
inline.innerHTML = `Remove password protection from “${escapeHtml(item.title)}”?
Anyone with the public link will be able to open it without a password.
Remove password Cancel
`;
inline.querySelector("[data-cancel]").addEventListener("click", closeInline);
inline.querySelector("[data-confirm-remove-password]").addEventListener("click", async event => {
event.currentTarget.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");
event.currentTarget.disabled = false;
}
});
});
row.querySelector("[data-delete]")?.addEventListener("click", () => {
inline.hidden = false;
inline.innerHTML = `Delete “${escapeHtml(item.title)}” permanently?
Delete Cancel
`;
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);
}