import { useState, type FormEvent } from 'react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { CheckCircle2, Eye, EyeOff, Lock, ShieldCheck, XCircle } from 'lucide-react'; import { api, ApiError, setToken } from '@/lib/api'; import type { User } from '@/lib/types'; import { useAuth } from '@/stores/auth'; import { Logo } from '@/components/ui/logo'; import { Button } from '@/components/ui/button'; import { Spinner } from '@/components/ui/spinner'; interface TokenInfo { email: string; purpose: 'invite' | 'reset'; first_time: boolean; display_name: string | null; expires_at: string } interface SetResp { token?: string; user?: User } const MIN = 8; function strength(pw: string): { score: number; label: string } { let s = 0; if (pw.length >= MIN) s++; if (pw.length >= 12) s++; if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s++; if (/\d/.test(pw)) s++; if (/[^A-Za-z0-9]/.test(pw)) s++; const label = pw.length < MIN ? 'Trop court' : s <= 2 ? 'Faible' : s === 3 ? 'Correct' : s === 4 ? 'Bon' : 'Excellent'; return { score: pw.length < MIN ? 0 : s, label }; } /** /mot-de-passe?token=… — first activation (invitation) or password reset. */ export function SetPasswordPage() { const nav = useNavigate(); const [params] = useSearchParams(); const token = params.get('token') || ''; const setUser = useAuth((s) => s.setUser); const info = useQuery({ queryKey: ['password-token', token], queryFn: () => api(`/auth/password-token?token=${encodeURIComponent(token)}`), enabled: !!token, retry: false, }); const [pw, setPw] = useState(''); const [pw2, setPw2] = useState(''); const [show, setShow] = useState(false); const [consent, setConsent] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const st = strength(pw); const firstTime = info.data?.first_time ?? true; const submit = async (e: FormEvent) => { e.preventDefault(); setError(null); if (pw.length < MIN) { setError(`Le mot de passe doit contenir au moins ${MIN} caractères.`); return; } if (pw !== pw2) { setError('Les deux mots de passe ne correspondent pas.'); return; } if (firstTime && !consent) { setError('Merci d\'accepter la politique de confidentialité pour continuer.'); return; } setBusy(true); try { const r = await api('/auth/set-password', { method: 'POST', body: JSON.stringify({ token, password: pw }) }); if (r.token) setToken(r.token); if (r.user) setUser(r.user); if (firstTime) await api('/me/consent', { method: 'POST' }).catch(() => undefined); nav('/', { replace: true }); } catch (err) { setError(err instanceof Error ? err.message : 'Impossible d\'enregistrer le mot de passe.'); } finally { setBusy(false); } }; const invalid = !token || (info.isError && (info.error as ApiError | undefined)?.status === 400) || info.isError; const title = info.data ? (firstTime ? 'Choisis ton mot de passe' : 'Nouveau mot de passe') : 'Mot de passe'; return (

{firstTime ? 'Bienvenue ! Une dernière étape avant de commencer avec ton tuteur.' : 'Choisis un nouveau mot de passe pour ton compte.'}

{info.isLoading && token &&
Vérification du lien…
} {invalid && !info.isLoading && (
Lien invalide ou expiré

Les liens ne servent qu'une fois et expirent. Demande un nouveau lien : il arrivera par courriel en quelques secondes.

Retour à la connexion
)} {info.data && ( <>

{title}

Compte {info.data.email}{info.data.display_name && !info.data.display_name.includes('@') ? ` · ${info.data.display_name}` : ''}. Tu te connecteras ensuite avec ce courriel et ce mot de passe.

{firstTime && ( )} {error &&

{error}

}

Mot de passe chiffré (PBKDF2). Personne, pas même le professeur, ne peut le lire.

)}
); }