import uuid from fasthtml.common import * from starlette.staticfiles import StaticFiles from starlette.responses import FileResponse, Response from starlette.middleware.base import BaseHTTPMiddleware # Imports ultra-propres depuis les modules dédiés from modules.theme import get_theme_hdrs from modules.database import nettoyer_vieilles_discussions, HISTORY_FILE, load_history, save_history from modules.ui_components import render_main_page, render_sidebar from modules.chat_handler import handle_chat_request from modules.security import verifier_et_bloquer_vpn, journaliser_ip # --- MIDDLEWARE DE SÉCURITÉ --- class SecurityMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): # Récupération de l'IP du client (gère le proxy NPM si configuré avec X-Forwarded-For) ip = request.headers.get("X-Forwarded-For", request.client.host).split(",")[0].strip() # Simulation du rôle de l'utilisateur via cookie (en attendant le module de connexion) utilisateur = request.cookies.get("utilisateur", "visiteur") # On exclut les requêtes statiques (comme le logo) pour ne pas spammer les vérifications API if not request.url.path.startswith("/Logo"): # 1. Vérification VPN (inclut désormais la Whitelist VIP/Admin) if verifier_et_bloquer_vpn(ip, utilisateur): return HTMLResponse("
Les connexions via VPN, Proxy ou Datacenter sont strictement interdites sur cette plateforme pour votre profil.
", status_code=403) # 2. Journalisation RGPD journaliser_ip(ip, f"Accès à {request.url.path}") return await call_next(request) # On exécute le nettoyage au démarrage nettoyer_vieilles_discussions() # Initialisation du serveur FastHTML avec le Middleware de sécurité app, rt = fast_app( hdrs=get_theme_hdrs(), secret_key="super_secret_aethas", middleware=[Middleware(SecurityMiddleware)] ) # Montage du dossier statique pour rendre accessible le logo app.mount("/Logo", StaticFiles(directory="Logo"), name="Logo") @rt('/') def get(session): if 'current_chat_id' not in session or not session['current_chat_id']: session['current_chat_id'] = str(uuid.uuid4()) return render_main_page(session['current_chat_id']) @rt('/chat') async def post(request, session: dict): return await handle_chat_request(request, session) @rt('/favicon.ico') def favicon_get(): return Response(status_code=204) @rt('/favicon.ico') def favicon_head(): return Response(status_code=204) @rt('/new_chat') def get(session): session['current_chat_id'] = str(uuid.uuid4()); return RedirectResponse('/', status_code=303) @rt('/load_chat/{cid}') def get(cid: str, session): if cid and cid != "null": session['current_chat_id'] = cid return RedirectResponse('/', status_code=303) @rt('/rename/{cid}') def post(cid: str, req): h = load_history() if cid in h and (nom := req.headers.get("HX-Prompt")): h[cid]['title'] = nom; save_history(h) return render_sidebar() @rt('/pin/{cid}') def post(cid: str): h = load_history() if cid in h: h[cid]['pinned'] = not h[cid].get('pinned', False); save_history(h) return render_sidebar() @rt('/delete/{cid}') def post(cid: str): h, cid_str = load_history(), str(cid) if cid_str in h: del h[cid_str]; save_history(h) return render_sidebar() @rt('/export_json') def get(): return FileResponse(HISTORY_FILE, media_type='application/json', filename='Aethas38_Discussion_Globale.json') @rt('/ping') def get(): return Response(status_code=204) if __name__ == '__main__': serve(port=5001)