150 lines
4.6 KiB
Python
150 lines
4.6 KiB
Python
import asyncio
|
|
import os
|
|
from time import perf_counter
|
|
|
|
from fastapi import Depends, FastAPI, Request
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from starlette.middleware.cors import CORSMiddleware
|
|
from starlette.staticfiles import StaticFiles
|
|
|
|
from audit import ensure_api_context, log_api_access, record_api_access_event_background
|
|
from config import API_LOGGING, API_VERSION, API_CORS_ORIGINS
|
|
from database import async_session_maker
|
|
from logger import logger
|
|
|
|
if API_VERSION == 1:
|
|
from api.v1 import router as api_router, VERSION as API_DOC_VERSION
|
|
else:
|
|
from api.v2 import VERSION as API_DOC_VERSION
|
|
from api.v2.router import router as api_router
|
|
|
|
app = FastAPI(
|
|
title=f"SoloBot API (Alpha) — API v{API_DOC_VERSION}",
|
|
version=API_DOC_VERSION,
|
|
description=f"Версия API: **v{API_DOC_VERSION}**.",
|
|
docs_url="/api/docs",
|
|
redoc_url="/api/redoc",
|
|
openapi_url="/api/openapi.json",
|
|
)
|
|
|
|
_cors_origins = API_CORS_ORIGINS if API_CORS_ORIGINS != ["*"] else API_CORS_ORIGINS
|
|
_cors_credentials = API_CORS_ORIGINS != ["*"]
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=_cors_origins,
|
|
allow_credentials=_cors_credentials,
|
|
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
allow_headers=["X-Identity-Id", "X-Token", "Content-Type", "Authorization"],
|
|
)
|
|
|
|
|
|
@app.middleware("http")
|
|
async def security_headers_middleware(request: Request, call_next):
|
|
response = await call_next(request)
|
|
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
|
response.headers.setdefault("X-Frame-Options", "DENY")
|
|
response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
response.headers.setdefault("X-XSS-Protection", "1; mode=block")
|
|
return response
|
|
|
|
|
|
@app.middleware("http")
|
|
async def api_access_log_middleware(request: Request, call_next):
|
|
context = ensure_api_context(request)
|
|
if not API_LOGGING:
|
|
response = await call_next(request)
|
|
response.headers["X-Request-Id"] = context.request_id
|
|
return response
|
|
|
|
started = perf_counter()
|
|
try:
|
|
response = await call_next(request)
|
|
except Exception as exc:
|
|
duration_ms = int((perf_counter() - started) * 1000)
|
|
log_api_access(
|
|
request,
|
|
status_code=500,
|
|
duration_ms=duration_ms,
|
|
result="fail",
|
|
reason=type(exc).__name__,
|
|
)
|
|
asyncio.create_task(
|
|
record_api_access_event_background(
|
|
async_session_maker,
|
|
request,
|
|
result="fail",
|
|
reason=type(exc).__name__,
|
|
status_code=500,
|
|
)
|
|
)
|
|
raise
|
|
|
|
duration_ms = int((perf_counter() - started) * 1000)
|
|
response.headers["X-Request-Id"] = context.request_id
|
|
result = "success" if response.status_code < 400 else "fail"
|
|
log_api_access(
|
|
request,
|
|
status_code=response.status_code,
|
|
duration_ms=duration_ms,
|
|
result=result,
|
|
)
|
|
asyncio.create_task(
|
|
record_api_access_event_background(
|
|
async_session_maker,
|
|
request,
|
|
result=result,
|
|
reason=None if response.status_code < 400 else str(response.status_code),
|
|
status_code=response.status_code,
|
|
)
|
|
)
|
|
return response
|
|
|
|
|
|
@app.get("/api/health", include_in_schema=False)
|
|
async def health():
|
|
return {"status": "ok"}
|
|
|
|
|
|
from api.depends import get_session as _get_session, verify_identity_admin as _verify_admin
|
|
|
|
|
|
@app.get("/api/health/detailed", include_in_schema=False)
|
|
async def health_detailed(
|
|
session: AsyncSession = Depends(_get_session),
|
|
_identity=Depends(_verify_admin),
|
|
):
|
|
import time
|
|
from sqlalchemy import text
|
|
from core.redis_cache import _get_redis
|
|
|
|
checks: dict[str, object] = {"status": "ok", "timestamp": int(time.time())}
|
|
|
|
try:
|
|
await session.execute(text("SELECT 1"))
|
|
checks["db"] = {"ok": True}
|
|
except Exception as e:
|
|
checks["db"] = {"ok": False, "error": str(e)[:200]}
|
|
checks["status"] = "degraded"
|
|
|
|
try:
|
|
client = await _get_redis()
|
|
if client is not None:
|
|
await client.ping()
|
|
checks["redis"] = {"ok": True}
|
|
else:
|
|
checks["redis"] = {"ok": False, "error": "unavailable"}
|
|
checks["status"] = "degraded"
|
|
except Exception as e:
|
|
checks["redis"] = {"ok": False, "error": str(e)[:200]}
|
|
checks["status"] = "degraded"
|
|
|
|
return checks
|
|
|
|
|
|
app.include_router(api_router)
|
|
|
|
_web_uploads_dir = "static/web_uploads"
|
|
os.makedirs(_web_uploads_dir, exist_ok=True)
|
|
app.mount("/api/web/uploads", StaticFiles(directory=_web_uploads_dir), name="web_uploads")
|