Compare commits

..
6 Commits
Author SHA1 Message Date
xavier d712d2845d docs: mise à jour du README avec les objectifs de sécurité et d'infrastructure validés
Build and Push Docker Image / build-and-push (push) Successful in 39s
2026-10-07 01:19:41 +02:00
xavier c3b10508d9 feat: implémentation des sessions JWT avec cookies sécurisés (expiration 60 min)
Build and Push Docker Image / build-and-push (push) Successful in 1m24s
2026-10-07 01:09:55 +02:00
xavier b48853d186 fix: remplacement du canvas par une balise image pour garantir l'affichage du QR code Vue.js
Build and Push Docker Image / build-and-push (push) Successful in 38s
2026-10-07 00:56:00 +02:00
xavier afd0a88fc0 Suppression de passlib par bcrypt et modification des fichiers liés
Build and Push Docker Image / build-and-push (push) Successful in 1m14s
2026-10-07 00:44:25 +02:00
xavier 70b9bdc19f ajout email-validator
Build and Push Docker Image / build-and-push (push) Successful in 1m16s
2026-10-07 00:33:11 +02:00
xavier b1983077cb modification requirements.txt
Build and Push Docker Image / build-and-push (push) Successful in 1m17s
2026-10-07 00:25:39 +02:00
7 changed files with 234 additions and 75 deletions

No files matched your search

+17 -17
View File
@@ -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
View File
@@ -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
View File
@@ -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} !"}
+6
View File
@@ -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
+15 -45
View File
@@ -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,14 +108,11 @@
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'); width: 200,
QRCode.toCanvas(canvas, data.totp_uri, { margin: 1,
width: 200, color: { dark: "#0f172a", light: "#ffffff" }
margin: 1,
color: { dark: "#0f172a", light: "#ffffff" }
});
}); });
} catch (error) { } catch (error) {
@@ -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>
+104
View File
@@ -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
View File
@@ -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