fix(audit): 目标列显示服务器名称而非 #id

审计列表对 server 类型批量解析 target_name,前端展示为「服务器 {name}」。
This commit is contained in:
Nexus Agent
2026-06-12 04:16:46 +08:00
parent 8092000880
commit bdbfef7110
8 changed files with 127 additions and 3 deletions
@@ -0,0 +1,20 @@
# 审计 — 审计日志目标列显示服务器名称
**日期**2026-06-12
**Changelog**: `docs/changelog/2026-06-12-audit-log-server-target-name.md`
## 范围
| 文件 |
|------|
| `server/api/settings.py` |
| `frontend/src/pages/AuditPage.vue` |
| `frontend/src/utils/auditLabels.ts` |
| `frontend/src/utils/auditNormalize.ts` |
| `frontend/src/types/api.ts` |
| `tests/integration/test_alerts_audit.py` |
## Step 3 / Closure / DoD
- PASS — 只读增强,批量 IN 查 name
- DoD: integration test passed
@@ -0,0 +1,18 @@
# Changelog — 审计日志目标列显示服务器名称
**日期**2026-06-12
## 摘要
审计页「目标」列对 `target_type=server` 显示 **服务器名称**(如 `服务器 web-01`),不再仅显示 `#372`
## 变更
- API `/api/audit/` 列表项增加 `target_name`(批量查 `servers.name`
- 前端 `auditTargetText()` + `AuditPage` 目标列
## 验证
```bash
.venv/bin/pytest tests/integration/test_alerts_audit.py::test_audit_logs_server_target_name -q
```
+2 -2
View File
@@ -67,7 +67,7 @@
<template #item.target="{ item }">
<span class="text-body-2">
{{ auditTargetLabel(item.resource_type) }}{{ item.resource_id ? ` #${item.resource_id}` : '' }}
{{ auditTargetText(item) }}
</span>
</template>
@@ -112,7 +112,7 @@ import { usePageActivateRefresh } from '@/composables/usePageActivateRefresh'
import { http } from '@/api'
import { useSnackbar } from '@/composables/useSnackbar'
import type { PaginatedResponse, AuditItem } from '@/types/api'
import { auditActionLabel, auditTargetLabel, auditActionFilterOptions } from '@/utils/auditLabels'
import { auditActionLabel, auditTargetLabel, auditTargetText, auditActionFilterOptions } from '@/utils/auditLabels'
import {
formatAuditBatchDetailText,
isBatchJobAuditAction,
+2
View File
@@ -419,6 +419,8 @@ export interface AuditItem {
detail: string | null
ip_address: string | null
created_at: string | null
/** 目标为 server 时由 API 解析的服务器名称 */
target_name?: string | null
}
/** DB credential from /api/scripts/credentials (password never returned) */
+21
View File
@@ -267,6 +267,27 @@ export function auditTargetLabel(type: string): string {
return fallbackActionLabel(key)
}
/** 审计列表「目标」列:服务器显示名称,其它类型保留 #id。 */
export function auditTargetText(item: {
resource_type?: string
target_type?: string
resource_id?: number | null
target_id?: number | null
target_name?: string | null
}): string {
const type = (item.resource_type || item.target_type || '').trim()
const id = item.resource_id ?? item.target_id ?? null
const label = auditTargetLabel(type)
if (type === 'server') {
const name = (item.target_name || '').trim()
if (name) return `${label} ${name}`
if (id != null && id > 0) return `${label} #${id}`
return label || '—'
}
if (id != null && id > 0) return `${label} #${id}`
return label || '—'
}
/** 审计页「操作类型」筛选(中文标签 + 英文 value) */
export const auditActionFilterOptions: { label: string; value: string }[] = [
{ label: '登录成功', value: 'login_success' },
+1
View File
@@ -15,6 +15,7 @@ export function normalizeAuditItem(raw: Record<string, unknown>): AuditItem {
detail: raw.detail != null ? String(raw.detail) : null,
ip_address: raw.ip_address != null ? String(raw.ip_address) : null,
created_at: raw.created_at != null ? String(raw.created_at) : null,
target_name: raw.target_name != null ? String(raw.target_name) : null,
}
}
+31 -1
View File
@@ -1539,10 +1539,40 @@ async def list_audit_logs(
"total": total,
"limit": limit,
"offset": offset,
"items": [_audit_to_dict(log) for log in logs],
"items": await _audit_items_enriched(db, logs),
}
async def _audit_items_enriched(db, logs: list[AuditLog]) -> list[dict]:
"""Attach target_name for server rows (audit UI shows name instead of #id)."""
from sqlalchemy import select
from server.domain.models import Server
server_ids = {
int(log.target_id)
for log in logs
if log.target_type == "server" and log.target_id
}
name_by_id: dict[int, str] = {}
if server_ids:
rows = (
await db.execute(
select(Server.id, Server.name).where(Server.id.in_(server_ids))
)
).all()
name_by_id = {int(sid): (name or f"server-{sid}") for sid, name in rows}
items: list[dict] = []
for log in logs:
row = _audit_to_dict(log)
if log.target_type == "server" and log.target_id:
tid = int(log.target_id)
row["target_name"] = name_by_id.get(tid)
items.append(row)
return items
# ── Retry Jobs ──
retry_router = APIRouter(prefix="/api/retries", tags=["retries"])
+32
View File
@@ -39,6 +39,38 @@ async def test_audit_logs_paginated(db_session, test_admin):
assert body["items"][0]["action"] == "test_action"
@pytest.mark.asyncio
async def test_audit_logs_server_target_name(db_session, test_admin):
from server.domain.models import Server
server = Server(name="audit-target-web", domain="at.example.com", port=22)
db_session.add(server)
await db_session.flush()
db_session.add(
AuditLog(
admin_username=test_admin["admin"].username,
action="watch_install_psutil",
target_type="server",
target_id=server.id,
detail="SSH 安装 psutil",
)
)
await db_session.commit()
body = await list_audit_logs(
action=None,
admin_username=None,
date_from=None,
date_to=None,
limit=10,
offset=0,
admin=test_admin["admin"],
db=db_session,
)
row = next(i for i in body["items"] if i.get("action") == "watch_install_psutil")
assert row["target_name"] == "audit-target-web"
@pytest.mark.asyncio
async def test_alert_history_list_200(db_session, test_admin):
body = await list_alert_history(