144 lines
4.6 KiB
Python
144 lines
4.6 KiB
Python
from contextlib import asynccontextmanager
|
|
from time import monotonic
|
|
from uuid import uuid4
|
|
|
|
from fastapi import Depends, FastAPI, Request, Response
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.staticfiles import StaticFiles
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api import (
|
|
admin,
|
|
cars,
|
|
catalog,
|
|
change_requests,
|
|
entries,
|
|
gamification,
|
|
my,
|
|
ocr,
|
|
parser,
|
|
service_centers,
|
|
service_visits,
|
|
sto_booking,
|
|
users,
|
|
work_orders,
|
|
)
|
|
from app.core.config import settings
|
|
from app.db.session import get_session
|
|
from app.services.rate_limit import get_redis_client
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
settings.validate_production_settings()
|
|
yield
|
|
|
|
|
|
app = FastAPI(title="Drivers Bot API", version="0.1.0", lifespan=lifespan)
|
|
|
|
REQUEST_COUNT = 0
|
|
REQUEST_ERRORS = 0
|
|
REQUEST_DURATION_TOTAL = 0.0
|
|
|
|
|
|
@app.middleware("http")
|
|
async def production_headers_and_metrics(request: Request, call_next):
|
|
global REQUEST_COUNT, REQUEST_DURATION_TOTAL, REQUEST_ERRORS
|
|
request_id = request.headers.get("X-Request-ID") or str(uuid4())
|
|
start = monotonic()
|
|
try:
|
|
response = await call_next(request)
|
|
except Exception:
|
|
REQUEST_ERRORS += 1
|
|
raise
|
|
duration = monotonic() - start
|
|
REQUEST_COUNT += 1
|
|
REQUEST_DURATION_TOTAL += duration
|
|
response.headers["X-Request-ID"] = request_id
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
response.headers["X-Frame-Options"] = "DENY"
|
|
if settings.is_production:
|
|
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
|
response.headers["Content-Security-Policy"] = (
|
|
"default-src 'self' https://telegram.org https://*.telegram.org; "
|
|
"connect-src 'self' https://api.telegram.org; "
|
|
"img-src 'self' data: https:; "
|
|
"script-src 'self' 'unsafe-inline' https://telegram.org https://*.telegram.org; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"frame-ancestors 'none'"
|
|
)
|
|
return response
|
|
|
|
dev_origins = ["http://localhost:8000", "http://127.0.0.1:8000"] if not settings.is_production else []
|
|
cors_origins = settings.cors_origin_list or dev_origins
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=cors_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.include_router(users.router, prefix="/api")
|
|
app.include_router(my.router, prefix="/api")
|
|
app.include_router(catalog.router, prefix="/api")
|
|
app.include_router(cars.router, prefix="/api")
|
|
app.include_router(entries.router, prefix="/api")
|
|
app.include_router(gamification.router, prefix="/api")
|
|
app.include_router(ocr.router, prefix="/api")
|
|
app.include_router(parser.router, prefix="/api")
|
|
app.include_router(service_centers.router, prefix="/api")
|
|
app.include_router(sto_booking.router, prefix="/api")
|
|
app.include_router(service_visits.router, prefix="/api")
|
|
app.include_router(work_orders.router, prefix="/api")
|
|
app.include_router(change_requests.router, prefix="/api")
|
|
app.include_router(admin.router, prefix="/api")
|
|
|
|
|
|
@app.get("/health")
|
|
async def health() -> dict[str, str]:
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.get("/metrics")
|
|
async def metrics() -> Response:
|
|
avg = REQUEST_DURATION_TOTAL / REQUEST_COUNT if REQUEST_COUNT else 0
|
|
body = "\n".join(
|
|
[
|
|
"# TYPE carpass_requests_total counter",
|
|
f"carpass_requests_total {REQUEST_COUNT}",
|
|
"# TYPE carpass_request_errors_total counter",
|
|
f"carpass_request_errors_total {REQUEST_ERRORS}",
|
|
"# TYPE carpass_request_duration_seconds_avg gauge",
|
|
f"carpass_request_duration_seconds_avg {avg:.6f}",
|
|
"",
|
|
]
|
|
)
|
|
return Response(body, media_type="text/plain; version=0.0.4")
|
|
|
|
|
|
@app.get("/ready")
|
|
async def ready(session: AsyncSession = Depends(get_session)) -> dict[str, str]:
|
|
await session.execute(text("select 1"))
|
|
migration = "unknown"
|
|
try:
|
|
version = await session.execute(text("select version_num from alembic_version limit 1"))
|
|
migration = version.scalar_one_or_none() or "unknown"
|
|
except Exception:
|
|
migration = "not_checked"
|
|
redis_status = "disabled"
|
|
if settings.redis_url:
|
|
redis = await get_redis_client()
|
|
if redis is None:
|
|
redis_status = "client_missing"
|
|
else:
|
|
await redis.ping()
|
|
redis_status = "ok"
|
|
return {"status": "ready", "database": "ok", "redis": redis_status, "migration": migration}
|
|
|
|
|
|
app.mount("/", StaticFiles(directory="web", html=True), name="web")
|