Add audit log UI and backend API seeded E2E

- Add Auth Context route role guards so doctors cannot directly enter template management, user management, or audit logs.

- Add Audit Logs page, sidebar entry, frontend audit API client, and API client test.

- Add backend audit log query endpoint with super/admin visibility rules and query filtering.

- Extend PostgreSQL integration tests to cover audit log query permissions.

- Move Playwright E2E away from localStorage seed data to real backend API login and seed helpers.

- Add E2E coverage for route guards and audit log visibility.

- Run Playwright backend on port 3100 and proxy Vite API requests there to avoid local port conflicts.

- Make server:dev use the compiled NestJS server path, avoiding tsx parameter-property injection issues.

- Update README, AGENTS, feature, testing, security, deployment, progress, API, backendization, and auth/user module docs.
This commit is contained in:
2026-05-02 02:04:56 +08:00
parent a16f522a4b
commit 750cf4129d
31 changed files with 719 additions and 261 deletions

View File

@@ -1,77 +1,83 @@
import { Page } from '@playwright/test';
import { expect, type APIRequestContext, type Page } from '@playwright/test';
export const baseUsers = [
{
username: 'admin',
password: '123456',
role: 'super',
name: '超级管理员',
department: 'admin',
status: 'active',
visibleTemplates: ['dept_surgery'],
manageableTemplates: ['dept_surgery'],
},
{
username: 'manager',
password: '123456',
role: 'admin',
name: '管理员',
department: '外科',
status: 'active',
visibleTemplates: ['dept_surgery'],
manageableTemplates: ['dept_surgery'],
},
{
username: '0001',
password: '123456',
role: 'user',
name: '张医生',
department: '外科',
status: 'active',
visibleTemplates: ['dept_surgery'],
manageableTemplates: [],
},
{
username: '0002',
password: '123456',
role: 'user',
name: '李医生',
department: '内科',
status: 'active',
visibleTemplates: ['dept_internal'],
manageableTemplates: [],
},
];
export const uniqueId = (prefix: string) =>
`${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
export const baseTemplates = [
{
id: 'dept_surgery',
name: '外科部门模板',
desc: 'E2E 外科模板',
content: '<p><span class="field-value" data-bind="patientName" contenteditable="true"></span></p><div class="ai-region" data-ai-id="手术步骤"><div class="ai-content"><p>默认内容</p></div></div>',
createdAt: '2026-05-01T00:00:00.000Z',
author: 'admin',
scope: 'department',
department: '外科',
},
{
id: 'dept_internal',
name: '内科部门模板',
desc: 'E2E 内科模板',
content: '<p>内科模板</p>',
createdAt: '2026-05-01T00:00:00.000Z',
author: 'admin',
scope: 'department',
department: '内科',
},
];
const toFrontendUser = (user: any) => ({
id: user.id,
username: user.username,
role: user.role === 'doctor' ? 'user' : user.role,
name: user.name,
departmentId: user.departmentId,
department: user.departmentName,
status: user.status,
signature: user.signature,
signatureFileId: user.signatureFileId,
visibleTemplates: [],
manageableTemplates: [],
});
export const seedLocalStorage = async (page: Page, data: Record<string, unknown>) => {
await page.addInitScript((seed) => {
export const apiRequest = async <T>(
request: APIRequestContext,
method: 'get' | 'post' | 'patch' | 'delete',
path: string,
data?: Record<string, unknown>,
) => {
const response = await request[method](path, data ? { data } : undefined);
const text = await response.text();
expect(response.ok(), `${method.toUpperCase()} ${path} failed: ${text}`).toBe(true);
return text ? (JSON.parse(text).data as T) : (null as T);
};
export const loginByApi = async (page: Page, username: string, password = '123456') => {
await page.goto('/');
await page.evaluate(() => {
window.localStorage.clear();
window.sessionStorage.clear();
for (const [key, value] of Object.entries(seed)) {
window.localStorage.setItem(key, JSON.stringify(value));
}
}, data);
});
await page.request.post('/api/auth/logout').catch(() => undefined);
const data = await apiRequest<{ user: any }>(page.request, 'post', '/api/auth/login', { username, password });
await page.evaluate((user) => {
window.localStorage.setItem('currentUser', JSON.stringify(user));
}, toFrontendUser(data.user));
return data.user;
};
export const createUserByApi = (
request: APIRequestContext,
body: {
username: string;
name: string;
role: 'admin' | 'user';
department?: string;
departmentId?: string;
visibleTemplates?: string[];
manageableTemplates?: string[];
},
) =>
apiRequest<{ user: any }>(request, 'post', '/api/users', {
password: '123456',
status: 'active',
...body,
}).then((data) => data.user);
export const createDepartmentByApi = (request: APIRequestContext, name: string, code: string) =>
apiRequest<{ department: any }>(request, 'post', '/api/departments', { name, code }).then((data) => data.department);
export const createReportByApi = (
request: APIRequestContext,
body: {
title: string;
patientName?: string;
hospitalId?: string;
content?: string;
status?: 'draft' | 'completed';
},
) =>
apiRequest<{ report: any }>(request, 'post', '/api/reports', {
patientName: 'E2E患者',
hospitalId: uniqueId('H'),
content: '<p>E2E报告内容</p>',
status: 'completed',
...body,
}).then((data) => data.report);