Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e663ab36c | ||
|
|
5cd22f52d4 | ||
|
|
aca0abc780 | ||
|
|
9175d75d61 |
No files matched your search
@@ -0,0 +1,197 @@
|
|||||||
|
# Working on Jarl
|
||||||
|
|
||||||
|
Notes for coding agents (and people) changing this repository. Read this
|
||||||
|
first. For more depth, see [docs/development.md](docs/development.md) for
|
||||||
|
layout, building, tests and releasing, and [docs/concepts.md](docs/concepts.md)
|
||||||
|
for how Jarl works from the outside.
|
||||||
|
|
||||||
|
Jarl manages a BIND 9.20 server. It has three parts:
|
||||||
|
|
||||||
|
* `jarld`, a daemon with an HTTP API, an embedded web UI and SQLite storage;
|
||||||
|
* `jarl`, a CLI for that API;
|
||||||
|
* a Debian package.
|
||||||
|
|
||||||
|
It's all Go: module path `jarl`, version in `go.mod`, `CGO_ENABLED=0`. The
|
||||||
|
SQLite driver is `modernc.org/sqlite`, so there is no cgo. The target is
|
||||||
|
Debian/Ubuntu with BIND 9.20, using Debian's paths (`/etc/bind`, the `bind`
|
||||||
|
group, AppArmor).
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
* **The database is the source of truth.** Jarl renders `named.conf`,
|
||||||
|
`keys.conf` and the zone files from it. Never hand-edit rendered output or
|
||||||
|
add code that reads BIND's files back as state; the importer is the only
|
||||||
|
exception.
|
||||||
|
* **Every change goes through the engine** (`internal/engine`). An API
|
||||||
|
request becomes ops in `expand.go`/`ops.go`. The engine then runs
|
||||||
|
replay → validate → render → `named-checkconf`/`named-checkzone` →
|
||||||
|
atomic install → `rndc reload` → verify serials, and rolls back on
|
||||||
|
failure. Going through the engine gives a change permissions, dry runs,
|
||||||
|
changesets, history and undo for free, so don't write to the store
|
||||||
|
directly from handlers.
|
||||||
|
* **Every user-visible change updates `CHANGELOG.md`** under
|
||||||
|
`## [Unreleased]`, and the matching page in `docs/` in the same commit.
|
||||||
|
Docs that may need it:
|
||||||
|
|
||||||
|
* `cli.md`, `api.md` and `configuration.md`;
|
||||||
|
* `validation.md`, for new validation codes;
|
||||||
|
* `metrics.md`;
|
||||||
|
* the feature's own page.
|
||||||
|
|
||||||
|
Write docs in plain, direct English, like the existing pages.
|
||||||
|
* **Error messages** say what is wrong and how to fix it, naming the object.
|
||||||
|
For example: `view external is reachable from the internet …; set
|
||||||
|
minimal-any yes`.
|
||||||
|
* **New validation rules** get a stable `code`, a test, and a row in
|
||||||
|
`docs/validation.md`.
|
||||||
|
* **Schema changes** append a migration to the `migrations` slice in
|
||||||
|
`internal/store/store.go`, and never edit an old one. The statistics
|
||||||
|
database has its own list, `schema` in `internal/stats/store.go`. Add a note
|
||||||
|
under "Upgrading" in the CHANGELOG when a downgrade would break.
|
||||||
|
* `make test` runs `go vet` and a `gofmt -l` check, and both must pass.
|
||||||
|
`goimports` isn't installed, so fix imports by hand.
|
||||||
|
|
||||||
|
## Building and testing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make # bin/jarld, bin/jarl, bash completion
|
||||||
|
make test # vet + gofmt check + go test ./...
|
||||||
|
JARL_BIND_ROOT=DIR make test # also runs the integration tests against real BIND
|
||||||
|
JARL_BIND_ROOT=DIR make e2e # scripts/e2e.sh: real named + jarld, drives every CLI command
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Real BIND without root.** Get the packages with `apt-get download` and
|
||||||
|
unpack them with `dpkg -x` into a directory; that directory is
|
||||||
|
`JARL_BIND_ROOT`. The package list is in docs/development.md, under "Tests
|
||||||
|
against real BIND". Without `JARL_BIND_ROOT` the integration tests skip,
|
||||||
|
so a green `make test` alone proves less than it looks.
|
||||||
|
* **Run e2e after any engine, render, CLI or server change.** It prints a
|
||||||
|
pass count, such as `606/606`; a failure prints the command and its
|
||||||
|
output.
|
||||||
|
* It uses fixed ports: DNS 25353, rndc 25953, HTTP 28053, lists 28054 and
|
||||||
|
metrics 28055. Stop anything left running on them first.
|
||||||
|
* `KEEP=1` leaves named and jarld running; the last line of the log has
|
||||||
|
their PIDs.
|
||||||
|
* `WORK=DIR` must name an empty directory.
|
||||||
|
* **Certificate tests** run against Pebble in Docker, unless you set
|
||||||
|
`PEBBLE=0`. If `docker info` fails, the Pebble block is silently skipped;
|
||||||
|
on this machine it needs `DOCKER_HOST=unix:///var/run/docker.sock`.
|
||||||
|
* **Unix socket paths** are limited to 107 bytes. Put test sockets under
|
||||||
|
`/run/user/$UID/`, not under a deep temporary directory.
|
||||||
|
* **The systemd sandbox isn't tested.** e2e runs outside systemd, so bugs
|
||||||
|
that only appear in the sandbox (`RestrictAddressFamilies`,
|
||||||
|
capabilities, read-only paths) need a package test. Run `make deb`, then
|
||||||
|
install it in a `debian:trixie` container. That container has BIND 9.20;
|
||||||
|
run it with `--privileged /sbin/init` when you need the real unit.
|
||||||
|
|
||||||
|
## Layout and where things live
|
||||||
|
|
||||||
|
docs/development.md has the full layout. Some places that are easy to miss:
|
||||||
|
|
||||||
|
* **Engine** (`internal/engine`):
|
||||||
|
* `expand.go` turns requests into ops.
|
||||||
|
* `linked.go`: linked zones, kept in step across views by `linkSync`.
|
||||||
|
* `reverse.go`: PTR fill, auto-PTR and reverse zone suggestions.
|
||||||
|
* `compare.go`: compare views.
|
||||||
|
* `exposure.go`: fixes for internet exposure.
|
||||||
|
* **Validation**: `internal/validate` holds all checks and lints.
|
||||||
|
`internal/catalog` is the BIND 9.20 grammar and ARM text for every
|
||||||
|
statement. Use it rather than hard-coding option knowledge.
|
||||||
|
* **Rendering**: `internal/render` writes the configuration.
|
||||||
|
`generated.go` holds the statements Jarl adds itself, shown to users as
|
||||||
|
"Added by Jarl".
|
||||||
|
* **Server**: `internal/server` has one `h_*.go` file per API area; routes
|
||||||
|
are in `routes.go`. The JSON types are in `internal/api`, shared with the
|
||||||
|
Go client in `internal/client`.
|
||||||
|
* **CLI**: `cmd/jarl` is built on cobra. `complete.go` completes the
|
||||||
|
placeholders in usage lines (`ZONE`, `VIEW`, …), so name them
|
||||||
|
consistently.
|
||||||
|
* **Statistics**: `internal/stats` is query statistics, kept in a
|
||||||
|
**separate** SQLite file, `stats.db`, with its own migrations.
|
||||||
|
* **Feature packages**: each has its own doc page.
|
||||||
|
* `encdns`: encrypted DNS;
|
||||||
|
* `certs` and `acme`: certificates and ACME;
|
||||||
|
* `blocking`: response policy zones;
|
||||||
|
* `dnssec`;
|
||||||
|
* `nscheck`: queries to other name servers;
|
||||||
|
* `mailcheck`: mail reverse DNS;
|
||||||
|
* `totp`: two-factor login;
|
||||||
|
* `backup`;
|
||||||
|
* `porkbun`: registrar API, with an in-memory `Fake`. Tests never call
|
||||||
|
the real API.
|
||||||
|
|
||||||
|
## Web UI rules
|
||||||
|
|
||||||
|
`internal/web/static/` holds the UI: plain ES modules, no framework, no
|
||||||
|
build step. The files are embedded with `go:embed`.
|
||||||
|
|
||||||
|
* **Rebuild jarld after editing them**, or you'll be testing the old files.
|
||||||
|
* **Build DOM only with `h()`** from `assets/ui.js`. Never use `innerHTML`
|
||||||
|
or `insertAdjacentHTML`. That's how record data can't inject markup.
|
||||||
|
* **The CSP is `self` only.** That rules out:
|
||||||
|
* inline scripts;
|
||||||
|
* `style="…"` attributes and inline style strings (`el.setAttribute("style", …)`);
|
||||||
|
* external fonts and CDNs.
|
||||||
|
|
||||||
|
Use a CSS class in `style.css`, or set individual properties with
|
||||||
|
`el.style.prop = …` (CSSOM is allowed).
|
||||||
|
* **Colors are CSS custom properties** with light and dark values. Reuse the
|
||||||
|
existing tokens.
|
||||||
|
* **Pages live in `assets/pages/`**, one file per route. The router is in
|
||||||
|
`app.js` and the API calls are in `api.js`.
|
||||||
|
* **What the user wants:** functionality and ease of use matter more than
|
||||||
|
visual polish. In pick lists, leave every checkbox unticked by default.
|
||||||
|
|
||||||
|
There's no JS test suite. To catch syntax and import errors without a
|
||||||
|
browser, bundle `assets/app.js` with esbuild, or load the page in headless
|
||||||
|
Chrome.
|
||||||
|
|
||||||
|
## BIND facts that bit us
|
||||||
|
|
||||||
|
* named 9.20 crashes on an empty `keys {}` in a `dnssec-policy`. Validation
|
||||||
|
refuses it.
|
||||||
|
* `named-checkconf` doesn't catch a TTL above `max-zone-ttl`, but named then
|
||||||
|
refuses to load the zone.
|
||||||
|
* `responselog` only takes effect when named starts; use `rndc responselog
|
||||||
|
on|off` instead.
|
||||||
|
* `named -g` sends all logging to stderr, so tests use `-f`.
|
||||||
|
* RPZ client-IP rules see IPv4 clients as v4-mapped (`::ffff:0:0/96`), so
|
||||||
|
IPv6 prefix covers must leave that range out.
|
||||||
|
* named serves DoH in RFC 8484 wire format only, not the JSON API. Test it
|
||||||
|
with BIND's `dig +https` / `dig +tls`, not curl or kdig.
|
||||||
|
* named can't forward over DoH, only over DoT (`forwarders port 853 tls NAME`).
|
||||||
|
* `rndc reconfig` makes named reload renewed TLS certificate files.
|
||||||
|
|
||||||
|
## Git and releases
|
||||||
|
|
||||||
|
These are the maintainer's rules; follow them unless told otherwise:
|
||||||
|
|
||||||
|
* **Commit only when asked. Never push**; the maintainer pushes, to Gitea
|
||||||
|
at gitea.adbraun.lol.
|
||||||
|
* **Work on `dev-X.Y.Z` branches** (for example `dev-0.6.1`) off `main`.
|
||||||
|
Don't name a branch like a tag (`v0.5.2`): `git push` then fails with
|
||||||
|
"src refspec matches more than one".
|
||||||
|
* **A release** is a commit titled `Release X`.
|
||||||
|
* It sets `VERSION`.
|
||||||
|
* It updates the status line in the README.
|
||||||
|
* For a final release, it moves `[Unreleased]` to
|
||||||
|
`## [X] - YYYY-MM-DD`. A release candidate (`X-rc.N`) may leave the
|
||||||
|
notes under `[Unreleased]`.
|
||||||
|
* Tag it annotated: `git tag -a vX -m "Jarl X"`.
|
||||||
|
* Before tagging, `make test` and the e2e run must pass.
|
||||||
|
* Check the notes with `VERSION=X scripts/gitea-release.sh notes`.
|
||||||
|
* **Pushing a `v*` tag** runs `.gitea/workflows/release.yml`. It builds the
|
||||||
|
tarballs, the CLI for each OS and the `.deb`s, and publishes them.
|
||||||
|
* **Before a minor release**, test an upgrade from the previous release's
|
||||||
|
`.deb`s in a container. Check data, history, answers from named, and that
|
||||||
|
an old backup restores.
|
||||||
|
|
||||||
|
## Shell hygiene
|
||||||
|
|
||||||
|
* **Never kill processes by pattern** with `pkill -f` or
|
||||||
|
`pgrep -f … | xargs kill` in the same shell command. The pattern matches
|
||||||
|
the shell running it, and kills that shell. Find the PID with `ps` and
|
||||||
|
kill it by number.
|
||||||
|
* **Start long-running test servers in the background** (your tool's
|
||||||
|
background mode, not `&` in a shell that exits), so they outlive the
|
||||||
|
command.
|
||||||
@@ -6,6 +6,20 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `jarl.service` has the alias `jarld.service`, so `systemctl restart
|
||||||
|
jarld` works too. The alias is linked when the unit is enabled; the
|
||||||
|
package relinks a unit enabled by an older version on upgrade.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- A linked zone's Compare views tab starts with a view it is linked to,
|
||||||
|
not the first other view.
|
||||||
|
- The New token form only offers templates within what the token's user
|
||||||
|
may do. A token never does more than its user, so offering more only
|
||||||
|
misled.
|
||||||
|
|
||||||
## [0.6.0] - 2026-10-13
|
## [0.6.0] - 2026-10-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@AGENTS.md
|
||||||
|
|
||||||
|
## Claude Code specifics
|
||||||
|
|
||||||
|
- End commit messages with a `Co-Authored-By:` trailer naming the model,
|
||||||
|
e.g. `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
|
||||||
|
- Keep temporary scripts, extracted BIND trees, screenshots and test
|
||||||
|
servers' work directories in the session scratchpad, not in the repo.
|
||||||
@@ -114,6 +114,7 @@ install-files:
|
|||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
rm -f $(DESTDIR)$(BINDIR)/jarld $(DESTDIR)$(BINDIR)/jarl $(DESTDIR)$(UNITDIR)/jarl.service \
|
rm -f $(DESTDIR)$(BINDIR)/jarld $(DESTDIR)$(BINDIR)/jarl $(DESTDIR)$(UNITDIR)/jarl.service \
|
||||||
|
$(DESTDIR)$(UNITDIR)/jarld.service \
|
||||||
$(DESTDIR)$(UNITDIR)/jarl-backup.service $(DESTDIR)$(UNITDIR)/jarl-backup.timer \
|
$(DESTDIR)$(UNITDIR)/jarl-backup.service $(DESTDIR)$(UNITDIR)/jarl-backup.timer \
|
||||||
$(DESTDIR)$(BASHCOMPDIR)/jarl $(DESTDIR)$(BASHCOMPDIR)/jarld \
|
$(DESTDIR)$(BASHCOMPDIR)/jarl $(DESTDIR)$(BASHCOMPDIR)/jarld \
|
||||||
$(DESTDIR)$(SYSUSERSDIR)/jarl.conf $(DESTDIR)$(TMPFILESDIR)/jarl.conf
|
$(DESTDIR)$(SYSUSERSDIR)/jarl.conf $(DESTDIR)$(TMPFILESDIR)/jarl.conf
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ if [ "$1" = configure ]; then
|
|||||||
if systemctl is-enabled --quiet jarl-backup.timer 2>/dev/null; then
|
if systemctl is-enabled --quiet jarl-backup.timer 2>/dev/null; then
|
||||||
systemctl start jarl-backup.timer || true
|
systemctl start jarl-backup.timer || true
|
||||||
fi
|
fi
|
||||||
|
# The jarld.service alias is linked when jarl.service is enabled;
|
||||||
|
# relink a unit enabled before the alias existed (this neither
|
||||||
|
# starts nor stops jarld).
|
||||||
|
if [ -n "$2" ] && systemctl is-enabled --quiet jarl.service 2>/dev/null \
|
||||||
|
&& [ ! -e /etc/systemd/system/jarld.service ]; then
|
||||||
|
systemctl reenable jarl.service >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
# An upgrade restarts a running jarld (its database is upgraded
|
# An upgrade restarts a running jarld (its database is upgraded
|
||||||
# when it starts). A first install starts nothing: Jarl needs
|
# when it starts). A first install starts nothing: Jarl needs
|
||||||
# 'jarld init' and 'jarld adopt' first.
|
# 'jarld init' and 'jarld adopt' first.
|
||||||
|
|||||||
@@ -65,3 +65,5 @@ SystemCallErrorNumber=EPERM
|
|||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
# 'systemctl restart jarld' works too (the daemon's binary is jarld).
|
||||||
|
Alias=jarld.service
|
||||||
@@ -32,6 +32,12 @@ internal/
|
|||||||
blocking/ blocklist parser, response policy zone writers, and matching a name the way named does
|
blocking/ blocklist parser, response policy zone writers, and matching a name the way named does
|
||||||
(the downloads are in engine/blocklists.go)
|
(the downloads are in engine/blocklists.go)
|
||||||
config/ jarld's YAML configuration
|
config/ jarld's YAML configuration
|
||||||
|
certs/ certificates: Jarl as an ACME client (lego), renewal, files for named
|
||||||
|
encdns/ encrypted DNS (DoT/DoH) setup, status and health checks
|
||||||
|
mailcheck/ reverse DNS check of the addresses mail is sent from
|
||||||
|
totp/ two-factor login codes and recovery codes
|
||||||
|
version/ what a build reports (release tag, commit, commits since a tag)
|
||||||
|
testutil/ test helpers, chiefly finding BIND's tools (JARL_BIND_ROOT)
|
||||||
deploy/ systemd units (jarld, the daily backup service and timer), sysusers.d and tmpfiles.d files
|
deploy/ systemd units (jarld, the daily backup service and timer), sysusers.d and tmpfiles.d files
|
||||||
deploy/debian/ the Debian package's control template and postinst/prerm/postrm
|
deploy/debian/ the Debian package's control template and postinst/prerm/postrm
|
||||||
contrib/acme.sh/ acme.sh DNS API hook
|
contrib/acme.sh/ acme.sh DNS API hook
|
||||||
|
|||||||
@@ -317,6 +317,10 @@ systemctl status jarl
|
|||||||
journalctl -u jarl -f
|
journalctl -u jarl -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The unit also answers to `jarld` (`Alias=jarld.service`), so
|
||||||
|
`systemctl restart jarld` and `journalctl -u jarl` work alike once it
|
||||||
|
is enabled.
|
||||||
|
|
||||||
Open `https://<management-address>:8053/` and log in as `admin`.
|
Open `https://<management-address>:8053/` and log in as `admin`.
|
||||||
|
|
||||||
## 9. Set up the CLI
|
## 9. Set up the CLI
|
||||||
|
|||||||
+4
-1
@@ -235,7 +235,10 @@ before it).
|
|||||||
## Comparing views
|
## Comparing views
|
||||||
|
|
||||||
A zone's **Compare views** tab, or `jarl zone diff`, compares it with the
|
A zone's **Compare views** tab, or `jarl zone diff`, compares it with the
|
||||||
same zone in another view (or all of them), from the view you're on:
|
same zone in another view (or all of them), from the view you're on.
|
||||||
|
The tab starts with a view the zone is linked to, if any, else the
|
||||||
|
first other view; `jarl zone diff` compares with every other view unless
|
||||||
|
`--with` names some:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ jarl zone diff adbraun.lol --view internal
|
$ jarl zone diff adbraun.lol --view internal
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export async function render(el) {
|
|||||||
const tokensEl = h("div");
|
const tokensEl = h("div");
|
||||||
const drawTokens = async () => {
|
const drawTokens = async () => {
|
||||||
const tokens = await get("/me/tokens");
|
const tokens = await get("/me/tokens");
|
||||||
mount(tokensEl, tokenTable(tokens, drawTokens), newTokenForm("/me/tokens", grants, drawTokens));
|
mount(tokensEl, tokenTable(tokens, drawTokens), newTokenForm("/me/tokens", grants, drawTokens, { admin: me.admin, grants: me.user.grants }));
|
||||||
};
|
};
|
||||||
const cur = input({ type: "password", autocomplete: "current-password" });
|
const cur = input({ type: "password", autocomplete: "current-password" });
|
||||||
const pw1 = input({ type: "password", autocomplete: "new-password" });
|
const pw1 = input({ type: "password", autocomplete: "new-password" });
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export async function tokensDialog(u, grants, own) {
|
|||||||
const base = own ? "/me/tokens" : "/users/" + u.id + "/tokens";
|
const base = own ? "/me/tokens" : "/users/" + u.id + "/tokens";
|
||||||
const draw = async () => {
|
const draw = async () => {
|
||||||
const tokens = await get(base);
|
const tokens = await get(base);
|
||||||
mount(box, tokenTable(tokens, draw), newTokenForm(base, grants, draw));
|
mount(box, tokenTable(tokens, draw), newTokenForm(base, grants, draw, u));
|
||||||
};
|
};
|
||||||
await draw();
|
await draw();
|
||||||
await modal("API tokens of " + u.username, box, [{ label: "Close", value: null }], { wide: true });
|
await modal("API tokens of " + u.username, box, [{ label: "Close", value: null }], { wide: true });
|
||||||
@@ -185,13 +185,26 @@ async function rotateDialog(t, redraw) {
|
|||||||
await modal("Rotate token " + t.name, box, [{ label: "Close", value: null }], { sticky: true });
|
await modal("Rotate token " + t.name, box, [{ label: "Close", value: null }], { sticky: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function newTokenForm(base, grants, redraw) {
|
// coveredBy reports whether a user (admin, or grants) may do what a grant
|
||||||
|
// string ("records:write names=..." ) allows, by resource and access. A
|
||||||
|
// filter on either side only narrows the token further, so it is ignored.
|
||||||
|
function coveredBy(user, grant) {
|
||||||
|
if (!user || user.admin) return true;
|
||||||
|
const [res, acc] = grant.split(/\s+/)[0].split(":");
|
||||||
|
return (user.grants || []).some((g) => (g.resource === "*" || g.resource === res) && (g.access === "write" || acc === "read"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTokenForm makes a token for owner (the user it belongs to). Its
|
||||||
|
// templates are only the roles within what owner may do: a token never
|
||||||
|
// does more than its user, so offering more would only mislead.
|
||||||
|
export function newTokenForm(base, grants, redraw, owner) {
|
||||||
const err = h("div");
|
const err = h("div");
|
||||||
const out = h("div");
|
const out = h("div");
|
||||||
const name = input({ placeholder: "e.g. certbot on web01" });
|
const name = input({ placeholder: "e.g. certbot on web01" });
|
||||||
const exp = select([["", "never expires"], ["30d", "30 days"], ["90d", "90 days"], ["365d", "1 year"]], "90d");
|
const exp = select([["", "never expires"], ["30d", "30 days"], ["90d", "90 days"], ["365d", "1 year"]], "90d");
|
||||||
const gr = textarea("", { class: "mono", rows: 3, placeholder: "optional restriction, e.g.\nrecords:write zones=example.com names=_acme-challenge.* types=TXT" });
|
const gr = textarea("", { class: "mono", rows: 3, placeholder: "optional restriction, e.g.\nrecords:write zones=example.com names=_acme-challenge.* types=TXT" });
|
||||||
const tmpl = select([["", "Insert a template…"], ...Object.keys(grants.roles).map((r) => [r, r])], "");
|
const roles = Object.keys(grants.roles).filter((r) => grants.roles[r].every((g) => coveredBy(owner, g)));
|
||||||
|
const tmpl = select([["", "Insert a template…"], ...roles.map((r) => [r, r])], "");
|
||||||
tmpl.addEventListener("change", () => {
|
tmpl.addEventListener("change", () => {
|
||||||
if (!tmpl.value) return;
|
if (!tmpl.value) return;
|
||||||
gr.value = (gr.value.trim() ? gr.value.trim() + "\n" : "") + grants.roles[tmpl.value].join("\n");
|
gr.value = (gr.value.trim() ? gr.value.trim() + "\n" : "") + grants.roles[tmpl.value].join("\n");
|
||||||
@@ -203,7 +216,7 @@ export function newTokenForm(base, grants, redraw) {
|
|||||||
field("Name", name),
|
field("Name", name),
|
||||||
field("Expires", exp),
|
field("Expires", exp),
|
||||||
field("Restrict to", gr, "Leave empty for all of the user's permissions. A token can never do more than its user."),
|
field("Restrict to", gr, "Leave empty for all of the user's permissions. A token can never do more than its user."),
|
||||||
h("span"), tmpl),
|
roles.length ? [h("span"), tmpl] : null),
|
||||||
err,
|
err,
|
||||||
h("div", { class: "actions", style: { marginTop: "8px" } }, h("button", { class: "primary", onclick: async () => {
|
h("div", { class: "actions", style: { marginTop: "8px" } }, h("button", { class: "primary", onclick: async () => {
|
||||||
mount(err); mount(out);
|
mount(err); mount(out);
|
||||||
|
|||||||
@@ -88,7 +88,10 @@ export async function render(el, params, query) {
|
|||||||
// the same. ?name= (and &type=) narrows it to one name.
|
// the same. ?name= (and &type=) narrows it to one name.
|
||||||
async function compareTab(el, zone, query) {
|
async function compareTab(el, zone, query) {
|
||||||
const others = zone.other_views || [];
|
const others = zone.other_views || [];
|
||||||
const withSel = select([...(others.length > 1 ? [["", "all other views"]] : []), ...others.map((v) => [v, v])], query.get("with") || others[0] || "");
|
// A linked zone starts with a view it is linked to: that is the
|
||||||
|
// comparison that matters (its differences are its own records).
|
||||||
|
const linked = (zone.linked_with || []).filter((v) => others.includes(v));
|
||||||
|
const withSel = select([...(others.length > 1 ? [["", "all other views"]] : []), ...others.map((v) => [v, v])], query.get("with") || linked[0] || others[0] || "");
|
||||||
const nameIn = input({ value: query.get("name") || "", placeholder: "all names, or one: www, @", class: "mono", style: { width: "200px" } });
|
const nameIn = input({ value: query.get("name") || "", placeholder: "all names, or one: www, @", class: "mono", style: { width: "200px" } });
|
||||||
const typeIn = input({ value: query.get("type") || "", placeholder: "any type", class: "mono", style: { width: "90px" } });
|
const typeIn = input({ value: query.get("type") || "", placeholder: "any type", class: "mono", style: { width: "90px" } });
|
||||||
let showSame = false;
|
let showSame = false;
|
||||||
|
|||||||
Reference in new issue
Block a user