import { h, mount, table, field, input, textarea, toast, errorBox, confirmDialog, modal, lines } from "../ui.js"; import { amlContext, suggestions } from "../aml.js"; import { get, change, describeResult } from "../api.js"; import { rerender } from "../app.js"; export async function render(el) { const acls = await get("/acls"); mount(el, h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "ACLs"), h("div", { class: "muted" }, "Named address match lists, used in options such as allow-query, allow-recursion, allow-transfer and in views' match-clients.")), h("div", { class: "actions" }, h("button", { class: "primary", onclick: () => aclDialog(null) }, "New ACL"))), h("div", { class: "msg info small" }, "Built-in ACLs: any, none, localhost (this server's addresses), localnets (networks this server is directly attached to). Elements are checked in order and the first match wins, so put exceptions (!addr) before broader prefixes."), h("div", { class: "panel" }, table([ { label: "Name", render: (a) => h("strong", { class: "mono" }, a.name) }, { label: "Elements", render: (a) => a.elements.join("; ") || h("span", { class: "muted" }, "(empty: matches nothing)"), tdClass: "data" }, { label: "Used by", render: (a) => a.used_by.length ? a.used_by.join(", ") : h("span", { class: "muted" }, "unused"), tdClass: "small" }, { label: "Comment", render: (a) => h("span", { class: "muted" }, a.comment || "") }, { label: "", render: (a) => h("span", null, h("button", { class: "small", onclick: () => aclDialog(a) }, "Edit"), " ", h("button", { class: "small danger", disabled: a.used_by.length > 0, title: a.used_by.length ? "Still in use" : "", onclick: () => del(a) }, "Delete")), tdClass: "actions-cell" }, ], acls, { empty: "No ACLs yet." }))); } async function aclDialog(a) { const err = h("div"); const ctx = await amlContext(); const name = input({ value: a ? a.name : "", placeholder: "homelab", class: "mono" }); const elems = textarea(a ? a.elements.join("\n") : "", { class: "mono", rows: 8, placeholder: "192.168.1.0/24\n10.0.0.0/8\n!10.0.0.66\nkey xfer-key\nlocalhost" }); const comment = input({ value: a ? a.comment : "" }); await modal(a ? "Edit ACL " + a.name : "New ACL", h("div", null, h("div", { class: "form-grid" }, field("Name", name, a ? "Renaming updates every reference automatically." : ""), field("Elements", h("div", null, elems, suggestions(elems, ctx, { exclude: a ? a.name : "" })), "One per line: address, prefix (host bits must be zero), !negated, key NAME, another ACL's name, any/none/localhost/localnets."), field("Comment", comment)), err), [ { label: "Cancel", value: null }, { label: a ? "Save" : "Create ACL", kind: "primary", onclick: async () => { const data = { name: name.value.trim(), elements: lines(elems.value), comment: comment.value }; try { const r = a ? await change("PATCH", "/acls/" + a.id, data) : await change("POST", "/acls", data); toast(describeResult(r)); (r.warnings || []).forEach((w) => toast(w, "warn", 8000)); rerender(); return true; } catch (e) { mount(err, errorBox(e)); return false; } } }, ], { sticky: true }); } async function del(a) { if (!(await confirmDialog("Delete ACL", "Delete ACL " + a.name + "?", "Delete", "danger"))) return; try { const r = await change("DELETE", "/acls/" + a.id); toast(describeResult(r)); rerender(); } catch (e) { toast(e.message, "err", 8000); } }