Compare commits

..
3 Commits
Author SHA1 Message Date
xavier 00a92de1c4 fix: alignement de l'alias de routage FastAPI sur le dossier physique assets pour corriger l'affichage du logo
Build and Push Docker Image / build-and-push (push) Successful in 42s
2026-10-07 10:50:50 +02:00
xavier 43be6b5be6 feat: intégration de tous les fournisseurs d'IA (OpenRouter, Gemini, Groq, DeepSeek, Mistral, Cloudflare, HF) dans la base de données et l'interface d'installation
Build and Push Docker Image / build-and-push (push) Successful in 42s
2026-10-07 10:26:04 +02:00
xavier 22c640837c feat: intégration du paramétrage obligatoire SMTP et optionnel des clés API lors du setup initial
Build and Push Docker Image / build-and-push (push) Successful in 42s
2026-10-07 10:17:23 +02:00
6 changed files with 235 additions and 86 deletions

No files matched your search

+26 -8
View File
@@ -10,6 +10,7 @@ from .database import engine, Base, get_db
from .models import User, Project, Message from .models import User, Project, Message
from .auth import get_password_hash, generate_totp_secret, get_totp_uri, verify_password, verify_totp, create_access_token, verify_token from .auth import get_password_hash, generate_totp_secret, get_totp_uri, verify_password, verify_totp, create_access_token, verify_token
from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse
from .models import User, Project, Message, SystemSettings
# Création des tables dans la base de données # Création des tables dans la base de données
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
@@ -19,7 +20,9 @@ app = FastAPI(title="AETHAS38 - Orchestrateur Multi-IA")
# Configuration des fichiers statiques (images, css locaux, etc.) # Configuration des fichiers statiques (images, css locaux, etc.)
assets_path = os.path.join(os.getcwd(), "frontend", "assets") assets_path = os.path.join(os.getcwd(), "frontend", "assets")
os.makedirs(assets_path, exist_ok=True) # Crée le dossier s'il n'existe pas os.makedirs(assets_path, exist_ok=True) # Crée le dossier s'il n'existe pas
app.mount("/static", StaticFiles(directory=assets_path), name="static") app.mount("/assets", StaticFiles(directory=assets_path), name="assets")
# Définition des routes du logo dans login
def is_setup_required(db: Session) -> bool: def is_setup_required(db: Session) -> bool:
"""Vérifie si la base de données contient au moins un administrateur.""" """Vérifie si la base de données contient au moins un administrateur."""
@@ -58,17 +61,16 @@ def login_page(db: Session = Depends(get_db)):
@app.post("/api/setup") @app.post("/api/setup")
def create_admin(admin_data: AdminCreate, db: Session = Depends(get_db)): def create_admin(admin_data: AdminCreate, db: Session = Depends(get_db)):
"""Reçoit les données du frontend, crée l'admin et retourne le QR Code 2FA.""" """Reçoit les données du frontend, crée l'admin, sauvegarde la configuration et retourne le QR Code 2FA."""
if not is_setup_required(db): if not is_setup_required(db):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="L'installation a déjà été effectuée." detail="L'installation a déjà été effectuée."
) )
# Sécurisation des accès # Création du Super Admin
hashed_pw = get_password_hash(admin_data.password) hashed_pw = get_password_hash(admin_data.password)
totp_secret = generate_totp_secret() totp_secret = generate_totp_secret()
new_admin = User( new_admin = User(
email=admin_data.email, email=admin_data.email,
username=admin_data.username, username=admin_data.username,
@@ -76,16 +78,32 @@ def create_admin(admin_data: AdminCreate, db: Session = Depends(get_db)):
totp_secret=totp_secret, totp_secret=totp_secret,
is_admin=True is_admin=True
) )
db.add(new_admin) db.add(new_admin)
# Sauvegarde des paramètres système
new_settings = SystemSettings(
smtp_host=admin_data.smtp_host,
smtp_port=admin_data.smtp_port,
smtp_user=admin_data.smtp_user,
smtp_password=admin_data.smtp_password,
openrouter_api_key=admin_data.openrouter_api_key,
openrouter_management_key=admin_data.openrouter_management_key,
groq_api_key=admin_data.groq_api_key,
gemini_api_key=admin_data.gemini_api_key,
deepseek_api_key=admin_data.deepseek_api_key,
mistral_api_key=admin_data.mistral_api_key,
cloudflare_account_id=admin_data.cloudflare_account_id,
cloudflare_api_token=admin_data.cloudflare_api_token,
huggingface_api_key=admin_data.huggingface_api_key
)
db.add(new_settings)
db.commit() db.commit()
db.refresh(new_admin) db.refresh(new_admin)
# Génération de l'URI pour l'affichage du QR Code côté frontend
totp_uri = get_totp_uri(totp_secret, new_admin.username) totp_uri = get_totp_uri(totp_secret, new_admin.username)
return { return {
"message": "Administrateur créé avec succès.", "message": "Configuration terminée avec succès.",
"totp_secret": totp_secret, "totp_secret": totp_secret,
"totp_uri": totp_uri "totp_uri": totp_uri
} }
+20
View File
@@ -42,3 +42,23 @@ class Message(Base):
# Relation # Relation
project = relationship("Project", back_populates="messages") project = relationship("Project", back_populates="messages")
class SystemSettings(Base):
__tablename__ = "system_settings"
id = Column(Integer, primary_key=True, index=True)
# Serveur Mail (Obligatoire)
smtp_host = Column(String, nullable=False)
smtp_port = Column(Integer, nullable=False)
smtp_user = Column(String, nullable=False)
smtp_password = Column(String, nullable=False)
# Clés API (Optionnelles)
openrouter_api_key = Column(String, nullable=True)
openrouter_management_key = Column(String, nullable=True)
groq_api_key = Column(String, nullable=True)
gemini_api_key = Column(String, nullable=True)
deepseek_api_key = Column(String, nullable=True)
mistral_api_key = Column(String, nullable=True)
cloudflare_account_id = Column(String, nullable=True)
cloudflare_api_token = Column(String, nullable=True)
huggingface_api_key = Column(String, nullable=True)
+17 -5
View File
@@ -6,6 +6,21 @@ class AdminCreate(BaseModel):
email: EmailStr email: EmailStr
username: str username: str
password: str password: str
# Serveur Mail (Obligatoire)
smtp_host: str
smtp_port: int
smtp_user: str
smtp_password: str
# Clés API (Optionnelles)
openrouter_api_key: Optional[str] = None
openrouter_management_key: Optional[str] = None
groq_api_key: Optional[str] = None
gemini_api_key: Optional[str] = None
deepseek_api_key: Optional[str] = None
mistral_api_key: Optional[str] = None
cloudflare_account_id: Optional[str] = None
cloudflare_api_token: Optional[str] = None
huggingface_api_key: Optional[str] = None
class LoginRequest(BaseModel): class LoginRequest(BaseModel):
username: str username: str
@@ -24,10 +39,10 @@ class ProjectResponse(ProjectBase):
created_at: datetime created_at: datetime
is_pinned: bool is_pinned: bool
class Config: class Config:
from_attributes = True from_attributes = True
# --- NOUVEAU : Schémas pour les Messages --- # --- NOUVEAU : Schémas pour les Messages ---
class MessageBase(BaseModel): class MessageBase(BaseModel):
role: str role: str
content: str content: str
@@ -40,8 +55,5 @@ class MessageResponse(MessageBase):
created_at: datetime created_at: datetime
project_id: int project_id: int
class Config:
from_attributes = True
class ProjectRename(BaseModel): class ProjectRename(BaseModel):
title: str title: str
+3 -3
View File
@@ -31,7 +31,7 @@
<aside :class="['absolute top-0 left-0 h-full w-72 bg-aethas-panel border-r border-gray-700 flex flex-col transition-transform duration-300', isSidebarOpen ? 'translate-x-0' : '-translate-x-full']"> <aside :class="['absolute top-0 left-0 h-full w-72 bg-aethas-panel border-r border-gray-700 flex flex-col transition-transform duration-300', isSidebarOpen ? 'translate-x-0' : '-translate-x-full']">
<div class="p-4 border-b border-gray-700 flex items-center justify-between"> <div class="p-4 border-b border-gray-700 flex items-center justify-between">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<img src="/static/logo.png" alt="Logo" class="w-10 h-10 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0"> <img src="/assets/logo.png" alt="Logo" class="w-10 h-10 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0">
<span class="font-bold text-lg tracking-wider neon-text">AETHAS38</span> <span class="font-bold text-lg tracking-wider neon-text">AETHAS38</span>
</div> </div>
<button @click="toggleSidebar" class="md:hidden text-gray-400 hover:text-white"><i data-lucide="x" class="w-5 h-5"></i></button> <button @click="toggleSidebar" class="md:hidden text-gray-400 hover:text-white"><i data-lucide="x" class="w-5 h-5"></i></button>
@@ -95,7 +95,7 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button @click="toggleSidebar" class="text-gray-400 hover:text-white transition-colors p-1.5 rounded hover:bg-gray-700"><i data-lucide="menu" class="w-6 h-6"></i></button> <button @click="toggleSidebar" class="text-gray-400 hover:text-white transition-colors p-1.5 rounded hover:bg-gray-700"><i data-lucide="menu" class="w-6 h-6"></i></button>
<div v-show="!isSidebarOpen" class="flex items-center gap-2 transition-opacity duration-300"> <div v-show="!isSidebarOpen" class="flex items-center gap-2 transition-opacity duration-300">
<img src="/static/logo.png" alt="Logo" class="w-8 h-8 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0"> <img src="/assets/logo.png" alt="Logo" class="w-8 h-8 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0">
<span class="font-bold text-lg neon-text">AETHAS38</span> <span class="font-bold text-lg neon-text">AETHAS38</span>
</div> </div>
</div> </div>
@@ -107,7 +107,7 @@
<!-- Affichage par défaut --> <!-- Affichage par défaut -->
<div v-if="!activeProject" class="flex-1 overflow-y-auto p-4 md:p-8 flex flex-col items-center justify-center"> <div v-if="!activeProject" class="flex-1 overflow-y-auto p-4 md:p-8 flex flex-col items-center justify-center">
<img src="/static/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mb-6 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800 flex-shrink-0"> <img src="/assets/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mb-6 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800 flex-shrink-0">
<h2 class="text-2xl font-bold text-gray-200 mb-2 text-center">Comment puis-je vous aider aujourd'hui ?</h2> <h2 class="text-2xl font-bold text-gray-200 mb-2 text-center">Comment puis-je vous aider aujourd'hui ?</h2>
<p class="text-gray-500 text-sm text-center">Sélectionnez ou créez un projet pour commencer.</p> <p class="text-gray-500 text-sm text-center">Sélectionnez ou créez un projet pour commencer.</p>
</div> </div>
+146 -37
View File
@@ -3,60 +3,166 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AETHAS38 - Installation</title> <title>AETHAS38 - Installation Complète</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
tailwind.config = { tailwind.config = { darkMode: 'class', theme: { extend: { colors: { aethas: { dark: '#0f172a', panel: '#1e293b', cyan: '#06b6d4', purple: '#a855f7' } } } } }
darkMode: 'class',
theme: {
extend: { colors: { aethas: { dark: '#0f172a', panel: '#1e293b', cyan: '#06b6d4', purple: '#a855f7' } } }
}
}
</script> </script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style> <style>
body { background-color: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; } body { background-color: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; }
.glass-panel { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(10px); border: 1px solid rgba(6, 182, 212, 0.2); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); } .glass-panel { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(10px); border: 1px solid rgba(6, 182, 212, 0.2); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); }
.neon-text { background: linear-gradient(to right, #06b6d4, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .neon-text { background: linear-gradient(to right, #06b6d4, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }
</style> </style>
</head> </head>
<body class="flex items-center justify-center min-h-screen p-4 sm:p-8"> <body class="flex items-center justify-center min-h-screen p-4 sm:p-8 overflow-y-auto">
<div id="app" class="w-full max-w-md transition-all duration-500"> <div id="app" class="w-full max-w-3xl transition-all duration-500 my-8">
<div class="text-center mb-8"> <div class="text-center mb-8">
<!-- Chemin du logo corrigé --> <img src="/assets/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mx-auto mb-4 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800">
<img src="/static/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mx-auto mb-4 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800">
<h1 class="text-3xl font-bold tracking-wider neon-text">AETHAS38</h1> <h1 class="text-3xl font-bold tracking-wider neon-text">AETHAS38</h1>
<p class="text-gray-400 text-sm mt-2">Configuration de l'Orchestrateur Multi-IA</p> <p class="text-gray-400 text-sm mt-2">Configuration Initiale (Super Admin)</p>
</div> </div>
<div class="glass-panel rounded-2xl p-6 sm:p-8"> <div class="glass-panel rounded-2xl p-6 sm:p-8">
<form v-if="!setupComplete" @submit.prevent="submitSetup" class="space-y-8">
<form v-if="!setupComplete" @submit.prevent="submitSetup" class="space-y-5">
<div v-if="errorMessage" class="p-3 bg-red-900/50 border border-red-500 rounded text-red-200 text-sm text-center">{{ errorMessage }}</div> <div v-if="errorMessage" class="p-3 bg-red-900/50 border border-red-500 rounded text-red-200 text-sm text-center">{{ errorMessage }}</div>
<!-- Section 1 : Compte Admin -->
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-1">Email Administrateur</label> <h2 class="text-lg font-semibold text-white border-b border-gray-700 pb-2 mb-4 flex items-center gap-2">
<input v-model="form.email" type="email" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors" placeholder="admin@domaine.com"> <i data-lucide="user-plus" class="w-5 h-5 text-aethas-cyan"></i> Compte Administrateur
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm text-gray-300 mb-1">Nom d'utilisateur</label>
<input v-model="form.username" type="text" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2 focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan outline-none" placeholder="Admin">
</div>
<div>
<label class="block text-sm text-gray-300 mb-1">Mot de passe</label>
<input v-model="form.password" type="password" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2 focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan outline-none" placeholder="••••••••">
</div>
<div class="md:col-span-2">
<label class="block text-sm text-gray-300 mb-1">Email de récupération</label>
<input v-model="form.email" type="email" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2 focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan outline-none" placeholder="admin@domaine.com">
</div>
</div>
</div> </div>
<!-- Section 2 : Serveur Mail -->
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-1">Nom d'utilisateur</label> <div class="flex items-center justify-between border-b border-gray-700 pb-2 mb-4">
<input v-model="form.username" type="text" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors" placeholder="Admin"> <h2 class="text-lg font-semibold text-white flex items-center gap-2">
<i data-lucide="mail" class="w-5 h-5 text-aethas-cyan"></i> Serveur Mail (Obligatoire)
</h2>
<button type="button" @click="showWiki = !showWiki" class="text-xs text-aethas-cyan hover:text-white underline">Aide FAI Français</button>
</div>
<div v-if="showWiki" class="mb-4 p-4 bg-gray-800 rounded-lg border border-gray-600 text-sm text-gray-300 space-y-2">
<p><strong>Freebox :</strong> Hôte: smtp.free.fr | Port: 465 (SSL) ou 587 (TLS)</p>
<p><strong>Orange :</strong> Hôte: smtp.orange.fr | Port: 465 (SSL)</p>
<p><strong>SFR :</strong> Hôte: smtp.sfr.fr | Port: 465 (SSL)</p>
<p><strong>Bouygues :</strong> Hôte: smtp.bbox.fr | Port: 587 (TLS)</p>
<p><strong>Gmail :</strong> Hôte: smtp.gmail.com | Port: 465 (Nécessite un "Mot de passe d'application" Google)</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="md:col-span-3">
<label class="block text-sm text-gray-300 mb-1">Hôte SMTP</label>
<input v-model="form.smtp_host" type="text" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2 focus:border-aethas-cyan outline-none" placeholder="ex: smtp.free.fr">
</div>
<div>
<label class="block text-sm text-gray-300 mb-1">Port</label>
<input v-model="form.smtp_port" type="number" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2 focus:border-aethas-cyan outline-none" placeholder="465">
</div>
<div class="md:col-span-2">
<label class="block text-sm text-gray-300 mb-1">Utilisateur SMTP</label>
<input v-model="form.smtp_user" type="text" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2 focus:border-aethas-cyan outline-none">
</div>
<div class="md:col-span-2">
<label class="block text-sm text-gray-300 mb-1">Mot de passe SMTP</label>
<input v-model="form.smtp_password" type="password" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2 focus:border-aethas-cyan outline-none">
</div>
</div>
</div> </div>
<!-- Section 3 : Clés API IA -->
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-1">Mot de passe</label> <h2 class="text-lg font-semibold text-white border-b border-gray-700 pb-2 mb-4 flex items-center gap-2">
<input v-model="form.password" type="password" required class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors" placeholder="••••••••"> <i data-lucide="cpu" class="w-5 h-5 text-aethas-cyan"></i> Moteurs IA (Optionnel - modifiable plus tard)
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- OpenRouter -->
<div class="space-y-2 bg-gray-800/50 p-3 rounded-lg border border-gray-700">
<h3 class="text-sm font-bold text-white flex items-center justify-between">OpenRouter <a href="https://openrouter.ai/keys" target="_blank" class="text-aethas-cyan hover:underline text-xs"><i data-lucide="external-link" class="w-3 h-3 inline"></i> Récupérer</a></h3>
<p class="text-xs text-gray-400">Générez une clé standard pour les requêtes, et une "Management Key" pour la récupération des crédits.</p>
<input v-model="form.openrouter_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-3 py-1.5 text-sm focus:border-aethas-cyan outline-none" placeholder="API Key (sk-or-v1-...)">
<input v-model="form.openrouter_management_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-3 py-1.5 text-sm focus:border-aethas-cyan outline-none" placeholder="Management Key">
</div>
<!-- Gemini -->
<div class="space-y-2 bg-gray-800/50 p-3 rounded-lg border border-gray-700">
<h3 class="text-sm font-bold text-white flex items-center justify-between">Google Gemini <a href="https://aistudio.google.com/app/apikey" target="_blank" class="text-aethas-cyan hover:underline text-xs"><i data-lucide="external-link" class="w-3 h-3 inline"></i> Récupérer</a></h3>
<p class="text-xs text-gray-400">Créez une API Key depuis Google AI Studio (Associez un projet Google Cloud pour lever les quotas gratuits limités).</p>
<input v-model="form.gemini_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-3 py-1.5 text-sm focus:border-aethas-cyan outline-none" placeholder="AIzaSy...">
</div>
<!-- Groq -->
<div class="space-y-2 bg-gray-800/50 p-3 rounded-lg border border-gray-700">
<h3 class="text-sm font-bold text-white flex items-center justify-between">Groq <a href="https://console.groq.com/keys" target="_blank" class="text-aethas-cyan hover:underline text-xs"><i data-lucide="external-link" class="w-3 h-3 inline"></i> Récupérer</a></h3>
<p class="text-xs text-gray-400">Créez une clé d'API. L'accès aux modèles Llama est gratuit avec quotas.</p>
<input v-model="form.groq_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-3 py-1.5 text-sm focus:border-aethas-cyan outline-none" placeholder="gsk_...">
</div>
<!-- DeepSeek -->
<div class="space-y-2 bg-gray-800/50 p-3 rounded-lg border border-gray-700">
<h3 class="text-sm font-bold text-white flex items-center justify-between">DeepSeek <a href="https://platform.deepseek.com/api_keys" target="_blank" class="text-aethas-cyan hover:underline text-xs"><i data-lucide="external-link" class="w-3 h-3 inline"></i> Récupérer</a></h3>
<p class="text-xs text-gray-400">Créez une clé d'API. Parfait pour le traitement du code de AETHAS38.</p>
<input v-model="form.deepseek_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-3 py-1.5 text-sm focus:border-aethas-cyan outline-none" placeholder="sk-...">
</div>
<!-- Mistral -->
<div class="space-y-2 bg-gray-800/50 p-3 rounded-lg border border-gray-700">
<h3 class="text-sm font-bold text-white flex items-center justify-between">Mistral AI <a href="https://console.mistral.ai/api-keys/" target="_blank" class="text-aethas-cyan hover:underline text-xs"><i data-lucide="external-link" class="w-3 h-3 inline"></i> Récupérer</a></h3>
<p class="text-xs text-gray-400">Assurez-vous d'avoir enregistré une méthode de facturation pour lever la période d'essai.</p>
<input v-model="form.mistral_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-3 py-1.5 text-sm focus:border-aethas-cyan outline-none" placeholder="Clé API Mistral">
</div>
<!-- HuggingFace -->
<div class="space-y-2 bg-gray-800/50 p-3 rounded-lg border border-gray-700">
<h3 class="text-sm font-bold text-white flex items-center justify-between">HuggingFace <a href="https://huggingface.co/settings/tokens" target="_blank" class="text-aethas-cyan hover:underline text-xs"><i data-lucide="external-link" class="w-3 h-3 inline"></i> Récupérer</a></h3>
<p class="text-xs text-gray-400">Générez un Access Token (Fine-grained) avec les droits de lecture et génération (Inference).</p>
<input v-model="form.huggingface_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-3 py-1.5 text-sm focus:border-aethas-cyan outline-none" placeholder="hf_...">
</div>
<!-- Cloudflare -->
<div class="space-y-2 bg-gray-800/50 p-3 rounded-lg border border-gray-700 md:col-span-2">
<h3 class="text-sm font-bold text-white flex items-center justify-between">Cloudflare Workers AI <a href="https://dash.cloudflare.com/profile/api-tokens" target="_blank" class="text-aethas-cyan hover:underline text-xs"><i data-lucide="external-link" class="w-3 h-3 inline"></i> Récupérer</a></h3>
<p class="text-xs text-gray-400">Créez un "Custom Token" avec l'autorisation "Workers AI : Read/Write". Votre ID de compte se trouve sur la page d'accueil Workers.</p>
<div class="flex gap-2">
<input v-model="form.cloudflare_account_id" type="text" class="w-1/3 bg-aethas-dark border border-gray-600 rounded-lg px-3 py-1.5 text-sm focus:border-aethas-cyan outline-none" placeholder="Account ID">
<input v-model="form.cloudflare_api_token" type="password" class="w-2/3 bg-aethas-dark border border-gray-600 rounded-lg px-3 py-1.5 text-sm focus:border-aethas-cyan outline-none" placeholder="API Token">
</div>
</div>
</div>
</div> </div>
<button type="submit" :disabled="isSubmitting" class="w-full bg-gradient-to-r from-aethas-cyan to-aethas-purple hover:opacity-90 text-white font-semibold py-3 px-4 rounded-lg transition-all shadow-[0_0_10px_rgba(168,85,247,0.4)] flex justify-center items-center mt-6">
<span v-if="isSubmitting">Création en cours...</span> <button type="submit" :disabled="isSubmitting" class="w-full bg-gradient-to-r from-aethas-cyan to-aethas-purple hover:opacity-90 text-white font-semibold py-3 px-4 rounded-lg transition-all shadow-[0_0_10px_rgba(168,85,247,0.4)] flex justify-center items-center mt-8">
<span v-else>Initialiser le système</span> <span v-if="isSubmitting">Initialisation et Chiffrement...</span>
<span v-else>Terminer l'installation</span>
</button> </button>
</form> </form>
<div v-else class="text-center space-y-6 animate-fade-in"> <div v-else class="text-center space-y-6 animate-fade-in">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-900/50 text-green-400 mb-2"> <div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-900/50 text-green-400 mb-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg> <i data-lucide="check" class="w-6 h-6"></i>
</div> </div>
<h2 class="text-xl font-bold text-white">Compte créé avec succès</h2> <h2 class="text-xl font-bold text-white">Plateforme Opérationnelle</h2>
<div class="bg-white p-4 rounded-xl inline-block mx-auto min-w-[200px] min-h-[200px] flex items-center justify-center"> <div class="bg-white p-4 rounded-xl inline-block mx-auto min-w-[200px] min-h-[200px] flex items-center justify-center">
<img v-if="qrCodeUrl" :src="qrCodeUrl" alt="QR Code 2FA" class="w-full h-full object-contain"> <img v-if="qrCodeUrl" :src="qrCodeUrl" alt="QR Code 2FA" class="w-full h-full object-contain">
@@ -65,7 +171,7 @@
<div class="text-sm text-gray-300 space-y-2"> <div class="text-sm text-gray-300 space-y-2">
<p>Scannez ce QR Code avec votre application 2FA (Keepassium, Google Authenticator...).</p> <p>Scannez ce QR Code avec votre application 2FA (Keepassium, Google Authenticator...).</p>
<p class="text-xs text-gray-500 break-all bg-aethas-dark p-2 rounded">Secret manuel : {{ totpSecret }}</p> <p class="text-xs text-gray-500 break-all bg-aethas-dark p-2 rounded border border-gray-600">Secret manuel : {{ totpSecret }}</p>
</div> </div>
<a href="/login" class="block w-full border border-aethas-cyan text-aethas-cyan hover:bg-aethas-cyan hover:text-aethas-dark font-semibold py-3 px-4 rounded-lg transition-colors mt-6"> <a href="/login" class="block w-full border border-aethas-cyan text-aethas-cyan hover:bg-aethas-cyan hover:text-aethas-dark font-semibold py-3 px-4 rounded-lg transition-colors mt-6">
@@ -76,16 +182,25 @@
</div> </div>
<script> <script>
const { createApp, ref, reactive } = Vue; const { createApp, ref, reactive, onMounted } = Vue;
createApp({ createApp({
setup() { setup() {
const form = reactive({ email: '', username: '', password: '' }); const form = reactive({
email: '', username: '', password: '',
smtp_host: '', smtp_port: 465, smtp_user: '', smtp_password: '',
openrouter_api_key: '', openrouter_management_key: '',
groq_api_key: '', gemini_api_key: '', deepseek_api_key: '', mistral_api_key: '',
cloudflare_account_id: '', cloudflare_api_token: '', huggingface_api_key: ''
});
const isSubmitting = ref(false); const isSubmitting = ref(false);
const setupComplete = ref(false); const setupComplete = ref(false);
const errorMessage = ref(''); const errorMessage = ref('');
const totpSecret = ref(''); const totpSecret = ref('');
const qrCodeUrl = ref(''); const qrCodeUrl = ref('');
const showWiki = ref(false);
onMounted(() => { lucide.createIcons(); });
const submitSetup = async () => { const submitSetup = async () => {
isSubmitting.value = true; isSubmitting.value = true;
@@ -97,27 +212,21 @@
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form) body: JSON.stringify(form)
}); });
const data = await response.json(); const data = await response.json();
if (!response.ok) throw new Error(data.detail || "Erreur lors de l'installation");
if (!response.ok) {
throw new Error(data.detail || "Erreur lors de l'installation");
}
totpSecret.value = data.totp_secret; totpSecret.value = data.totp_secret;
setupComplete.value = true; setupComplete.value = true;
// CORRECTION ICI : Génération par API externe ultra fiable (plus besoin de librairie)
qrCodeUrl.value = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(data.totp_uri)}&color=0f172a&bgcolor=ffffff`; qrCodeUrl.value = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(data.totp_uri)}&color=0f172a&bgcolor=ffffff`;
setTimeout(() => lucide.createIcons(), 100);
} catch (error) { } catch (error) {
errorMessage.value = error.message; errorMessage.value = error.message;
} finally { } finally {
isSubmitting.value = false; isSubmitting.value = false;
} }
}; };
return { form, isSubmitting, setupComplete, errorMessage, totpSecret, qrCodeUrl, showWiki, submitSetup };
return { form, isSubmitting, setupComplete, errorMessage, totpSecret, qrCodeUrl, submitSetup };
} }
}).mount('#app'); }).mount('#app');
</script> </script>
+23 -33
View File
@@ -6,26 +6,12 @@
<title>AETHAS38 - Connexion</title> <title>AETHAS38 - Connexion</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
tailwind.config = { tailwind.config = { darkMode: 'class', theme: { extend: { colors: { aethas: { dark: '#0f172a', panel: '#1e293b', cyan: '#06b6d4', purple: '#a855f7' } } } } }
darkMode: 'class',
theme: {
extend: {
colors: {
aethas: {
dark: '#0f172a', panel: '#1e293b', cyan: '#06b6d4', purple: '#a855f7'
}
}
}
}
}
</script> </script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style> <style>
body { background-color: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; } body { background-color: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; }
.glass-panel { .glass-panel { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(10px); border: 1px solid rgba(6, 182, 212, 0.2); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); }
background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(10px);
border: 1px solid rgba(6, 182, 212, 0.2); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
}
.neon-text { background: linear-gradient(to right, #06b6d4, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .neon-text { background: linear-gradient(to right, #06b6d4, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
</style> </style>
</head> </head>
@@ -33,16 +19,14 @@
<div id="app" class="w-full max-w-md transition-all duration-500"> <div id="app" class="w-full max-w-md transition-all duration-500">
<div class="text-center mb-8"> <div class="text-center mb-8">
<img src="https://via.placeholder.com/150x150/0f172a/06b6d4?text=Logo+A38" alt="AETHAS38 Logo" class="w-24 h-24 mx-auto mb-4 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)]"> <img src="/assets/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mx-auto mb-4 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800">
<h1 class="text-3xl font-bold tracking-wider neon-text">AETHAS38</h1> <h1 class="text-3xl font-bold tracking-wider neon-text">AETHAS38</h1>
<p class="text-gray-400 text-sm mt-2">Authentification Sécurisée</p> <p class="text-gray-400 text-sm mt-2">Authentification Sécurisée</p>
</div> </div>
<div class="glass-panel rounded-2xl p-6 sm:p-8"> <div class="glass-panel rounded-2xl p-6 sm:p-8">
<form @submit.prevent="submitLogin" class="space-y-5"> <form @submit.prevent="submitLogin" class="space-y-5">
<div v-if="errorMessage" class="p-3 bg-red-900/50 border border-red-500 rounded text-red-200 text-sm text-center"> <div v-if="errorMessage" class="p-3 bg-red-900/50 border border-red-500 rounded text-red-200 text-sm text-center">{{ errorMessage }}</div>
{{ errorMessage }}
</div>
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-1">Nom d'utilisateur</label> <label class="block text-sm font-medium text-gray-300 mb-1">Nom d'utilisateur</label>
@@ -56,11 +40,11 @@
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-1">Code 2FA (6 chiffres)</label> <label class="block text-sm font-medium text-gray-300 mb-1">Code 2FA (6 chiffres)</label>
<input v-model="form.totp_code" type="text" inputmode="numeric" pattern="[0-9]{6}" required maxlength="6" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors text-center tracking-widest text-lg" placeholder="123456"> <input v-model="form.totp_code" type="text" required pattern="[0-9]{6}" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors text-center tracking-widest text-lg" placeholder="123456">
</div> </div>
<button type="submit" :disabled="isSubmitting" class="w-full bg-gradient-to-r from-aethas-cyan to-aethas-purple hover:opacity-90 text-white font-semibold py-3 px-4 rounded-lg transition-all shadow-[0_0_10px_rgba(168,85,247,0.4)] flex justify-center items-center mt-6"> <button type="submit" :disabled="isLoading" class="w-full bg-gradient-to-r from-aethas-cyan to-aethas-purple hover:opacity-90 text-white font-semibold py-3 px-4 rounded-lg transition-all shadow-[0_0_10px_rgba(168,85,247,0.4)] flex justify-center items-center mt-6">
<span v-if="isSubmitting">Vérification...</span> <span v-if="isLoading">Connexion...</span>
<span v-else>Se connecter</span> <span v-else>Se connecter</span>
</button> </button>
</form> </form>
@@ -68,37 +52,43 @@
</div> </div>
<script> <script>
const { createApp, reactive, ref } = Vue; const { createApp, ref, reactive } = Vue;
createApp({ createApp({
setup() { setup() {
const form = reactive({ username: '', password: '', totp_code: '' }); const form = reactive({ username: '', password: '', totp_code: '' });
const isSubmitting = ref(false); const isLoading = ref(false);
const errorMessage = ref(''); const errorMessage = ref('');
const submitLogin = async () => { const submitLogin = async () => {
isSubmitting.value = true; isLoading.value = true;
errorMessage.value = ''; errorMessage.value = '';
try { try {
const response = await fetch('/api/login', { const response = await fetch('/api/login', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form) body: JSON.stringify(form)
}); });
const data = await response.json();
if (!response.ok) throw new Error(data.detail || "Erreur de connexion");
// Redirection vers le tableau de bord une fois connecté const data = await response.json();
window.location.href = "/dashboard";
if (!response.ok) {
throw new Error(data.detail || "Erreur de connexion");
}
window.location.href = '/dashboard';
} catch (error) { } catch (error) {
errorMessage.value = error.message; errorMessage.value = error.message;
} finally { } finally {
isSubmitting.value = false; isLoading.value = false;
} }
}; };
return { form, isSubmitting, errorMessage, submitLogin };
return { form, isLoading, errorMessage, submitLogin };
} }
}).mount('#app'); }).mount('#app');
</script> </script>
</body> </body>
</html> </html>