import os from dotenv import load_dotenv load_dotenv() # Force la lecture du fichier .env en local import uuid import json import threading from fasthtml.common import * from starlette.staticfiles import StaticFiles from starlette.responses import FileResponse, Response, HTMLResponse, RedirectResponse from starlette.middleware.base import BaseHTTPMiddleware 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, render_login_page, render_rag_upload_modal from modules.chat_handler import handle_chat_request from modules.security import verifier_et_bloquer_vpn, journaliser_ip from modules.auth_manager import IDENTIFIANTS_ADMIN, verifier_mot_de_passe from modules.logger import log_event from modules.billing_manager import charger_soldes, sauvegarder_soldes from modules.rag_manager import auto_apprendre_rag from modules.vector_rag import indexer_dossier_thematique # Lancement du module d'auto-mise à jour en arrière-plan import modules.model_updater dossier_media = "/DATA/AppData/MULTI-IA-AETHAS38/RAG/Media" os.makedirs(dossier_media, exist_ok=True) # --- NOUVEAUX COMPOSANTS UI (SIDEBAR & PAYPAL) --- def render_paypal_recharge_widget(montant_usd: float = 10.00): client_id = os.getenv("PAYPAL_CLIENT_ID", "test") script_paypal = Script(src=f"https://www.paypal.com/sdk/js?client-id={client_id}¤cy=USD") container_id = f"paypal-button-container-{str(montant_usd).replace('.', '')}" script_logique = Script(f""" function chargerBoutonPayPal() {{ if (typeof paypal !== 'undefined') {{ paypal.Buttons({{ style: {{ layout: 'vertical', height: 35, shape: 'rect', label: 'paypal' }}, createOrder: function(data, actions) {{ return actions.order.create({{ purchase_units: [{{ amount: {{ value: '{montant_usd}' }} }}] }}); }}, onApprove: function(data, actions) {{ return actions.order.capture().then(function(details) {{ alert('Paiement validé par ' + details.payer.name.given_name + ' ! Vos crédits vont être ajoutés.'); fetch('/api/paypal/success', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ orderID: data.orderID }}) }}).then(res => {{ if(res.ok) {{ document.getElementById('modal-recharge-paypal').style.display = 'none'; window.location.reload(); }} else {{ alert("Erreur lors de la validation serveur."); }} }}); }}); }} }}).render('#{container_id}'); }} else {{ setTimeout(chargerBoutonPayPal, 100); }} }} chargerBoutonPayPal(); """) paypal_box = Div( H4(f"Recharge : {montant_usd} $", style="color: #fff; margin: 0 0 10px 0; font-size: 1.1em; text-align: center;"), Div(id=container_id, style="min-height: 100px; width: 100%;"), Button("Annuler", type="button", onclick="document.getElementById('modal-recharge-paypal').style.display='none'", style="background: transparent; border: 1px solid #555; color: #aaa; width: 100%; margin-top: 5px; padding: 6px; border-radius: 6px; font-size: 0.85em; cursor: pointer;"), style="background: #1a1a1a; padding: 20px; border-radius: 10px; border: 1px solid #333; width: 320px; max-width: 90vw; box-shadow: 0 8px 24px rgba(0,0,0,0.9);" ) modal_overlay = Div( script_paypal, paypal_box, script_logique, id="modal-recharge-paypal", style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); backdrop-filter: blur(3px); align-items: center; justify-content: center; z-index: 10000;" ) return modal_overlay def render_sidebar_menu(): sidebar_style = Style(""" :root { --nav-sidebar-width: 260px; } #nav-app-sidebar { position: fixed; top: 0; left: 0; width: var(--nav-sidebar-width); height: 100vh; background: rgba(17, 17, 17, 0.95); backdrop-filter: blur(5px); border-right: 1px solid #333; overflow-y: auto; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 9000; padding: 60px 20px 20px 20px; } #nav-app-sidebar.collapsed { transform: translateX(-100%); } #nav-app-sidebar::-webkit-scrollbar { width: 6px; } #nav-app-sidebar::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; } #nav-sidebar-toggle { position: fixed; top: 15px; left: 15px; z-index: 9001; background: rgba(0, 0, 0, 0.5); border: 1px solid #333; color: #fff; font-size: 1.2em; cursor: pointer; padding: 5px 10px; border-radius: 5px; transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1); } body:has(#nav-app-sidebar:not(.collapsed)) #nav-sidebar-toggle { left: calc(var(--nav-sidebar-width) - 50px); } """) sidebar_js = Script(""" function toggleNavSidebar() { document.getElementById('nav-app-sidebar').classList.toggle('collapsed'); } """) toggle_btn = Button("☰ Menu", id="nav-sidebar-toggle", onclick="toggleNavSidebar()") sidebar_content = Div( H3("Interface Multi-IA", style="color: #888; font-size: 0.9em; text-transform: uppercase; margin-bottom: 15px;"), Div("Comptabilité (Export)", style="padding: 10px; color: #ddd; cursor: pointer; border-radius: 5px; margin-bottom: 5px; background: #222;", onclick="window.location.href='/admin/export_comptable'"), Div("Gestion RAG", style="padding: 10px; color: #ddd; cursor: pointer; border-radius: 5px; margin-bottom: 5px;", onclick="window.location.href='/admin/rag_modal'"), Hr(style="border-color: #333; margin: 20px 0;"), Button( "Recharger des crédits", onclick="document.getElementById('modal-recharge-paypal').style.display='flex'", style="width: 100%; padding: 10px; background: #00e5ff; color: #000; border: none; border-radius: 6px; font-weight: bold; cursor: pointer;" ), id="nav-app-sidebar", cls="collapsed" ) return Div(sidebar_style, sidebar_js, toggle_btn, sidebar_content) # --- MIDDLEWARE DE SÉCURITÉ & SERVEUR MÉDIA UNIFIÉ --- class SecurityMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): ip = request.headers.get("X-Forwarded-For", request.client.host).split(",")[0].strip() utilisateur = request.cookies.get("aethas_user", "visiteur") # --- LECTURE EN RAM ULTRA-DIRECTE PAR LE MIDDLEWARE --- if request.url.path.startswith("/Media/"): fname = request.url.path.replace("/Media/", "", 1) chemin = os.path.join(dossier_media, fname) if os.path.exists(chemin): try: with open(chemin, "rb") as f: return Response(content=f.read(), media_type="image/png") except Exception as e: return HTMLResponse(f"Erreur : {e}", status_code=500) return HTMLResponse("Fichier introuvable", status_code=404) # EXCLUSION DE /Media POUR NE PAS BLOQUER LE RESTE if not request.url.path.startswith(("/Logo", "/login", "/Media")): if verifier_et_bloquer_vpn(ip, utilisateur): return HTMLResponse("
VPN ou Proxy strictement interdit.
", status_code=403) # Exclusion stricte du spam de logs (Silence pour /ping et /favicon) if request.url.path not in ["/ping", "/favicon.ico"]: journaliser_ip(ip, f"Accès à {request.url.path}") return await call_next(request) nettoyer_vieilles_discussions() app, rt = fast_app( hdrs=get_theme_hdrs(), secret_key="super_secret_aethas", middleware=[Middleware(SecurityMiddleware)] ) app.mount("/Logo", StaticFiles(directory="Logo"), name="Logo") app.mount("/Media", StaticFiles(directory=dossier_media), name="Media") # --- ROUTES D'AUTHENTIFICATION --- @rt('/login') def get(): return render_login_page() @rt('/login') async def post(request, session): form = await request.form() if form.get("email") == IDENTIFIANTS_ADMIN["email"] and verifier_mot_de_passe(form.get("password"), IDENTIFIANTS_ADMIN["mot_de_passe_hash"]): session['user'] = "xavier" resp = RedirectResponse('/', status_code=303) resp.set_cookie("aethas_user", "xavier", max_age=86400) return resp return render_login_page(error="Accès refusé. Identifiants incorrects.") @rt('/logout') def get(session): session.clear() resp = RedirectResponse('/login', status_code=303) resp.delete_cookie("aethas_user") return resp # --- ROUTES PROTÉGÉES --- @rt('/') def get(session, request): utilisateur = session.get('user') or request.cookies.get('aethas_user') if not utilisateur: return RedirectResponse('/login', status_code=303) session['user'] = utilisateur if 'current_chat_id' not in session or not session['current_chat_id']: session['current_chat_id'] = str(uuid.uuid4()) # Génération de la page principale existante main_page = render_main_page(session['current_chat_id'], utilisateur_nom=session['user']) # Génération des nouveaux composants paypal_modal = render_paypal_recharge_widget(10.00) # Renvoi combiné (les éléments fixed/absolute s'afficheront par-dessus) return paypal_modal, main_page @rt('/chat') async def post(request, session): utilisateur = session.get('user') or request.cookies.get('aethas_user') if not utilisateur: return HTMLResponse( "