// Users and everyone's tokens (administrators). import { h, mount, table, time, modal, input, field, checkbox, select, errorBox, confirmDialog, toast, badge } from "../ui.js"; import { get, api, catalog } from "../api.js"; import { rerender } from "../app.js"; export async function render(el) { const [users, tokens, cat] = await Promise.all([get("/users"), get("/tokens"), catalog()]); mount(el, h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Users"), h("div", { class: "muted" }, "Administrators may do everything. Others get a role or grants: config, leases and audit, each read or write.")), h("div", { class: "actions" }, h("button", { class: "primary", onclick: () => userDialog(null, cat) }, "New user"))), h("div", { class: "panel" }, table([ { label: "User", render: (u) => [h("strong", null, u.username), u.disabled ? [" ", badge("disabled", "off")] : null] }, { label: "May", render: (u) => u.admin ? badge("administrator", "accent") : h("span", { class: "mono small" }, u.grant_strings.join(" ") || "nothing") }, { label: "Password", render: (u) => u.has_password ? "yes" : h("span", { class: "muted" }, "tokens only") }, { label: "Last login", render: (u) => u.last_login_at ? time(u.last_login_at) : "never" }, { label: "Comment", render: (u) => u.comment }, { label: "", tdClass: "actions-cell", render: (u) => [ h("button", { class: "small reveal", onclick: () => userDialog(u, cat) }, "Edit"), " ", h("button", { class: "small reveal", onclick: () => del(u) }, "Delete")] }, ], users)), h("h2", null, "API tokens"), h("div", { class: "panel" }, table([ { label: "User", render: (t) => t.username }, { label: "Name", render: (t) => t.name }, { label: "May", render: (t) => h("span", { class: "mono small" }, t.grant_strings.join(" ")) }, { label: "Expires", render: (t) => t.expires_at ? time(t.expires_at) : "never" }, { label: "Last used", render: (t) => t.last_used_at ? [time(t.last_used_at), h("span", { class: "muted small" }, " from " + t.last_used_ip)] : "never" }, { label: "", tdClass: "actions-cell", render: (t) => h("button", { class: "small reveal", onclick: () => revoke(t) }, "Revoke") }, ], tokens, { empty: "No tokens." }))); } export async function revoke(t) { if (!await confirmDialog("Revoke token", "Revoke " + t.username + "'s token “" + t.name + "”? Anything using it is refused from now on.", "Revoke", "danger")) return; try { await api("DELETE", "/tokens/" + t.id); toast("Revoked"); rerender(); } catch (e) { toast(e.message, "err", 8000); } } async function del(u) { if (!await confirmDialog("Delete user", "Delete " + u.username + " and their tokens?", "Delete", "danger")) return; try { await api("DELETE", "/users/" + u.id); rerender(); } catch (e) { toast(e.message, "err", 8000); } } async function userDialog(u, cat) { const name = input({ value: u ? u.username : "", autocomplete: "off" }); const pw = input({ type: "password", autocomplete: "new-password", placeholder: u ? "unchanged" : "at least 8 characters" }); const admin = checkbox("Administrator", u ? u.admin : false); const role = select([["", "custom grants"], ...Object.keys(cat.roles).map((r) => [r, r + ": " + cat.roles[r].join(" ")])], ""); const grants = input({ value: u ? u.grant_strings.join(" ") : "", placeholder: "config:read leases:write", class: "mono" }); const disabled = checkbox("Disabled (logs them out)", u ? u.disabled : false); const comment = input({ value: u ? u.comment : "" }); const err = h("div"); await modal(u ? "Edit " + u.username : "New user", h("div", null, h("div", { class: "form-grid" }, field("Username", name), field("Password", pw, u ? "Leave empty to keep it." : "Leave empty for a service account that only uses API tokens."), h("label"), admin, field("Role", role), field("Grants", grants, "resource:access, separated by spaces; resources config, leases, audit; access read or write."), u ? [h("label"), disabled] : null, field("Comment", comment)), err), [ { label: "Cancel", value: null }, { label: u ? "Save" : "Add user", kind: "primary", onclick: async () => { mount(err); const body = { username: name.value.trim(), admin: admin.input.checked, comment: comment.value }; if (role.value) body.role = role.value; else body.grants = grants.value.split(/[\s,]+/).filter(Boolean); if (pw.value || !u) body.password = pw.value; if (u) body.disabled = disabled.input.checked; try { await api(u ? "PATCH" : "POST", u ? "/users/" + u.id : "/users", { body }); rerender(); return true; } catch (e) { mount(err, errorBox(e)); return false; } } }, ], { sticky: true }); }