import { useState, useEffect, useRef } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext.jsx'; import { api } from '../api.js'; import AuthBgCol from '../components/AuthBgCol.jsx'; import PasswordInput from '../components/PasswordInput.jsx'; // ── Helpers ──────────────────────────────────────────────────────────────── const DEVICE_KEY = 'cl_device_token'; function fmtCountdown(sec) { const m = Math.floor(sec / 60).toString().padStart(2, '0'); const s = (sec % 60).toString().padStart(2, '0'); return `${m}:${s}`; } // ── Header logo (colonne droite) ────────────────────────────────────────── function AppHeader({ appInfo }) { return (
{appInfo.iconUrl && {appInfo.appName}} {appInfo.appName}
); } // ── Composant principal ──────────────────────────────────────────────────── export default function Login() { const { login, completeLogin } = useAuth(); const navigate = useNavigate(); const [appInfo, setAppInfo] = useState({ appName: 'Crowdlending Tracker', iconUrl: null, allowRegistration: true }); // Étape : 'form' | 'method' | 'code' const [step, setStep] = useState('form'); const [sessionToken, setSessionToken] = useState(null); const [method, setMethod] = useState(null); // 'totp' | 'email' // Formulaire login const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [trustDev, setTrustDev] = useState(false); const [err, setErr] = useState(null); const [busy, setBusy] = useState(false); const [unverified, setUnverified] = useState(false); const [resendBusy, setResendBusy] = useState(false); const [resendDone, setResendDone] = useState(false); // Code 2FA const [code, setCode] = useState(''); const [codeErr, setCodeErr] = useState(null); const [codeBusy, setCodeBusy] = useState(false); const [countdown, setCountdown] = useState(300); // 5 min pour email const timerRef = useRef(null); useEffect(() => { fetch('/api/app-info').then(r => r.json()).then(d => setAppInfo(d)).catch(() => {}); }, []); // Countdown pour OTP email useEffect(() => { if (step === 'code' && method === 'email') { setCountdown(300); timerRef.current = setInterval(() => { setCountdown(c => { if (c <= 1) { clearInterval(timerRef.current); return 0; } return c - 1; }); }, 1000); } return () => clearInterval(timerRef.current); }, [step, method]); // ── Soumission du formulaire ────────────────────────────────────────────── const submit = async (e) => { e.preventDefault(); setErr(null); setUnverified(false); setResendDone(false); setBusy(true); try { const deviceToken = localStorage.getItem(DEVICE_KEY) || undefined; const result = await login(email, password, deviceToken); if (result?.requires2FA) { setSessionToken(result.sessionToken); setStep('method'); } else { navigate('/'); } } catch (e) { if (e.code === 'EMAIL_NOT_VERIFIED') { setUnverified(true); } else { setErr(e.message || 'Identifiants incorrects.'); } } finally { setBusy(false); } }; const resend = async () => { setResendBusy(true); try { await fetch('/api/auth/resend-verification', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); setResendDone(true); } catch (_) {} finally { setResendBusy(false); } }; // ── Choix de méthode 2FA ────────────────────────────────────────────────── const chooseMethod = async (m) => { setMethod(m); setCodeErr(null); setCode(''); if (m === 'email') { try { await api.post('/auth/2fa/send-email-code', { sessionToken }); } catch (e) { setCodeErr(e.message); return; } } setStep('code'); }; // ── Vérification du code ────────────────────────────────────────────────── const verifyCode = async (e) => { e.preventDefault(); setCodeErr(null); setCodeBusy(true); try { const result = await api.post('/auth/2fa/verify', { sessionToken, code, method, trustDevice: trustDev, }); if (result.deviceToken) { localStorage.setItem(DEVICE_KEY, result.deviceToken); } completeLogin(result.token, result.user); navigate('/'); } catch (e) { setCodeErr(e.message || 'Code invalide.'); } finally { setCodeBusy(false); } }; const resendEmailCode = async () => { setCodeErr(null); try { await api.post('/auth/2fa/send-email-code', { sessionToken }); setCountdown(300); timerRef.current = setInterval(() => { setCountdown(c => { if (c <= 1) { clearInterval(timerRef.current); return 0; } return c - 1; }); }, 1000); } catch (e) { setCodeErr(e.message); } }; // ── Rendu ───────────────────────────────────────────────────────────────── const BtnPrimary = ({ children, disabled, onClick, type = 'submit' }) => ( ); const ErrBox = ({ msg }) => msg ? (
{msg}
) : null; return (
{/* ══ ÉTAPE 1 : Formulaire email/mot de passe ══════════════════ */} {step === 'form' && ( <>

Bienvenue

Connectez-vous à votre compte

{unverified && (
Email non vérifié. Vérifiez votre boîte mail et cliquez sur le lien reçu.
{resendDone ? ✓ Email renvoyé ! : }
)}
setEmail(e.target.value)} style={{ width: '100%' }} />
Mot de passe oublié ?
setPassword(e.target.value)} style={{ width: '100%' }} />
{busy ? 'Connexion…' : 'Se connecter'} {appInfo.allowRegistration !== false && (

Pas encore de compte ?{' '} Créer un compte

)} )} {/* ══ ÉTAPE 2 : Choix de méthode 2FA ══════════════════════════ */} {step === 'method' && ( <>

Vérification en deux étapes

Choisissez votre méthode de vérification

)} {/* ══ ÉTAPE 3 : Saisie du code ═════════════════════════════════ */} {step === 'code' && ( <>
{method === 'totp' ? : }

{method === 'totp' ? 'Code de l\'application' : 'Code par email'}

{method === 'totp' ? 'Entrez le code à 6 chiffres affiché dans votre application d\'authentification.' : <>Code envoyé à {email}. }

{method === 'email' && ( 0 ? 'var(--text-muted)' : 'var(--danger,#dc2626)' }}> {countdown > 0 ? fmtCountdown(countdown) : 'Expiré'} )}
setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} style={{ width: '100%', fontSize: 24, letterSpacing: 8, textAlign: 'center', fontWeight: 700 }} />
{codeBusy ? 'Vérification…' : 'Vérifier'} {method === 'email' && countdown === 0 && ( )} )}
); }