accounts and email
This commit is contained in:
@@ -34,10 +34,14 @@
|
||||
const loginSection = document.querySelector('[data-login-section]');
|
||||
const registerCancel = document.querySelector('[data-register-cancel]');
|
||||
|
||||
const authHeading = document.querySelector('[data-auth-heading]');
|
||||
|
||||
if (registerToggle && registerPanel && loginSection) {
|
||||
registerToggle.addEventListener('click', () => {
|
||||
loginSection.hidden = true;
|
||||
registerPanel.hidden = false;
|
||||
if (authHeading) authHeading.textContent = 'Create an Account';
|
||||
document.title = 'Create an Account';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,6 +49,8 @@
|
||||
registerCancel.addEventListener('click', () => {
|
||||
registerPanel.hidden = true;
|
||||
loginSection.hidden = false;
|
||||
if (authHeading) authHeading.textContent = 'Log In';
|
||||
document.title = 'Sign in';
|
||||
// Clear any registration form status messages
|
||||
document.querySelector('[data-passkey-register-status]').textContent = '';
|
||||
document.querySelector('[data-register-status]').textContent = '';
|
||||
@@ -152,6 +158,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("[data-dev-login]")?.addEventListener("click", async () => {
|
||||
try {
|
||||
await postJSON("/api/auth/login/dev", {});
|
||||
window.location.href = authNext();
|
||||
} catch (error) {
|
||||
setStatus("[data-dev-login-status]", error.message, true);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("[data-auth-register]")?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
@@ -237,19 +252,37 @@
|
||||
});
|
||||
|
||||
document.querySelector("[data-auth-logout]")?.addEventListener("click", async () => {
|
||||
await postJSON("/api/auth/logout", {});
|
||||
window.location.href = "/login";
|
||||
try {
|
||||
await postJSON("/api/auth/logout", {});
|
||||
} catch (error) {
|
||||
console.warn("Logout request failed:", error);
|
||||
} finally {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
});
|
||||
|
||||
const loadTokens = async () => {
|
||||
const list = document.querySelector("[data-token-list]");
|
||||
if (!list) return;
|
||||
const data = await fetch("/api/tokens", { credentials: "same-origin" }).then((response) => response.json());
|
||||
if (!data.tokens?.length) {
|
||||
const empty = document.createElement("li");
|
||||
empty.className = "token-empty";
|
||||
empty.textContent = "No access tokens yet. Create one when you connect the macOS app, CLI, or another trusted client.";
|
||||
list.replaceChildren(empty);
|
||||
return;
|
||||
}
|
||||
list.replaceChildren(
|
||||
...(data.tokens || []).map((token) => {
|
||||
...data.tokens.map((token) => {
|
||||
const item = document.createElement("li");
|
||||
item.className = `token-row${token.revokedAt ? " is-revoked" : ""}`;
|
||||
const meta = document.createElement("span");
|
||||
meta.textContent = `${token.name} - ${token.scopes.join(", ")}${token.revokedAt ? " - revoked" : ""}`;
|
||||
meta.className = "token-meta";
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = token.name;
|
||||
const details = document.createElement("small");
|
||||
details.textContent = `${token.scopes.join(", ")}${token.revokedAt ? " - revoked" : ""}`;
|
||||
meta.append(name, details);
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = "Revoke";
|
||||
@@ -297,6 +330,17 @@
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("[data-password-reset]")?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await postJSON("/api/auth/password/reset", formJSON(event.currentTarget));
|
||||
setStatus("[data-password-reset-status]", "Password reset. You can sign in with your new password.");
|
||||
event.currentTarget.querySelector('input[name="newPassword"]').value = "";
|
||||
} catch (error) {
|
||||
setStatus("[data-password-reset-status]", error.message, true);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("[data-account-delete]")?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!confirm("Delete this account permanently?")) return;
|
||||
@@ -324,7 +368,7 @@
|
||||
|
||||
const loadAdminUsers = async () => {
|
||||
const section = document.querySelector("[data-admin-users-section]");
|
||||
const list = document.querySelector("[data-admin-users]");
|
||||
const list = document.querySelector("[data-admin-user-list]");
|
||||
if (!section || !list) return;
|
||||
const me = await fetch("/api/auth/me", { credentials: "same-origin" }).then((response) => response.json()).catch(() => null);
|
||||
if (me?.principal?.role !== "admin") return;
|
||||
@@ -332,8 +376,9 @@
|
||||
const data = await fetch("/api/admin/auth-users", { credentials: "same-origin" }).then((response) => response.json());
|
||||
list.replaceChildren(
|
||||
...(data.users || []).map((user) => {
|
||||
const row = document.createElement("form");
|
||||
const row = document.createElement("div");
|
||||
row.className = "user-row";
|
||||
row.dataset.userId = user.id;
|
||||
const identity = document.createElement("span");
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = user.displayName;
|
||||
@@ -348,24 +393,52 @@
|
||||
option.selected = user.role === role;
|
||||
select.append(option);
|
||||
}
|
||||
const button = document.createElement("button");
|
||||
button.type = "submit";
|
||||
button.textContent = "Save";
|
||||
row.append(identity, select, button);
|
||||
row.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
select.name = "role";
|
||||
select.setAttribute("aria-label", `Role for ${user.displayName}`);
|
||||
const disabledLabel = document.createElement("label");
|
||||
disabledLabel.className = "user-disable-toggle";
|
||||
const disabled = document.createElement("input");
|
||||
disabled.type = "checkbox";
|
||||
disabled.name = "disabled";
|
||||
disabled.checked = Boolean(user.disabled);
|
||||
const disabledText = document.createElement("span");
|
||||
disabledText.textContent = "Disabled";
|
||||
disabledLabel.append(disabled, disabledText);
|
||||
const reset = document.createElement("button");
|
||||
reset.type = "button";
|
||||
reset.textContent = "Send password reset";
|
||||
reset.addEventListener("click", async () => {
|
||||
if (!confirm(`Send a new password reset email to ${user.email}? Any earlier reset link will stop working.`)) return;
|
||||
try {
|
||||
await postJSON(`/api/admin/users/${encodeURIComponent(user.id)}/role`, { role: select.value }, { method: "PATCH" });
|
||||
setStatus("[data-admin-users-status]", "Role updated.");
|
||||
await postJSON(`/api/admin/auth-users/${encodeURIComponent(user.id)}/password-reset`, {});
|
||||
setStatus("[data-admin-users-status]", `Password reset email sent to ${user.email}.`);
|
||||
} catch (error) {
|
||||
setStatus("[data-admin-users-status]", error.message, true);
|
||||
}
|
||||
});
|
||||
row.append(identity, select, disabledLabel, reset);
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
document.querySelector("[data-admin-users]")?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget;
|
||||
const users = [...form.querySelectorAll("[data-user-id]")].map((row) => ({
|
||||
id: row.dataset.userId,
|
||||
role: row.querySelector('select[name="role"]').value,
|
||||
disabled: row.querySelector('input[name="disabled"]').checked,
|
||||
}));
|
||||
try {
|
||||
await postJSON("/api/admin/auth-users", { users }, { method: "PATCH" });
|
||||
setStatus("[data-admin-users-status]", "User access settings saved.");
|
||||
await loadAdminUsers();
|
||||
} catch (error) {
|
||||
setStatus("[data-admin-users-status]", error.message, true);
|
||||
}
|
||||
});
|
||||
|
||||
const loadAdminSettings = async () => {
|
||||
const section = document.querySelector("[data-admin-settings-section]");
|
||||
const form = document.querySelector("[data-admin-settings]");
|
||||
|
||||
Reference in New Issue
Block a user