Comment ajouter la réinitialisation de mot de passe
Apprends à implémenter un flow sécurisé de réinitialisation de mot de passe dans React Router 7 avec Prisma, Conform et Zod. Guide complet et concret.
Pourquoi une réinitialisation de mot de passe ?
Dès que tu imposes un couple e-mail / mot de passe, tu dois prévoir l’oubli. Le flux idéal :
- L’internaute clique sur « Mot de passe oublié » depuis la page de connexion.
- Il saisit son e-mail.
- Si cet e-mail correspond à un compte, on lui expédie un lien signé / temporaire.
- Le lien ouvre une page « Choisis un nouveau mot de passe ».
- Après validation : mot de passe mis à jour, token invalidé, l’utilisateur se connecte avec sa nouvelle clé.
Voyons comment produire ce flow dans React Router 7 avec Prisma, Conform et Zod.
1 Ajouter le lien « Mot de passe oublié »
1<Form …>2{/* …inputs e-mail / mdp… */}3<div className="flex justify-between">4<Link to="/register" className="text-xs text-sky-700">S’inscrire</Link>5<Link to="/forgot-password" className="text-xs text-gray-400">6Mot de passe oublié ?7</Link>8</div>9</Form>
Ce simple Link pointe vers une route dédiée. Créons-la.
2 La route /forgot-password
2.1 Le loader
Si l’utilisateur est déjà loggé, inutile de lui proposer le reset :
1export async function loader({ request }: LoaderArgs) {2const user = await getOptionalUser({ request })3if (user) throw redirect("/") // déjà connecté4return null5}
2.2 Le schéma du formulaire
1import { z } from "zod"23export const ForgotPasswordSchema = z.object({4email: z.string().email("Adresse invalide"),5})
2.3 L’action : créer et stocker le token
1export async function triggerResetPasswordRequest({ email }: { email: string }) {2// 1 - le compte existe-t-il ?3const user = await prisma.user.findUnique({ where: { email } })4if (!user) return // pas d’info : pas de faille UX56// 2 - générer un token + date d’expiration7const token = crypto.randomUUID().slice(0, 32)8const expiresAt = new Date(Date.now() + 1000 * 60 * 30) // 30 min910await prisma.user.update({11where: { id: user.id },12data: { resetToken: token, resetTokenExpiresAt: expiresAt },13})1415// 3 - construire le lien16const link = href("/reset-password") + `?token=${token}`17console.log({ link }) // @todo : envoyer un e-mail réel18}
Le modèle Prisma se dote donc de deux colonnes facultatives :
1resetToken String? @unique2resetTokenExpiresAt DateTime?
Small hack : on logue le lien au lieu d’envoyer un e-mail – parfait pour le dev local.
2.4 La page côté client
1<Form method="POST" …>2<Field … name="email" … />3{actionData?.message && (4<p className="text-center text-sm text-gray-500">5{actionData.message}6</p>7)}8<button className="btn-primary w-full">Réinitialiser le mot de passe</button>9</Form>
Quel que soit l’e-mail, la réponse est la même : « Si un compte est associé, tu recevras un lien… » → on ne révèle rien aux robots malveillants.
3 Créer la route /reset-password
3.1 Validation du token dans le loader
1export async function loader({ request }: LoaderArgs) {2const token = new URL(request.url).searchParams.get("token") ?? ""3const user = await prisma.user.findUnique({4where: { resetToken: token },5select: { id: true, resetTokenExpiresAt: true },6})78const isValid =9user && user.resetTokenExpiresAt && user.resetTokenExpiresAt > new Date()1011return data({ isValid, token }) // token renvoyé pour l’action12}
3.2 Le formulaire « Nouveau mot de passe »
1{isValid ? (2<Form method="POST" …>3<Field … name="password" />4<Field … name="passwordConfirmation" />5<button className="btn-primary w-full">6Réinitialiser le mot de passe7</button>8</Form>9) : (10<p className="text-gray-500 text-center">11Lien invalide ou expiré.12</p>13)}
3.3 Schéma de vérification
1export const ResetPasswordSchema = z2.object({3password: z.string().min(6),4passwordConfirmation: z.string(),5})6.refine((d) => d.password === d.passwordConfirmation, {7message: "Les mots de passe ne correspondent pas",8path: ["passwordConfirmation"],9})
3.4 Action : mettre à jour le mot de passe et invalider le token
1export async function resetUserPassword({2token, password,3}: { token: string; password: string }) {4const user = await prisma.user.findUnique({ where: { resetToken: token } })5if (6!user ||7!user.resetTokenExpiresAt ||8user.resetTokenExpiresAt < new Date()9) throw new Error("Token expiré")1011await prisma.user.update({12where: { id: user.id },13data: {14password: await hashPassword({ password }),15resetToken: null,16resetTokenExpiresAt: null,17},18})19}
Le token est à usage unique : seconde tentative => message d’expiration.
4 Affiner l’expérience
- Message de succès : « Ton mot de passe a bien été changé, connecte-toi. »
- Lien direct vers
/loginpour gagner un clic. - Sécurité supplémentaire : le
superRefinede l’action reteste le token au cas où il aurait expiré pendant que l’utilisateur tapait le formulaire.
Points clés
- Un seul champ e-mail pour éviter de dévoiler l’existence d’un compte.
- Token signé & daté stocké en DB, jamais en clair dans le cookie.
- Deux routes distinctes :
/forgot-passworddéclenche l’envoi./reset-password?token=…gère le changement.
- Après succès, on supprime le token en DB pour le rendre inutilisable.
- Toute la logique critique (token, dates, hash BCrypt) vit côté serveur – aucune faille XSS possible.
Comprendre les concepts fondamentaux
Quelle est la principale différence entre les composants client et serveur dans React ?
Optimisation des performances
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Architecture des données
Quel hook permet de gérer les effets de bord dans un composant React ?