first commit
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Platform Web Routes
|
||||
|
||||
First-party routes must be declared here before page implementation.
|
||||
|
||||
## Required Navigation
|
||||
|
||||
- `/`: 平台概览(平台管理员默认落地页).
|
||||
- `/servers`: 服务器管理(服主/服务器管理员默认落地页).
|
||||
- `/servers/:serverId`: 服务器详情 route(日常运维工作台:概览、日志、配置、插件控制、AI 助手、操作历史).
|
||||
- `/plugins`: 插件市场.
|
||||
- `/users`: 用户管理.
|
||||
- `/ai-providers`: AI 提供商管理.
|
||||
- `/maintenance`: 系统维护(审计事件与运行节点).
|
||||
- `/plugin-pages/:pluginId/:routeKey`: platform-hosted plugin page route.
|
||||
|
||||
## Role Scoping
|
||||
|
||||
Navigation entries are generated from the current user's capability set (`contracts/workspace.ts`):
|
||||
|
||||
- Platform administrators see 平台概览、服务器管理、插件市场、用户管理、AI 提供商管理、系统维护.
|
||||
- Server owners and server administrators see only 服务器管理 and their server detail workspaces; unauthorized hashes redirect to the role default workspace.
|
||||
- Default landing: platform administrators land on `/`, server owners/administrators land on `/servers`.
|
||||
- Server creation opens inline from 服务器管理 (no separate `/servers/new` page).
|
||||
|
||||
Server and plugin detail flows must use routes, modals, or drawers. Do not build a fixed left-list/right-detail page.
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { defaultPageForUser, firstPartyRoutes, hashForPage, navigationRoutesForUser, resolveRouteHash, routeForPage } from "./routes";
|
||||
import { initialNavigationState } from "../stores/navigation";
|
||||
import { capabilitiesForRoles, type CurrentUserView } from "../contracts/workspace";
|
||||
|
||||
const platformAdmin: CurrentUserView = {
|
||||
id: "user-admin",
|
||||
displayName: "Admin",
|
||||
status: "active",
|
||||
roles: ["platformAdmin"],
|
||||
capabilities: capabilitiesForRoles(["platformAdmin"]),
|
||||
profile: {},
|
||||
source: "local"
|
||||
};
|
||||
|
||||
const serverAdmin: CurrentUserView = {
|
||||
id: "user-server",
|
||||
displayName: "Server Operator",
|
||||
status: "active",
|
||||
roles: ["serverAdmin"],
|
||||
capabilities: capabilitiesForRoles(["serverAdmin"]),
|
||||
profile: {},
|
||||
source: "local"
|
||||
};
|
||||
|
||||
describe("console shell routes", () => {
|
||||
it("resolves known hashes and paths", () => {
|
||||
expect(resolveRouteHash("#/servers", platformAdmin).route.id).toBe("servers");
|
||||
expect(resolveRouteHash("#/profile", platformAdmin).route.id).toBe("profileSettings");
|
||||
expect(resolveRouteHash("#/aiProviders", platformAdmin).route.id).toBe("aiProviders");
|
||||
expect(resolveRouteHash("#/ai-providers", platformAdmin).route.id).toBe("aiProviders");
|
||||
expect(resolveRouteHash("#/unknown", platformAdmin).route.id).toBe("home");
|
||||
expect(resolveRouteHash("#/plugins", platformAdmin).route.id).toBe("plugins");
|
||||
});
|
||||
|
||||
it("resolves server detail hashes with params", () => {
|
||||
const resolved = resolveRouteHash("#/servers/server-example-1", platformAdmin);
|
||||
expect(resolved.route.id).toBe("serverDetail");
|
||||
expect(resolved.params.serverId).toBe("server-example-1");
|
||||
expect(hashForPage("serverDetail", { serverId: "server-example-1" })).toBe("#/servers/server-example-1");
|
||||
});
|
||||
|
||||
it("keeps route metadata available for shell navigation", () => {
|
||||
expect(firstPartyRoutes.map((route) => route.id)).toContain("maintenance");
|
||||
expect(firstPartyRoutes.map((route) => route.id)).toContain("profileSettings");
|
||||
expect(routeForPage("users")).toMatchObject({ label: "用户管理", hash: "#/users" });
|
||||
expect(routeForPage("profileSettings")).toMatchObject({ label: "个人设置", hash: "#/profile" });
|
||||
});
|
||||
|
||||
it("routes platform administrators to the platform overview by default", () => {
|
||||
expect(defaultPageForUser(platformAdmin)).toBe("home");
|
||||
expect(initialNavigationState(platformAdmin, "").pageId).toBe("home");
|
||||
});
|
||||
|
||||
it("routes server owners and administrators to the server list by default", () => {
|
||||
expect(defaultPageForUser(serverAdmin)).toBe("servers");
|
||||
expect(initialNavigationState(serverAdmin, "").pageId).toBe("servers");
|
||||
});
|
||||
|
||||
it("hides platform-only navigation from server-only users", () => {
|
||||
const labels = navigationRoutesForUser(serverAdmin).map((route) => route.label);
|
||||
expect(labels).toContain("服务器管理");
|
||||
expect(labels).not.toContain("平台概览");
|
||||
expect(labels).not.toContain("用户管理");
|
||||
expect(labels).not.toContain("AI 提供商管理");
|
||||
expect(labels).not.toContain("系统维护");
|
||||
expect(labels).not.toContain("个人设置");
|
||||
});
|
||||
|
||||
it("shows all platform areas to platform administrators", () => {
|
||||
const labels = navigationRoutesForUser(platformAdmin).map((route) => route.label);
|
||||
expect(labels).toEqual(["平台概览", "服务器管理", "插件市场", "用户管理", "AI 提供商管理", "系统维护"]);
|
||||
});
|
||||
|
||||
it("allows every authenticated role to open profile settings", () => {
|
||||
expect(initialNavigationState(platformAdmin, "#/profile").pageId).toBe("profileSettings");
|
||||
expect(initialNavigationState(serverAdmin, "#/profile").pageId).toBe("profileSettings");
|
||||
});
|
||||
|
||||
it("redirects unauthorized hashes to the role default workspace", () => {
|
||||
expect(initialNavigationState(serverAdmin, "#/home").pageId).toBe("servers");
|
||||
expect(initialNavigationState(serverAdmin, "#/users").pageId).toBe("servers");
|
||||
expect(initialNavigationState(serverAdmin, "#/servers/server-1").pageId).toBe("serverDetail");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { PageId, PageParams, PageRoute } from "../contracts/page";
|
||||
import type { CurrentUserView } from "../contracts/workspace";
|
||||
import { isPlatformAdmin } from "../contracts/workspace";
|
||||
|
||||
export const firstPartyRoutes: PageRoute[] = [
|
||||
{
|
||||
id: "profileSettings",
|
||||
label: "个人设置",
|
||||
path: "/profile",
|
||||
hash: "#/profile",
|
||||
description: "个人资料与界面偏好",
|
||||
requiredCapability: "profile.settings.manage",
|
||||
showInNav: false
|
||||
},
|
||||
{
|
||||
id: "home",
|
||||
label: "平台概览",
|
||||
path: "/",
|
||||
hash: "#/home",
|
||||
description: "平台运行健康概览",
|
||||
requiredCapability: "platform.overview.read",
|
||||
showInNav: true
|
||||
},
|
||||
{
|
||||
id: "servers",
|
||||
label: "服务器管理",
|
||||
path: "/servers",
|
||||
hash: "#/servers",
|
||||
description: "服务器列表、状态与生命周期",
|
||||
requiredCapability: "servers.read",
|
||||
showInNav: true
|
||||
},
|
||||
{
|
||||
id: "serverDetail",
|
||||
label: "服务器详情",
|
||||
path: "/servers/:serverId",
|
||||
hash: "#/servers/:serverId",
|
||||
description: "单服务器日常运维工作台",
|
||||
requiredCapability: "servers.read",
|
||||
showInNav: false
|
||||
},
|
||||
{
|
||||
id: "plugins",
|
||||
label: "插件市场",
|
||||
path: "/plugins",
|
||||
hash: "#/plugins",
|
||||
description: "插件发现、安装状态与文档",
|
||||
requiredCapability: "plugins.market.read",
|
||||
showInNav: true
|
||||
},
|
||||
{
|
||||
id: "users",
|
||||
label: "用户管理",
|
||||
path: "/users",
|
||||
hash: "#/users",
|
||||
description: "用户、角色和访问审核",
|
||||
requiredCapability: "users.manage",
|
||||
showInNav: true
|
||||
},
|
||||
{
|
||||
id: "aiProviders",
|
||||
label: "AI 提供商管理",
|
||||
path: "/ai-providers",
|
||||
hash: "#/aiProviders",
|
||||
description: "平台中介模型配置",
|
||||
requiredCapability: "aiProviders.manage",
|
||||
showInNav: true
|
||||
},
|
||||
{
|
||||
id: "maintenance",
|
||||
label: "系统维护",
|
||||
path: "/maintenance",
|
||||
hash: "#/maintenance",
|
||||
description: "审计事件与运行节点健康",
|
||||
requiredCapability: "system.maintenance",
|
||||
showInNav: true
|
||||
}
|
||||
];
|
||||
|
||||
const routesById = new Map(firstPartyRoutes.map((route) => [route.id, route]));
|
||||
|
||||
export interface ResolvedRoute {
|
||||
route: PageRoute;
|
||||
params: PageParams;
|
||||
}
|
||||
|
||||
export function routeForPage(pageId: PageId): PageRoute {
|
||||
return routesById.get(pageId) ?? firstPartyRoutes[0];
|
||||
}
|
||||
|
||||
export function hashForPage(pageId: PageId, params: PageParams = {}): string {
|
||||
const route = routeForPage(pageId);
|
||||
if (pageId === "serverDetail" && params.serverId) {
|
||||
return `#/servers/${encodeURIComponent(params.serverId)}`;
|
||||
}
|
||||
return route.hash;
|
||||
}
|
||||
|
||||
export function defaultPageForUser(user: CurrentUserView): PageId {
|
||||
return isPlatformAdmin(user) ? "home" : "servers";
|
||||
}
|
||||
|
||||
export function navigationRoutesForUser(user: CurrentUserView): PageRoute[] {
|
||||
return firstPartyRoutes.filter((route) => route.showInNav && user.capabilities.includes(route.requiredCapability));
|
||||
}
|
||||
|
||||
export function canAccessRoute(user: CurrentUserView, pageId: PageId): boolean {
|
||||
const route = routesById.get(pageId);
|
||||
if (!route) {
|
||||
return false;
|
||||
}
|
||||
return user.capabilities.includes(route.requiredCapability);
|
||||
}
|
||||
|
||||
export function resolveRouteHash(hash: string, user?: CurrentUserView): ResolvedRoute {
|
||||
const fallback: ResolvedRoute = {
|
||||
route: routeForPage(user ? defaultPageForUser(user) : "servers"),
|
||||
params: {}
|
||||
};
|
||||
const normalized = hash.replace(/^#/, "");
|
||||
if (!normalized || normalized === "/") {
|
||||
return fallback;
|
||||
}
|
||||
const segments = normalized.replace(/^\//, "").split("/").filter(Boolean);
|
||||
if (segments.length === 0) {
|
||||
return fallback;
|
||||
}
|
||||
if (segments[0] === "servers" && segments.length > 1) {
|
||||
return { route: routeForPage("serverDetail"), params: { serverId: decodeURIComponent(segments[1]) } };
|
||||
}
|
||||
const key = segments[0];
|
||||
const byId = routesById.get(key as PageId);
|
||||
if (byId) {
|
||||
return { route: byId, params: {} };
|
||||
}
|
||||
const byPath = firstPartyRoutes.find((route) => route.path === `/${key}`);
|
||||
if (byPath) {
|
||||
return { route: byPath, params: {} };
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user