""" 操作审计日志 API """ import logging from datetime import datetime from typing import Optional, List from fastapi import APIRouter, Depends, Query, HTTPException, status from sqlalchemy.orm import Session from sqlalchemy import func, desc from pydantic import BaseModel from app.core.database import get_db from app.api.auth import get_current_user from app.models.user import User from app.models.audit_log import AuditLog logger = logging.getLogger(__name__) router = APIRouter( prefix="/api/v1/audit-logs", tags=["audit-logs"], responses={ 401: {"description": "未授权"}, 403: {"description": "无权访问"}, 500: {"description": "服务器内部错误"} } ) # ── Pydantic Schemas ───────────────────────────────────────────── class AuditLogItem(BaseModel): id: str username: str action: str resource_type: str resource_id: Optional[str] = None resource_name: Optional[str] = None detail: Optional[dict] = None ip_address: Optional[str] = None status: str created_at: datetime class Config: from_attributes = True class AuditLogStats(BaseModel): total: int by_action: dict # {"CREATE": 10, "UPDATE": 5, ...} by_resource: dict # {"agent": 8, "workflow": 7, ...} # ── Helpers ────────────────────────────────────────────────────── def _check_admin(current_user: User): if getattr(current_user, "role", None) != "admin": from app.core.exceptions import ForbiddenError raise ForbiddenError("仅管理员可访问审计日志") # ── Endpoints ──────────────────────────────────────────────────── @router.get("", response_model=List[AuditLogItem]) async def get_audit_logs( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), user_id: Optional[str] = Query(None, description="按用户ID过滤"), action: Optional[str] = Query(None, description="操作类型: CREATE/UPDATE/DELETE/EXECUTE/LOGIN"), resource_type: Optional[str] = Query(None, description="资源类型: agent/workflow/user"), status: Optional[str] = Query(None, description="操作状态: success/failure"), start_date: Optional[str] = Query(None, description="开始时间 ISO格式"), end_date: Optional[str] = Query(None, description="结束时间 ISO格式"), keyword: Optional[str] = Query(None, description="资源名称关键词搜索"), skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=1000), ): """查询操作审计日志(仅管理员)""" _check_admin(current_user) query = db.query(AuditLog) if user_id: query = query.filter(AuditLog.user_id == user_id) if action: query = query.filter(AuditLog.action == action.upper()) if resource_type: query = query.filter(AuditLog.resource_type == resource_type) if status: query = query.filter(AuditLog.status == status) if keyword: query = query.filter(AuditLog.resource_name.contains(keyword)) if start_date: query = query.filter(AuditLog.created_at >= datetime.fromisoformat(start_date)) if end_date: query = query.filter(AuditLog.created_at <= datetime.fromisoformat(end_date)) total = query.count() logs = query.order_by(desc(AuditLog.created_at)).offset(skip).limit(limit).all() return logs @router.get("/stats", response_model=AuditLogStats) async def get_audit_logs_stats( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """获取审计日志统计(仅管理员)""" _check_admin(current_user) total = db.query(func.count(AuditLog.id)).scalar() or 0 action_rows = db.query( AuditLog.action, func.count(AuditLog.id) ).group_by(AuditLog.action).all() by_action = {row[0]: row[1] for row in action_rows} resource_rows = db.query( AuditLog.resource_type, func.count(AuditLog.id) ).group_by(AuditLog.resource_type).all() by_resource = {row[0]: row[1] for row in resource_rows} return {"total": total, "by_action": by_action, "by_resource": by_resource}