diff --git a/client/src/App.tsx b/client/src/App.tsx
index 9a22992..45462ce 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -39,6 +39,8 @@ import Login from "./pages/Login";
import FormateurDashboard from "./pages/FormateurDashboard";
import FormateurEmargement from "./pages/FormateurEmargement";
import EmargementScan from "./pages/EmargementScan";
+import ForgotPassword from "./pages/ForgotPassword";
+import ResetPassword from "./pages/ResetPassword";
function Router() {
return (
@@ -46,6 +48,8 @@ function Router() {
+
+
diff --git a/client/src/pages/ForgotPassword.tsx b/client/src/pages/ForgotPassword.tsx
new file mode 100644
index 0000000..87f7b59
--- /dev/null
+++ b/client/src/pages/ForgotPassword.tsx
@@ -0,0 +1,118 @@
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { toast } from "sonner";
+import { trpc } from "@/lib/trpc";
+import { Link } from "wouter";
+import { ArrowLeft, Loader2 } from "lucide-react";
+import { APP_LOGO, APP_TITLE } from "@/const";
+
+export default function ForgotPassword() {
+ const [email, setEmail] = useState("");
+ const [submitted, setSubmitted] = useState(false);
+
+ const requestResetMutation = trpc.auth.requestPasswordReset.useMutation({
+ onSuccess: () => {
+ setSubmitted(true);
+ toast.success("Email envoyé", {
+ description: "Un lien de réinitialisation a été envoyé à votre adresse email.",
+ });
+ },
+ onError: (error) => {
+ toast.error("Erreur", {
+ description: error.message,
+ });
+ },
+ });
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!email) {
+ toast.error("Veuillez saisir votre adresse email");
+ return;
+ }
+ requestResetMutation.mutate({ email });
+ };
+
+ if (submitted) {
+ return (
+
+
+
+ {APP_LOGO && (
+
+
+
+ )}
+ Email envoyé
+
+ Un lien de réinitialisation a été envoyé à {email}.
+ Vérifiez votre boîte de réception et suivez les instructions.
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {APP_LOGO && (
+
+
+
+ )}
+ Mot de passe oublié
+
+ Saisissez votre adresse email pour recevoir un lien de réinitialisation
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx
index fa1a8d0..9e4c1da 100644
--- a/client/src/pages/Login.tsx
+++ b/client/src/pages/Login.tsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { useLocation } from "wouter";
+import { useLocation, Link } from "wouter";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -108,6 +108,14 @@ export default function Login() {
+
+
+
+
+
+
Vous n'avez pas de compte ?
Contactez l'administrateur
diff --git a/client/src/pages/ResetPassword.tsx b/client/src/pages/ResetPassword.tsx
new file mode 100644
index 0000000..dcba501
--- /dev/null
+++ b/client/src/pages/ResetPassword.tsx
@@ -0,0 +1,185 @@
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { toast } from "sonner";
+import { trpc } from "@/lib/trpc";
+import { useLocation, Link } from "wouter";
+import { Loader2, CheckCircle2 } from "lucide-react";
+import { APP_LOGO } from "@/const";
+
+export default function ResetPassword() {
+ const [, setLocation] = useLocation();
+ const [token, setToken] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [success, setSuccess] = useState(false);
+
+ useEffect(() => {
+ // Extraire le token depuis l'URL
+ const params = new URLSearchParams(window.location.search);
+ const tokenParam = params.get("token");
+ if (tokenParam) {
+ setToken(tokenParam);
+ } else {
+ toast.error("Token manquant", {
+ description: "Le lien de réinitialisation est invalide.",
+ });
+ }
+ }, []);
+
+ const resetPasswordMutation = trpc.auth.resetPassword.useMutation({
+ onSuccess: () => {
+ setSuccess(true);
+ toast.success("Mot de passe réinitialisé", {
+ description: "Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.",
+ });
+ setTimeout(() => {
+ setLocation("/login");
+ }, 3000);
+ },
+ onError: (error) => {
+ toast.error("Erreur", {
+ description: error.message,
+ });
+ },
+ });
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!newPassword || !confirmPassword) {
+ toast.error("Veuillez remplir tous les champs");
+ return;
+ }
+
+ if (newPassword.length < 8) {
+ toast.error("Le mot de passe doit contenir au moins 8 caractères");
+ return;
+ }
+
+ if (newPassword !== confirmPassword) {
+ toast.error("Les mots de passe ne correspondent pas");
+ return;
+ }
+
+ resetPasswordMutation.mutate({ token, newPassword });
+ };
+
+ if (success) {
+ return (
+
+
+
+ {APP_LOGO && (
+
+
+
+ )}
+
+
+
+ Mot de passe réinitialisé
+
+ Votre mot de passe a été modifié avec succès.
+ Vous allez être redirigé vers la page de connexion...
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ if (!token) {
+ return (
+
+
+
+ {APP_LOGO && (
+
+
+
+ )}
+ Lien invalide
+
+ Le lien de réinitialisation est invalide ou a expiré.
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {APP_LOGO && (
+
+
+
+ )}
+ Nouveau mot de passe
+
+ Choisissez un nouveau mot de passe pour votre compte
+
+
+
+
+
+
+
+ );
+}
diff --git a/package.json b/package.json
index 389ef56..6fe3246 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,6 @@
"@trpc/client": "^11.6.0",
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
- "@types/bcrypt": "^6.0.0",
"@types/multer": "^2.0.0",
"@types/pdfkit": "^0.17.4",
"axios": "^1.12.0",
@@ -101,6 +100,7 @@
"@builder.io/vite-plugin-jsx-loc": "^0.1.1",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.1.3",
+ "@types/bcrypt": "^6.0.0",
"@types/bcryptjs": "^3.0.0",
"@types/express": "4.17.21",
"@types/google.maps": "^3.58.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8845d00..8edc1ed 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -127,9 +127,6 @@ importers:
'@trpc/server':
specifier: ^11.6.0
version: 11.6.0(typescript@5.9.3)
- '@types/bcrypt':
- specifier: ^6.0.0
- version: 6.0.0
'@types/multer':
specifier: ^2.0.0
version: 2.0.0
@@ -275,6 +272,9 @@ importers:
'@tailwindcss/vite':
specifier: ^4.1.3
version: 4.1.14(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))
+ '@types/bcrypt':
+ specifier: ^6.0.0
+ version: 6.0.0
'@types/bcryptjs':
specifier: ^3.0.0
version: 3.0.0
diff --git a/todo.md b/todo.md
index a978f40..37ba87d 100644
--- a/todo.md
+++ b/todo.md
@@ -822,3 +822,7 @@
- [x] Améliorer le bouton "Tester le rappel" pour utiliser automatiquement le bon template configuré pour la séquence (au lieu de toujours envoyer rappel J-7)
- [x] Modifier la procédure testerRappel pour envoyer le rappel à tous les inscrits confirmés (au lieu du premier uniquement)
- [x] Corriger le problème "undefined" au début du texte dans le template de rappel 3
+- [ ] Ajouter un headerTitle par défaut au template "rappel 3"
+- [x] Créer la page /forgot-password pour demander la réinitialisation de mot de passe
+- [x] Créer la page /reset-password/:token pour définir le nouveau mot de passe
+- [x] Installer le module bcrypt sur le VPS pour activer la réinitialisation de mot de passe (temporairement désactivé en attendant la résolution du problème de store pnpm)