"""FastAPI 入口""" import os from pathlib import Path import uvicorn from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from starlette.middleware.base import BaseHTTPMiddleware from .database import init_db from .routers import auth, users, accounts, login, proxy, cookies from utils import setup_logger @asynccontextmanager async def lifespan(app: FastAPI): # 初始化日志(控制台 + 按天命名文件) _log_level = os.getenv("LOG_LEVEL", "DEBUG") _log_dir = Path(__file__).resolve().parents[2] / "logs" setup_logger(level=_log_level, log_dir=str(_log_dir)) init_db() yield app = FastAPI( title="斗鱼批量登录后台", version="1.0.0", lifespan=lifespan, ) # CORS(支持通过环境变量配置,逗号分隔;默认开发环境) _cors_env = os.getenv("CORS_ORIGINS", "") if _cors_env: _cors_origins = [o.strip() for o in _cors_env.split(",") if o.strip()] else: _cors_origins = ["http://localhost:5173", "http://localhost:3000"] app.add_middleware( CORSMiddleware, allow_origins=_cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 安全响应头中间件 class SecurityHeadersMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): response = await call_next(request) response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" return response app.add_middleware(SecurityHeadersMiddleware) # 注册路由 app.include_router(auth.router) app.include_router(users.router) app.include_router(accounts.router) app.include_router(login.router) app.include_router(proxy.router) app.include_router(cookies.router) @app.get("/api/health") def health(): return {"status": "ok"} # ---- 生产环境:serve 前端静态文件 ---- # Docker 部署时前端构建产物会被复制到 web/frontend/dist _FRONTEND_DIST = Path(__file__).resolve().parents[2] / "web" / "frontend" / "dist" _INDEX_HTML = _FRONTEND_DIST / "index.html" if _INDEX_HTML.exists(): # 挂载静态资源目录(js/css/图片等) _ASSETS_DIR = _FRONTEND_DIST / "assets" if _ASSETS_DIR.exists(): app.mount("/assets", StaticFiles(directory=str(_ASSETS_DIR)), name="assets") @app.get("/{full_path:path}") async def serve_spa(full_path: str, request: Request): """SPA fallback:非 /api 路径返回 index.html""" # 排除 API 路径 if full_path.startswith("api"): return {"detail": "Not Found"} # 尝试返回静态文件(防护路径遍历) file_path = (_FRONTEND_DIST / full_path).resolve() try: file_path.relative_to(_FRONTEND_DIST.resolve()) except ValueError: # 路径逃逸出 dist 目录,拒绝访问 return {"detail": "Not Found"} if file_path.is_file(): return FileResponse(str(file_path)) # SPA fallback 到 index.html return FileResponse(str(_INDEX_HTML)) def run(): _reload = os.getenv("UVICORN_RELOAD", "false").lower() == "true" uvicorn.run("web.backend.main:app", host="0.0.0.0", port=8000, reload=_reload) if __name__ == "__main__": run()