v3 : socle Rust (moteur pur, accès trustinWallet, serveur) ; maquette Nuxt rangée dans maquette/
- crates/ld-engine : formule inertielle (parité avec la maquette, garde C), coquille / changement de fond, assistant d'édition à règles, composition d'un document sous vote permanent (adoptions, aperçus, rivales). - crates/trustin-access : qui voit, qui ouvre, qui gère les accès. - crates/ld-server : API axum + SQLite, connexion Ğ1 (client OAuth2 de connect.monnaie-libre.fr), invitations, entrée en vigueur automatique, signalements de contradiction, traductions éditoriales, publication git, taille de la toile lue sur la chaîne. - seeds/acte-engagement-certification.json : 33 articles, fr + en + es ; en vigueur : préambule, E1, E2 (seed a minima). - maquette/ : frontend, backend et docker v2 déplacés tels quels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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
|
||||
from app.routers import qualify
|
||||
from app.routers import groups
|
||||
|
||||
|
||||
# ── 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 ──────────────────────────────────────────────────────
|
||||
# add_middleware is LIFO: last added = outermost = first to execute on request,
|
||||
# last to execute on response (wraps everything inside it).
|
||||
#
|
||||
# Required order so CORS headers appear on ALL responses including 429:
|
||||
# CORS (outermost) → RateLimiter → SecurityHeaders → Application
|
||||
#
|
||||
# If RateLimiter were outside CORS, its 429 responses would have no CORS
|
||||
# headers and the browser would silently discard them as network errors.
|
||||
|
||||
app.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
# Prototype mode: use RATE_LIMIT_DEFAULT for auth so demos/testing don't hit
|
||||
# the stricter RATE_LIMIT_AUTH (10/min). Set RATE_LIMIT_AUTH >= RATE_LIMIT_DEFAULT
|
||||
# in .env only when going to real production.
|
||||
_auth_rate_limit = settings.RATE_LIMIT_DEFAULT
|
||||
|
||||
app.add_middleware(
|
||||
RateLimiterMiddleware,
|
||||
rate_limit_default=settings.RATE_LIMIT_DEFAULT,
|
||||
rate_limit_auth=_auth_rate_limit,
|
||||
rate_limit_vote=settings.RATE_LIMIT_VOTE,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# ── 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"])
|
||||
app.include_router(qualify.router, prefix="/api/v1/qualify", tags=["qualify"])
|
||||
app.include_router(groups.router, prefix="/api/v1/groups", tags=["groups"])
|
||||
|
||||
|
||||
# ── 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",
|
||||
}
|
||||
Reference in New Issue
Block a user