import os import secrets from contextlib import asynccontextmanager from fastapi import FastAPI, Depends, HTTPException, status from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from rate_limiter import limiter from bd.database import wait_for_db, engine, Base from route.public import router as public_router from route.admin_auth import router as auth_router, ensure_default_admin from route.admin_articles import router as articles_router from route.admin_categories import router as categories_router from route.admin_media import router as media_router from route.admin_vk import router as vk_router import scheduler DOCS_USER = os.getenv("DOCS_USERNAME", "admin") DOCS_PASS = os.getenv("DOCS_PASSWORD", "admin") _basic = HTTPBasic() # Rate limiter — ключ по IP-адресу def _verify_docs(creds: HTTPBasicCredentials = Depends(_basic)): ok_u = secrets.compare_digest(creds.username.encode(), DOCS_USER.encode()) ok_p = secrets.compare_digest(creds.password.encode(), DOCS_PASS.encode()) if not (ok_u and ok_p): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, headers={"WWW-Authenticate": "Basic"}) return creds.username @asynccontextmanager async def lifespan(app: FastAPI): await wait_for_db() async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) await ensure_default_admin() scheduler.start() yield scheduler.shutdown() app = FastAPI( title="News API", version="1.0.0", lifespan=lifespan, docs_url=None, redoc_url=None, openapi_url=None, ) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # CORS: разрешаем только наш домен (меняй на продакшн-домен) _ALLOWED_ORIGINS = [o.strip() for o in os.getenv("ALLOWED_ORIGINS", "http://localhost,http://localhost:80").split(",") if o.strip()] app.add_middleware( CORSMiddleware, allow_origins=_ALLOWED_ORIGINS, allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"], allow_headers=["Authorization", "Content-Type"], ) @app.get("/docs", include_in_schema=False) async def docs(username: str = Depends(_verify_docs)): return get_swagger_ui_html(openapi_url="/openapi.json", title="News API Docs") @app.get("/redoc", include_in_schema=False) async def redoc(username: str = Depends(_verify_docs)): return get_redoc_html(openapi_url="/openapi.json", title="News API Redoc") @app.get("/openapi.json", include_in_schema=False) async def openapi(username: str = Depends(_verify_docs)): return JSONResponse(app.openapi()) @app.get("/health", include_in_schema=False) async def health(): return {"ok": True} app.include_router(public_router, prefix="/news", tags=["public"]) app.include_router(auth_router, prefix="/admin", tags=["admin-auth"]) app.include_router(articles_router, prefix="/admin/articles", tags=["admin-articles"]) app.include_router(categories_router,prefix="/admin/categories",tags=["admin-categories"]) app.include_router(media_router, prefix="/admin/media", tags=["admin-media"]) app.include_router(vk_router, prefix="/admin/vk", tags=["admin-vk"]) if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=False)