Task 004: add demo auth and role checks
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"test:roles": "node tests/role-navigation.test.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -19,3 +19,70 @@ main {
|
||||
min-height: 100vh;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
.pageHeader {
|
||||
display: flex;
|
||||
max-width: 920px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.pageHeader h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.pageHeader p {
|
||||
margin: 0;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.roleBadge {
|
||||
display: inline-flex;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
border: 1px solid #b7c0cc;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
background: #ffffff;
|
||||
color: #344054;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.roleNavigation {
|
||||
display: grid;
|
||||
max-width: 920px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.roleNavigation a {
|
||||
display: flex;
|
||||
min-height: 52px;
|
||||
align-items: center;
|
||||
border: 1px solid #d0d5dd;
|
||||
border-radius: 8px;
|
||||
padding: 0 16px;
|
||||
background: #ffffff;
|
||||
color: #1d2733;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
main {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.pageHeader {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ArticleListResponse } from "@pipeline/shared";
|
||||
|
||||
import { RoleNavigation } from "@/widgets/role-navigation";
|
||||
|
||||
const initialArticles: ArticleListResponse = {
|
||||
articles: [],
|
||||
};
|
||||
@@ -7,11 +9,14 @@ const initialArticles: ArticleListResponse = {
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main>
|
||||
<h1>AI Content Pipeline</h1>
|
||||
<p>Dockerized monorepo foundation is running.</p>
|
||||
<p>
|
||||
{initialArticles.articles.length} articles in the contract-backed dashboard.
|
||||
</p>
|
||||
<header className="pageHeader">
|
||||
<div>
|
||||
<h1>AI Content Pipeline</h1>
|
||||
<p>{initialArticles.articles.length} active article workflows</p>
|
||||
</div>
|
||||
<span className="roleBadge">Editor demo</span>
|
||||
</header>
|
||||
<RoleNavigation role="EDITOR" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { getRoleNavigationItems, ROLE_NAVIGATION_ITEMS } from "./model";
|
||||
export type { NavigationItem } from "./model";
|
||||
export { RoleNavigation } from "./ui";
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Role } from "@pipeline/shared";
|
||||
|
||||
export type NavigationItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
href: string;
|
||||
roles: readonly Role[];
|
||||
};
|
||||
|
||||
export const ROLE_NAVIGATION_ITEMS = [
|
||||
{
|
||||
id: "dashboard",
|
||||
label: "Dashboard",
|
||||
href: "/",
|
||||
roles: ["ADMIN", "EDITOR"],
|
||||
},
|
||||
{
|
||||
id: "new-article",
|
||||
label: "New article",
|
||||
href: "/articles/new",
|
||||
roles: ["ADMIN", "EDITOR"],
|
||||
},
|
||||
{
|
||||
id: "review",
|
||||
label: "Review",
|
||||
href: "/review",
|
||||
roles: ["ADMIN", "EDITOR"],
|
||||
},
|
||||
{
|
||||
id: "site-config",
|
||||
label: "Site configuration",
|
||||
href: "/admin/sites",
|
||||
roles: ["ADMIN"],
|
||||
},
|
||||
{
|
||||
id: "script-versions",
|
||||
label: "Script versions",
|
||||
href: "/admin/scripts",
|
||||
roles: ["ADMIN"],
|
||||
},
|
||||
] as const satisfies readonly NavigationItem[];
|
||||
|
||||
export function getRoleNavigationItems(role: Role): NavigationItem[] {
|
||||
return ROLE_NAVIGATION_ITEMS.filter((item) =>
|
||||
item.roles.some((allowedRole) => allowedRole === role),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Role } from "@pipeline/shared";
|
||||
|
||||
import { getRoleNavigationItems } from "./model";
|
||||
|
||||
type RoleNavigationProps = {
|
||||
role: Role;
|
||||
};
|
||||
|
||||
export function RoleNavigation({ role }: RoleNavigationProps) {
|
||||
const items = getRoleNavigationItems(role);
|
||||
|
||||
return (
|
||||
<nav aria-label="Primary navigation">
|
||||
<ul className="roleNavigation">
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>
|
||||
<a href={item.href}>{item.label}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const testDir = dirname(fileURLToPath(import.meta.url));
|
||||
const sourcePath = resolve(testDir, "../src/widgets/role-navigation/model.ts");
|
||||
const source = readFileSync(sourcePath, "utf8");
|
||||
const transpiled = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
},
|
||||
});
|
||||
|
||||
const exports = {};
|
||||
new Function("exports", transpiled.outputText)(exports);
|
||||
|
||||
const editorItems = exports.getRoleNavigationItems("EDITOR");
|
||||
const adminItems = exports.getRoleNavigationItems("ADMIN");
|
||||
|
||||
assert.deepEqual(
|
||||
editorItems.map((item) => item.id),
|
||||
["dashboard", "new-article", "review"],
|
||||
);
|
||||
assert.equal(
|
||||
editorItems.some((item) => item.id === "site-config" || item.id === "script-versions"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
adminItems.some((item) => item.id === "site-config"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
adminItems.some((item) => item.id === "script-versions"),
|
||||
true,
|
||||
);
|
||||
|
||||
console.log("role navigation hides Admin-only items for Editors");
|
||||
Reference in New Issue
Block a user