94 lines
3.0 KiB
Python
94 lines
3.0 KiB
Python
"""FastAPI 主入口"""
|
||
from contextlib import asynccontextmanager
|
||
from pathlib import Path
|
||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, status
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||
from fastapi.staticfiles import StaticFiles
|
||
from fastapi.responses import FileResponse
|
||
|
||
from .database import init_db
|
||
from .routers import config, apps, tasks, auth, users
|
||
from .services.log_streamer import log_streamer
|
||
from .config import BACKEND_PORT, CORS_ALLOWED_ORIGINS, TRUSTED_HOSTS, validate_production_security
|
||
from .deps import decode_current_user
|
||
|
||
STATIC_DIR = Path(__file__).parent / "static"
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
"""应用生命周期"""
|
||
validate_production_security()
|
||
init_db()
|
||
yield
|
||
|
||
|
||
app = FastAPI(
|
||
title="iOS 自动打包服务",
|
||
version="1.0.0",
|
||
lifespan=lifespan,
|
||
)
|
||
|
||
# 仅接受显式允许的跨域前端;同域部署不需要 CORS 配置。
|
||
if CORS_ALLOWED_ORIGINS:
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=CORS_ALLOWED_ORIGINS,
|
||
allow_credentials=False,
|
||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||
allow_headers=["Authorization", "Content-Type"],
|
||
)
|
||
|
||
app.add_middleware(TrustedHostMiddleware, allowed_hosts=TRUSTED_HOSTS)
|
||
|
||
# 路由
|
||
app.include_router(auth.router)
|
||
app.include_router(users.router)
|
||
app.include_router(config.router)
|
||
app.include_router(apps.router)
|
||
app.include_router(tasks.router)
|
||
|
||
|
||
@app.websocket("/ws/tasks/{task_id}")
|
||
async def websocket_logs(websocket: WebSocket, task_id: str):
|
||
"""经 JWT 鉴权的 WebSocket 实时日志。"""
|
||
protocol = websocket.headers.get("sec-websocket-protocol", "")
|
||
token_protocol = next((item.strip() for item in protocol.split(",") if item.strip().startswith("jwt.")), "")
|
||
if not token_protocol:
|
||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||
return
|
||
try:
|
||
decode_current_user(token_protocol.removeprefix("jwt."))
|
||
except Exception:
|
||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||
return
|
||
|
||
await websocket.accept(subprotocol=token_protocol)
|
||
try:
|
||
async for msg in log_streamer.subscribe(task_id):
|
||
await websocket.send_json(msg)
|
||
except WebSocketDisconnect:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
@app.get("/api/health")
|
||
async def health():
|
||
"""健康检查"""
|
||
return {"status": "ok", "port": BACKEND_PORT}
|
||
|
||
|
||
# 静态文件服务(生产模式:前端 build 产物由 FastAPI 直接 serving)
|
||
if STATIC_DIR.exists():
|
||
app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="assets")
|
||
|
||
@app.get("/{full_path:path}")
|
||
async def serve_spa(request: Request, full_path: str):
|
||
"""SPA catch-all:非 API 路由统一返回 index.html"""
|
||
file_path = STATIC_DIR / full_path
|
||
if file_path.is_file():
|
||
return FileResponse(str(file_path))
|
||
return FileResponse(str(STATIC_DIR / "index.html"))
|