feat: ship enterprise control and desktop governance
This commit is contained in:
219
src/app/api/v1/admin/backoffice/route.ts
Normal file
219
src/app/api/v1/admin/backoffice/route.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { jsonNoStore } from "@/lib/api-response";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { BOSS_PERMISSION_TEMPLATES } from "@/lib/boss-access-templates";
|
||||
import { buildAdminOverview } from "@/lib/boss-admin-overview";
|
||||
import { readState, type BossState } from "@/lib/boss-data";
|
||||
|
||||
const MENU_TREE = [
|
||||
{ key: "workbench", label: "工作台" },
|
||||
{ key: "tenant", label: "租户管理" },
|
||||
{ key: "user", label: "账号管理" },
|
||||
{ key: "role", label: "角色权限" },
|
||||
{
|
||||
key: "resource",
|
||||
label: "资源授权",
|
||||
children: [
|
||||
{ key: "resource.devices", label: "设备资源" },
|
||||
{ key: "resource.projects", label: "项目线程" },
|
||||
{ key: "resource.skills", label: "Skill 资源" },
|
||||
],
|
||||
},
|
||||
{ key: "skills", label: "Skill 中心" },
|
||||
{ key: "risk", label: "风险告警" },
|
||||
{ key: "audit", label: "审计日志" },
|
||||
{ key: "system", label: "系统设置" },
|
||||
] as const;
|
||||
|
||||
function companyNameMap(state: BossState) {
|
||||
return new Map(state.adminCompanies.map((company) => [company.companyId, company.name]));
|
||||
}
|
||||
|
||||
function companyNameFor(state: BossState, companyId?: string) {
|
||||
if (!companyId || companyId === "default") return "默认公司";
|
||||
return companyNameMap(state).get(companyId) ?? companyId;
|
||||
}
|
||||
|
||||
function safeUsers(state: BossState) {
|
||||
return state.authAccounts.map((account) => ({
|
||||
id: account.id,
|
||||
account: account.account,
|
||||
displayName: account.displayName,
|
||||
role: account.role,
|
||||
status: account.status ?? "active",
|
||||
companyId: account.companyId ?? "default",
|
||||
companyName: companyNameFor(state, account.companyId),
|
||||
primaryDeviceId: account.primaryDeviceId,
|
||||
codexNodeId: account.codexNodeId,
|
||||
codexNodeLabel: account.codexNodeLabel,
|
||||
lastLoginAt: account.lastLoginAt,
|
||||
lastLoginMethod: account.lastLoginMethod,
|
||||
mfaRequired: Boolean(account.mfaRequired),
|
||||
createdAt: account.createdAt,
|
||||
updatedAt: account.updatedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
function skillResources(state: BossState) {
|
||||
const byName = new Map<
|
||||
string,
|
||||
{
|
||||
skillId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category?: string;
|
||||
invocation?: string;
|
||||
sourceType: "device" | "catalog";
|
||||
deviceCount: number;
|
||||
devices: Array<{ deviceId: string; updatedAt: string }>;
|
||||
updatedAt: string;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const skill of state.deviceSkills) {
|
||||
const existing = byName.get(skill.name);
|
||||
if (existing) {
|
||||
existing.deviceCount += existing.devices.some((device) => device.deviceId === skill.deviceId) ? 0 : 1;
|
||||
existing.devices.push({ deviceId: skill.deviceId, updatedAt: skill.updatedAt });
|
||||
if (skill.updatedAt.localeCompare(existing.updatedAt) > 0) {
|
||||
existing.updatedAt = skill.updatedAt;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
byName.set(skill.name, {
|
||||
skillId: skill.skillId,
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
category: skill.category,
|
||||
invocation: skill.invocation,
|
||||
sourceType: "device",
|
||||
deviceCount: 1,
|
||||
devices: [{ deviceId: skill.deviceId, updatedAt: skill.updatedAt }],
|
||||
updatedAt: skill.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
for (const catalogItem of state.skillCatalog) {
|
||||
if (byName.has(catalogItem.name)) continue;
|
||||
byName.set(catalogItem.name, {
|
||||
skillId: catalogItem.skillId,
|
||||
name: catalogItem.name,
|
||||
description: catalogItem.description,
|
||||
category: catalogItem.category,
|
||||
sourceType: "catalog",
|
||||
deviceCount: 0,
|
||||
devices: [],
|
||||
updatedAt: catalogItem.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
return [...byName.values()].sort(
|
||||
(left, right) => right.deviceCount - left.deviceCount || left.name.localeCompare(right.name, "zh-CN"),
|
||||
);
|
||||
}
|
||||
|
||||
function projectResources(state: BossState) {
|
||||
return state.projects.map((project) => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
deviceIds: project.deviceIds,
|
||||
deviceCount: project.deviceIds.length,
|
||||
folderName: project.threadMeta.folderName,
|
||||
threadId: project.threadMeta.threadId,
|
||||
threadDisplayName: project.threadMeta.threadDisplayName,
|
||||
isGroup: project.isGroup,
|
||||
collaborationMode: project.collaborationMode,
|
||||
unreadCount: project.unreadCount,
|
||||
riskLevel: project.riskLevel,
|
||||
updatedAt: project.updatedAt,
|
||||
lastMessageAt: project.lastMessageAt,
|
||||
}));
|
||||
}
|
||||
|
||||
function rolesContract() {
|
||||
return {
|
||||
builtInRoles: [
|
||||
{
|
||||
role: "highest_admin",
|
||||
label: "超级管理员",
|
||||
description: "平台侧最高权限,可管理全部公司、账号、设备、项目、Skill、风险和审计。",
|
||||
},
|
||||
{
|
||||
role: "admin",
|
||||
label: "企业管理员",
|
||||
description: "企业内管理员,按授权范围管理本公司资源。",
|
||||
},
|
||||
{
|
||||
role: "member",
|
||||
label: "成员账号",
|
||||
description: "企业子账号,只能访问已分配的电脑、项目和 Skill。",
|
||||
},
|
||||
],
|
||||
permissionTemplates: BOSS_PERMISSION_TEMPLATES,
|
||||
};
|
||||
}
|
||||
|
||||
function buildBackofficePayload(state: BossState) {
|
||||
const overview = buildAdminOverview(state);
|
||||
const skills = skillResources(state);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
menuTree: MENU_TREE,
|
||||
workbench: {
|
||||
summary: overview.summary,
|
||||
companies: overview.companies.slice(0, 10),
|
||||
devices: overview.devices.slice(0, 20),
|
||||
risks: overview.risks.slice(0, 20),
|
||||
notifications: overview.notifications,
|
||||
grantsSummary: overview.grantsSummary,
|
||||
},
|
||||
tenants: overview.companies.map((company) => ({
|
||||
...company,
|
||||
lifecycleStatus: company.status ?? "active",
|
||||
})),
|
||||
users: safeUsers(state),
|
||||
roles: rolesContract(),
|
||||
resourceGroups: {
|
||||
devices: overview.devices,
|
||||
projects: projectResources(state),
|
||||
skills,
|
||||
grants: {
|
||||
devices: state.accountDeviceGrants,
|
||||
projects: state.accountProjectGrants,
|
||||
skills: state.accountSkillGrants,
|
||||
},
|
||||
},
|
||||
audit: {
|
||||
risks: overview.risks,
|
||||
notifications: overview.notifications,
|
||||
riskTimeline: overview.riskTimeline,
|
||||
permissionLogs: state.permissionAuditLogs
|
||||
.slice()
|
||||
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
|
||||
.slice(0, 100),
|
||||
},
|
||||
yudaoMapping: {
|
||||
tenant: "adminCompanies",
|
||||
user: "authAccounts",
|
||||
role: "BOSS_PERMISSION_TEMPLATES",
|
||||
menu: "menuTree",
|
||||
operateLog: "permissionAuditLogs",
|
||||
resource: "devices/projects/deviceSkills",
|
||||
risk: "opsFaults/threadContextAlerts/masterAgentTasks/adminNotifications",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
if (session.role !== "highest_admin") {
|
||||
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
|
||||
}
|
||||
|
||||
const state = await readState();
|
||||
return jsonNoStore(buildBackofficePayload(state));
|
||||
}
|
||||
Reference in New Issue
Block a user