main.py 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. """压缩机 3D 部件查看服务(FastAPI)。
  2. 公共接口(无需登录):
  3. POST /api/login 登录
  4. POST /api/logout 退出
  5. GET /api/auth-config 运行模式与(测试模式下的)默认账号密码
  6. GET /models/<file> GLB / glTF 模型文件(静态资源,不鉴权)
  7. GET /* 前端构建产物(frontend/dist),SPA 回退 index.html
  8. 需登录接口(Bearer token):
  9. /api/models、/api/alarms、/api/alarm-filters、/api/pks-series、
  10. /api/scene-positions、/api/fault-part-map
  11. """
  12. from typing import Any
  13. from fastapi import Depends, FastAPI, Header, HTTPException
  14. from fastapi.middleware.cors import CORSMiddleware
  15. from fastapi.responses import FileResponse
  16. from pydantic import BaseModel, Field
  17. from auth import create_token, require_auth, revoke_token, verify_credentials
  18. from config import APP_MODE, AUTH_PASSWORD, AUTH_USER, FRONTEND_DIST, IS_TEST
  19. from routers import alarms, fault_map, files, scenes, series
  20. class LoginInput(BaseModel):
  21. username: str = Field(min_length=1)
  22. password: str = Field(min_length=1)
  23. app = FastAPI(title="压缩机 3D 部件查看服务", version="0.1.0")
  24. app.add_middleware(
  25. CORSMiddleware,
  26. allow_origins=["*"],
  27. allow_methods=["*"],
  28. allow_headers=["*"],
  29. )
  30. @app.post("/api/login")
  31. def login(payload: LoginInput) -> dict[str, Any]:
  32. if not verify_credentials(payload.username, payload.password):
  33. raise HTTPException(status_code=401, detail="用户名或密码错误")
  34. return {"token": create_token(), "username": payload.username}
  35. @app.post("/api/logout")
  36. def logout(authorization: str | None = Header(default=None)) -> dict[str, Any]:
  37. token = (authorization or "").removeprefix("Bearer ").strip()
  38. revoke_token(token)
  39. return {"ok": True}
  40. @app.get("/api/auth-config")
  41. def auth_config() -> dict[str, Any]:
  42. # 测试模式把默认账号密码给到前端登录页自动填入;生产不返回。
  43. if IS_TEST:
  44. return {"mode": APP_MODE, "test": True, "username": AUTH_USER, "password": AUTH_PASSWORD}
  45. return {"mode": APP_MODE, "test": False}
  46. # 数据接口需登录;files 里只有 /api/models 需登录,模型文件保持公开。
  47. app.include_router(alarms.router, dependencies=[Depends(require_auth)])
  48. app.include_router(fault_map.router, dependencies=[Depends(require_auth)])
  49. app.include_router(scenes.router, dependencies=[Depends(require_auth)])
  50. app.include_router(series.router, dependencies=[Depends(require_auth)])
  51. app.include_router(files.router)
  52. @app.get("/{full_path:path}")
  53. def serve_frontend(full_path: str) -> FileResponse:
  54. if full_path.startswith(("api/", "models/")):
  55. raise HTTPException(status_code=404, detail="Not Found")
  56. resolved_dist = FRONTEND_DIST.resolve()
  57. if full_path:
  58. candidate = (FRONTEND_DIST / full_path).resolve()
  59. if resolved_dist in candidate.parents and candidate.is_file():
  60. return FileResponse(candidate)
  61. index = FRONTEND_DIST / "index.html"
  62. if index.is_file():
  63. return FileResponse(index)
  64. raise HTTPException(status_code=404, detail="前端未构建:请先 cd 3d/frontend && npm run build")