Gate a route or nav node on a session, not only a permission #103
@@ -186,7 +186,9 @@ Revisit only if the stated reason stops holding.
|
|||||||
node names exactly one of `public`, `session`, `permission` — discovery refuses none, two, and a
|
node names exactly one of `public`, `session`, `permission` — discovery refuses none, two, and a
|
||||||
flag spelled anything but `true`, so a forgotten gate fails the boot rather than publishing a page.
|
flag spelled anything but `true`, so a forgotten gate fails the boot rather than publishing a page.
|
||||||
`src/auth/gate.ts` is the one home of the rule the plugin router, the host's own route table and
|
`src/auth/gate.ts` is the one home of the rule the plugin router, the host's own route table and
|
||||||
the menu all read. `session` exists because a plugin whose data is
|
the menu all read. Exactly-one-gate is a discovery-time rule on manifests, not a runtime invariant:
|
||||||
|
`allows({}, user)` stays open **by design**, because the central override's `groups` builds header
|
||||||
|
nodes that carry no gate. Making `allows` fail closed would hide every operator-grouped section. `session` exists because a plugin whose data is
|
||||||
the visitor's own — their upstream account, their own tokens — has no distinction a permission could
|
the visitor's own — their upstream account, their own tokens — has no distinction a permission could
|
||||||
name; the alternative, granting every newly registered user a permission, couples the identity
|
name; the alternative, granting every newly registered user a permission, couples the identity
|
||||||
lifecycle to a Keto write that nothing retries when it fails. A page scoped to "mine" joins on
|
lifecycle to a Keto write that nothing retries when it fails. A page scoped to "mine" joins on
|
||||||
|
|||||||
@@ -920,9 +920,10 @@ The menu is **driven entirely by config** and assembled from two sources:
|
|||||||
export default defineMenu({ branding: { name: "Acme Ops" }, override: { hide: ["teams"] } });
|
export default defineMenu({ branding: { name: "Acme Ops" }, override: { hide: ["teams"] } });
|
||||||
```
|
```
|
||||||
|
|
||||||
Every nav item names one gate — a `permission`, **`public: true`** (everyone) or **`session: true`**
|
Every nav node a **plugin** declares names one gate — a `permission`, **`public: true`** (everyone)
|
||||||
(anyone signed in); the rendered tree is **filtered per user** from the session JWT (no per-request
|
or **`session: true`** (anyone signed in); a header this override groups takes none, and shows
|
||||||
authz call), so the menu only shows what that person can reach.
|
whenever a child does. The rendered tree is **filtered per user** from the session JWT (no
|
||||||
|
per-request authz call), so the menu only shows what that person can reach.
|
||||||
Branding (name, logo, default theme) renders in the app shell.
|
Branding (name, logo, default theme) renders in the app shell.
|
||||||
|
|
||||||
**One menu, one shell, everywhere.** A single menu (`src/ui/chrome.ts` `buildPluginChrome`) renders
|
**One menu, one shell, everywhere.** A single menu (`src/ui/chrome.ts` `buildPluginChrome`) renders
|
||||||
|
|||||||
@@ -35,10 +35,9 @@ export function actionForMethod(method: string): AdminAction {
|
|||||||
return verb === "GET" || verb === "HEAD" ? "read" : "write";
|
return verb === "GET" || verb === "HEAD" ? "read" : "write";
|
||||||
}
|
}
|
||||||
|
|
||||||
// The plugin's nav fragment: an ungated "Admin" header + its three screens, each gated on its own
|
// The plugin's nav fragment: the "Admin" header + its four screens, each gated on its own read
|
||||||
// read permission. The header carries no `permission` because a user may hold one screen's and not
|
// permission. composeNav drops a header left with no visible children, so a user holding none of
|
||||||
// another's; composeNav drops a header left with no visible children, so a user holding none of the
|
// them never sees the section. The host current-marks the active item — no `current`/`open` here.
|
||||||
// three never sees the section. The host current-marks the active item — no `current`/`open` here.
|
|
||||||
export const ADMIN_NAV: NavNode = {
|
export const ADMIN_NAV: NavNode = {
|
||||||
children: [
|
children: [
|
||||||
{ href: ADMIN_USERS_BASE, icon: "i-users", id: "users", label: "admin.nav.users", permission: permissionName("users", "read") },
|
{ href: ADMIN_USERS_BASE, icon: "i-users", id: "users", label: "admin.nav.users", permission: permissionName("users", "read") },
|
||||||
|
|||||||
Reference in New Issue
Block a user