"""Создание FastAPI-приложения админки.""" from __future__ import annotations import logging import secrets from collections.abc import AsyncIterator from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.templating import Jinja2Templates from starlette.middleware.sessions import SessionMiddleware from admin.media import ensure_media from admin.middleware import AdminAuthMiddleware from admin.routes import admins, auth, health, logs, pages, preview, schedules, send, sequences, titles, users from common import __version__ from common.analytics.partitions import ensure_partitions from common.config import AdminSettings, get_admin_settings from common.db.pool import close_pool, init_pool from common.logging_config import setup_logging from common.repositories import admins_repo BASE_DIR = Path(__file__).resolve().parent TEMPLATES_DIR = BASE_DIR / "templates" logger = logging.getLogger(__name__) def create_app(settings: AdminSettings | None = None, *, init_db: bool = True) -> FastAPI: """Собрать приложение: настройки, шаблоны, роутеры. Настройки можно передать явно (тесты); по умолчанию читаются из окружения. ``init_db=False`` — не открывать пул Postgres (юнит-тесты без БД). """ setup_logging() settings = settings or get_admin_settings() @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: if init_db: await init_pool(app.state.settings) ensure_media(app.state.settings) try: await ensure_partitions(app.state.settings) except Exception: logger.warning("Не удалось создать партиции аналитики", exc_info=True) try: inserted = await admins_repo.ensure_admins(app.state.settings.admin_ids) if inserted: logger.info("Добавлены администраторы из ADMIN_IDS: %s", inserted) except Exception: logger.warning( "Не удалось засеять администраторов (применены ли миграции?)", exc_info=True, ) try: yield finally: if init_db: await close_pool() app = FastAPI( title="DispelEvilBot Admin", version=__version__, # Swagger/OpenAPI наружу пока не нужны: админка закрыта логином (Блок 4.2). docs_url=None, redoc_url=None, openapi_url=None, lifespan=lifespan, ) app.state.settings = settings app.state.templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) # Порядок важен: SessionMiddleware добавляется последним и оборачивает auth, # поэтому request.session доступен в проверке администратора. app.add_middleware(AdminAuthMiddleware) session_secret = settings.admin_session_secret if not session_secret: session_secret = secrets.token_urlsafe(32) logger.warning("ADMIN_SESSION_SECRET не задан — сессии слетят при перезапуске") app.add_middleware( SessionMiddleware, secret_key=session_secret, session_cookie="debot_admin", max_age=settings.admin_session_max_age, same_site="lax", https_only=settings.is_prod, ) app.include_router(health.router) app.include_router(auth.router) app.include_router(pages.router) app.include_router(preview.router) app.include_router(sequences.router) app.include_router(titles.router) app.include_router(schedules.router) app.include_router(users.router) app.include_router(admins.router) app.include_router(logs.router) app.include_router(send.router) return app