Files
decision/backend/app/main.py
Yvv 79e468b40f Multi-tenancy : espaces de travail + fix auth reload (rate limiter OPTIONS)
- Modèles Organization + OrgMember, migration Alembic (SQLite compatible)
- organization_id nullable sur Document, Decision, Mandate, VotingProtocol
- Service, schéma, router /organizations + dependency get_active_org_id
- Seed : Duniter G1 + Axiom Team ; tout le contenu seed attaché à Duniter G1
- Backend : list/create filtrés par header X-Organization
- Frontend : store organizations, WorkspaceSelector réel, useApi injecte l'org
- Fix critique : rate_limiter exclut les requêtes OPTIONS (CORS preflight)
  → résout le bug "Failed to fetch /auth/me" au reload (429 sur preflight)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 15:17:14 +02:00

150 lines
5.6 KiB
Python

import logging
import sys
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database import get_db, init_db
from app.middleware.rate_limiter import RateLimiterMiddleware
from app.middleware.security_headers import SecurityHeadersMiddleware
from app.routers import auth, documents, decisions, votes, mandates, protocols, sanctuary, websocket
from app.routers import public
from app.routers import organizations
# ── Structured logging setup ───────────────────────────────────────────────
def _setup_logging() -> None:
"""Configure structured logging based on environment.
- Production/staging: JSON-formatted log lines for log aggregation.
- Development: human-readable format with colors.
"""
log_level = getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO)
if settings.ENVIRONMENT in ("production", "staging"):
# JSON formatter for structured logging
formatter = logging.Formatter(
'{"timestamp":"%(asctime)s","level":"%(levelname)s",'
'"logger":"%(name)s","message":"%(message)s"}',
datefmt="%Y-%m-%dT%H:%M:%S",
)
else:
# Human-readable format for development
formatter = logging.Formatter(
"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
datefmt="%H:%M:%S",
)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
root_logger.handlers.clear()
root_logger.addHandler(handler)
# Reduce noise from third-party libraries
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
logging.getLogger("sqlalchemy.engine").setLevel(
logging.INFO if settings.ENVIRONMENT == "development" else logging.WARNING
)
_setup_logging()
logger = logging.getLogger(__name__)
# ── Application lifespan ───────────────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info(
"Demarrage %s (env=%s, log_level=%s)",
settings.APP_NAME, settings.ENVIRONMENT, settings.LOG_LEVEL,
)
await init_db()
yield
logger.info("Arret %s", settings.APP_NAME)
# ── FastAPI application ───────────────────────────────────────────────────
app = FastAPI(
title=settings.APP_NAME,
description="Plateforme de decisions collectives pour la communaute Duniter/G1",
version="0.5.0",
lifespan=lifespan,
)
# ── Middleware stack ──────────────────────────────────────────────────────
# Middleware is applied in reverse order: last added = first executed.
# Order: SecurityHeaders -> RateLimiter -> CORS -> Application
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(
RateLimiterMiddleware,
rate_limit_default=settings.RATE_LIMIT_DEFAULT,
rate_limit_auth=settings.RATE_LIMIT_AUTH,
rate_limit_vote=settings.RATE_LIMIT_VOTE,
)
app.add_middleware(SecurityHeadersMiddleware)
# ── Routers ──────────────────────────────────────────────────────────────
app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
app.include_router(documents.router, prefix="/api/v1/documents", tags=["documents"])
app.include_router(decisions.router, prefix="/api/v1/decisions", tags=["decisions"])
app.include_router(votes.router, prefix="/api/v1/votes", tags=["votes"])
app.include_router(mandates.router, prefix="/api/v1/mandates", tags=["mandates"])
app.include_router(protocols.router, prefix="/api/v1/protocols", tags=["protocols"])
app.include_router(sanctuary.router, prefix="/api/v1/sanctuary", tags=["sanctuary"])
app.include_router(websocket.router, prefix="/api/v1/ws", tags=["websocket"])
app.include_router(public.router, prefix="/api/v1/public", tags=["public"])
app.include_router(organizations.router, prefix="/api/v1/organizations", tags=["organizations"])
# ── Health check ─────────────────────────────────────────────────────────
@app.get("/api/health")
async def health(db: AsyncSession = Depends(get_db)):
"""Health check endpoint that verifies database connectivity.
Returns status "ok" with database connection info if healthy,
or status "degraded" if the database is unreachable.
"""
try:
result = await db.execute(text("SELECT 1"))
result.scalar()
db_status = "connected"
except Exception as exc:
logger.warning("Health check: base de donnees inaccessible - %s", exc)
db_status = "disconnected"
overall_status = "ok" if db_status == "connected" else "degraded"
return {
"status": overall_status,
"environment": settings.ENVIRONMENT,
"database": db_status,
"version": "0.5.0",
}