Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d712d2845d | ||
|
|
c3b10508d9 | ||
|
|
b48853d186 | ||
|
|
afd0a88fc0 | ||
|
|
70b9bdc19f | ||
|
|
b1983077cb |
No files matched your search
@@ -1,49 +1,49 @@
|
|||||||
# AETHAS38 - Orchestrateur Multi-IA 🤖
|
# AETHAS38 - Orchestrateur Multi-IA 🤖
|
||||||
|
|
||||||
Ce projet est un laboratoire d'intelligence artificielle personnel[cite: 5]. Il agit comme un orchestrateur avancé (Rédacteur ➔ Travailleurs ➔ Concaténeur) permettant de solliciter jusqu'à 5 modèles en parallèle via une interface unifiée, moderne et hautement sécurisée[cite: 5].
|
Ce projet est un laboratoire d'intelligence artificielle personnel. Il agit comme un orchestrateur avancé (Rédacteur ➔ Travailleurs ➔ Concaténeur) permettant de solliciter jusqu'à 5 modèles en parallèle via une interface unifiée, moderne et hautement sécurisée.
|
||||||
|
|
||||||
L'interface visuelle est inspirée de l'ergonomie de Gemini, avec le logo stylisé A38 (cerveau en réseau neuronal néon) comme charte graphique centrale[cite: 6].
|
L'interface visuelle est inspirée de l'ergonomie de Gemini, avec le logo stylisé A38 (cerveau en réseau neuronal néon) comme charte graphique centrale.
|
||||||
|
|
||||||
## 🏗️ Stack Technique & Architecture
|
## 🏗️ Stack Technique & Architecture
|
||||||
- **Backend :** FastAPI (Python) modulaire (une fonction = un module).
|
- **Backend :** FastAPI (Python) modulaire (une fonction = un module).
|
||||||
- **Frontend :** Vue.js + Tailwind CSS (Responsive / Mobile-first).
|
- **Frontend :** Vue.js + Tailwind CSS (Responsive / Mobile-first).
|
||||||
- **Base de Données :** PostgreSQL (gestion des utilisateurs, historiques, suivi financier).
|
- **Base de Données :** PostgreSQL (gestion des utilisateurs, historiques, suivi financier).
|
||||||
- **Traitement Documentaire :** LibreOffice Headless (génération/conversion Excel, Word, PDF).
|
- **Traitement Documentaire :** LibreOffice Headless (génération/conversion Excel, Word, PDF).
|
||||||
- **Déploiement :** Docker, géré via Dockge et Gitea, sécurisé sur grappe RAID[cite: 5].
|
- **Déploiement :** Docker, géré via Dockge et Gitea, sécurisé sur grappe RAID.
|
||||||
|
|
||||||
## 📌 Règles de Versioning (SemVer)
|
## 📌 Règles de Versioning (SemVer)
|
||||||
Le développement suit un versioning strict (format `vX.Y.Z`) synchronisé avec les tags Gitea et GitHub, affiché dynamiquement sur l'interface[cite: 5] :
|
Le développement suit un versioning strict (format `vX.Y.Z`) synchronisé avec les tags Gitea et GitHub, affiché dynamiquement sur l'interface :
|
||||||
- **Majeur (X) :** Changement profond d'architecture ou refonte globale[cite: 5].
|
- **Majeur (X) :** Changement profond d'architecture ou refonte globale.
|
||||||
- **Mineur (Y) :** Validation et ajout d'une nouvelle fonctionnalité de la roadmap[cite: 5].
|
- **Mineur (Y) :** Validation et ajout d'une nouvelle fonctionnalité de la roadmap.
|
||||||
- **Patch (Z) :** Corrections de bugs, correctifs de sécurité ou ajustements visuels mineurs[cite: 5].
|
- **Patch (Z) :** Corrections de bugs, correctifs de sécurité ou ajustements visuels mineurs.
|
||||||
|
|
||||||
## 🚀 ROADMAP & CAHIER DES CHARGES
|
## 🚀 ROADMAP & CAHIER DES CHARGES
|
||||||
|
|
||||||
### Sécurité & Infrastructure
|
### Sécurité & Infrastructure
|
||||||
- [ ] Installation stricte via Dockge et Gitea (création automatique des dossiers inclus).
|
- [x] Installation stricte via Dockge et Gitea (création automatique des dossiers inclus).
|
||||||
- [ ] Sécurisation de la totalité du projet (fichiers, BDD PostgreSQL) sur le stockage RAID.
|
- [x] Sécurisation de la totalité du projet (fichiers, BDD PostgreSQL) sur le stockage RAID.
|
||||||
- [ ] Mots de passe cryptés, protection anti-brute force et anti-DDOS.
|
- [x] Mots de passe cryptés (bcrypt), protection anti-brute force et anti-DDOS (à compléter pour le DDOS).
|
||||||
- [ ] Authentification 2FA obligatoire (Keepassium, Authenticator, etc.) sans accès SSH pour les utilisateurs.
|
- [x] Authentification 2FA obligatoire (Keepassium, Authenticator, etc.) sans accès SSH pour les utilisateurs.
|
||||||
- [ ] Déconnexion automatique (timer invisible) après 60 minutes d'inactivité.
|
- [x] Déconnexion automatique (timer invisible / session JWT) après 60 minutes d'inactivité.
|
||||||
- [ ] Procédure de réinitialisation/modification du mot de passe (Admin et Utilisateurs).
|
- [ ] Procédure de réinitialisation/modification du mot de passe (Admin et Utilisateurs).
|
||||||
- [ ] Configuration de GitHub Actions pour la création automatique des paquets Docker.
|
- [x] Configuration de GitHub Actions / Gitea Actions pour la création automatique des paquets Docker.
|
||||||
|
|
||||||
### Administration & Gestion des Utilisateurs
|
### Administration & Gestion des Utilisateurs
|
||||||
- [ ] Page d'administration complète : changement des clés, ajout de fournisseurs, configuration et test du serveur mail.
|
- [ ] Page d'administration complète : changement des clés, ajout de fournisseurs, configuration et test du serveur mail.
|
||||||
- [ ] Assistant de première installation (création du compte admin complet), puis auto-destruction du script.
|
- [x] Assistant de première installation (création du compte admin complet), puis auto-destruction / blocage du script.
|
||||||
- [ ] Système d'invitation par mail : création de compte avec génération de QR Code 2FA à la première connexion.
|
- [ ] Système d'invitation par mail : création de compte avec génération de QR Code 2FA à la première connexion.
|
||||||
- [ ] Vérification du budget API toutes les 15 minutes et actualisation après chaque requête.
|
- [ ] Vérification du budget API toutes les 15 minutes et actualisation après chaque requête.
|
||||||
- [ ] Suivi financier détaillé par fournisseur d'IA.
|
- [ ] Suivi financier détaillé par fournisseur d'IA.
|
||||||
|
|
||||||
### Expérience Utilisateur (UI/UX)
|
### Expérience Utilisateur (UI/UX)
|
||||||
- [ ] Thème moderne (Gemini-like) et 100% compatible smartphones.
|
- [~] Thème moderne (Gemini-like) et 100% compatible smartphones (Setup et Login terminés).
|
||||||
- [ ] Refonte de la gestion des projets : séparation Épinglés (en haut) / Non Épinglés, avec le dernier utilisé en tête de liste.
|
- [ ] Refonte de la gestion des projets : séparation Épinglés (en haut) / Non Épinglés, avec le dernier utilisé en tête de liste.
|
||||||
- [ ] Plus de création automatique de nouveau projet à l'ouverture.
|
- [ ] Plus de création automatique de nouveau projet à l'ouverture.
|
||||||
- [ ] Pré-filtre et classement automatique des modèles par catégorie (Texte/Code, Photo/Image, Vidéo, Son) avec l'aide de l'API Gemini lors de l'import.
|
- [ ] Pré-filtre et classement automatique des modèles par catégorie (Texte/Code, Photo/Image, Vidéo, Son) avec l'aide de l'API Gemini lors de l'import.
|
||||||
- [ ] Intégration permanente du logo AETHAS38 (hébergé sur Drive) dans l'interface.
|
- [x] Intégration permanente du logo AETHAS38 dans l'interface (Setup / Login).
|
||||||
|
|
||||||
### Moteur IA & Fonctionnalités d'Export
|
### Moteur IA & Fonctionnalités d'Export
|
||||||
- [ ] Récupération dynamique de la liste des modèles directement auprès des 9 fournisseurs par défaut (OpenRouter, Groq, Gemini, DeepSeek, Mistral, Cloudflare, HuggingFace, etc.).
|
- [ ] Récupération dynamique de la liste des modèles directement auprès des fournisseurs par défaut (OpenRouter, Groq, Gemini, DeepSeek, Mistral, Cloudflare, HuggingFace, etc.).
|
||||||
- [ ] Pipeline multi-agents : 1 Rédacteur de prompt, jusqu'à 5 Travailleurs en parallèle, 1 Concaténeur final.
|
- [ ] Pipeline multi-agents : 1 Rédacteur de prompt, jusqu'à 5 Travailleurs en parallèle, 1 Concaténeur final.
|
||||||
- [ ] Prise en charge de modèles IA pour le traitement de texte, la génération d'images, la synthèse vocale, la musique (calage sur vidéo) et la vidéo.
|
- [ ] Prise en charge de modèles IA pour le traitement de texte, la génération d'images, la synthèse vocale, la musique (calage sur vidéo) et la vidéo.
|
||||||
- [ ] Téléchargement des prompts, des réponses, et de l'historique complet en PDF, TXT, MD ou JSON.
|
- [ ] Téléchargement des prompts, des réponses, et de l'historique complet en PDF, TXT, MD ou JSON.
|
||||||
|
|||||||
+31
-7
@@ -1,16 +1,14 @@
|
|||||||
from passlib.context import CryptContext
|
import bcrypt
|
||||||
import pyotp
|
import pyotp
|
||||||
|
|
||||||
# Configuration du hachage (bcrypt)
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
|
|
||||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
"""Vérifie si le mot de passe en clair correspond au hash."""
|
"""Vérifie si le mot de passe en clair correspond au hash."""
|
||||||
return pwd_context.verify(plain_password, hashed_password)
|
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
|
||||||
|
|
||||||
def get_password_hash(password: str) -> str:
|
def get_password_hash(password: str) -> str:
|
||||||
"""Génère le hash d'un mot de passe."""
|
"""Génère le hash sécurisé d'un mot de passe."""
|
||||||
return pwd_context.hash(password)
|
salt = bcrypt.gensalt()
|
||||||
|
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
|
||||||
|
|
||||||
def generate_totp_secret() -> str:
|
def generate_totp_secret() -> str:
|
||||||
"""Génère un secret aléatoire pour l'application 2FA (Authenticator, Keepassium)."""
|
"""Génère un secret aléatoire pour l'application 2FA (Authenticator, Keepassium)."""
|
||||||
@@ -24,3 +22,29 @@ def verify_totp(secret: str, code: str) -> bool:
|
|||||||
"""Vérifie le code à 6 chiffres soumis par l'utilisateur."""
|
"""Vérifie le code à 6 chiffres soumis par l'utilisateur."""
|
||||||
totp = pyotp.TOTP(secret)
|
totp = pyotp.TOTP(secret)
|
||||||
return totp.verify(code)
|
return totp.verify(code)
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Dans l'idéal, cette clé secrète sera à placer dans votre fichier .env plus tard
|
||||||
|
SECRET_KEY = os.getenv("SECRET_KEY", "`*\$Tu*CCc5hglT$mX'HykxERDljBz")
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
||||||
|
|
||||||
|
def create_access_token(data: dict):
|
||||||
|
"""Crée un jeton JWT valable 60 minutes."""
|
||||||
|
to_encode = data.copy()
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
|
||||||
|
def verify_token(token: str):
|
||||||
|
"""Vérifie la validité du jeton JWT."""
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
return payload
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
return None # Le jeton a expiré (60 min écoulées)
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
return None # Le jeton est invalide
|
||||||
+57
-4
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import FastAPI, Depends, HTTPException, status
|
from fastapi import FastAPI, Depends, HTTPException, status, Request, Response
|
||||||
from fastapi.responses import RedirectResponse, FileResponse
|
from fastapi.responses import RedirectResponse, FileResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
import os
|
import os
|
||||||
@@ -6,8 +6,8 @@ import os
|
|||||||
# Importation de nos modules locaux
|
# Importation de nos modules locaux
|
||||||
from .database import engine, Base, get_db
|
from .database import engine, Base, get_db
|
||||||
from .models import User
|
from .models import User
|
||||||
from .auth import get_password_hash, generate_totp_secret, get_totp_uri
|
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
|
from .schemas import AdminCreate, LoginRequest
|
||||||
|
|
||||||
# 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)
|
||||||
@@ -43,7 +43,12 @@ def login_page(db: Session = Depends(get_db)):
|
|||||||
"""Affiche la page de connexion sécurisée."""
|
"""Affiche la page de connexion sécurisée."""
|
||||||
if is_setup_required(db):
|
if is_setup_required(db):
|
||||||
return RedirectResponse(url="/setup")
|
return RedirectResponse(url="/setup")
|
||||||
return {"message": "Page de connexion (Interface Vue.js à venir)."}
|
|
||||||
|
frontend_path = os.path.join(os.getcwd(), "frontend", "login.html")
|
||||||
|
if not os.path.exists(frontend_path):
|
||||||
|
raise HTTPException(status_code=404, detail="Interface de connexion introuvable.")
|
||||||
|
|
||||||
|
return FileResponse(frontend_path)
|
||||||
|
|
||||||
@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)):
|
||||||
@@ -78,3 +83,51 @@ def create_admin(admin_data: AdminCreate, db: Session = Depends(get_db)):
|
|||||||
"totp_secret": totp_secret,
|
"totp_secret": totp_secret,
|
||||||
"totp_uri": totp_uri
|
"totp_uri": totp_uri
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@app.post("/api/login")
|
||||||
|
def login(login_data: LoginRequest, response: Response, db: Session = Depends(get_db)):
|
||||||
|
"""Vérifie les identifiants et crée une session de 60 minutes."""
|
||||||
|
user = db.query(User).filter(User.username == login_data.username).first()
|
||||||
|
|
||||||
|
# Vérification de l'utilisateur et du mot de passe
|
||||||
|
if not user or not verify_password(login_data.password, user.hashed_password):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Nom d'utilisateur ou mot de passe incorrect."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Vérification du code 2FA
|
||||||
|
if not verify_totp(user.totp_secret, login_data.totp_code):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Code 2FA invalide."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Création du jeton de session
|
||||||
|
access_token = create_access_token(data={"sub": user.username})
|
||||||
|
|
||||||
|
# Injection du jeton dans un cookie sécurisé (invisible pour le JavaScript côté client)
|
||||||
|
response.set_cookie(
|
||||||
|
key="session_token",
|
||||||
|
value=access_token,
|
||||||
|
httponly=True,
|
||||||
|
max_age=3600, # 3600 secondes = 60 minutes
|
||||||
|
samesite="lax"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"message": "Connexion réussie"}
|
||||||
|
|
||||||
|
@app.get("/dashboard")
|
||||||
|
def dashboard(request: Request):
|
||||||
|
"""Route protégée : nécessite un cookie de session valide."""
|
||||||
|
token = request.cookies.get("session_token")
|
||||||
|
if not token:
|
||||||
|
return RedirectResponse(url="/login")
|
||||||
|
|
||||||
|
payload = verify_token(token)
|
||||||
|
if not payload:
|
||||||
|
# Si le token a expiré (60 min) ou est invalide, retour au login
|
||||||
|
return RedirectResponse(url="/login")
|
||||||
|
|
||||||
|
username = payload.get("sub")
|
||||||
|
return {"message": f"Bienvenue sur le tableau de bord sécurisé, {username} !"}
|
||||||
@@ -4,3 +4,9 @@ class AdminCreate(BaseModel):
|
|||||||
email: EmailStr
|
email: EmailStr
|
||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
# NOUVEAU : Schéma pour la connexion
|
||||||
|
class LoginRequest(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
totp_code: str
|
||||||
+12
-42
@@ -4,91 +4,64 @@
|
|||||||
<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</title>
|
||||||
<!-- Tailwind CSS -->
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script>
|
<script>
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: { colors: { aethas: { dark: '#0f172a', panel: '#1e293b', cyan: '#06b6d4', purple: '#a855f7' } } }
|
||||||
colors: {
|
|
||||||
aethas: {
|
|
||||||
dark: '#0f172a',
|
|
||||||
panel: '#1e293b',
|
|
||||||
cyan: '#06b6d4',
|
|
||||||
purple: '#a855f7'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<!-- Vue.js 3 -->
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
<!-- QRCode.js pour générer le flashcode 2FA -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.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>
|
||||||
<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">
|
||||||
|
|
||||||
<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">
|
||||||
|
|
||||||
<!-- En-tête / Logo -->
|
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
<!-- Remplacer le src par le lien direct de votre image hébergée sur Google Drive -->
|
|
||||||
<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="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)]">
|
||||||
<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 de l'Orchestrateur Multi-IA</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Panneau principal -->
|
|
||||||
<div class="glass-panel rounded-2xl p-6 sm:p-8">
|
<div class="glass-panel rounded-2xl p-6 sm:p-8">
|
||||||
|
|
||||||
<!-- Étape 1 : Formulaire -->
|
|
||||||
<form v-if="!setupComplete" @submit.prevent="submitSetup" class="space-y-5">
|
<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">
|
<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">Email Administrateur</label>
|
<label class="block text-sm font-medium text-gray-300 mb-1">Email Administrateur</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.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors" placeholder="admin@domaine.com">
|
<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">
|
||||||
</div>
|
</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>
|
||||||
<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">
|
<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">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-1">Mot de passe</label>
|
<label class="block text-sm font-medium 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.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors" placeholder="••••••••">
|
<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="••••••••">
|
||||||
</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="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>
|
<span v-if="isSubmitting">Création en cours...</span>
|
||||||
<span v-else>Initialiser le système</span>
|
<span v-else>Initialiser le système</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Étape 2 : Validation 2FA -->
|
|
||||||
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-xl font-bold text-white">Compte créé avec succès</h2>
|
<h2 class="text-xl font-bold text-white">Compte créé avec succès</h2>
|
||||||
|
|
||||||
<div class="bg-white p-4 rounded-xl inline-block mx-auto">
|
<!-- CORRECTION ICI : Utilisation d'une balise <img> avec rendu direct -->
|
||||||
<canvas id="qrcode"></canvas>
|
<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">
|
||||||
|
<span v-else class="text-gray-500 text-sm">Génération...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-sm text-gray-300 space-y-2">
|
<div class="text-sm text-gray-300 space-y-2">
|
||||||
@@ -104,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const { createApp, ref, reactive, nextTick } = Vue;
|
const { createApp, ref, reactive } = Vue;
|
||||||
|
|
||||||
createApp({
|
createApp({
|
||||||
setup() {
|
setup() {
|
||||||
@@ -113,6 +86,7 @@
|
|||||||
const setupComplete = ref(false);
|
const setupComplete = ref(false);
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
const totpSecret = ref('');
|
const totpSecret = ref('');
|
||||||
|
const qrCodeUrl = ref('');
|
||||||
|
|
||||||
const submitSetup = async () => {
|
const submitSetup = async () => {
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
@@ -134,15 +108,12 @@
|
|||||||
totpSecret.value = data.totp_secret;
|
totpSecret.value = data.totp_secret;
|
||||||
setupComplete.value = true;
|
setupComplete.value = true;
|
||||||
|
|
||||||
// Génération du QR code sur le canvas après le rendu Vue
|
// CORRECTION ICI : Génération d'une URL d'image (Data URL) au lieu du Canvas
|
||||||
nextTick(() => {
|
qrCodeUrl.value = await QRCode.toDataURL(data.totp_uri, {
|
||||||
const canvas = document.getElementById('qrcode');
|
|
||||||
QRCode.toCanvas(canvas, data.totp_uri, {
|
|
||||||
width: 200,
|
width: 200,
|
||||||
margin: 1,
|
margin: 1,
|
||||||
color: { dark: "#0f172a", light: "#ffffff" }
|
color: { dark: "#0f172a", light: "#ffffff" }
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error.message;
|
errorMessage.value = error.message;
|
||||||
@@ -151,10 +122,9 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { form, isSubmitting, setupComplete, errorMessage, totpSecret, submitSetup };
|
return { form, isSubmitting, setupComplete, errorMessage, totpSecret, qrCodeUrl, submitSetup };
|
||||||
}
|
}
|
||||||
}).mount('#app');
|
}).mount('#app');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AETHAS38 - Connexion</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
aethas: {
|
||||||
|
dark: '#0f172a', panel: '#1e293b', cyan: '#06b6d4', purple: '#a855f7'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
|
<style>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
.neon-text { background: linear-gradient(to right, #06b6d4, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex items-center justify-center min-h-screen p-4 sm:p-8">
|
||||||
|
|
||||||
|
<div id="app" class="w-full max-w-md transition-all duration-500">
|
||||||
|
<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)]">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-panel rounded-2xl p-6 sm:p-8">
|
||||||
|
<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">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium 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.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors" placeholder="Admin">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium 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.5 focus:outline-none focus:border-aethas-cyan focus:ring-1 focus:ring-aethas-cyan transition-colors" placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<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">
|
||||||
|
</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">Vérification...</span>
|
||||||
|
<span v-else>Se connecter</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const { createApp, reactive, ref } = Vue;
|
||||||
|
createApp({
|
||||||
|
setup() {
|
||||||
|
const form = reactive({ username: '', password: '', totp_code: '' });
|
||||||
|
const isSubmitting = ref(false);
|
||||||
|
const errorMessage = ref('');
|
||||||
|
|
||||||
|
const submitLogin = async () => {
|
||||||
|
isSubmitting.value = true;
|
||||||
|
errorMessage.value = '';
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
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é
|
||||||
|
window.location.href = "/dashboard";
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = error.message;
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { form, isSubmitting, errorMessage, submitLogin };
|
||||||
|
}
|
||||||
|
}).mount('#app');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+4
-2
@@ -1,11 +1,13 @@
|
|||||||
fastapi
|
fastapi
|
||||||
uvicorn
|
uvicorn
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
psycopg2-binary
|
psycopg[binary]
|
||||||
requests
|
requests
|
||||||
pyotp
|
pyotp
|
||||||
qrcode
|
qrcode
|
||||||
psutil
|
psutil
|
||||||
python-dotenv
|
python-dotenv
|
||||||
python-multipart
|
python-multipart
|
||||||
passlib[bcrypt]
|
bcrypt
|
||||||
|
email-validator
|
||||||
|
PyJWT
|
||||||
Reference in new issue
Block a user