Files
aethas-free/main.py
T
xavier b96d402ae9
Build and Push Docker Image / build-and-push (push) Successful in 2m35s
Isertion module financier
2026-08-29 07:50:32 +02:00

217 lines
8.5 KiB
Python

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)
# --- 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("<h1 style='color:red; text-align:center;'>Accès refusé</h1><p style='text-align:center;'>VPN ou Proxy strictement interdit.</p>", 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())
return render_main_page(session['current_chat_id'], utilisateur_nom=session['user'])
@rt('/chat')
async def post(request, session):
utilisateur = session.get('user') or request.cookies.get('aethas_user')
if not utilisateur:
return HTMLResponse(
"<div style='color:red; background:#300; padding:15px; border-radius:10px; margin-top:10px;'>"
"<b>❌ Session expirée ou non autorisée.</b> Veuillez rafraîchir la page (F5) pour vous reconnecter."
"</div>",
status_code=401
)
session['user'] = utilisateur
return await handle_chat_request(request, session)
@rt('/favicon.ico')
def favicon_get(): return Response(status_code=204)
@rt('/ping')
def get(): return Response(status_code=204)
@rt('/new_chat')
def get(session):
if 'user' in 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" and 'user' in session: 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()
# --- NOUVELLE ROUTE POUR LE RAG (BOUTON MANUEL) ---
@rt('/api/forcer_rag')
def post(prompt: str, reponse: str, theme: str):
"""Intercepte la demande manuelle, lance l'indexation et renvoie un badge vert."""
auto_apprendre_rag(prompt, reponse, theme)
return Span("✅ Indexé", cls="msg-action-btn", style="background-color: #4caf50; color: white; border: none; cursor: default;")
# --- NOUVELLES ROUTES POUR L'UPLOAD MODAL DU RAG ---
@rt('/admin/rag_modal')
def get(session):
if session.get('user', '') != 'xavier':
return HTMLResponse("Accès refusé.")
return render_rag_upload_modal()
@rt('/api/upload_rag')
async def post(request, session):
if session.get('user', '') != 'xavier':
return Span("❌ Accès refusé", style="color: red;")
form = await request.form()
theme = form.get("theme")
fichiers = form.getlist("fichiers_rag")
if not theme or not fichiers:
return Span("❌ Thème ou fichiers manquants", style="color: red;")
dossier_cible = f"/DATA/AppData/MULTI-IA-AETHAS38/RAG/{theme}"
os.makedirs(dossier_cible, exist_ok=True)
fichiers_sauves = 0
for f in fichiers:
if f.filename:
chemin_fichier = os.path.join(dossier_cible, f.filename)
contenu = await f.read()
with open(chemin_fichier, "wb") as out:
out.write(contenu)
fichiers_sauves += 1
if fichiers_sauves > 0:
threading.Thread(target=indexer_dossier_thematique, args=(theme,)).start()
return Span(f"✅ {fichiers_sauves} fichier(s) sauvé(s). Indexation FAISS démarrée en arrière-plan !", style="color: #4caf50;")
else:
return Span("❌ Aucun fichier valide détecté.", style="color: red;")
from modules.export_utils import generer_export_comptable
# --- NOUVELLE ROUTE POUR L'EXPORT COMPTABLE EXCEL ---
@rt('/admin/export_comptable')
def get(session):
if session.get('user', '') != 'xavier':
return HTMLResponse("Accès refusé. Droits d'administration requis.")
try:
chemin_fichier = generer_export_comptable()
if os.path.exists(chemin_fichier):
return FileResponse(
chemin_fichier,
filename=os.path.basename(chemin_fichier),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
else:
return HTMLResponse("Erreur : Impossible de générer le fichier Excel.", status_code=500)
except Exception as e:
log_event("ERROR", "FINANCE", f"Échec de l'export : {e}")
return HTMLResponse(f"Erreur système lors de l'export : {e}", status_code=500)
if __name__ == '__main__':
serve(port=5001)