feat(admin): 增加审计与一致性页面
This commit is contained in:
17
apps/web/lib/admin-v25-source.test.ts
Normal file
17
apps/web/lib/admin-v25-source.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
test('V2.5 admin pages are routed, guarded, and visible through permissions', () => {
|
||||
const auditPage = readFileSync('app/admin/audit/page.tsx', 'utf8');
|
||||
const consistencyPage = readFileSync('app/admin/consistency/page.tsx', 'utf8');
|
||||
const sidebar = readFileSync('components/layout/Sidebar.tsx', 'utf8');
|
||||
const permissions = readFileSync('lib/permissions.ts', 'utf8');
|
||||
|
||||
assert.match(auditPage, /RouteGuard permission="audit:view"/);
|
||||
assert.match(consistencyPage, /RouteGuard permission="consistency:view"/);
|
||||
assert.ok(sidebar.includes("path: '/admin/audit'"));
|
||||
assert.ok(sidebar.includes("path: '/admin/consistency'"));
|
||||
assert.match(permissions, /permission: 'audit:view'/);
|
||||
assert.match(permissions, /permission: 'consistency:view'/);
|
||||
});
|
||||
16
apps/web/lib/audit-api.test.ts
Normal file
16
apps/web/lib/audit-api.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { buildAuditQueryPath } from './audit-api';
|
||||
|
||||
test('buildAuditQueryPath keeps only non-empty audit filters', () => {
|
||||
assert.equal(
|
||||
buildAuditQueryPath({
|
||||
actorId: 'm-8',
|
||||
entityType: 'dev_task',
|
||||
entityId: '',
|
||||
take: '25',
|
||||
}),
|
||||
'/audit?actorId=m-8&entityType=dev_task&take=25',
|
||||
);
|
||||
});
|
||||
43
apps/web/lib/audit-api.ts
Normal file
43
apps/web/lib/audit-api.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { api } from './api';
|
||||
|
||||
export interface AuditEvent {
|
||||
id: string;
|
||||
actorId?: string | null;
|
||||
actorName?: string | null;
|
||||
action: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
productId?: string | null;
|
||||
projectId?: string | null;
|
||||
versionId?: string | null;
|
||||
scope?: unknown;
|
||||
before?: unknown;
|
||||
after?: unknown;
|
||||
metadata?: unknown;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AuditQuery {
|
||||
actorId?: string;
|
||||
entityType?: string;
|
||||
entityId?: string;
|
||||
productId?: string;
|
||||
projectId?: string;
|
||||
versionId?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
take?: string;
|
||||
}
|
||||
|
||||
export function buildAuditQueryPath(query: AuditQuery = {}) {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (typeof value === 'string' && value.trim()) params.set(key, value.trim());
|
||||
}
|
||||
const search = params.toString();
|
||||
return search ? `/audit?${search}` : '/audit';
|
||||
}
|
||||
|
||||
export function listAuditEvents(query: AuditQuery = {}) {
|
||||
return api.get<AuditEvent[]>(buildAuditQueryPath(query));
|
||||
}
|
||||
25
apps/web/lib/consistency-api.test.ts
Normal file
25
apps/web/lib/consistency-api.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { summarizeConsistencyResult, type ConsistencyResult } from './consistency-api';
|
||||
|
||||
test('summarizeConsistencyResult counts ok warn and error checks', () => {
|
||||
const result: ConsistencyResult = {
|
||||
generatedAt: '2026-07-08T08:00:00.000Z',
|
||||
status: 'fail',
|
||||
counts: { products: 1 },
|
||||
checks: {
|
||||
partitionKeys: [{ id: 'p', label: 'p', severity: 'ok', count: 0, message: 'ok' }],
|
||||
orphanReferences: [{ id: 'o', label: 'o', severity: 'error', count: 2, message: 'bad' }],
|
||||
auditCoverage: [{ id: 'a', label: 'a', severity: 'warn', count: 0, message: 'missing' }],
|
||||
},
|
||||
summary: { errors: 1, warnings: 1, human: 'summary' },
|
||||
};
|
||||
|
||||
assert.deepEqual(summarizeConsistencyResult(result), {
|
||||
ok: 1,
|
||||
warn: 1,
|
||||
error: 1,
|
||||
total: 3,
|
||||
});
|
||||
});
|
||||
46
apps/web/lib/consistency-api.ts
Normal file
46
apps/web/lib/consistency-api.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { api } from './api';
|
||||
|
||||
export type ConsistencySeverity = 'ok' | 'warn' | 'error';
|
||||
export type ConsistencyStatus = 'pass' | 'fail';
|
||||
|
||||
export interface ConsistencyCheckResult {
|
||||
id: string;
|
||||
label: string;
|
||||
severity: ConsistencySeverity;
|
||||
count: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ConsistencyResult {
|
||||
generatedAt: string;
|
||||
status: ConsistencyStatus;
|
||||
counts: Record<string, number>;
|
||||
checks: {
|
||||
partitionKeys: ConsistencyCheckResult[];
|
||||
orphanReferences: ConsistencyCheckResult[];
|
||||
auditCoverage: ConsistencyCheckResult[];
|
||||
};
|
||||
summary: {
|
||||
errors: number;
|
||||
warnings: number;
|
||||
human: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function getConsistencyReport() {
|
||||
return api.get<ConsistencyResult>('/consistency');
|
||||
}
|
||||
|
||||
export function summarizeConsistencyResult(result: ConsistencyResult) {
|
||||
const checks = [
|
||||
...result.checks.partitionKeys,
|
||||
...result.checks.orphanReferences,
|
||||
...result.checks.auditCoverage,
|
||||
];
|
||||
return {
|
||||
ok: checks.filter((item) => item.severity === 'ok').length,
|
||||
warn: checks.filter((item) => item.severity === 'warn').length,
|
||||
error: checks.filter((item) => item.severity === 'error').length,
|
||||
total: checks.length,
|
||||
};
|
||||
}
|
||||
@@ -54,6 +54,8 @@ export const PERMISSION_GROUPS: PermissionGroup[] = [
|
||||
},
|
||||
{ module: 'member', moduleLabel: '成员', category: 'main', actions: std4('member') },
|
||||
{ module: 'role', moduleLabel: '角色', category: 'main', actions: std4('role') },
|
||||
{ module: 'audit', moduleLabel: '审计', category: 'main', actions: [{ action: 'view', label: '查看', permission: 'audit:view' }] },
|
||||
{ module: 'consistency', moduleLabel: '一致性', category: 'main', actions: [{ action: 'view', label: '查看', permission: 'consistency:view' }] },
|
||||
{ module: 'version.req', moduleLabel: '需求 Tab', category: 'version_tab', actions: stdTab('version.req') },
|
||||
{ module: 'version.research', moduleLabel: '调研 Tab', category: 'version_tab', actions: stdTab('version.research') },
|
||||
{ module: 'version.product_plan', moduleLabel: '产品方案 Tab', category: 'version_tab', actions: stdTab('version.product_plan') },
|
||||
|
||||
Reference in New Issue
Block a user