fix(audit): 目标列显示服务器名称而非 #id
审计列表对 server 类型批量解析 target_name,前端展示为「服务器 {name}」。
This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -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,
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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
@@ -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"])
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user