Public Access
340 lines
15 KiB
Python
340 lines
15 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)
|
|
|
|
# --- 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("<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())
|
|
|
|
# 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
|
|
nav_sidebar = render_sidebar_menu()
|
|
paypal_modal = render_paypal_recharge_widget(10.00)
|
|
|
|
# Renvoi combiné (les éléments fixed/absolute s'afficheront par-dessus)
|
|
return nav_sidebar, 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(
|
|
"<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 WEBHOOK PAYPAL ---
|
|
@rt('/api/paypal/success')
|
|
async def post(request):
|
|
data = await request.json()
|
|
order_id = data.get("orderID")
|
|
log_event("INFO", "FINANCE", f"Recharge PayPal validée - OrderID: {order_id}")
|
|
return {"status": "success"}
|
|
|
|
# --- 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) |