Files
opsxsql/backend/app/api/v1/endpoints/audit_log.py
T
logan 344c519209 feat: P0生产加固 — 认证安全+操作审计+错误提示
P0-1 认证安全:
- JWT secret 改为环境变量配置, 未设置则随机生成(重启失效)
- 新增 LoginRateLimitMiddleware: 5次/分钟登录频率限制(滑动窗口)
- 429 响应 + 前端 toast 提示

P0-2 操作审计日志:
- 新增 OperationLog 模型 (user/username/action/resource/detail/ip)
- 新增 AuditLogMiddleware 自动记录 POST/PUT/DELETE/PATCH 操作
- 新增 GET /audit-logs API (分页/过滤/统计)
- 前端 AuditLogsPage 对接真实 API (字段/路径修正)

P0-3 实例密码加密:
- 已确认 crypto.py AES-256-GCM 加密存储, 无需修改

P1 前端错误提示:
- 新增 useToast hook + ToastContainer 组件
- request.ts 拦截器: 4xx/5xx 错误 toast 显示后端 detail 信息
- 422 验证错误提取第一条 msg
- 网络失败显示通用提示

1182 tests passed
2026-05-25 12:57:59 +08:00

95 lines
3.2 KiB
Python

"""操作审计日志 API。"""
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc
from app.core.database import get_db
from app.core.security import require_role
from app.models.user import User
from app.models.operation_log import OperationLog
router = APIRouter(prefix="/audit-logs", tags=["audit-logs"])
@router.get("")
async def list_audit_logs(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
action: Optional[str] = Query(None),
resource_type: Optional[str] = Query(None),
username: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
"""查询操作审计日志(仅管理员)。"""
query = select(OperationLog)
count_query = select(func.count(OperationLog.id))
if action:
query = query.where(OperationLog.action == action)
count_query = count_query.where(OperationLog.action == action)
if resource_type:
query = query.where(OperationLog.resource_type == resource_type)
count_query = count_query.where(OperationLog.resource_type == resource_type)
if username:
query = query.where(OperationLog.username.contains(username))
count_query = count_query.where(OperationLog.username.contains(username))
total = (await db.execute(count_query)).scalar() or 0
query = query.order_by(desc(OperationLog.created_at))
query = query.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
logs = result.scalars().all()
return {
"total": total,
"page": page,
"page_size": page_size,
"items": [
{
"id": l.id,
"user_id": l.user_id,
"username": l.username,
"action": l.action,
"resource_type": l.resource_type,
"resource_id": l.resource_id,
"detail": l.detail,
"ip_address": l.ip_address,
"created_at": l.created_at.isoformat() if l.created_at else None,
}
for l in logs
],
}
@router.get("/stats")
async def audit_log_stats(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
"""审计日志统计概览。"""
# 最近7天操作分布
from datetime import datetime, timedelta, timezone
since = datetime.now(timezone.utc) - timedelta(days=7)
result = await db.execute(
select(OperationLog.action, func.count(OperationLog.id))
.where(OperationLog.created_at >= since)
.group_by(OperationLog.action)
)
action_stats = {row[0]: row[1] for row in result.all()}
# 操作最多的用户
result = await db.execute(
select(OperationLog.username, func.count(OperationLog.id))
.where(OperationLog.created_at >= since)
.group_by(OperationLog.username)
.order_by(desc(func.count(OperationLog.id)))
.limit(5)
)
top_users = [{"username": row[0], "count": row[1]} for row in result.all()]
return {"action_stats": action_stats, "top_users": top_users}