Files

135 lines
4.6 KiB
Python

import json
import uuid
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_operator_role_label, get_current_user, get_db_session, require_api_permission
from app.crud import audit as audit_crud
from app.crud import study as study_crud
from app.models.user import User
from app.schemas.audit import AuditEventCreate, AuditLogRead
from app.services.ip_location import resolve_ip_location
router = APIRouter()
async def _build_export_audit_detail(db: AsyncSession, study_id: uuid.UUID, action: str) -> str:
study = await study_crud.get(db, study_id)
if not study:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
scope = "system" if action == "AUDIT_EXPORT_SYSTEM" else "project"
description = "导出了系统审计日志" if scope == "system" else f"导出了项目审计日志:{study.name}"
return json.dumps(
{
"targetName": "系统审计日志" if scope == "system" else study.name,
"scope": scope,
"description": description,
"result": "SUCCESS",
},
ensure_ascii=False,
)
@router.get(
"/",
response_model=list[AuditLogRead],
dependencies=[Depends(require_api_permission("audit_logs:read"))],
)
async def list_audit_logs(
study_id: uuid.UUID,
entity_type: str | None = None,
entity_id: uuid.UUID | None = None,
action: str | None = None,
operator_id: uuid.UUID | None = None,
client_ip: str | None = None,
client_type: str | None = None,
start_time: datetime | None = None,
end_time: datetime | None = None,
skip: int = 0,
limit: int = 100,
db: AsyncSession = Depends(get_db_session),
) -> list[AuditLogRead]:
logs = await audit_crud.list_logs(
db,
study_id=study_id,
entity_type=entity_type,
entity_id=entity_id,
action=action,
operator_id=operator_id,
client_ip=client_ip,
client_type=client_type,
start_time=start_time,
end_time=end_time,
skip=skip,
limit=limit,
)
operator_ids = {log.operator_id for log in logs}
operator_map: dict[uuid.UUID, User] = {}
if operator_ids:
result = await db.execute(select(User).where(User.id.in_(operator_ids)))
operator_map = {user.id: user for user in result.scalars().all()}
items: list[AuditLogRead] = []
for log in logs:
ip_location = resolve_ip_location(log.client_ip)
operator = operator_map.get(log.operator_id)
items.append(
AuditLogRead(
id=log.id,
study_id=log.study_id,
entity_type=log.entity_type,
entity_id=log.entity_id,
action=log.action,
detail=log.detail,
operator_id=log.operator_id,
operator_name=operator.full_name if operator else None,
operator_email=operator.email if operator else None,
operator_role=log.operator_role,
client_ip=log.client_ip,
ip_location=ip_location.location,
ip_country=ip_location.country,
ip_province=ip_location.province,
ip_city=ip_location.city,
ip_isp=ip_location.isp,
user_agent=log.user_agent,
client_type=log.client_type,
client_version=log.client_version,
client_platform=log.client_platform,
build_channel=log.build_channel,
build_commit=log.build_commit,
created_at=log.created_at,
)
)
return items
@router.post(
"/events",
status_code=status.HTTP_201_CREATED,
)
async def create_audit_event(
study_id: uuid.UUID,
payload: AuditEventCreate,
_export_permission=Depends(require_api_permission("audit_logs:export")),
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
):
allowed_actions = {"AUDIT_EXPORT_SYSTEM", "AUDIT_EXPORT_PROJECT"}
if payload.action not in allowed_actions:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不支持的审计事件")
await audit_crud.log_action(
db,
study_id=study_id,
entity_type="audit_log",
entity_id=None,
action=payload.action,
detail=await _build_export_audit_detail(db, study_id, payload.action),
operator_id=current_user.id,
operator_role=await get_operator_role_label(db, study_id, current_user),
)
return {"ok": True}