import React, { useState } from 'react'; import { pb } from '../../config/pocketbase'; import * as OTPAuth from 'otpauth'; import { QRCodeSVG } from 'qrcode.react'; interface LoginProps { onLoginSuccess: () => void; } export default function Login({ onLoginSuccess }: LoginProps) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [mfaCode, setMfaCode] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const [step, setStep] = useState<1 | 2>(1); // États pour la cryptographie TOTP (Flux Local) const [userId, setUserId] = useState(''); const [totpSecret, setTotpSecret] = useState(''); const [qrUrl, setQrUrl] = useState(''); const [isFirstSetup, setIsFirstSetup] = useState(false); // -------------------------------------------------------- // 1 Connexion Zitadel (SSO) // -------------------------------------------------------- const handleZitadelLogin = async () => { setError(''); setIsLoading(true); try { // PocketBase gère automatiquement la popup vers Zitadel et le retour du token const authData = await pb.collection('aegis_users').authWithOAuth2({ provider: 'oidc' }); console.log("Données d'authentification SSO :", authData); if (authData) { // Zitadel a déjà géré la sécurité et le MFA de son côté. // On ouvre directement le coffre-fort. onLoginSuccess(); } } catch (err: any) { console.error("Erreur d'authentification SSO :", err); setError("Échec de la connexion sécurisée via GISE Identity."); pb.authStore.clear(); } finally { setIsLoading(false); } }; // -------------------------------------------------------- // 2 Connexion Email - Mot de passe // -------------------------------------------------------- const handleLocalLogin = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setIsLoading(true); try { const authData = await pb.collection('aegis_users').authWithPassword(email, password); if (authData.record.mfa_enabled) { setUserId(authData.record.id); if (!authData.record.totp_secret) { const totp = new OTPAuth.TOTP({ issuer: 'AEGIS by GISE', label: email, algorithm: 'SHA1', digits: 6, period: 30, secret: new OTPAuth.Secret({ size: 20 }) }); setTotpSecret(totp.secret.base32); setQrUrl(totp.toString()); setIsFirstSetup(true); } else { setTotpSecret(authData.record.totp_secret); setIsFirstSetup(false); } setStep(2); // On passe à l'étape MFA locale } else { onLoginSuccess(); } } catch (err: any) { setError("Identifiants institutionnels incorrects ou accès révoqué."); pb.authStore.clear(); } finally { setIsLoading(false); } }; const handleFinalStep = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setIsLoading(true); try { const totp = new OTPAuth.TOTP({ issuer: 'AEGIS by GISE', label: email, algorithm: 'SHA1', digits: 6, period: 30, secret: OTPAuth.Secret.fromBase32(totpSecret) }); const isValid = totp.validate({ token: mfaCode, window: 1 }) !== null; if (isValid) { if (isFirstSetup) { await pb.collection('aegis_users').update(userId, { totp_secret: totpSecret }); } onLoginSuccess(); } else { setError("Code de sécurité invalide ou expiré."); setMfaCode(''); } } catch (err) { setError("Une erreur critique est survenue lors de la vérification."); } finally { setIsLoading(false); } }; const handleCancelMFA = () => { pb.authStore.clear(); setStep(1); setPassword(''); setMfaCode(''); setError(''); setIsFirstSetup(false); }; return (

AEGIS by GISE

Accès restreint

{step === 1 ? (
{error && (
{error}
)} {/* BOUTON SSO ZITADEL */}
{/* SÉPARATEUR VISUEL */}
Ou via vos identifiants locaux
{/* FORMULAIRE CLASSIQUE */}
setEmail(e.target.value)} required className="block w-full appearance-none rounded-md border border-slate-300 px-3 py-2 placeholder-slate-400 shadow-sm focus:border-blue-900 focus:outline-none focus:ring-blue-900 sm:text-sm" placeholder="direction@client.com" />
setPassword(e.target.value)} required className="block w-full appearance-none rounded-md border border-slate-300 px-3 py-2 placeholder-slate-400 shadow-sm focus:border-blue-900 focus:outline-none focus:ring-blue-900 sm:text-sm" placeholder="••••••••••••" />
) : (
{error && (
{error}
)} {isFirstSetup ? (

Configuration de la sécurité

Scannez ce QR Code avec Google Authenticator ou Authy pour lier votre appareil.

) : (

Validation à double facteur

Saisissez le code généré par votre application d'authentification.

)}
setMfaCode(e.target.value)} required maxLength={6} className="block w-full appearance-none rounded-md border border-slate-300 px-3 py-3 text-center text-2xl tracking-[0.5em] text-slate-900 placeholder-slate-300 shadow-sm focus:border-blue-900 focus:outline-none font-mono" placeholder="000000" />
)}
); }