Wire built-in admin screens into the global menu (todo §5); extract adminSection() = one permission-gated 'Admin' header (Users/Groups/Roles), reused by both the home dashboard menu and the in-screen adminNav so they can't drift. composeNav drops the whole gated header+subtree for a non-admin/anonymous (cosmetic — the admin routes stay independently GuardError(403)-gated); narrowed AdminScreen to groups|roles|users. Reuses existing sprite icons (no icon-guard change); default anonymous / render byte-equivalent so visual E2E unaffected. Tests-first: dashboard model gating (admin→3 hrefs, non-admin→absent) + app HTTP (admin JWT→/admin/users link, anon→absent). Stability-reviewer run as a local PR: APPROVE, no Critical/High/Medium. README Layout updated. 242→244 units + typecheck green

This commit is contained in:
2026-06-18 18:33:19 +02:00
parent a016a0131e
commit 6920751cb8
6 changed files with 50 additions and 13 deletions

View File

@@ -216,6 +216,11 @@ test("a verified session JWT authorizes a role-gated route; no cookie / expired
// No cookie and an expired token both render anonymous → the gate denies (403).
assert.equal((await secret()).status, 403);
assert.equal((await secret(`${SESSION_COOKIE}=${mintJwt({ email: "a@b.c", exp: nowSec - 600, roles: ["demo:read"], sub: "u1" })}`)).status, 403);
// The home menu wires in the permission-gated Admin section: an admin's roles surface the links.
const home = (cookie?: string) => fetch(url + "/", cookie ? { headers: { cookie } } : {});
assert.match(await (await home(`${SESSION_COOKIE}=${mintJwt({ email: "a@b.c", exp: nowSec + 600, roles: ["admin"], sub: "u1" })}`)).text(), /href="\/admin\/users"/);
assert.doesNotMatch(await (await home()).text(), /href="\/admin\/users"/); // anonymous → no admin section
});
test("session re-mint: an expired JWT backed by a live Kratos session is silently re-minted; a dead session clears it", async (t) => {