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

27
src/api/audit.test.ts Normal file
View File

@@ -0,0 +1,27 @@
import { describe, expect, it, vi } from 'vitest';
import { listAuditLogs } from './audit';
describe('audit api', () => {
it('loads audit logs with query params', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'application/json' }),
json: () => Promise.resolve({
data: {
items: [{ id: 'log-1', action: 'report.create', targetType: 'Report', createdAt: '2026-05-02T00:00:00.000Z' }],
total: 1,
page: 2,
pageSize: 20,
},
}),
text: () => Promise.resolve(''),
} as Response);
await expect(listAuditLogs({ page: 2, pageSize: 20, action: 'report.create' })).resolves.toMatchObject({
total: 1,
page: 2,
});
expect(fetch).toHaveBeenCalledWith('/api/audit-logs?page=2&pageSize=20&action=report.create', expect.objectContaining({ credentials: 'include' }));
});
});

47
src/api/audit.ts Normal file
View File

@@ -0,0 +1,47 @@
import { apiRequest } from './client';
export interface AuditLogItem {
id: string;
actorUserId?: string | null;
actorUsername?: string | null;
actorName?: string | null;
actorRole?: string | null;
action: string;
targetType: string;
targetId?: string | null;
departmentId?: string | null;
ip?: string | null;
userAgent?: string | null;
metadata?: unknown;
createdAt: string;
}
export interface AuditLogListResponse {
items: AuditLogItem[];
total: number;
page: number;
pageSize: number;
}
export interface AuditLogQuery {
page?: number;
pageSize?: number;
action?: string;
targetType?: string;
actor?: string;
}
export const listAuditLogs = async (query: AuditLogQuery = {}) => {
const params = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined && value !== '') {
params.set(key, String(value));
}
});
const suffix = params.toString() ? `?${params}` : '';
const response = await apiRequest<AuditLogListResponse>(`/api/audit-logs${suffix}`);
if (!response || !Array.isArray(response.items)) {
throw new Error('Invalid audit logs response');
}
return response;
};