116 lines
4.3 KiB
Python
116 lines
4.3 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Body, Query, Request
|
|
from fastapi.responses import StreamingResponse
|
|
|
|
from app.db.platform_store import ALL_PERMISSIONS, get_platform_store
|
|
|
|
|
|
router = APIRouter(prefix="/system", tags=["system"])
|
|
|
|
|
|
@router.post("/audit/visit")
|
|
def record_visit(payload: dict = Body(...), request: Request = None) -> dict:
|
|
"""记录用户访问业务模块的行为,用于看板用户操作分布统计。"""
|
|
action = str(payload.get("action") or payload.get("module") or "").strip()
|
|
if not action:
|
|
return {"code": 0, "message": "ok", "data": {"recorded": False}}
|
|
actor_id = ""
|
|
if request is not None:
|
|
auth = request.headers.get("Authorization", "")
|
|
token = auth.replace("Bearer ", "").strip()
|
|
if token.startswith("platform-token-"):
|
|
actor_id = token[len("platform-token-"):]
|
|
get_platform_store().record_audit(
|
|
action=action,
|
|
actor_id=actor_id or None,
|
|
target_type="module",
|
|
target_id=action,
|
|
detail=str(payload.get("detail") or ""),
|
|
)
|
|
return {"code": 0, "message": "ok", "data": {"recorded": True}}
|
|
|
|
|
|
@router.get("/permissions/codes")
|
|
def permission_codes() -> dict:
|
|
"""返回平台权限码清单(权限码接口)。"""
|
|
return {"code": 0, "message": "ok", "data": {"codes": ALL_PERMISSIONS}}
|
|
|
|
|
|
@router.get("/permissions")
|
|
def permissions_overview() -> dict:
|
|
"""返回权限码清单与角色定义。"""
|
|
store = get_platform_store()
|
|
return {
|
|
"code": 0,
|
|
"message": "ok",
|
|
"data": {"codes": ALL_PERMISSIONS, "roles": store.roles()},
|
|
}
|
|
|
|
|
|
@router.get("/audit-logs")
|
|
def audit_logs(
|
|
tenant_id: str | None = Query(default=None, description="租户 ID"),
|
|
project_id: str | None = Query(default=None, description="项目 ID"),
|
|
actor_id: str | None = Query(default=None, description="操作人 ID"),
|
|
action: str | None = Query(default=None, description="动作类型"),
|
|
target_type: str | None = Query(default=None, description="目标类型"),
|
|
start_time: str | None = Query(default=None, description="ISO8601 起始时间"),
|
|
end_time: str | None = Query(default=None, description="ISO8601 结束时间"),
|
|
limit: int = Query(default=50, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
) -> dict:
|
|
"""审计日志查询:按租户/项目/操作人/动作/目标类型/时间范围分页过滤。"""
|
|
store = get_platform_store()
|
|
result = store.audit_logs(
|
|
tenant_id=tenant_id,
|
|
project_id=project_id,
|
|
actor_id=actor_id,
|
|
action=action,
|
|
target_type=target_type,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
return {"code": 0, "message": "ok", "data": result}
|
|
|
|
|
|
@router.get("/audit-logs/export")
|
|
def audit_logs_export(
|
|
tenant_id: str | None = Query(default=None, description="租户 ID"),
|
|
project_id: str | None = Query(default=None, description="项目 ID"),
|
|
actor_id: str | None = Query(default=None, description="操作人 ID"),
|
|
action: str | None = Query(default=None, description="动作类型"),
|
|
target_type: str | None = Query(default=None, description="目标类型"),
|
|
start_time: str | None = Query(default=None, description="ISO8601 起始时间"),
|
|
end_time: str | None = Query(default=None, description="ISO8601 结束时间"),
|
|
) -> StreamingResponse:
|
|
"""审计日志导出:返回 CSV 流,与应用查询相同的过滤条件。"""
|
|
store = get_platform_store()
|
|
result = store.audit_logs(
|
|
tenant_id=tenant_id,
|
|
project_id=project_id,
|
|
actor_id=actor_id,
|
|
action=action,
|
|
target_type=target_type,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
limit=10000,
|
|
offset=0,
|
|
)
|
|
items = result["items"]
|
|
columns = ["time", "tenant_id", "project_id", "actor_id", "action", "target_type", "target_id", "detail", "client_ip"]
|
|
header = ",".join(columns) + "\n"
|
|
|
|
def iter_rows():
|
|
yield header
|
|
for row in items:
|
|
yield ",".join(f'"{str(row.get(c, "") or "")}"' for c in columns) + "\n"
|
|
|
|
return StreamingResponse(
|
|
iter_rows(),
|
|
media_type="text/csv",
|
|
headers={"Content-Disposition": "attachment; filename=audit_logs.csv"},
|
|
)
|