mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
- 移除原监控仪表盘独立模块,将相关功能合并到在线监控模块 - 重构租户配置字段名,统一使用logo_url和name替代tenant_logo/tenant_name - 优化搜索工具函数,移除重复导入 - 调整参数配置模型字段长度限制,移除config_value的max_length约束 - 清理冗余的常量定义和导入语句 - 修复批量状态设置接口的redis依赖注入 - 增强OAuth登录安全性,添加租户默认归属和state一次性消费 - 优化资源目录缓存逻辑,减少重复计算 - 新增API Token模块基础框架 - 完善用户token版本管理,支持主动失效JWT - 调整AI模型配置缓存过期时间 - 修复菜单类型字段索引,提升查询性能 - 简化前端刷新token调用逻辑 - 新增滑块验证完成接口和忘记密码验证码校验 - 调整系统配置默认值,添加操作日志保留天数和接口白名单配置 - 限制Mock支付回调仅在开发环境可用 - 重构websocket认证方式,支持更安全的subprotocol传参
107 lines
4.3 KiB
Python
107 lines
4.3 KiB
Python
import json
|
|
import time
|
|
from collections.abc import Callable, Coroutine
|
|
from typing import Any
|
|
|
|
from fastapi import Request, Response
|
|
from fastapi.routing import APIRoute
|
|
from starlette.background import BackgroundTask
|
|
|
|
from app.config.setting import settings
|
|
from app.core.logger import logger
|
|
from app.utils.ip_local_util import get_client_ip
|
|
|
|
_WRITE_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
|
|
|
|
# (通常在登录前调用,没有 JWT token)
|
|
_PUBLIC_WRITE_PATHS: set[str] = {
|
|
"/auth/login",
|
|
"/auth/token/refresh",
|
|
"/auth/captcha/slider/complete",
|
|
"/auth/tenant/register",
|
|
"/auth/user/register",
|
|
}
|
|
|
|
|
|
async def _write_operation_log_async(log_data: dict) -> None:
|
|
"""直接写入操作日志(函数体内导入避免循环依赖)。"""
|
|
try:
|
|
from app.api.v1.module_system.log.crud import OperationLogCRUD
|
|
from app.api.v1.module_system.log.schema import OperationLogCreateSchema
|
|
from app.core.base_schema import AuthSchema
|
|
from app.core.database import async_db_session
|
|
|
|
async with async_db_session() as _session, _session.begin():
|
|
auth = AuthSchema(check_data_scope=False)
|
|
await OperationLogCRUD(auth, _session).create(data=OperationLogCreateSchema(**log_data))
|
|
except Exception:
|
|
logger.exception("操作日志写入失败: path={}", log_data.get("request_path"))
|
|
|
|
|
|
class OperationLogRoute(APIRoute):
|
|
"""操作日志路由 — 自动记录请求/响应并后台异步写入。
|
|
|
|
根据 HTTP 方法判断:
|
|
- 写方法 (POST/PUT/DELETE/PATCH):注入租户写权限检查
|
|
- 读方法 (GET/HEAD/OPTIONS):不注入
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
methods = getattr(self, "methods", set())
|
|
if methods & _WRITE_METHODS and self.path not in _PUBLIC_WRITE_PATHS:
|
|
if self.dependencies is None:
|
|
self.dependencies = []
|
|
|
|
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
|
original_route_handler = super().get_route_handler()
|
|
|
|
async def custom_route_handler(request: Request) -> Response:
|
|
start = time.time()
|
|
response: Response = await original_route_handler(request)
|
|
|
|
if request.method not in settings.OPERATION_RECORD_METHOD:
|
|
return response
|
|
route: APIRoute = request.scope.get("route", None)
|
|
|
|
try:
|
|
oper_param: dict[str, Any] = {}
|
|
content_type = request.headers.get("Content-Type", "")
|
|
if content_type.startswith(("multipart/form-data", "application/x-www-form-urlencoded")):
|
|
form_data = await request.form()
|
|
oper_param["form"] = dict(form_data.items())
|
|
else:
|
|
payload = await request.body()
|
|
if payload:
|
|
try:
|
|
oper_param["body"] = json.loads(payload.decode())
|
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
oper_param["body"] = payload.decode("utf-8", errors="ignore")
|
|
|
|
if request.path_params:
|
|
oper_param["path_params"] = dict(request.path_params)
|
|
|
|
log_payload = json.dumps(oper_param, ensure_ascii=False)
|
|
if len(log_payload) > 2000:
|
|
log_payload = "请求参数过长"
|
|
|
|
is_json = "application/json" in response.headers.get("Content-Type", "")
|
|
response_data = response.body if is_json else b"{}"
|
|
|
|
log_data: dict[str, Any] = {
|
|
"request_path": request.url.path,
|
|
"request_method": request.method,
|
|
"request_payload": log_payload,
|
|
"response_code": response.status_code,
|
|
"response_json": bytes(response_data).decode(),
|
|
"process_time": f"{(time.time() - start):.2f}s",
|
|
"description": route.summary if route else "",
|
|
"request_ip": get_client_ip(request),
|
|
}
|
|
response.background = BackgroundTask(_write_operation_log_async, log_data)
|
|
except Exception:
|
|
logger.warning("操作日志采集异常: {}", request.url.path, exc_info=True)
|
|
return response
|
|
|
|
return custom_route_handler
|