feat(audit): login audit log with account info and IPv4
- 审计日志记录具体账号(用户名<邮箱>) - 优先显示 IPv4,保留 IPv6 - Program Logs 页面新增 account/IPv4 列
This commit is contained in:
@@ -95,7 +95,7 @@ describe('ProgramLogs status label', () => {
|
||||
expect(targetRow).toBeTruthy();
|
||||
|
||||
const tds = targetRow!.findAll((node) => node.type === 'td');
|
||||
const statusCell = tds[5];
|
||||
const statusCell = tds[7];
|
||||
expect(collectText(statusCell).trim()).toBe('成功');
|
||||
const statusBadge = statusCell.find((node) => node.type === 'span');
|
||||
expect(String(statusBadge.props.className || '')).toContain('badge-success');
|
||||
@@ -131,12 +131,51 @@ describe('ProgramLogs status label', () => {
|
||||
expect(targetRow).toBeTruthy();
|
||||
|
||||
const tds = targetRow!.findAll((node) => node.type === 'td');
|
||||
const statusCell = tds[5];
|
||||
const statusCell = tds[7];
|
||||
expect(collectText(statusCell).trim()).toBe('成功');
|
||||
const statusBadge = statusCell.find((node) => node.type === 'span');
|
||||
expect(String(statusBadge.props.className || '')).toContain('badge-success');
|
||||
});
|
||||
|
||||
it('shows login account and client IP as first-class columns', async () => {
|
||||
apiMock.getEvents.mockResolvedValue([
|
||||
{
|
||||
id: 8,
|
||||
type: 'user_login',
|
||||
title: '用户登录成功:姜先生 <user@example.test>',
|
||||
account: '姜先生 <user@example.test>',
|
||||
clientIp: '203.0.113.8',
|
||||
message: '账号=姜先生 <user@example.test> · 邮箱=user@example.test · 用户ID=12 · 方式=api_key · IP=203.0.113.8',
|
||||
level: 'info',
|
||||
read: false,
|
||||
relatedType: 'user',
|
||||
relatedId: 12,
|
||||
createdAt: '2026-06-10T08:00:00.000Z',
|
||||
},
|
||||
]);
|
||||
|
||||
let root!: WebTestRenderer;
|
||||
await act(async () => {
|
||||
root = create(
|
||||
<MemoryRouter initialEntries={['/events']}>
|
||||
<ToastProvider>
|
||||
<ProgramLogs />
|
||||
</ToastProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
const rows = root!.root.findAll((node) => node.type === 'tr');
|
||||
const targetRow = rows.find((row) => collectText(row).includes('用户登录成功'));
|
||||
expect(targetRow).toBeTruthy();
|
||||
|
||||
const tds = targetRow!.findAll((node) => node.type === 'td');
|
||||
expect(collectText(tds[4]!).trim()).toBe('姜先生 <user@example.test>');
|
||||
expect(collectText(tds[5]!).trim()).toBe('203.0.113.8');
|
||||
expect(collectText(targetRow!)).toContain('IP=203.0.113.8');
|
||||
});
|
||||
|
||||
it('opens related trace and route targets from desktop rows', async () => {
|
||||
apiMock.getEvents.mockResolvedValue([
|
||||
{
|
||||
|
||||
@@ -16,6 +16,8 @@ type ProgramEvent = {
|
||||
type: string;
|
||||
title: string;
|
||||
message?: string | null;
|
||||
account?: string | null;
|
||||
clientIp?: string | null;
|
||||
level: 'info' | 'warning' | 'error';
|
||||
read: boolean;
|
||||
relatedId?: number | null;
|
||||
@@ -462,6 +464,12 @@ export default function ProgramLogs() {
|
||||
<MobileField label="类型" value={<span className="badge badge-muted" style={{ fontSize: 11 }}>{typeLabel(row.type)}</span>} />
|
||||
<MobileField label="级别" value={<span className={`badge ${level.cls}`} style={{ fontSize: 11 }}>{level.label}</span>} />
|
||||
<MobileField label="状态" value={<span className={`badge ${eventStatus.cls}`} style={{ fontSize: 11 }}>{eventStatus.label}</span>} />
|
||||
{row.type === 'user_login' && (
|
||||
<>
|
||||
<MobileField label="账号" value={row.account || '-'} />
|
||||
<MobileField label="IPv4/IP" value={row.clientIp || '-'} />
|
||||
</>
|
||||
)}
|
||||
<MobileField label="内容" value={row.message || '-'} stacked />
|
||||
</MobileCard>
|
||||
);
|
||||
@@ -482,6 +490,8 @@ export default function ProgramLogs() {
|
||||
<col style={{ width: 90 }} />
|
||||
<col style={{ width: 90 }} />
|
||||
<col style={{ width: 260 }} />
|
||||
<col style={{ width: 220 }} />
|
||||
<col style={{ width: 150 }} />
|
||||
<col />
|
||||
<col style={{ width: 110 }} />
|
||||
<col style={{ width: 180 }} />
|
||||
@@ -492,6 +502,8 @@ export default function ProgramLogs() {
|
||||
<th>类型</th>
|
||||
<th>级别</th>
|
||||
<th>标题</th>
|
||||
<th>账号</th>
|
||||
<th>IPv4/IP</th>
|
||||
<th>内容</th>
|
||||
<th>状态</th>
|
||||
<th style={{ textAlign: 'right' }}>操作</th>
|
||||
@@ -519,6 +531,12 @@ export default function ProgramLogs() {
|
||||
<td className="program-logs-title-cell">
|
||||
{row.title || '-'}
|
||||
</td>
|
||||
<td className="program-logs-account-cell">
|
||||
{row.account || '-'}
|
||||
</td>
|
||||
<td className="program-logs-ip-cell">
|
||||
{row.clientIp || '-'}
|
||||
</td>
|
||||
<td className="program-logs-content-cell">
|
||||
{row.message || '-'}
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user