Rename the Keto User namespace to Identity, matching Kratos
CI / full-gate (push) Successful in 2m34s
CI / full-gate (push) Successful in 2m34s
This commit is contained in:
@@ -48,7 +48,7 @@ test("anonymous shell Sign-in link carries the current page as return_to", () =>
|
||||
test("a role holder sees the Dashboard link + plugin nav; current path opens the active leaf", () => {
|
||||
const chrome = buildPluginChrome({
|
||||
currentPath: "/scheduling/shifts", menu: DEFAULT_MENU, plugins: [scheduling],
|
||||
user: { email: "ada@x.io", id: "u1", roles: ["scheduling:read"] },
|
||||
identity: { email: "ada@x.io", id: "u1", roles: ["scheduling:read"] },
|
||||
});
|
||||
assert.deepEqual(labels(chrome.nav), ["Dashboard", "Scheduling"]); // Dashboard shown to a signed-in user
|
||||
const section = chrome.nav.find((n) => n.label === "Scheduling")!;
|
||||
@@ -58,7 +58,7 @@ test("a role holder sees the Dashboard link + plugin nav; current path opens the
|
||||
});
|
||||
|
||||
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 chrome = buildPluginChrome({ currentPath: "/admin/users/new", menu: DEFAULT_MENU, plugins: [adminLike], identity: { 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
|
||||
|
||||
+5
-5
@@ -5,7 +5,7 @@
|
||||
// admin plugin is installed) — run through composeNav (override + per-user filter) and
|
||||
// current-marked for the request path.
|
||||
|
||||
import type { User } from "../http/context.ts";
|
||||
import type { SessionIdentity } from "../http/context.ts";
|
||||
import { type MenuConfig } from "./menu-config.ts";
|
||||
import { composeNav, type NavNode } from "./nav.ts";
|
||||
import type { Plugin } from "../plugin-host/plugin.ts";
|
||||
@@ -29,17 +29,17 @@ export interface ChromeOptions {
|
||||
currentPath?: string; // request pathname; the matching nav leaf is marked current
|
||||
menu: MenuConfig;
|
||||
plugins?: Plugin[];
|
||||
user?: User | null;
|
||||
identity?: SessionIdentity | null;
|
||||
}
|
||||
|
||||
export function buildPluginChrome(opts: ChromeOptions): PageChrome {
|
||||
// The Dashboard link targets the gated /dashboard, so show it only to a signed-in user — to an
|
||||
// anonymous visitor (a public page in the shell) it would only dead-end at /login. The admin
|
||||
// section, when present, is just another plugin's nav fragment (examples/plugins/admin).
|
||||
const fragments: NavNode[][] = opts.user ? [[DASHBOARD_NAV]] : [];
|
||||
const fragments: NavNode[][] = opts.identity ? [[DASHBOARD_NAV]] : [];
|
||||
for (const p of opts.plugins ?? []) if (p.nav?.length) fragments.push(p.nav);
|
||||
|
||||
const roles = opts.user?.roles ?? [];
|
||||
const roles = opts.identity?.roles ?? [];
|
||||
const nav = composeNav(fragments, opts.menu.override, roles);
|
||||
if (opts.currentPath) {
|
||||
// Mark by the *best* (longest) href that is the path or a parent of it, so a sub-path like
|
||||
@@ -56,7 +56,7 @@ export function buildPluginChrome(opts: ChromeOptions): PageChrome {
|
||||
// Anonymous "Sign in" returns to the current page (it's host-relative, our own pathname).
|
||||
signInHref: opts.currentPath ? `/login?return_to=${encodeURIComponent(opts.currentPath)}` : "/login",
|
||||
...(b.theme != null ? { theme: b.theme } : {}),
|
||||
user: shellUser(opts.user),
|
||||
user: shellUser(opts.identity),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { NavNode } from "./nav.ts";
|
||||
const NAV: NavNode[] = [{ href: "/dashboard", label: "Dashboard" }, { children: [{ href: "/admin/users", label: "Users" }], label: "Admin" }];
|
||||
|
||||
test("dashboard model: titled shell, passes the unified nav + csrf + user through", () => {
|
||||
const m = buildDashboardModel({ csrfToken: "tok.sig", nav: NAV, user: { email: "ada@x.io", id: "u1", roles: ["admin"] } });
|
||||
const m = buildDashboardModel({ csrfToken: "tok.sig", identity: { email: "ada@x.io", id: "u1", roles: ["admin"] }, nav: NAV });
|
||||
assert.equal(m.shell.title, "Dashboard");
|
||||
assert.equal(m.shell.csrfToken, "tok.sig");
|
||||
assert.equal(m.shell.user.name, "ada"); // real signed-in identity, not a demo profile
|
||||
|
||||
+3
-3
@@ -4,12 +4,12 @@
|
||||
// this placeholder renders until then. Pure: `nav` is the one global menu (ctx.chrome.nav), built
|
||||
// once per request by the host, so the dashboard shows the exact same menu as every other page.
|
||||
|
||||
import type { User } from "../http/context.ts";
|
||||
import type { SessionIdentity } from "../http/context.ts";
|
||||
import { DEFAULT_MENU, type MenuConfig } from "./menu-config.ts";
|
||||
import type { NavNode } from "./nav.ts";
|
||||
import { buildShellContext } from "./shell-context.ts";
|
||||
|
||||
export function buildDashboardModel(opts: { csrfToken?: string; menu?: MenuConfig; nav?: NavNode[]; user?: User | null } = {}) {
|
||||
export function buildDashboardModel(opts: { csrfToken?: string; menu?: MenuConfig; nav?: NavNode[]; identity?: SessionIdentity | null } = {}) {
|
||||
return {
|
||||
nav: opts.nav ?? [],
|
||||
shell: buildShellContext({
|
||||
@@ -17,7 +17,7 @@ export function buildDashboardModel(opts: { csrfToken?: string; menu?: MenuConfi
|
||||
csrfToken: opts.csrfToken ?? "",
|
||||
menu: opts.menu ?? DEFAULT_MENU,
|
||||
title: "Dashboard",
|
||||
user: opts.user ?? null,
|
||||
identity: opts.identity ?? null,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ test("buildShellContext maps branding + breadcrumbs, omitting unset optional fie
|
||||
menu: { branding: { logo: "/l.svg", name: "Acme", sub: "Ops", theme: "dark" }, override: {} },
|
||||
signInHref: "/login?return_to=%2Fx",
|
||||
title: "Users",
|
||||
user: { email: "a@b.c", id: "u1", roles: ["admin"] },
|
||||
identity: { email: "a@b.c", id: "u1", roles: ["admin"] },
|
||||
});
|
||||
assert.deepEqual(full.brand, { logo: "/l.svg", name: "Acme", sub: "Ops" });
|
||||
assert.equal(full.theme, "dark");
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// the profile shows the email's local part as the name with the full email beneath, initials from
|
||||
// the local part; anonymous ⇒ "Guest".
|
||||
|
||||
import type { User } from "../http/context.ts";
|
||||
import type { SessionIdentity } from "../http/context.ts";
|
||||
import { type MenuConfig } from "./menu-config.ts";
|
||||
|
||||
export interface ShellUser {
|
||||
@@ -24,10 +24,10 @@ export interface ShellModel {
|
||||
user: ShellUser;
|
||||
}
|
||||
|
||||
export function shellUser(user: User | null | undefined): ShellUser {
|
||||
if (!user) return { email: "", initials: "G", name: "Guest" };
|
||||
const local = user.email.split("@")[0] || user.email;
|
||||
return { email: user.email, initials: (local.slice(0, 2) || "U").toUpperCase(), name: local };
|
||||
export function shellUser(identity: SessionIdentity | null | undefined): ShellUser {
|
||||
if (!identity) return { email: "", initials: "G", name: "Guest" };
|
||||
const local = identity.email.split("@")[0] || identity.email;
|
||||
return { email: identity.email, initials: (local.slice(0, 2) || "U").toUpperCase(), name: local };
|
||||
}
|
||||
|
||||
export function buildShellContext(opts: {
|
||||
@@ -36,7 +36,7 @@ export function buildShellContext(opts: {
|
||||
menu: MenuConfig;
|
||||
signInHref?: string;
|
||||
title: string;
|
||||
user?: User | null;
|
||||
identity?: SessionIdentity | null;
|
||||
}): ShellModel {
|
||||
const b = opts.menu.branding;
|
||||
return {
|
||||
@@ -46,6 +46,6 @@ export function buildShellContext(opts: {
|
||||
...(opts.signInHref != null ? { signInHref: opts.signInHref } : {}),
|
||||
...(b.theme != null ? { theme: b.theme } : {}),
|
||||
title: opts.title,
|
||||
user: shellUser(opts.user),
|
||||
user: shellUser(opts.identity),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user