Files
innotexBoard/backend/main.py

179 lines
5.6 KiB
Python

"""
InnotexBoard - Interface d'administration Debian
Backend FastAPI
"""
from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
import uvicorn
import logging
import os
from pathlib import Path
from app.core.config import settings
from app.api.routes import api_router
from app.api.websocket import router as ws_router
# Configuration du répertoire de logs
log_dir = Path(os.getenv('LOG_DIR', '/var/log/innotexboard'))
if not log_dir.exists():
try:
log_dir.mkdir(parents=True, exist_ok=True)
except PermissionError:
# Fallback sur un répertoire local si pas de permissions
log_dir = Path('./logs')
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / 'security.log'
# Configuration du logging de sécurité
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(str(log_file)),
logging.StreamHandler()
]
)
logger = logging.getLogger("innotexboard.security")
# Réduire le bruit des logs watchfiles en développement
logging.getLogger("watchfiles.main").setLevel(logging.WARNING)
logging.getLogger("watchfiles").setLevel(logging.WARNING)
# Rate limiting pour prévenir les attaques brute force et DDoS
limiter = Limiter(key_func=get_remote_address, default_limits=["200/minute"])
# Initialiser l'application FastAPI
app = FastAPI(
title=settings.API_TITLE,
description=settings.API_DESCRIPTION,
version=settings.API_VERSION,
docs_url="/docs" if settings.DEBUG else None, # Désactiver docs en production
redoc_url="/redoc" if settings.DEBUG else None,
openapi_url="/openapi.json" if settings.DEBUG else None,
)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# Middleware de sécurité CORS - Configuration stricte
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS, # Liste blanche uniquement
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"], # Méthodes explicites
allow_headers=["Authorization", "Content-Type", "X-Requested-With"],
expose_headers=["X-RateLimit-Limit", "X-RateLimit-Remaining"],
max_age=3600, # Cache preflight 1h
)
# Middleware pour les hôtes de confiance
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=settings.ALLOWED_HOSTS,
)
@app.middleware("http")
async def security_headers_middleware(request: Request, call_next):
"""Ajoute les headers de sécurité HTTP à toutes les réponses"""
response = await call_next(request)
# Protection contre le clickjacking
response.headers["X-Frame-Options"] = "DENY"
# Force HTTPS et HSTS (HTTP Strict Transport Security)
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
# Prévention du MIME-sniffing
response.headers["X-Content-Type-Options"] = "nosniff"
# Protection XSS intégrée au navigateur
response.headers["X-XSS-Protection"] = "1; mode=block"
# Content Security Policy - Politique stricte
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; "
"font-src 'self' data:; "
"connect-src 'self'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self';"
)
# Contrôle des permissions
response.headers["Permissions-Policy"] = (
"geolocation=(), microphone=(), camera=(), payment=()"
)
# Désactiver la divulgation de la version du serveur
response.headers["Server"] = "InnotexBoard"
return response
@app.middleware("http")
async def log_security_events(request: Request, call_next):
"""Log les événements de sécurité suspects"""
client_ip = get_remote_address(request)
# Logger les tentatives d'accès aux endpoints sensibles
if "/api/v1/auth" in request.url.path:
logger.info(f"Auth attempt from {client_ip} to {request.url.path}")
response = await call_next(request)
# Logger les erreurs d'authentification
if response.status_code == 401:
logger.warning(f"Failed authentication from {client_ip} to {request.url.path}")
# Logger les tentatives d'accès non autorisé
if response.status_code == 403:
logger.warning(f"Forbidden access attempt from {client_ip} to {request.url.path}")
return response
# Inclure les routes API
app.include_router(api_router, prefix="/api/v1")
app.include_router(ws_router, prefix="/api/v1")
@app.get("/")
@limiter.limit("10/minute")
async def root(request: Request):
"""Endpoint racine"""
return {
"message": "Bienvenue sur InnotexBoard",
"version": settings.API_VERSION,
"docs": "/docs" if settings.DEBUG else "Documentation disabled in production",
"openapi": "/openapi.json" if settings.DEBUG else "OpenAPI disabled in production"
}
@app.get("/health")
async def health_check():
"""Vérification de la santé de l'application"""
return {
"status": "healthy",
"service": "InnotexBoard API"
}
if __name__ == "__main__":
# Configuration pour le développement
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info",
)