Steward 0.1.0 (in development): a DHCPv4 server with a web UI, API and CLI

Subnets served per interface, pools, reservations, options at three
levels, leases in SQLite saved before the ACK, ping and decline conflict
checks, validated changes with history and undo, users and tokens, an
audit log, a Kea importer, a sandboxed systemd unit and Debian packages.
Tested with real udhcpc and dhcpcd clients in Docker (scripts/e2e.sh).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-14 12:14:07 +01:00
commit 3c0808591f
111 files changed
+17851

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
// 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 });
}