- stewardd init asks for the first administrator's name on a terminal.
- Tokens can be rotated (steward token rotate, POST /tokens/{id}/rotate,
Rotate in the web UI), with a grace period for the old secret, as in
Jarl. Schema migration 2 adds the token columns.
- Read-only users see the settings and a subnet's settings locked,
instead of editable fields without a Save button.
- The New token dialog only offers roles within what the user may do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
46 lines
3.0 KiB
JavaScript
46 lines
3.0 KiB
JavaScript
// Settings: the default lease time, conflict checks, authority, and the
|
|
// options every subnet gets.
|
|
import { h, mount, input, field, checkbox, errorBox, readOnly } from "../ui.js";
|
|
import { get, change, can } from "../api.js";
|
|
import { optionsEditor, fmtSeconds, parseSeconds } from "../widgets.js";
|
|
import { rerender } from "../app.js";
|
|
|
|
export async function render(el) {
|
|
const s = await get("/settings");
|
|
const write = can("config", true);
|
|
const lease = input({ value: fmtSeconds(s.lease_time || 43200) });
|
|
const ping = checkbox("Ping an address before offering it, and skip it if something answers", s.ping_check);
|
|
const timeout = input({ type: "number", min: 50, max: 5000, value: s.ping_timeout_ms || 500 });
|
|
timeout.style.width = "7em";
|
|
const hold = input({ value: fmtSeconds(s.decline_hold || 86400) });
|
|
const auth = checkbox("Authoritative: refuse (NAK) requests for addresses that don't belong on the interface", !s.not_authoritative);
|
|
const opts = await optionsEditor(s.options);
|
|
const err = h("div");
|
|
const save = async (dry) => {
|
|
mount(err);
|
|
const lt = parseSeconds(lease.value), dh = parseSeconds(hold.value);
|
|
if (lt === null || dh === null) { mount(err, h("div", { class: "msg err" }, "Write times as 12h, 1d or 30m.")); return; }
|
|
try {
|
|
const r = await change("PATCH", "/settings", { lease_time: lt, ping_check: ping.input.checked, ping_timeout_ms: parseInt(timeout.value, 10) || 0,
|
|
decline_hold: dh, not_authoritative: !auth.input.checked, options: opts.value() }, { query: dry ? { dry_run: 1 } : undefined });
|
|
if (!dry && r.applied) rerender();
|
|
} catch (e) { mount(err, errorBox(e)); }
|
|
};
|
|
mount(el,
|
|
write ? null : h("div", { class: "msg info" }, "You can see the settings but not change them (that needs config:write)."),
|
|
h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Settings and options"),
|
|
h("div", { class: "muted" }, "Server-wide settings, and the options every subnet sends. A subnet's or a reservation's own option of the same name wins."))),
|
|
h("div", { class: "panel" },
|
|
h("div", { class: "form-grid" },
|
|
field("Default lease time", lease, "How long clients keep an address unless a subnet says otherwise (12h, 1d). They renew halfway."),
|
|
field("Options for every subnet", opts, "Usually domain-name-servers and domain-name; routers belong to each subnet."),
|
|
field("Conflict check", h("div", null, ping, h("div", { class: "small" }, "wait ", timeout, " ms for an answer"))),
|
|
field("Hold back for", hold, "How long an address a client declined (or one that answered a ping) isn't handed out."),
|
|
field("Authority", auth, "Turn this off only if another DHCP server serves the same network.")),
|
|
err,
|
|
write ? h("div", { class: "actions", style: { marginTop: "14px" } },
|
|
h("button", { class: "primary", onclick: () => save(false) }, "Save"),
|
|
h("button", { onclick: () => save(true), title: "Check without saving" }, "Check")) : null));
|
|
if (!write) readOnly(el.querySelector(".panel"));
|
|
}
|