import os import secrets import tomllib from pathlib import Path from fastapi import Depends, FastAPI, HTTPException, status from fastapi.middleware.cors import CORSMiddleware from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html from fastapi.security import HTTPBasic, HTTPBasicCredentials from route import base, holland_crud, klimov_crud, glomshtok_crud, init_data_base, groups_crud, organizations_crud, users_crud, question_crud, choice_crud, response_crud, poll_crud, scale_crud, radar_crud, stats_crud from route import auth from bd import wait_for_db import uvicorn # Читаем версию из pyproject.toml try: pyproject_path = Path(__file__).parent / "pyproject.toml" with open(pyproject_path, "rb") as f: pyproject = tomllib.load(f) VERSION = pyproject["project"]["version"] except Exception as e: print(f"Warning: Could not read version from pyproject.toml: {e}") VERSION = "0.1.0" APP_PATH = os.getenv("APP_PATH", "/home/user/api-copp") # --------------------------------------------------------------------------- # Защита документации HTTP Basic Auth # --------------------------------------------------------------------------- _basic = HTTPBasic() def _verify_docs(credentials: HTTPBasicCredentials = Depends(_basic)): """Проверяет логин/пароль для доступа к /docs и /redoc.""" docs_user = os.getenv("DOCS_USERNAME", "") docs_pass = os.getenv("DOCS_PASSWORD", "") if not docs_user or not docs_pass: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="API docs are disabled: DOCS_USERNAME / DOCS_PASSWORD not configured", ) # secrets.compare_digest защищает от timing-атак (перебора по времени ответа) ok_user = secrets.compare_digest(credentials.username.encode(), docs_user.encode()) ok_pass = secrets.compare_digest(credentials.password.encode(), docs_pass.encode()) if not (ok_user and ok_pass): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Basic"}, ) # docs_url/openapi_url/redoc_url отключены — добавляем защищённые маршруты вручную app = FastAPI(title="api", version=VERSION, docs_url=None, openapi_url=None, redoc_url=None) # Разрешаем CORS для фронтенда (dev). При необходимости сузить список origins. app.add_middleware( CORSMiddleware, allow_origins=["http://localhost", "http://127.0.0.1"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(auth.router) app.include_router(base.router) app.include_router(init_data_base.router) app.include_router(groups_crud.router) app.include_router(organizations_crud.router) app.include_router(users_crud.router) app.include_router(question_crud.router) app.include_router(choice_crud.router) app.include_router(response_crud.router) app.include_router(poll_crud.router) app.include_router(holland_crud.router) app.include_router(klimov_crud.router) app.include_router(glomshtok_crud.router) app.include_router(scale_crud.router) app.include_router(radar_crud.router) app.include_router(stats_crud.router) # --------------------------------------------------------------------------- # Защищённые маршруты документации # --------------------------------------------------------------------------- @app.get("/docs", include_in_schema=False) def get_docs(_: None = Depends(_verify_docs)): return get_swagger_ui_html(openapi_url="/openapi.json", title="API Docs") @app.get("/redoc", include_in_schema=False) def get_redoc(_: None = Depends(_verify_docs)): return get_redoc_html(openapi_url="/openapi.json", title="API Docs") @app.get("/openapi.json", include_in_schema=False) def get_openapi_schema(_: None = Depends(_verify_docs)): return app.openapi() print(f"Application path: {APP_PATH}") wait_for_db() if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)