mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 20:55:14 +00:00
fix(auth): 在请求上下文中添加会话ID设置
feat(gencode): 改进导入验证错误信息格式 fix(scheduler): 修复任务重复添加问题并合并错过的任务 refactor(router): 优化路由转换逻辑避免Layout嵌套 feat(middleware): 增强会话ID提取功能并改进日志记录
This commit is contained in:
@@ -44,7 +44,7 @@ executors = {
|
|||||||
}
|
}
|
||||||
# 配置默认参数
|
# 配置默认参数
|
||||||
job_defaults = {
|
job_defaults = {
|
||||||
'coalesce': False, # 是否合并执行
|
'coalesce': True, # 合并执行错过的任务
|
||||||
'max_instances': 1, # 最大实例数
|
'max_instances': 1, # 最大实例数
|
||||||
}
|
}
|
||||||
# 配置调度器
|
# 配置调度器
|
||||||
@@ -166,7 +166,11 @@ class SchedulerUtil:
|
|||||||
job_list = await JobCRUD(auth).get_obj_list_crud()
|
job_list = await JobCRUD(auth).get_obj_list_crud()
|
||||||
for item in job_list:
|
for item in job_list:
|
||||||
cls.remove_job(job_id=item.id) # 删除旧任务
|
cls.remove_job(job_id=item.id) # 删除旧任务
|
||||||
cls.add_job(item)
|
# 检查任务是否已经存在
|
||||||
|
existing_job = cls.get_job(job_id=item.id)
|
||||||
|
if not existing_job:
|
||||||
|
# 任务不存在才添加
|
||||||
|
cls.add_job(item)
|
||||||
# 根据数据库中保存的状态来设置任务状态
|
# 根据数据库中保存的状态来设置任务状态
|
||||||
if hasattr(item, 'status') and item.status == "1":
|
if hasattr(item, 'status') and item.status == "1":
|
||||||
# 如果任务状态为暂停,则立即暂停刚添加的任务
|
# 如果任务状态为暂停,则立即暂停刚添加的任务
|
||||||
|
|||||||
@@ -236,9 +236,15 @@ class DemoService:
|
|||||||
|
|
||||||
# 验证必填字段
|
# 验证必填字段
|
||||||
required_fields = ['name', 'status']
|
required_fields = ['name', 'status']
|
||||||
|
errors = []
|
||||||
for field in required_fields:
|
for field in required_fields:
|
||||||
missing_rows = df[df[field].isnull()].index.tolist()
|
missing_rows = df[df[field].isnull()].index.tolist()
|
||||||
raise CustomException(msg=f"{[k for k,v in header_dict.items() if v == field][0]}不能为空,第{[i+1 for i in missing_rows]}行")
|
if missing_rows:
|
||||||
|
field_name = [k for k,v in header_dict.items() if v == field][0]
|
||||||
|
rows_str = "、".join([str(i+1) for i in missing_rows])
|
||||||
|
errors.append(f"{field_name}不能为空,第{rows_str}行")
|
||||||
|
if errors:
|
||||||
|
raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}")
|
||||||
|
|
||||||
error_msgs = []
|
error_msgs = []
|
||||||
success_count = 0
|
success_count = 0
|
||||||
|
|||||||
@@ -147,9 +147,14 @@ class {{ class_name }}Service:
|
|||||||
# 验证必填字段
|
# 验证必填字段
|
||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
{% if column.required == '1' %}
|
{% if column.required == '1' %}
|
||||||
|
errors = []
|
||||||
missing_rows = df[df['{{ column.column_name }}'].isnull()].index.tolist()
|
missing_rows = df[df['{{ column.column_name }}'].isnull()].index.tolist()
|
||||||
if missing_rows:
|
if missing_rows:
|
||||||
raise CustomException(msg="{{ column.column_comment }}不能为空,第{0}行".format([i+1 for i in missing_rows]))
|
field_name = [k for k,v in header_dict.items() if v == field][0]
|
||||||
|
rows_str = "、".join([str(i+1) for i in missing_rows])
|
||||||
|
errors.append(f"{field_name}不能为空,第{rows_str}行")
|
||||||
|
if errors:
|
||||||
|
raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}")
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class LoginService:
|
|||||||
login_location = await IpLocalUtil.get_ip_location(request_ip)
|
login_location = await IpLocalUtil.get_ip_location(request_ip)
|
||||||
request.scope["login_location"] = login_location
|
request.scope["login_location"] = login_location
|
||||||
|
|
||||||
# 确保在请求上下文中设置用户名
|
# 确保在请求上下文中设置用户名和会话ID
|
||||||
request.scope["user_username"] = user.username
|
request.scope["user_username"] = user.username
|
||||||
|
|
||||||
access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from starlette.types import ASGIApp
|
from starlette.types import ASGIApp
|
||||||
@@ -12,6 +13,7 @@ from app.common.response import ErrorResponse
|
|||||||
from app.config.setting import settings
|
from app.config.setting import settings
|
||||||
from app.core.logger import log
|
from app.core.logger import log
|
||||||
from app.core.exceptions import CustomException
|
from app.core.exceptions import CustomException
|
||||||
|
from app.core.security import decode_access_token
|
||||||
from app.api.v1.module_system.params.service import ParamsService
|
from app.api.v1.module_system.params.service import ParamsService
|
||||||
|
|
||||||
|
|
||||||
@@ -35,14 +37,60 @@ class RequestLogMiddleware(BaseHTTPMiddleware):
|
|||||||
def __init__(self, app: ASGIApp) -> None:
|
def __init__(self, app: ASGIApp) -> None:
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_session_id_from_request(request: Request) -> str | None:
|
||||||
|
"""
|
||||||
|
从请求中提取session_id(支持从Token或已设置的scope中获取)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- request (Request): 请求对象
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 会话ID,如果无法提取则返回None
|
||||||
|
"""
|
||||||
|
# 1. 先检查 scope 中是否已经有 session_id(登录接口会设置)
|
||||||
|
session_id = request.scope.get('session_id')
|
||||||
|
if session_id:
|
||||||
|
return session_id
|
||||||
|
|
||||||
|
# 2. 尝试从 Authorization Header 中提取
|
||||||
|
try:
|
||||||
|
authorization = request.headers.get("Authorization")
|
||||||
|
if not authorization:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 处理Bearer token
|
||||||
|
token = authorization.replace('Bearer ', '').strip()
|
||||||
|
|
||||||
|
# 解码token
|
||||||
|
payload = decode_access_token(token)
|
||||||
|
if not payload or not hasattr(payload, 'sub'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 从payload中提取session_id
|
||||||
|
user_info = json.loads(payload.sub)
|
||||||
|
session_id = user_info.get("session_id")
|
||||||
|
|
||||||
|
# 同时设置到request.scope中,避免后续重复解析
|
||||||
|
if session_id:
|
||||||
|
request.scope["session_id"] = session_id
|
||||||
|
|
||||||
|
return session_id
|
||||||
|
except Exception:
|
||||||
|
# 解析失败静默处理,返回None(可能是未认证请求)
|
||||||
|
return None
|
||||||
|
|
||||||
async def dispatch(
|
async def dispatch(
|
||||||
self, request: Request, call_next: RequestResponseEndpoint
|
self, request: Request, call_next: RequestResponseEndpoint
|
||||||
) -> Response:
|
) -> Response:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
session_id = request.scope.get('session_id')
|
|
||||||
|
# 尝试提取session_id
|
||||||
|
session_id = self._extract_session_id_from_request(request)
|
||||||
|
|
||||||
# 组装请求日志字段
|
# 组装请求日志字段
|
||||||
log_fields = [
|
log_fields = [
|
||||||
f"会话ID: {session_id}",
|
|
||||||
f"请求来源: {request.client.host if request.client else '未知'}",
|
f"请求来源: {request.client.host if request.client else '未知'}",
|
||||||
f"请求方法: {request.method}",
|
f"请求方法: {request.method}",
|
||||||
f"请求路径: {request.url.path}",
|
f"请求路径: {request.url.path}",
|
||||||
@@ -111,6 +159,7 @@ class RequestLogMiddleware(BaseHTTPMiddleware):
|
|||||||
if should_block:
|
if should_block:
|
||||||
# 增强安全审计:记录详细的拦截日志
|
# 增强安全审计:记录详细的拦截日志
|
||||||
log.warning([
|
log.warning([
|
||||||
|
f"会话ID: {session_id or '未认证'}",
|
||||||
f"请求被拦截: {block_reason}",
|
f"请求被拦截: {block_reason}",
|
||||||
f"请求来源: {request_ip}",
|
f"请求来源: {request_ip}",
|
||||||
f"请求方法: {request.method}",
|
f"请求方法: {request.method}",
|
||||||
|
|||||||
@@ -149,20 +149,41 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||||||
* 转换后端路由数据为Vue Router配置
|
* 转换后端路由数据为Vue Router配置
|
||||||
* 处理组件路径映射和Layout层级嵌套
|
* 处理组件路径映射和Layout层级嵌套
|
||||||
*/
|
*/
|
||||||
const transformRoutes = (routes: RouteVO[]): RouteRecordRaw[] => {
|
const transformRoutes = (routes: RouteVO[], isTopLevel: boolean = true): RouteRecordRaw[] => {
|
||||||
return routes.map((route) => {
|
return routes.map((route) => {
|
||||||
// 创建路由对象,保留所有路由属性
|
// 创建路由对象,保留所有路由属性
|
||||||
const normalizedRoute = { ...route } as RouteRecordRaw;
|
const normalizedRoute = { ...route } as RouteRecordRaw;
|
||||||
|
|
||||||
// 处理组件路径映射
|
// 处理组件路径映射
|
||||||
normalizedRoute.component = !normalizedRoute.component
|
// normalizedRoute.component = !normalizedRoute.component
|
||||||
? Layout
|
// ? Layout
|
||||||
: modules[`../../views/${normalizedRoute.component}.vue`] ||
|
// : modules[`../../views/${normalizedRoute.component}.vue`] ||
|
||||||
modules["../../views/error/404.vue"];
|
// modules["../../views/error/404.vue"];
|
||||||
|
|
||||||
// 递归处理子路由
|
// 关键优化:
|
||||||
|
// 1. 顶级路由(一级目录)使用Layout组件,确保菜单和navbar能正常显示
|
||||||
|
// 2. 二级及以上的父路由不使用Layout组件,只作为路由容器,避免Layout嵌套
|
||||||
|
// 3. 叶子路由使用实际组件
|
||||||
|
// 4. 递归处理子路由,实现无限层级菜单
|
||||||
if (normalizedRoute.children && normalizedRoute.children.length > 0) {
|
if (normalizedRoute.children && normalizedRoute.children.length > 0) {
|
||||||
normalizedRoute.children = transformRoutes(route.children);
|
// normalizedRoute.children = transformRoutes(route.children);
|
||||||
|
|
||||||
|
// 非叶子路由
|
||||||
|
if (isTopLevel) {
|
||||||
|
// 顶级路由(一级目录),使用Layout组件
|
||||||
|
normalizedRoute.component = Layout;
|
||||||
|
} else {
|
||||||
|
// 二级及以上的父路由,不使用Layout组件,只作为路由容器
|
||||||
|
normalizedRoute.component = undefined;
|
||||||
|
}
|
||||||
|
// 递归处理子路由,标记为非顶级路由
|
||||||
|
normalizedRoute.children = transformRoutes(route.children, false);
|
||||||
|
} else {
|
||||||
|
// 叶子路由,使用实际组件
|
||||||
|
normalizedRoute.component = normalizedRoute.component
|
||||||
|
? modules[`../../views/${normalizedRoute.component}.vue`] ||
|
||||||
|
modules["../../views/error/404.vue"]
|
||||||
|
: modules["../../views/error/404.vue"];
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizedRoute;
|
return normalizedRoute;
|
||||||
|
|||||||
Reference in New Issue
Block a user