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,17 +1,13 @@
import { expect, test } from '@playwright/test';
import { baseTemplates, baseUsers, seedLocalStorage } from './helpers';
import { apiRequest, loginByApi, uniqueId } from './helpers';
test('doctor can save current report as a personal template visible only to self', async ({ page }) => {
await seedLocalStorage(page, {
users: baseUsers,
templates: baseTemplates,
currentUser: baseUsers[2],
reports: [],
});
await loginByApi(page, '0001');
const templateName = `我的测试模板 ${uniqueId('tpl')}`;
page.on('dialog', async (dialog) => {
if (dialog.type() === 'prompt') {
await dialog.accept('我的测试模板');
await dialog.accept(templateName);
return;
}
await dialog.accept();
@@ -22,11 +18,9 @@ test('doctor can save current report as a personal template visible only to self
await page.getByRole('button', { name: '保存为我的模板' }).click();
await expect.poll(async () => {
return page.evaluate(() => {
const templates = JSON.parse(window.localStorage.getItem('templates') || '[]');
return templates.some((template: any) => template.name === '我的测试模板' && template.scope === 'personal' && template.ownerUser === '0001');
});
const data = await apiRequest<{ items: any[] }>(page.request, 'get', '/api/templates?access=use');
return data.items.some((template) => template.name === templateName && template.scope === 'personal' && template.ownerUser === '0001');
}).toBe(true);
await expect(page.locator('option:not([disabled])', { hasText: '我的测试模板' })).toHaveCount(1);
await expect(page.locator('option:not([disabled])', { hasText: templateName })).toHaveCount(1);
});