Files
lux_fiscal/backend/app/main.py
T
lauadminandClaude Sonnet 5 2664cb8213 Initial commit: backend scaffold, auth, frontend login
- FastAPI + SQLAlchemy async + Alembic + Postgres backend
- Auth: JWT access + rotating refresh tokens, argon2, roles, audit log
- React 19 + Vite frontend: login page, protected route, auth context
- Docker Compose: postgres, redis, migrate, api, worker (placeholder), frontend/nginx

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:07:15 +03:00

105 lines
3.5 KiB
Python

"""Точка входа FastAPI."""
from __future__ import annotations
import time
import uuid
from collections.abc import AsyncGenerator, Awaitable, Callable
from contextlib import asynccontextmanager
import structlog
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from app.api.v1.router import api_router
from app.core.config import settings
from app.core.logging import configure_logging, get_logger
from app.db.session import engine
log = get_logger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
configure_logging()
log.info("app_starting", environment=settings.environment, version=app.version)
yield
await engine.dispose()
log.info("app_stopped")
class RequestContextMiddleware(BaseHTTPMiddleware):
"""Присваивает каждому запросу идентификатор и пишет структурный access-лог.
request_id возвращается заголовком `X-Request-ID`: по нему оператор,
столкнувшийся с ошибкой, находится в логах за одну команду.
"""
async def dispatch(
self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
request_id = request.headers.get("x-request-id") or uuid.uuid4().hex
structlog.contextvars.clear_contextvars()
structlog.contextvars.bind_contextvars(request_id=request_id)
started = time.perf_counter()
try:
response = await call_next(request)
except Exception:
log.exception(
"request_failed",
method=request.method,
path=request.url.path,
duration_ms=round((time.perf_counter() - started) * 1000, 2),
)
raise
duration_ms = round((time.perf_counter() - started) * 1000, 2)
# Health-check'и опрашиваются постоянно и засоряют лог.
if not request.url.path.startswith("/api/v1/health"):
log.info(
"request",
method=request.method,
path=request.url.path,
status=response.status_code,
duration_ms=duration_ms,
)
response.headers["X-Request-ID"] = request_id
return response
def create_app() -> FastAPI:
configure_logging()
app = FastAPI(
title=settings.project_name,
version="0.1.0",
description="Фискализация заказов через Checkbox",
lifespan=lifespan,
# В проде интерактивная документация закрыта: схема API — лишняя
# подсказка для того, кто ищет незащищённый эндпоинт.
docs_url=None if settings.is_production else "/docs",
redoc_url=None if settings.is_production else "/redoc",
openapi_url=None if settings.is_production else "/openapi.json",
)
app.add_middleware(RequestContextMiddleware)
if settings.cors_origins:
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Request-ID"],
)
app.include_router(api_router, prefix="/api/v1")
return app
app = create_app()