Files
ipam/backend/app/api/v1/audit.py
T
Your Name 1e1e5957e8 fix(ipam): 修复IP管理页面网段筛选不生效及下拉框过窄
- Ips.vue: el-select 添加 width: 280px,修复网段下拉框过窄
- Ips.vue: query 参数 networkId → network_id(与 FastAPI snake_case 一致)
- Scan.vue: el-select 用 network.id 代替整个 network 对象作为 value,修复切换网段不生效
- enhanced_scan_service.py: 替换失效的 scapy ARP 扫描为 arp-scan 命令;扫描结果自动创建缺失 IP 记录
2026-07-18 09:45:30 +08:00

164 lines
5.4 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from typing import Optional
from datetime import datetime
import csv
import io
from app.core.database import get_db
from app.core.security import get_current_user
from app.models.auth import User
from app.services.audit_service import AuditService
router = APIRouter(
prefix="/audit",
tags=["审计日志"],
dependencies=[Depends(get_current_user)],
)
@router.get("/logs", summary="查询审计日志")
def get_audit_logs(
user_id: Optional[int] = None,
username: Optional[str] = None,
action: Optional[str] = None,
resource_type: Optional[str] = None,
status: Optional[str] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=500),
# 当前用户必须是 admin 或 super_admin 才能看审计
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
# 角色检查:super_admin / admin 可以看所有人的,operator/viewer 只能看自己的
if current_user.role not in ("super_admin", "admin"):
user_id = current_user.id
total, items = AuditService.get_logs(
db,
user_id=user_id,
username=username,
action=action,
resource_type=resource_type,
status=status,
start_time=start_time,
end_time=end_time,
skip=skip,
limit=limit,
)
serialized = []
for log in items:
serialized.append({
"id": log.id,
"user_id": log.user_id,
"username": log.username,
"action": log.action,
"resource_type": log.resource_type,
"resource_id": log.resource_id,
"resource_name": log.resource_name,
"method": log.method,
"path": log.path,
"ip_address": log.ip_address,
"user_agent": log.user_agent,
"status": log.status,
"detail": log.detail,
"created_at": log.created_at.isoformat() if log.created_at else None,
})
return {"total": total, "items": serialized}
@router.get("/logs/export", summary="导出审计日志为 CSV")
def export_audit_logs(
user_id: Optional[int] = None,
username: Optional[str] = None,
action: Optional[str] = None,
resource_type: Optional[str] = None,
status: Optional[str] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""导出 CSV 流。admin/super_admin 看全部;operator/viewer 仅能导出自己的。"""
if current_user.role not in ("super_admin", "admin"):
user_id = current_user.id
# 导出上限 50000 行,避免一次拉太多 OOM
total, items = AuditService.get_logs(
db,
user_id=user_id,
username=username,
action=action,
resource_type=resource_type,
status=status,
start_time=start_time,
end_time=end_time,
skip=0,
limit=50000,
)
output = io.StringIO()
# 写入 UTF-8 BOM 让 Excel 直接打开不乱码
output.write("\ufeff")
writer = csv.writer(output)
writer.writerow([
"ID", "时间", "用户", "操作", "资源类型", "资源ID", "资源名称",
"HTTP方法", "路径", "客户端IP", "状态", "详情"
])
for log in items:
writer.writerow([
log.id,
log.created_at.isoformat() if log.created_at else "",
log.username or "",
log.action or "",
log.resource_type or "",
log.resource_id or "",
log.resource_name or "",
log.method or "",
log.path or "",
log.ip_address or "",
log.status or "",
(log.detail or "").replace("\n", " ")[:500],
])
output.seek(0)
filename = f"audit_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
@router.get("/stats", summary="审计日志统计")
def get_audit_stats(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
if current_user.role not in ("super_admin", "admin"):
# operator/viewer 只能看自己的简单计数
from app.models.audit import AuditLog
from sqlalchemy import func
my_count = db.query(func.count(AuditLog.id)).filter(AuditLog.user_id == current_user.id).scalar()
return {"recent_24h": 0, "by_action_24h": {}, "my_total": int(my_count or 0)}
return AuditService.get_stats(db)
@router.get("/actions", summary="获取所有审计操作类型(用于前端过滤下拉框)")
def get_action_types():
"""硬编码返回常用 action 列表,避免每次都查 DB"""
from app.services.audit_service import AuditAction
actions = []
for name in dir(AuditAction):
if name.startswith("_") or name.isupper() and not name.startswith("__"):
continue
val = getattr(AuditAction, name, None)
if isinstance(val, str):
actions.append({"code": val, "name": name})
return actions