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 }">
|
<template #item.target="{ item }">
|
||||||
<span class="text-body-2">
|
<span class="text-body-2">
|
||||||
{{ auditTargetLabel(item.resource_type) }}{{ item.resource_id ? ` #${item.resource_id}` : '' }}
|
{{ auditTargetText(item) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ import { usePageActivateRefresh } from '@/composables/usePageActivateRefresh'
|
|||||||
import { http } from '@/api'
|
import { http } from '@/api'
|
||||||
import { useSnackbar } from '@/composables/useSnackbar'
|
import { useSnackbar } from '@/composables/useSnackbar'
|
||||||
import type { PaginatedResponse, AuditItem } from '@/types/api'
|
import type { PaginatedResponse, AuditItem } from '@/types/api'
|
||||||
import { auditActionLabel, auditTargetLabel, auditActionFilterOptions } from '@/utils/auditLabels'
|
import { auditActionLabel, auditTargetLabel, auditTargetText, auditActionFilterOptions } from '@/utils/auditLabels'
|
||||||
import {
|
import {
|
||||||
formatAuditBatchDetailText,
|
formatAuditBatchDetailText,
|
||||||
isBatchJobAuditAction,
|
isBatchJobAuditAction,
|
||||||
|
|||||||
@@ -419,6 +419,8 @@ export interface AuditItem {
|
|||||||
detail: string | null
|
detail: string | null
|
||||||
ip_address: string | null
|
ip_address: string | null
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
|
/** 目标为 server 时由 API 解析的服务器名称 */
|
||||||
|
target_name?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** DB credential from /api/scripts/credentials (password never returned) */
|
/** DB credential from /api/scripts/credentials (password never returned) */
|
||||||
|
|||||||
@@ -267,6 +267,27 @@ export function auditTargetLabel(type: string): string {
|
|||||||
return fallbackActionLabel(key)
|
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) */
|
/** 审计页「操作类型」筛选(中文标签 + 英文 value) */
|
||||||
export const auditActionFilterOptions: { label: string; value: string }[] = [
|
export const auditActionFilterOptions: { label: string; value: string }[] = [
|
||||||
{ label: '登录成功', value: 'login_success' },
|
{ label: '登录成功', value: 'login_success' },
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export function normalizeAuditItem(raw: Record<string, unknown>): AuditItem {
|
|||||||
detail: raw.detail != null ? String(raw.detail) : null,
|
detail: raw.detail != null ? String(raw.detail) : null,
|
||||||
ip_address: raw.ip_address != null ? String(raw.ip_address) : null,
|
ip_address: raw.ip_address != null ? String(raw.ip_address) : null,
|
||||||
created_at: raw.created_at != null ? String(raw.created_at) : 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,
|
"total": total,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"offset": offset,
|
"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 Jobs ──
|
||||||
|
|
||||||
retry_router = APIRouter(prefix="/api/retries", tags=["retries"])
|
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"
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_alert_history_list_200(db_session, test_admin):
|
async def test_alert_history_list_200(db_session, test_admin):
|
||||||
body = await list_alert_history(
|
body = await list_alert_history(
|
||||||
|
|||||||
Reference in New Issue
Block a user