"""压缩机 3D 部件查看服务(FastAPI)。 公共接口(无需登录): POST /api/login 登录 POST /api/logout 退出 GET /api/auth-config 运行模式与(测试模式下的)默认账号密码 GET /models/ GLB / glTF 模型文件(静态资源,不鉴权) GET /* 前端构建产物(frontend/dist),SPA 回退 index.html 需登录接口(Bearer token): /api/models、/api/alarms、/api/alarm-filters、/api/pks-series、 /api/recent-series、/api/indicator、/api/scene-positions、/api/fault-part-map """ from typing import Any from fastapi import Depends, FastAPI, Header, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from pydantic import BaseModel, Field from auth import create_token, require_auth, revoke_token, verify_credentials from config import APP_MODE, AUTH_PASSWORD, AUTH_USER, FRONTEND_DIST, IS_TEST from routers import alarms, fault_map, files, indicator, scenes, series class LoginInput(BaseModel): username: str = Field(min_length=1) password: str = Field(min_length=1) app = FastAPI(title="压缩机 3D 部件查看服务", version="0.1.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) @app.post("/api/login") def login(payload: LoginInput) -> dict[str, Any]: if not verify_credentials(payload.username, payload.password): raise HTTPException(status_code=401, detail="用户名或密码错误") return {"token": create_token(), "username": payload.username} @app.post("/api/logout") def logout(authorization: str | None = Header(default=None)) -> dict[str, Any]: token = (authorization or "").removeprefix("Bearer ").strip() revoke_token(token) return {"ok": True} @app.get("/api/auth-config") def auth_config() -> dict[str, Any]: # 测试模式把默认账号密码给到前端登录页自动填入;生产不返回。 if IS_TEST: return {"mode": APP_MODE, "test": True, "username": AUTH_USER, "password": AUTH_PASSWORD} return {"mode": APP_MODE, "test": False} # 数据接口需登录;files 里只有 /api/models 需登录,模型文件保持公开。 app.include_router(alarms.router, dependencies=[Depends(require_auth)]) app.include_router(fault_map.router, dependencies=[Depends(require_auth)]) app.include_router(scenes.router, dependencies=[Depends(require_auth)]) app.include_router(series.router, dependencies=[Depends(require_auth)]) app.include_router(indicator.router, dependencies=[Depends(require_auth)]) app.include_router(files.router) @app.get("/{full_path:path}") def serve_frontend(full_path: str) -> FileResponse: if full_path.startswith(("api/", "models/")): raise HTTPException(status_code=404, detail="Not Found") resolved_dist = FRONTEND_DIST.resolve() if full_path: candidate = (FRONTEND_DIST / full_path).resolve() if resolved_dist in candidate.parents and candidate.is_file(): return FileResponse(candidate) index = FRONTEND_DIST / "index.html" if index.is_file(): return FileResponse(index) raise HTTPException(status_code=404, detail="前端未构建:请先 cd 3d/frontend && npm run build")