c47242aa3a
The web UI is multi-instance and can connect to the backend at a different origin (the direct :8000 port, a LAN IP, 127.0.0.1 vs localhost), which the browser blocks without CORS headers. Adds CORSMiddleware driven by a new cors_allow_origins setting (default "*", safe here: bearer-token auth with allow_credentials=False). Accepts a comma-separated string in .env. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
2.3 KiB
Python
72 lines
2.3 KiB
Python
"""FastAPI composition root: wiring, lifespan, middleware, routers."""
|
|
|
|
from collections.abc import AsyncIterator
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, WebSocket
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
from app.api.errors import register_exception_handlers
|
|
from app.api.health import router as health_router
|
|
from app.api.middleware import CorrelationIdMiddleware
|
|
from app.api.rest import subsonic_router
|
|
from app.api.v1 import api_v1_router
|
|
from app.core.config import app_version, get_settings
|
|
from app.core.logging import configure_logging, get_logger
|
|
from app.infrastructure.cache import close_redis
|
|
from app.infrastructure.db import dispose_engine
|
|
|
|
log = get_logger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
|
settings = get_settings()
|
|
log.info("startup", environment=settings.environment)
|
|
yield
|
|
log.info("shutdown")
|
|
await dispose_engine()
|
|
await close_redis()
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
settings = get_settings()
|
|
configure_logging(level=settings.log_level, json=settings.log_json)
|
|
|
|
app = FastAPI(
|
|
title="mcma-backend",
|
|
version=app_version(),
|
|
summary="Self-hosted, offline-first music service.",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
app.add_middleware(CorrelationIdMiddleware)
|
|
# CORS added last → outermost, so browser preflight (OPTIONS) is answered
|
|
# before anything else. The web UI can connect cross-origin (direct :8000,
|
|
# a LAN IP, 127.0.0.1 vs localhost), which needs these headers. Bearer-token
|
|
# auth (no cookies) → wildcard origins are safe with allow_credentials=False.
|
|
if settings.cors_allow_origins:
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.cors_allow_origins,
|
|
allow_credentials=False,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
expose_headers=["Content-Range", "Accept-Ranges", "Content-Length"],
|
|
)
|
|
register_exception_handlers(app)
|
|
|
|
app.include_router(health_router)
|
|
app.include_router(api_v1_router)
|
|
app.include_router(subsonic_router, prefix="/rest")
|
|
|
|
@app.websocket("/ws")
|
|
async def ws_stub(websocket: WebSocket) -> None:
|
|
await websocket.accept()
|
|
await websocket.close(1001)
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|