Built-in Users admin screen (todo §5); /admin/users list (filter/sort/paginate) + create/edit/deactivate/delete + trigger-recovery, writing only to Kratos via the admin client — gated admin-only (anon→/login, non-admin→403) and CSRF-guarded like logout. New kratosAdmin.createRecoveryCode; reserved the "admin" plugin id; views:[viewsDir] so subfolder views reuse partials/. Reviewer §5 opener: extracted shell-context.ts (buildShellContext/shellUser) shared by dashboard+admin, threading the real signed-in user (drops the hardcoded demo profile). 217→228 units + 8 visual E2E green; boot-verified full CRUD+recovery live on the Ory stack

This commit is contained in:
2026-06-18 12:26:19 +02:00
parent cb050bd4c1
commit 79cfa2ee7f
19 changed files with 837 additions and 20 deletions

View File

@@ -3,10 +3,12 @@
// parseListQuery → filter/sort/paginate a mock dataset → composeNav. Mock data stands in for
// upstream until §4; the filter form, sortable headers and pager all round-trip the URL (zero-JS).
import type { User } from "./context.ts";
import { DEFAULT_MENU, type MenuConfig } from "./menu-config.ts";
import { composeNav, type NavNode, type NavOverride } from "./nav.ts";
import { parseListQuery } from "./list-query.ts";
import { paginate } from "./paginate.ts";
import { buildShellContext } from "./shell-context.ts";
interface Person {
id: string;
@@ -77,7 +79,7 @@ function href(state: State, overrides: Partial<State> = {}): string {
return qs ? `?${qs}` : "?";
}
export function buildDashboardModel(url: URL | URLSearchParams | string, roles: string[] = [], menu: MenuConfig = DEFAULT_MENU, csrfToken = "") {
export function buildDashboardModel(url: URL | URLSearchParams | string, roles: string[] = [], menu: MenuConfig = DEFAULT_MENU, csrfToken = "", user: User | null = null) {
const query = parseListQuery(url, { defaultPageSize: DEFAULT_PAGE_SIZE });
const status = query.filters.status?.[0] ?? "all";
const team = query.filters.team?.[0] ?? "";
@@ -104,18 +106,13 @@ export function buildDashboardModel(url: URL | URLSearchParams | string, roles:
filterBar: filterBar(state),
nav: nav(roles, menu.override),
pagination: pagination(state, page),
shell: {
brand: {
...(menu.branding.logo != null ? { logo: menu.branding.logo } : {}),
name: menu.branding.name,
...(menu.branding.sub != null ? { sub: menu.branding.sub } : {}),
},
shell: buildShellContext({
breadcrumbs: [{ href: "?", label: "Directory" }, { label: "People" }],
csrfToken, // hidden field for the shell's Sign-out POST form (§4)
...(menu.branding.theme != null ? { theme: menu.branding.theme } : {}),
menu,
title: "People",
user: { email: "sam.rivers@example.com", initials: "SR", name: "Sam Rivers" }, // demo until §4
},
user, // real signed-in identity (§4); anonymous ⇒ Guest
}),
table: table(rows, state, sort),
};
}