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 && ( +
+ 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 && ( +
+ Logo +
+ )} + Mot de passe oublié + + Saisissez votre adresse email pour recevoir un lien de réinitialisation + +
+ +
+
+ + setEmail(e.target.value)} + required + /> +
+ + + + + + +
+
+
+
+ ); +} 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 && ( +
+ 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 && ( +
+ Logo +
+ )} + Lien invalide + + Le lien de réinitialisation est invalide ou a expiré. + +
+ + + + + +
+
+ ); + } + + return ( +
+ + + {APP_LOGO && ( +
+ Logo +
+ )} + Nouveau mot de passe + + Choisissez un nouveau mot de passe pour votre compte + +
+ +
+
+ + setNewPassword(e.target.value)} + required + minLength={8} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + required + minLength={8} + /> +
+ + +
+
+
+
+ ); +} 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)