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:
@@ -1,25 +1,42 @@
|
||||
// Shared sidebar nav for the built-in admin screens (todo §5). Both the Users and Groups
|
||||
// screens render the same admin section (Dashboard · Users · Groups), with `current` set on the
|
||||
// active item. Extracted so the two screens can't drift. The global config-driven menu wiring
|
||||
// (an admin section gated per user) is the separate §5 menu item; this is the local in-screen nav.
|
||||
// The built-in admin section of the menu (todo §5). One definition of the Users/Groups/Roles links
|
||||
// + their gate, reused two ways so they can't drift: `adminSection()` is the permission-gated
|
||||
// "Admin" header wired into the global dashboard menu (composeNav drops the whole header + subtree
|
||||
// for a non-admin), and `adminNav()` is the in-screen sidebar each admin screen renders (a link home
|
||||
// + the same section, with the active item marked `current`).
|
||||
|
||||
import { type MenuConfig } from "./menu-config.ts";
|
||||
import { composeNav, type NavNode } from "./nav.ts";
|
||||
|
||||
export const ADMIN_PERMISSION = "admin"; // role token gating every admin screen
|
||||
export const ADMIN_PERMISSION = "admin"; // role token gating the admin section
|
||||
export const ADMIN_USERS_BASE = "/admin/users";
|
||||
export const ADMIN_GROUPS_BASE = "/admin/groups";
|
||||
export const ADMIN_ROLES_BASE = "/admin/roles";
|
||||
|
||||
type AdminScreen = "dashboard" | "groups" | "roles" | "users";
|
||||
export type AdminScreen = "groups" | "roles" | "users";
|
||||
|
||||
const ITEMS: { href: string; icon: string; id: AdminScreen; label: string }[] = [
|
||||
{ href: ADMIN_USERS_BASE, icon: "i-users", id: "users", label: "Users" },
|
||||
{ href: ADMIN_GROUPS_BASE, icon: "i-layers", id: "groups", label: "Groups" },
|
||||
{ href: ADMIN_ROLES_BASE, icon: "i-shield", id: "roles", label: "Roles" },
|
||||
];
|
||||
|
||||
// The gated "Admin" header + its three screens; `current` marks the active screen and opens the
|
||||
// header. The permission lives on the header, so composeNav drops the whole section for a non-admin.
|
||||
export function adminSection(current?: AdminScreen): NavNode {
|
||||
return {
|
||||
children: ITEMS.map((it) => ({ ...it, ...(it.id === current ? { current: true } : {}) })),
|
||||
icon: "i-shield",
|
||||
id: "admin",
|
||||
label: "Admin",
|
||||
permission: ADMIN_PERMISSION,
|
||||
...(current ? { open: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// In-screen sidebar for the admin screens: a link home + the admin section (active item marked).
|
||||
export function adminNav(roles: string[], menu: MenuConfig, current: AdminScreen): NavNode[] {
|
||||
const gated = (id: AdminScreen, href: string, icon: string, label: string): NavNode =>
|
||||
({ ...(current === id ? { current: true } : {}), href, icon, id, label, permission: ADMIN_PERMISSION });
|
||||
return composeNav([[
|
||||
{ href: "/", icon: "i-grid", id: "dashboard", label: "Dashboard" },
|
||||
gated("users", ADMIN_USERS_BASE, "i-users", "Users"),
|
||||
gated("groups", ADMIN_GROUPS_BASE, "i-layers", "Groups"),
|
||||
gated("roles", ADMIN_ROLES_BASE, "i-shield", "Roles"),
|
||||
adminSection(current),
|
||||
]], menu.override, roles);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user