You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
4.9 KiB
153 lines
4.9 KiB
import uuid
|
|
import csv
|
|
import io
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, Query
|
|
from fastapi.responses import StreamingResponse
|
|
from sqlalchemy import select, func, and_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from database import get_db
|
|
from models import AuditLog
|
|
from schemas import AuditLogOut, AuditLogPage
|
|
from dependencies import get_current_user
|
|
|
|
router = APIRouter(prefix="/api/audit", tags=["audit"])
|
|
|
|
|
|
@router.get("/logs", response_model=AuditLogPage)
|
|
async def list_logs(
|
|
request: Request,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(20, ge=1, le=100),
|
|
action: str | None = Query(None),
|
|
resource: str | None = Query(None),
|
|
operator_id: uuid.UUID | None = Query(None),
|
|
date_from: datetime | None = Query(None),
|
|
date_to: datetime | None = Query(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
conditions = []
|
|
if action:
|
|
conditions.append(AuditLog.action == action)
|
|
if resource:
|
|
conditions.append(AuditLog.resource == resource)
|
|
if operator_id:
|
|
conditions.append(AuditLog.operator_id == operator_id)
|
|
if date_from:
|
|
conditions.append(AuditLog.created_at >= date_from)
|
|
if date_to:
|
|
conditions.append(AuditLog.created_at <= date_to)
|
|
|
|
where = and_(*conditions) if conditions else None
|
|
|
|
count_q = select(func.count(AuditLog.id))
|
|
if where is not None:
|
|
count_q = count_q.where(where)
|
|
total_result = await db.execute(count_q)
|
|
total = total_result.scalar() or 0
|
|
|
|
q = select(AuditLog).order_by(AuditLog.created_at.desc())
|
|
if where is not None:
|
|
q = q.where(where)
|
|
q = q.offset((page - 1) * page_size).limit(page_size)
|
|
result = await db.execute(q)
|
|
logs = result.scalars().all()
|
|
|
|
return AuditLogPage(
|
|
items=[AuditLogOut.model_validate(log, from_attributes=True) for log in logs],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
)
|
|
|
|
|
|
@router.get("/actions")
|
|
async def list_action_types(request: Request, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(AuditLog.action, func.count(AuditLog.id)).group_by(AuditLog.action)
|
|
)
|
|
return {
|
|
"code": 200,
|
|
"data": [{"action": r[0], "count": r[1]} for r in result.all()],
|
|
}
|
|
|
|
|
|
@router.get("/stats")
|
|
async def audit_stats(request: Request, db: AsyncSession = Depends(get_db)):
|
|
total_result = await db.execute(select(func.count(AuditLog.id)))
|
|
total = total_result.scalar() or 0
|
|
|
|
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
today_result = await db.execute(
|
|
select(func.count(AuditLog.id)).where(AuditLog.created_at >= today_start)
|
|
)
|
|
today = today_result.scalar() or 0
|
|
|
|
top_result = await db.execute(
|
|
select(AuditLog.action, func.count(AuditLog.id))
|
|
.group_by(AuditLog.action)
|
|
.order_by(func.count(AuditLog.id).desc())
|
|
.limit(10)
|
|
)
|
|
top_actions = [{"action": r[0], "count": r[1]} for r in top_result.all()]
|
|
|
|
top_resources = await db.execute(
|
|
select(AuditLog.resource, func.count(AuditLog.id))
|
|
.group_by(AuditLog.resource)
|
|
.order_by(func.count(AuditLog.id).desc())
|
|
.limit(10)
|
|
)
|
|
top_resources_list = [{"resource": r[0], "count": r[1]} for r in top_resources.all()]
|
|
|
|
return {
|
|
"code": 200,
|
|
"data": {
|
|
"total": total,
|
|
"today": today,
|
|
"top_actions": top_actions,
|
|
"top_resources": top_resources_list,
|
|
},
|
|
}
|
|
|
|
|
|
@router.get("/export")
|
|
async def export_logs(
|
|
request: Request,
|
|
date_from: datetime | None = Query(None),
|
|
date_to: datetime | None = Query(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
conditions = []
|
|
if date_from:
|
|
conditions.append(AuditLog.created_at >= date_from)
|
|
if date_to:
|
|
conditions.append(AuditLog.created_at <= date_to)
|
|
|
|
q = select(AuditLog).order_by(AuditLog.created_at.desc())
|
|
if conditions:
|
|
q = q.where(and_(*conditions))
|
|
q = q.limit(10000)
|
|
result = await db.execute(q)
|
|
logs = result.scalars().all()
|
|
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
writer.writerow(["ID", "操作时间", "操作人ID", "操作", "资源", "资源ID", "详情", "IP地址"])
|
|
for log in logs:
|
|
writer.writerow([
|
|
str(log.id),
|
|
log.created_at.isoformat() if log.created_at else "",
|
|
str(log.operator_id) if log.operator_id else "",
|
|
log.action,
|
|
log.resource or "",
|
|
log.resource_id or "",
|
|
str(log.detail)[:500] if log.detail else "",
|
|
log.ip_address or "",
|
|
])
|
|
output.seek(0)
|
|
|
|
return StreamingResponse(
|
|
iter([output.getvalue()]),
|
|
media_type="text/csv",
|
|
headers={"Content-Disposition": f"attachment; filename=audit_logs_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.csv"},
|
|
)
|