style: normalize python formatting gates
This commit is contained in:
@@ -5,17 +5,14 @@ Disabled because it depends on the removed DI container. Rebuild it around the
|
||||
canonical JWT settings and SQLAlchemy-backed user repository before reuse.
|
||||
"""
|
||||
|
||||
raise RuntimeError(
|
||||
"apps.api.app.middleware.auth is disabled: rebuild auth dependency wiring before importing it"
|
||||
)
|
||||
raise RuntimeError("apps.api.app.middleware.auth is disabled: rebuild auth dependency wiring before importing it")
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from apps.api.app.dependencies import get_container
|
||||
from packages.domain.auth import jwt_service
|
||||
from packages.domain.entities import User
|
||||
from apps.api.app.dependencies import get_container
|
||||
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
@@ -25,42 +22,42 @@ async def get_current_user(
|
||||
) -> User:
|
||||
"""
|
||||
获取当前登录用户
|
||||
|
||||
|
||||
从 Authorization header 中提取 JWT token 并验证
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: Token 无效或过期
|
||||
|
||||
|
||||
Returns:
|
||||
当前用户对象
|
||||
"""
|
||||
token = credentials.credentials
|
||||
|
||||
|
||||
try:
|
||||
# 验证 token
|
||||
payload = jwt_service.verify_token(token)
|
||||
user_id = payload.get("sub")
|
||||
|
||||
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token: missing user_id",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
# 从数据库获取用户
|
||||
container = get_container()
|
||||
user = container.user_repository.find_by_id(user_id)
|
||||
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
return user
|
||||
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -74,15 +71,15 @@ async def get_current_user_optional(
|
||||
) -> User | None:
|
||||
"""
|
||||
获取当前登录用户(可选)
|
||||
|
||||
|
||||
如果没有提供 token,返回 None 而不是抛出异常
|
||||
|
||||
|
||||
Returns:
|
||||
当前用户对象或 None
|
||||
"""
|
||||
if not credentials:
|
||||
return None
|
||||
|
||||
|
||||
try:
|
||||
return await get_current_user(credentials)
|
||||
except HTTPException:
|
||||
@@ -92,78 +89,78 @@ async def get_current_user_optional(
|
||||
def require_workspace_access(workspace_id: str, user: User = Depends(get_current_user)) -> tuple[str, str]:
|
||||
"""
|
||||
要求用户可以访问指定工作空间
|
||||
|
||||
|
||||
Args:
|
||||
workspace_id: 工作空间 ID
|
||||
user: 当前用户
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: 用户没有访问权限
|
||||
|
||||
|
||||
Returns:
|
||||
(workspace_id, user_role)
|
||||
"""
|
||||
container = get_container()
|
||||
permission_checker = container.permission_checker
|
||||
|
||||
|
||||
has_access, role = permission_checker.check_workspace_access(workspace_id, user.id)
|
||||
|
||||
|
||||
if not has_access:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You don't have access to this workspace",
|
||||
)
|
||||
|
||||
|
||||
return workspace_id, role
|
||||
|
||||
|
||||
def require_workspace_admin(workspace_id: str, user: User = Depends(get_current_user)) -> str:
|
||||
"""
|
||||
要求用户是工作空间的 Admin 或 Owner
|
||||
|
||||
|
||||
Args:
|
||||
workspace_id: 工作空间 ID
|
||||
user: 当前用户
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: 用户没有管理权限
|
||||
|
||||
|
||||
Returns:
|
||||
workspace_id
|
||||
"""
|
||||
container = get_container()
|
||||
permission_checker = container.permission_checker
|
||||
|
||||
|
||||
if not permission_checker.check_is_admin_or_owner(workspace_id, user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only workspace owner or admin can perform this action",
|
||||
)
|
||||
|
||||
|
||||
return workspace_id
|
||||
|
||||
|
||||
def require_workspace_owner(workspace_id: str, user: User = Depends(get_current_user)) -> str:
|
||||
"""
|
||||
要求用户是工作空间的 Owner
|
||||
|
||||
|
||||
Args:
|
||||
workspace_id: 工作空间 ID
|
||||
user: 当前用户
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: 用户不是 Owner
|
||||
|
||||
|
||||
Returns:
|
||||
workspace_id
|
||||
"""
|
||||
container = get_container()
|
||||
permission_checker = container.permission_checker
|
||||
|
||||
|
||||
if not permission_checker.check_is_owner(workspace_id, user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only workspace owner can perform this action",
|
||||
)
|
||||
|
||||
|
||||
return workspace_id
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
"""
|
||||
全局异常处理和错误响应
|
||||
"""
|
||||
from fastapi import Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
import traceback
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from fastapi import Request, status
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class APIException(Exception):
|
||||
"""API 异常基类"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
@@ -28,7 +30,7 @@ class APIException(Exception):
|
||||
|
||||
class AuthenticationError(APIException):
|
||||
"""认证错误"""
|
||||
|
||||
|
||||
def __init__(self, message: str = "Authentication failed"):
|
||||
super().__init__(
|
||||
message=message,
|
||||
@@ -39,7 +41,7 @@ class AuthenticationError(APIException):
|
||||
|
||||
class PermissionDeniedError(APIException):
|
||||
"""权限拒绝"""
|
||||
|
||||
|
||||
def __init__(self, message: str = "Permission denied"):
|
||||
super().__init__(
|
||||
message=message,
|
||||
@@ -50,7 +52,7 @@ class PermissionDeniedError(APIException):
|
||||
|
||||
class ResourceNotFoundError(APIException):
|
||||
"""资源不存在"""
|
||||
|
||||
|
||||
def __init__(self, resource: str = "Resource"):
|
||||
super().__init__(
|
||||
message=f"{resource} not found",
|
||||
@@ -61,7 +63,7 @@ class ResourceNotFoundError(APIException):
|
||||
|
||||
class ValidationError(APIException):
|
||||
"""验证错误"""
|
||||
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(
|
||||
message=message,
|
||||
@@ -100,12 +102,14 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
||||
"""请求验证异常处理"""
|
||||
errors = []
|
||||
for error in exc.errors():
|
||||
errors.append({
|
||||
"field": ".".join(str(loc) for loc in error["loc"]),
|
||||
"message": error["msg"],
|
||||
"type": error["type"],
|
||||
})
|
||||
|
||||
errors.append(
|
||||
{
|
||||
"field": ".".join(str(loc) for loc in error["loc"]),
|
||||
"message": error["msg"],
|
||||
"type": error["type"],
|
||||
}
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={
|
||||
@@ -121,7 +125,7 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
||||
async def general_exception_handler(request: Request, exc: Exception):
|
||||
"""通用异常处理"""
|
||||
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
||||
|
||||
|
||||
# 生产环境不返回详细错误信息
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
请求日志中间件
|
||||
"""
|
||||
import time
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
@@ -11,58 +13,57 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
||||
"""请求日志中间件"""
|
||||
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
# 记录请求开始时间
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
# 记录请求信息
|
||||
logger.info(f"Request: {request.method} {request.url.path}")
|
||||
|
||||
|
||||
# 处理请求
|
||||
response = await call_next(request)
|
||||
|
||||
|
||||
# 计算处理时间
|
||||
process_time = time.time() - start_time
|
||||
|
||||
|
||||
# 记录响应信息
|
||||
logger.info(
|
||||
f"Response: {request.method} {request.url.path} "
|
||||
f"status={response.status_code} time={process_time:.3f}s"
|
||||
f"Response: {request.method} {request.url.path} " f"status={response.status_code} time={process_time:.3f}s"
|
||||
)
|
||||
|
||||
|
||||
# 添加响应头
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class RateLimitMiddleware(BaseHTTPMiddleware):
|
||||
"""简单的速率限制中间件(基于内存)"""
|
||||
|
||||
|
||||
def __init__(self, app, max_requests: int = 100, window_seconds: int = 60):
|
||||
super().__init__(app)
|
||||
self.max_requests = max_requests
|
||||
self.window_seconds = window_seconds
|
||||
self.requests = {} # {ip: [(timestamp, ...)]}
|
||||
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
# 获取客户端 IP
|
||||
client_ip = request.client.host
|
||||
current_time = time.time()
|
||||
|
||||
|
||||
# 清理过期记录
|
||||
if client_ip in self.requests:
|
||||
self.requests[client_ip] = [
|
||||
ts for ts in self.requests[client_ip]
|
||||
if current_time - ts < self.window_seconds
|
||||
ts for ts in self.requests[client_ip] if current_time - ts < self.window_seconds
|
||||
]
|
||||
|
||||
|
||||
# 检查速率限制
|
||||
request_count = len(self.requests.get(client_ip, []))
|
||||
|
||||
|
||||
if request_count >= self.max_requests:
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
return JSONResponse(
|
||||
status_code=429,
|
||||
content={
|
||||
@@ -72,19 +73,17 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# 记录请求
|
||||
if client_ip not in self.requests:
|
||||
self.requests[client_ip] = []
|
||||
self.requests[client_ip].append(current_time)
|
||||
|
||||
|
||||
# 处理请求
|
||||
response = await call_next(request)
|
||||
|
||||
|
||||
# 添加速率限制信息到响应头
|
||||
response.headers["X-RateLimit-Limit"] = str(self.max_requests)
|
||||
response.headers["X-RateLimit-Remaining"] = str(
|
||||
self.max_requests - len(self.requests[client_ip])
|
||||
)
|
||||
|
||||
response.headers["X-RateLimit-Remaining"] = str(self.max_requests - len(self.requests[client_ip]))
|
||||
|
||||
return response
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""
|
||||
性能监控中间件
|
||||
"""
|
||||
import time
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import Request, Response
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
@@ -12,30 +14,30 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class PerformanceMonitoringMiddleware(BaseHTTPMiddleware):
|
||||
"""性能监控中间件"""
|
||||
|
||||
|
||||
def __init__(self, app, slow_request_threshold: float = 1.0):
|
||||
super().__init__(app)
|
||||
self.slow_request_threshold = slow_request_threshold # 慢请求阈值(秒)
|
||||
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable):
|
||||
# 记录请求开始时间
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
# 生成请求 ID
|
||||
request_id = self._generate_request_id()
|
||||
request.state.request_id = request_id
|
||||
|
||||
|
||||
# 处理请求
|
||||
try:
|
||||
response = await call_next(request)
|
||||
|
||||
|
||||
# 计算处理时间
|
||||
process_time = time.time() - start_time
|
||||
|
||||
|
||||
# 添加响应头
|
||||
response.headers["X-Request-ID"] = request_id
|
||||
response.headers["X-Process-Time"] = f"{process_time:.3f}"
|
||||
|
||||
|
||||
# 记录慢请求
|
||||
if process_time > self.slow_request_threshold:
|
||||
logger.warning(
|
||||
@@ -43,55 +45,55 @@ class PerformanceMonitoringMiddleware(BaseHTTPMiddleware):
|
||||
f"took {process_time:.3f}s (threshold: {self.slow_request_threshold}s) "
|
||||
f"[request_id={request_id}]"
|
||||
)
|
||||
|
||||
|
||||
# 记录请求日志
|
||||
logger.info(
|
||||
f"{request.method} {request.url.path} "
|
||||
f"status={response.status_code} time={process_time:.3f}s "
|
||||
f"[request_id={request_id}]"
|
||||
)
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
except Exception as e:
|
||||
process_time = time.time() - start_time
|
||||
logger.error(
|
||||
f"Request failed: {request.method} {request.url.path} "
|
||||
f"error={str(e)} time={process_time:.3f}s "
|
||||
f"[request_id={request_id}]",
|
||||
exc_info=True
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def _generate_request_id(self) -> str:
|
||||
"""生成请求 ID"""
|
||||
import uuid
|
||||
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
class DatabaseQueryLogger:
|
||||
"""数据库查询日志记录器"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.queries = []
|
||||
self.total_time = 0
|
||||
|
||||
|
||||
def log_query(self, query: str, params: tuple, duration: float):
|
||||
"""记录查询"""
|
||||
self.queries.append({
|
||||
"query": query,
|
||||
"params": params,
|
||||
"duration": duration,
|
||||
})
|
||||
self.queries.append(
|
||||
{
|
||||
"query": query,
|
||||
"params": params,
|
||||
"duration": duration,
|
||||
}
|
||||
)
|
||||
self.total_time += duration
|
||||
|
||||
|
||||
# 记录慢查询(超过 100ms)
|
||||
if duration > 0.1:
|
||||
logger.warning(
|
||||
f"Slow query detected: {query[:100]}... "
|
||||
f"took {duration:.3f}s with params {params}"
|
||||
)
|
||||
|
||||
logger.warning(f"Slow query detected: {query[:100]}... " f"took {duration:.3f}s with params {params}")
|
||||
|
||||
def get_stats(self):
|
||||
"""获取统计信息"""
|
||||
return {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
"""
|
||||
API 版本管理中间件
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class APIVersionMiddleware(BaseHTTPMiddleware):
|
||||
"""API 版本管理中间件"""
|
||||
|
||||
|
||||
# 版本配置
|
||||
VERSIONS = {
|
||||
"v1": {
|
||||
@@ -24,33 +26,31 @@ class APIVersionMiddleware(BaseHTTPMiddleware):
|
||||
"release_date": None,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
# 提取版本号
|
||||
version = self._extract_version(request.url.path)
|
||||
|
||||
|
||||
# 处理请求
|
||||
response = await call_next(request)
|
||||
|
||||
|
||||
# 添加版本信息头
|
||||
if version:
|
||||
response.headers["X-API-Version"] = version
|
||||
|
||||
|
||||
# 添加弃用警告
|
||||
version_info = self.VERSIONS.get(version, {})
|
||||
if version_info.get("deprecated"):
|
||||
response.headers["X-API-Deprecated"] = "true"
|
||||
|
||||
|
||||
sunset_date = version_info.get("sunset_date")
|
||||
if sunset_date:
|
||||
response.headers["X-API-Sunset-Date"] = sunset_date
|
||||
|
||||
response.headers["X-API-Deprecation-Info"] = (
|
||||
f"https://docs.xiaoxia-saas.com/api/deprecation/{version}"
|
||||
)
|
||||
|
||||
|
||||
response.headers["X-API-Deprecation-Info"] = f"https://docs.xiaoxia-saas.com/api/deprecation/{version}"
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _extract_version(self, path: str) -> str:
|
||||
"""从路径中提取版本号"""
|
||||
parts = path.split("/")
|
||||
@@ -62,14 +62,15 @@ class APIVersionMiddleware(BaseHTTPMiddleware):
|
||||
|
||||
class VersionNotFoundMiddleware(BaseHTTPMiddleware):
|
||||
"""处理已下线的 API 版本"""
|
||||
|
||||
|
||||
SUNSET_VERSIONS = [] # 已下线的版本列表
|
||||
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
version = self._extract_version(request.url.path)
|
||||
|
||||
|
||||
if version in self.SUNSET_VERSIONS:
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
return JSONResponse(
|
||||
status_code=410,
|
||||
content={
|
||||
@@ -77,13 +78,13 @@ class VersionNotFoundMiddleware(BaseHTTPMiddleware):
|
||||
"code": "API_VERSION_SUNSET",
|
||||
"message": f"API {version} has been sunset and is no longer available",
|
||||
"sunset_date": "2028-07-01",
|
||||
"migration_guide": f"https://docs.xiaoxia-saas.com/api/migration/{version}"
|
||||
"migration_guide": f"https://docs.xiaoxia-saas.com/api/migration/{version}",
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
def _extract_version(self, path: str) -> str:
|
||||
"""从路径中提取版本号"""
|
||||
parts = path.split("/")
|
||||
|
||||
Reference in New Issue
Block a user