Newer
Older
gnexus-tasks / backend / app / main.py
"""Точка входа FastAPI: health + OAuth-флоу gnexus-gauth + защищённое /api + /mcp."""

from collections.abc import AsyncIterator, Awaitable, Callable
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse, Response

from app import mcp_server
from app.api import attachments, projects, tags, tasks, xp
from app.api.settings import router as settings_router
from app.auth.routes import router as auth_router
from app.config import get_settings


@asynccontextmanager
async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
    """Менеджер сессий MCP требует запуска: mounted ASGI-приложение
    собственный lifespan не получает."""
    async with mcp_server.mcp.session_manager.run():
        yield


app = FastAPI(title="gntodo API", version="0.1.0", lifespan=_lifespan)

# CORS: в разработке — vite dev server; в проде фронт отдаётся тем же origin через nginx.
settings = get_settings()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:15134"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
# Сессия — подписанная cookie (HttpOnly); секрет из окружения.
app.add_middleware(SessionMiddleware, secret_key=settings.session_secret)


class McpAuthMiddleware(BaseHTTPMiddleware):
    """Bearer-токен для /mcp (до интеграции SSO-токенов, ТЗ 3.9)."""

    async def dispatch(
        self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
    ) -> Response:
        if request.url.path.startswith("/mcp"):
            token = get_settings().mcp_token
            auth = request.headers.get("authorization", "")
            if not token or auth != f"Bearer {token}":
                return JSONResponse({"detail": "Not authenticated"}, status_code=401)
        return await call_next(request)


app.add_middleware(McpAuthMiddleware)

app.include_router(auth_router)
app.include_router(tasks.router)
app.include_router(projects.router)
app.include_router(tags.router)
app.include_router(attachments.router)
app.include_router(settings_router)
app.include_router(xp.router)

# MCP-сервер для ИИ-агентов (M5)
app.mount("/mcp", mcp_server.mcp.streamable_http_app())


@app.get("/api/health")
async def health() -> dict[str, str]:
    return {"status": "ok"}