test zitadel
Deploy Aegis Portal / build-and-deploy (push) Successful in 15s

This commit is contained in:
LathanDevers
2026-07-26 14:01:51 +02:00
parent bf6071f785
commit 69a1826f73
3 changed files with 129 additions and 45 deletions
+80 -41
View File
@@ -15,28 +15,51 @@ export default function Login({ onLoginSuccess }: LoginProps) {
const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState<1 | 2>(1);
// Nouveaux états pour la cryptographie TOTP
// États pour la cryptographie TOTP (Flux Local)
const [userId, setUserId] = useState('');
const [totpSecret, setTotpSecret] = useState('');
const [qrUrl, setQrUrl] = useState('');
const [isFirstSetup, setIsFirstSetup] = useState(false);
const handleLogin = async (e: React.FormEvent) => {
// --------------------------------------------------------
// FLUX 1 : Connexion SSO via Zitadel (OIDC)
// --------------------------------------------------------
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' });
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);
}
};
// --------------------------------------------------------
// FLUX 2 : Connexion Classique (Email + Mot de passe)
// --------------------------------------------------------
const handleLocalLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
// 1. Authentification PocketBase
const authData = await pb.collection('aegis_users').authWithPassword(email, password);
// 2. Vérification du MFA
if (authData.record.mfa_enabled) {
setUserId(authData.record.id);
// Si le client n'a pas encore configuré son MFA
if (!authData.record.totp_secret) {
// Génération cryptographique native pour le navigateur
const totp = new OTPAuth.TOTP({
issuer: 'AEGIS by GISE',
label: email,
@@ -46,23 +69,18 @@ export default function Login({ onLoginSuccess }: LoginProps) {
secret: new OTPAuth.Secret({ size: 20 })
});
const newSecret = totp.secret.base32;
const otpauth = totp.toString(); // Génère l'URL pour le QR Code
setTotpSecret(newSecret);
setQrUrl(otpauth);
setTotpSecret(totp.secret.base32);
setQrUrl(totp.toString());
setIsFirstSetup(true);
} else {
// Le client l'a déjà configuré dans le passé
setTotpSecret(authData.record.totp_secret);
setIsFirstSetup(false);
}
setStep(2);
setStep(2); // On passe à l'étape MFA locale
} else {
onLoginSuccess();
}
} catch (err: any) {
console.error("Erreur d'authentification", err);
setError("Identifiants institutionnels incorrects ou accès révoqué.");
pb.authStore.clear();
} finally {
@@ -76,7 +94,6 @@ export default function Login({ onLoginSuccess }: LoginProps) {
setIsLoading(true);
try {
// On recrée l'instance TOTP avec le secret pour vérifier le code saisi
const totp = new OTPAuth.TOTP({
issuer: 'AEGIS by GISE',
label: email,
@@ -86,23 +103,18 @@ export default function Login({ onLoginSuccess }: LoginProps) {
secret: OTPAuth.Secret.fromBase32(totpSecret)
});
// Validation : Retourne un nombre si valide, null sinon (tolérance de 1 fenêtre de 30s)
const isValid = totp.validate({ token: mfaCode, window: 1 }) !== null;
if (isValid) {
// Si c'était la première configuration, on sauvegarde le secret en base de données
if (isFirstSetup) {
await pb.collection('aegis_users').update(userId, {
totp_secret: totpSecret
});
await pb.collection('aegis_users').update(userId, { totp_secret: totpSecret });
}
onLoginSuccess(); // La porte du coffre s'ouvre !
onLoginSuccess();
} else {
setError("Code de sécurité invalide ou expiré.");
setMfaCode('');
}
} catch (err) {
console.error(err);
setError("Une erreur critique est survenue lors de la vérification.");
} finally {
setIsLoading(false);
@@ -141,33 +153,61 @@ export default function Login({ onLoginSuccess }: LoginProps) {
<div className="bg-white py-8 px-4 shadow-sm border border-slate-200 rounded-xl sm:px-10">
{step === 1 ? (
<form className="space-y-6" onSubmit={handleLogin}>
<div className="space-y-6">
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm font-medium animate-in fade-in slide-in-from-top-1">
{error}
</div>
)}
{/* BOUTON SSO ZITADEL */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700">Identifiant institutionnel</label>
<div className="mt-1">
<input id="email" type="email" value={email} onChange={(e) => 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" />
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-700">Mot de passe</label>
<div className="mt-1">
<input id="password" type="password" value={password} onChange={(e) => 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="••••••••••••" />
</div>
</div>
<div>
<button type="submit" disabled={isLoading} className="flex w-full justify-center rounded-md border border-transparent bg-blue-900 py-2.5 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-800 focus:outline-none transition-colors disabled:opacity-70">
{isLoading ? 'Chiffrement en cours...' : 'Authentification'}
<button
type="button"
onClick={handleZitadelLogin}
disabled={isLoading}
className="flex w-full justify-center items-center rounded-md border border-slate-300 bg-white py-2.5 px-4 text-sm font-semibold text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none transition-colors disabled:opacity-70"
>
<svg className="w-5 h-5 mr-2 text-blue-900" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
</svg>
{isLoading ? 'Connexion en cours...' : 'Connexion via GISE Identity'}
</button>
</div>
</form>
{/* SÉPARATEUR VISUEL */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-200" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-slate-400">Ou via vos identifiants locaux</span>
</div>
</div>
{/* FORMULAIRE CLASSIQUE */}
<form className="space-y-6" onSubmit={handleLocalLogin}>
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700">Identifiant institutionnel</label>
<div className="mt-1">
<input id="email" type="email" value={email} onChange={(e) => 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" />
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-700">Mot de passe</label>
<div className="mt-1">
<input id="password" type="password" value={password} onChange={(e) => 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="••••••••••••" />
</div>
</div>
<div>
<button type="submit" disabled={isLoading} className="flex w-full justify-center rounded-md border border-transparent bg-blue-900 py-2.5 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-800 focus:outline-none transition-colors disabled:opacity-70">
{isLoading ? 'Chiffrement en cours...' : 'Authentification classique'}
</button>
</div>
</form>
</div>
) : (
<form className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300" onSubmit={handleFinalStep}>
{error && (
@@ -213,7 +253,6 @@ export default function Login({ onLoginSuccess }: LoginProps) {
</div>
</form>
)}
</div>
</div>
</div>