Backend: rate limiter, security headers, blockchain cache service avec RPC, public API (7 endpoints read-only), WebSocket auth + heartbeat, DB connection pooling, structured logging, health check DB. Frontend: API retry/timeout, WebSocket auth + heartbeat + typed events, notifications toast, mobile hamburger + drawer, error boundary, offline banner, loading skeletons, dashboard enrichi. Documentation: guides utilisateur complets (demarrage, vote, sanctuaire, FAQ 30+), guide deploiement, politique securite. 123 tests, 155 fichiers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
5.4 KiB
Python
148 lines
5.4 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
|
|
|
|
|
|
# ── 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"])
|
|
|
|
|
|
# ── 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",
|
|
}
|