JWT_SECRET retombait sur "data-sentinel-secret-change-in-prod", une valeur lisible par quiconque a accès au code : un oubli de configuration suffisait à permettre de forger un jeton d'administrateur. Sans JWT_SECRET, une clé aléatoire est désormais tirée au démarrage, avec un avertissement explicite. Le démarrage n'est volontairement pas bloqué : un oubli de variable d'environnement ne doit pas transformer une configuration incomplète en indisponibilité totale du service. Contrepartie assumée et documentée : les sessions ne survivent pas à un redémarrage tant que la variable n'est pas définie. Vérifié : la production signe déjà avec un secret propre, ce correctif ne change donc rien à son fonctionnement. Ajoute par ailleurs APP_BUILD, injecté par la CI depuis le SHA du commit et exposé par GET /health (et /version.json côté front). Jusqu'ici, rien ne permettait de savoir quelle version tournait réellement : un déploiement non appliqué était indiscernable d'un déploiement réussi.
123 lines
3.9 KiB
Python
123 lines
3.9 KiB
Python
# ============================================================
|
|
# main.py — API FastAPI Data Sentinel
|
|
# COYAUD Anthony | 2026
|
|
# V2 : une table dédiée par monitoring
|
|
#
|
|
# Ce module ne contient que l'assemblage de l'application :
|
|
# configuration, middlewares et montage des routeurs. Chaque
|
|
# domaine fonctionnel vit dans routers/.
|
|
#
|
|
# Lancement : uvicorn main:app --reload --port 8000
|
|
# Swagger : http://localhost:8000/docs
|
|
# ============================================================
|
|
|
|
import os
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
from slowapi import _rate_limit_exceeded_handler
|
|
from slowapi.errors import RateLimitExceeded
|
|
|
|
from config import Config, get_cursor
|
|
from domain import MONITO_TABLES
|
|
from rate_limit import limiter
|
|
from routers import (
|
|
admin,
|
|
authentification,
|
|
dashboard,
|
|
evolution,
|
|
historique,
|
|
monitorings,
|
|
referentiels,
|
|
rgpd,
|
|
)
|
|
|
|
# Origines autorisées : depuis CORS_ORIGINS (séparées par des virgules) en prod,
|
|
# localhost par défaut en dev.
|
|
CORS_ORIGINS = [
|
|
o.strip()
|
|
for o in os.getenv("CORS_ORIGINS", "http://localhost:5173,http://localhost:3000").split(",")
|
|
if o.strip()
|
|
]
|
|
|
|
# ============================================================
|
|
# Initialisation
|
|
# ============================================================
|
|
|
|
app = FastAPI(
|
|
title = Config.API_TITLE,
|
|
version = Config.API_VERSION,
|
|
description = Config.API_DESCRIPTION,
|
|
)
|
|
|
|
# Limiteur de débit (anti brute-force sur /auth/login).
|
|
app.state.limiter = limiter
|
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins = CORS_ORIGINS,
|
|
allow_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
allow_headers = ["*"],
|
|
allow_credentials = True,
|
|
)
|
|
|
|
|
|
# En-têtes de sécurité sur toutes les réponses.
|
|
@app.middleware("http")
|
|
async def security_headers(request: Request, call_next):
|
|
response = await call_next(request)
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
response.headers["X-Frame-Options"] = "DENY"
|
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
|
return response
|
|
|
|
|
|
# ============================================================
|
|
# SANTÉ — route publique
|
|
# ============================================================
|
|
|
|
@app.get("/health", tags=["Système"])
|
|
def health_check():
|
|
"""
|
|
Ping API + test connexion SQL Server.
|
|
|
|
`build` porte le SHA du commit dont l'image a été construite (injecté par
|
|
la CI). C'est le seul moyen fiable de vérifier qu'un déploiement a bien
|
|
pris : comparer ce champ au dernier commit poussé.
|
|
"""
|
|
try:
|
|
with get_cursor() as cursor:
|
|
cursor.execute("SELECT 1")
|
|
cursor.fetchone()
|
|
db_status = "ok"
|
|
except Exception as e:
|
|
db_status = f"error: {str(e)}"
|
|
|
|
return {
|
|
"api" : "ok",
|
|
"database" : db_status,
|
|
"version" : Config.API_VERSION,
|
|
"build" : Config.BUILD,
|
|
"nb_monitorings" : len(MONITO_TABLES),
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# Montage des routeurs
|
|
#
|
|
# Les routeurs de données déclarent eux-mêmes leur dépendance
|
|
# d'authentification. /health et /auth/login restent publics.
|
|
# ============================================================
|
|
|
|
app.include_router(authentification.router)
|
|
app.include_router(referentiels.router)
|
|
app.include_router(monitorings.router, prefix="/monitorings")
|
|
app.include_router(dashboard.router, prefix="/dashboard")
|
|
app.include_router(historique.router, prefix="/historique")
|
|
app.include_router(evolution.router, prefix="/evolution")
|
|
app.include_router(admin.router)
|
|
app.include_router(rgpd.router)
|