Files
news_all_spo/api/main.py
2026-05-15 20:12:44 +05:00

110 lines
3.8 KiB
Python

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
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
def _run_migrations():
import subprocess, sys
result = subprocess.run(
["alembic", "upgrade", "head"],
capture_output=True, text=True
)
if result.returncode != 0:
print(f"[Alembic] Ошибка миграции:\n{result.stderr}", file=sys.stderr)
else:
print(f"[Alembic] {result.stdout.strip() or 'Миграции применены'}")
@asynccontextmanager
async def lifespan(app: FastAPI):
await wait_for_db()
_run_migrations()
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)