Files
wehub-resource-sync 1443d3fdf9
Ruff Format Check / Ruff Format & Lint (push) Failing after 7m39s
Deploy VitePress site to Pages / build (push) Failing after 9m11s
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:26 +08:00

148 lines
5.1 KiB
Python

import asyncio
import os
import sys
# ==============================================================================
# 解决 Windows 下 psycopg 异步模式不支持 ProactorEventLoop 的问题
# 注意:这段代码必须放在应用的极早期,最好在导入 FastAPI 或初始化数据库之前
# ==============================================================================
if sys.platform == "win32":
# 把当前文件 (main.py) 的上一级的上一级 (即根目录 Yuxi) 加入到 sys.path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
import time
from collections import defaultdict, deque
import uvicorn
from fastapi import FastAPI, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from server.routers import router
from server.utils.lifespan import lifespan
from server.utils.common_utils import setup_logging
from server.utils.access_log_middleware import AccessLogMiddleware
# 设置日志配置
setup_logging()
RATE_LIMIT_MAX_ATTEMPTS = 10
RATE_LIMIT_WINDOW_SECONDS = 60
RATE_LIMIT_ENDPOINTS = {("/api/auth/token", "POST")}
DEFAULT_DEVELOPMENT_CORS_ORIGINS = ("http://localhost:5173", "http://127.0.0.1:5173")
EXPLICIT_CORS_METHODS = ("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT")
EXPLICIT_CORS_HEADERS = ("Accept", "Authorization", "Content-Type", "Last-Event-ID", "X-Requested-With")
# In-memory login attempt tracker to reduce brute-force exposure per worker
_login_attempts: defaultdict[str, deque[float]] = defaultdict(deque)
_attempt_lock = asyncio.Lock()
def _parse_cors_origins() -> list[str]:
value = os.getenv("YUXI_CORS_ORIGINS")
origins = [origin.strip() for origin in (value or "").split(",") if origin.strip()]
if origins:
return origins
environment = (os.getenv("YUXI_ENV") or "development").strip().lower()
if environment in {"production", "prod"}:
return []
return list(DEFAULT_DEVELOPMENT_CORS_ORIGINS)
def _build_cors_options(origins: list[str] | None = None) -> dict[str, object]:
allow_origins = _parse_cors_origins() if origins is None else origins
if "*" in allow_origins:
return {
"allow_origins": ["*"],
"allow_credentials": False,
"allow_methods": ["*"],
"allow_headers": ["*"],
}
return {
"allow_origins": allow_origins,
"allow_credentials": True,
"allow_methods": list(EXPLICIT_CORS_METHODS),
"allow_headers": list(EXPLICIT_CORS_HEADERS),
"expose_headers": ["Content-Disposition", "X-Lock-Remaining"],
}
app = FastAPI(lifespan=lifespan)
# 所有业务接口统一挂载到 /api,具体分组在 server.routers 中集中注册。
app.include_router(router, prefix="/api")
# CORS 设置
app.add_middleware(
CORSMiddleware,
**_build_cors_options(),
)
def _extract_client_ip(request: Request) -> str:
forwarded_for = request.headers.get("x-forwarded-for")
if forwarded_for:
return forwarded_for.split(",")[0].strip()
if request.client:
return request.client.host
return "unknown"
class LoginRateLimitMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
normalized_path = request.url.path.rstrip("/") or "/"
request_signature = (normalized_path, request.method.upper())
if request_signature in RATE_LIMIT_ENDPOINTS:
client_ip = _extract_client_ip(request)
now = time.monotonic()
async with _attempt_lock:
attempt_history = _login_attempts[client_ip]
while attempt_history and now - attempt_history[0] > RATE_LIMIT_WINDOW_SECONDS:
attempt_history.popleft()
if len(attempt_history) >= RATE_LIMIT_MAX_ATTEMPTS:
retry_after = int(max(1, RATE_LIMIT_WINDOW_SECONDS - (now - attempt_history[0])))
return JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content={"detail": "登录尝试过于频繁,请稍后再试"},
headers={"Retry-After": str(retry_after)},
)
attempt_history.append(now)
response = await call_next(request)
if response.status_code < 400:
async with _attempt_lock:
_login_attempts.pop(client_ip, None)
return response
return await call_next(request)
# 添加访问日志中间件(记录请求处理时间)
app.add_middleware(AccessLogMiddleware)
# 添加登录限流中间件
app.add_middleware(LoginRateLimitMiddleware)
if __name__ == "__main__":
# uvicorn.run(app, host="0.0.0.0", port=5050, threads=10, workers=10, reload=True)
uvicorn.run(
"server.main:app",
host="0.0.0.0",
port=5050,
reload=True,
# 与 docker-compose 开发环境保持一致,避免 package 下代码变更不触发热重载。
reload_dirs=["server", "package"],
)