Move admin screens (users/groups/roles/oauth2-clients) into a drop-in example plugin; add the ctx.system capability surface

This commit is contained in:
2026-07-02 08:01:15 +02:00
parent 2202bdbaa0
commit e8ea911b80
59 changed files with 1095 additions and 1022 deletions
+17 -5
View File
@@ -15,6 +15,18 @@ const scheduling: Plugin = {
};
// A plugin with a public nav node (reachable by anyone, signed in or not).
const portal: Plugin = { apiVersion: "1.0.0", id: "portal", nav: [{ href: "/portal", id: "portal", label: "Portal", public: true }] };
// A gated section fragment like the admin plugin's nav: the header carries the permission, so
// composeNav drops the whole subtree for a non-holder (the admin screens ship as a drop-in plugin).
const adminLike: Plugin = {
apiVersion: "1.0.0", id: "admin",
nav: [{
children: [
{ href: "/admin/users", id: "users", label: "Users" },
{ href: "/admin/groups", id: "groups", label: "Groups" },
],
icon: "i-shield", id: "admin", label: "Admin", permission: "admin",
}],
};
const labels = (nodes: NavNode[]): string[] => nodes.map((n) => n.label);
@@ -23,8 +35,8 @@ test("anonymous: brand from menu, Guest user; the gated Dashboard link is hidden
assert.equal(chrome.brand.name, DEFAULT_MENU.branding.name);
assert.equal(chrome.user.name, "Guest");
// Dashboard points at the gated /dashboard — showing it to an anonymous visitor only dead-ends them
// at /login, so it's dropped. Scheduling's only child is gated (dropped), admin gated (dropped);
// the explicitly public Portal node remains.
// at /login, so it's dropped. Scheduling's only child is gated (dropped); the explicitly public
// Portal node remains.
assert.deepEqual(labels(chrome.nav), ["Portal"]);
});
@@ -45,12 +57,12 @@ test("a permission holder sees the Dashboard link + plugin nav; current path ope
assert.equal(chrome.user.name, "ada"); // email local part
});
test("an admin sees the gated admin section; a sub-path marks its base leaf current", () => {
const chrome = buildPluginChrome({ currentPath: "/admin/users/new", menu: DEFAULT_MENU, user: { email: "a@b.c", id: "u1", roles: ["admin"] } });
test("a gated section (like the admin plugin) shows to a holder; a sub-path marks its base leaf current", () => {
const chrome = buildPluginChrome({ currentPath: "/admin/users/new", menu: DEFAULT_MENU, plugins: [adminLike], user: { email: "a@b.c", id: "u1", roles: ["admin"] } });
const admin = chrome.nav.find((n) => n.label === "Admin")!;
assert.ok(admin); // gated section visible to an admin
assert.equal(admin.open, true); // ancestor of the current leaf opened
// /admin/users/new is under the Users base (/admin/users) → that leaf is current, not Groups/Roles.
// /admin/users/new is under the Users base (/admin/users) → that leaf is current, not Groups.
assert.equal(admin.children!.find((c) => c.label === "Users")!.current, true);
assert.equal(admin.children!.find((c) => c.label === "Groups")!.current, undefined);
});