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.

5 min read
Déverrouillez votre potentiel

avec React Router 7

Vous en avez marre de...

❌ perdre du temps à chercher des informations éparpillées
❌ ne pas avoir de retour sur votre progression
Assistant IA spécialisé

Posez vos questions 24/7 à notre IA experte en React Router 7

Quiz interactifs

Validez vos acquis avec des quiz personnalisés et un feedback instantané

9 modules
45 leçons
Accès à vie
299.00
-50%

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 :

  1. L’internaute clique sur « Mot de passe oublié » depuis la page de connexion.
  2. Il saisit son e-mail.
  3. Si cet e-mail correspond à un compte, on lui expédie un lien signé / temporaire.
  4. Le lien ouvre une page « Choisis un nouveau mot de passe ».
  5. 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é »

app/routes/login.tsx {7,38}
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">
6
Mot 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 :

app/routes/forgot-password.tsx
1
export async function loader({ request }: LoaderArgs) {
2
const user = await getOptionalUser({ request })
3
if (user) throw redirect("/") // déjà connecté
4
return null
5
}

2.2 Le schéma du formulaire

utils/schemas/auth.ts
1
import { z } from "zod"
2
3
export const ForgotPasswordSchema = z.object({
4
email: z.string().email("Adresse invalide"),
5
})

2.3 L’action : créer et stocker le token

app/server/sessions.server.ts
1
export async function triggerResetPasswordRequest({ email }: { email: string }) {
2
// 1 - le compte existe-t-il ?
3
const user = await prisma.user.findUnique({ where: { email } })
4
if (!user) return // pas d’info : pas de faille UX
5
6
// 2 - générer un token + date d’expiration
7
const token = crypto.randomUUID().slice(0, 32)
8
const expiresAt = new Date(Date.now() + 1000 * 60 * 30) // 30 min
9
10
await prisma.user.update({
11
where: { id: user.id },
12
data: { resetToken: token, resetTokenExpiresAt: expiresAt },
13
})
14
15
// 3 - construire le lien
16
const link = href("/reset-password") + `?token=${token}`
17
console.log({ link }) // @todo : envoyer un e-mail réel
18
}

Le modèle Prisma se dote donc de deux colonnes facultatives :

prisma/schema.prisma {19-22}
1
resetToken String? @unique
2
resetTokenExpiresAt 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

app/routes/forgot-password.tsx {51-79}
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

app/routes/reset-password.tsx
1
export async function loader({ request }: LoaderArgs) {
2
const token = new URL(request.url).searchParams.get("token") ?? ""
3
const user = await prisma.user.findUnique({
4
where: { resetToken: token },
5
select: { id: true, resetTokenExpiresAt: true },
6
})
7
8
const isValid =
9
user && user.resetTokenExpiresAt && user.resetTokenExpiresAt > new Date()
10
11
return data({ isValid, token }) // token renvoyé pour l’action
12
}

3.2 Le formulaire « Nouveau mot de passe »

app/routes/reset-password.tsx {66-94}
1
{isValid ? (
2
<Form method="POST" …>
3
<Field … name="password" />
4
<Field name="passwordConfirmation" />
5
<button className="btn-primary w-full">
6
Réinitialiser le mot de passe
7
</button>
8
</Form>
9
) : (
10
<p className="text-gray-500 text-center">
11
Lien invalide ou expiré.
12
</p>
13
)}

3.3 Schéma de vérification

utils/schemas/auth.ts {15-25}
1
export const ResetPasswordSchema = z
2
.object({
3
password: z.string().min(6),
4
passwordConfirmation: z.string(),
5
})
6
.refine((d) => d.password === d.passwordConfirmation, {
7
message: "Les mots de passe ne correspondent pas",
8
path: ["passwordConfirmation"],
9
})

3.4 Action : mettre à jour le mot de passe et invalider le token

app/server/sessions.server.ts {74-96}
1
export async function resetUserPassword({
2
token, password,
3
}: { token: string; password: string }) {
4
const user = await prisma.user.findUnique({ where: { resetToken: token } })
5
if (
6
!user ||
7
!user.resetTokenExpiresAt ||
8
user.resetTokenExpiresAt < new Date()
9
) throw new Error("Token expiré")
10
11
await prisma.user.update({
12
where: { id: user.id },
13
data: {
14
password: await hashPassword({ password }),
15
resetToken: null,
16
resetTokenExpiresAt: 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 /login pour gagner un clic.
  • Sécurité supplémentaire : le superRefine de 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-password dé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.
Premium
Quiz interactif
Testez vos connaissances et validez votre compréhension du module avec notre quiz interactif.
1

Comprendre les concepts fondamentaux

Quelle est la principale différence entre les composants client et serveur dans React ?

Les composants client s'exécutent uniquement dans le navigateur
Les composants serveur peuvent utiliser useState
Les composants client sont plus rapides
Il n'y a aucune différence significative
2

Optimisation des performances

Quelle technique est recommandée pour éviter les rendus inutiles dans React ?

Utiliser React.memo pour les composants fonctionnels
Ajouter plus d'états locaux
Éviter d'utiliser les props
Toujours utiliser les class components
3

Architecture des données

Quel hook permet de gérer les effets de bord dans un composant React ?

useEffect
useState
useMemo
useContext
4

Gestion des erreurs

Comment implémenter la gestion des erreurs pour les requêtes API dans React ?

Utiliser try/catch avec async/await
Ignorer les erreurs
Toujours afficher un message d'erreur
Rediriger l'utilisateur
5

Déploiement et CI/CD

Quelle est la meilleure pratique pour déployer une application React en production ?

Utiliser un service CI/CD comme GitHub Actions
Copier les fichiers manuellement via FTP
Envoyer le code source complet
Ne jamais mettre à jour l'application

Débloquez ce quiz et tous les autres contenus premium en achetant ce cours