changement

This commit is contained in:
Emma781227
2026-09-14 22:30:40 +02:00
parent 8a6355cde1
commit 2201446ee6
54 changed files with 2495 additions and 139 deletions

View File

@@ -4,6 +4,8 @@ const crypto = require("crypto");
const prisma = require("../config/prisma");
const transporter = require("../config/mailer");
const getFrontendUrl = require("../utils/frontendUrl");
const { passwordRuleError } = require("../validations/password.validation");
const { normalizeEmail } = require("../validations/email.validation");
const register = async (req, res) => {
try {
@@ -211,6 +213,7 @@ const login = async (req, res) => {
lastname: user.lastname,
email: user.email,
role: user.role,
mustChangePassword: user.mustChangePassword,
},
});
} catch (error) {
@@ -221,9 +224,47 @@ const login = async (req, res) => {
}
};
const RESET_LINK_EXPIRY_MS = 1000 * 60 * 30; // 30 min
// Enregistre un jeton de réinitialisation (qui remplace le précédent) et envoie
// le lien par email. Partagé par « mot de passe oublié » et par le profil.
async function sendPasswordResetEmail(user) {
const resetPasswordToken = crypto.randomBytes(32).toString("hex");
await prisma.user.update({
where: { id: user.id },
data: {
resetPasswordToken,
resetPasswordExpires: new Date(Date.now() + RESET_LINK_EXPIRY_MS),
},
});
// Le lien doit ouvrir le formulaire du front, pas la route POST de l'API :
// un clic depuis un email est une requête GET, qui tombait en 404.
const resetUrl = `${getFrontendUrl()}/definir-mot-de-passe/${resetPasswordToken}`;
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: user.email,
subject: "Réinitialisation de votre mot de passe Thé Tip Top",
html: `
<h2>Réinitialisation du mot de passe</h2>
<p>Bonjour ${user.firstname},</p>
<p>Vous avez demandé à définir un nouveau mot de passe.</p>
<p>Cliquez sur le lien ci-dessous pour le choisir :</p>
<a href="${resetUrl}">Choisir mon nouveau mot de passe</a>
<p>Ce lien expire dans 30 minutes.</p>
<p>Si vous n'êtes pas à l'origine de cette demande, ignorez cet email.</p>
`,
});
}
const FORGOT_PASSWORD_MESSAGE =
"Si un compte existe avec cet email, un lien de réinitialisation vient de lui être envoyé.";
const forgotPassword = async (req, res) => {
try {
const { email } = req.body;
const email = normalizeEmail(req.body.email);
if (!email) {
return res.status(400).json({
@@ -237,45 +278,12 @@ const forgotPassword = async (req, res) => {
// Sécurité : ne pas dire si l'email existe ou non
if (!user) {
return res.status(200).json({
message: "Si un compte existe avec cet email, un lien de réinitialisation a été envoyé.",
});
return res.status(200).json({ message: FORGOT_PASSWORD_MESSAGE });
}
const resetPasswordToken = crypto.randomBytes(32).toString("hex");
const resetPasswordExpires = new Date(Date.now() + 1000 * 60 * 30); // 30 min
await sendPasswordResetEmail(user);
await prisma.user.update({
where: { id: user.id },
data: {
resetPasswordToken,
resetPasswordExpires,
},
});
// Le lien doit ouvrir le formulaire du front, pas la route POST de l'API :
// un clic depuis un email est une requête GET, qui tombait en 404.
const frontendUrl = getFrontendUrl();
const resetUrl = `${frontendUrl}/definir-mot-de-passe/${resetPasswordToken}`;
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: user.email,
subject: "Réinitialisation de votre mot de passe Thé Tip Top",
html: `
<h2>Réinitialisation du mot de passe</h2>
<p>Bonjour ${user.firstname},</p>
<p>Vous avez demandé la réinitialisation de votre mot de passe.</p>
<p>Cliquez sur le lien ci-dessous pour définir un nouveau mot de passe :</p>
<a href="${resetUrl}">Réinitialiser mon mot de passe</a>
<p>Ce lien expire dans 30 minutes.</p>
<p>Si vous n'êtes pas à l'origine de cette demande, ignorez cet email.</p>
`,
});
return res.status(200).json({
message: "Si un compte existe avec cet email, un lien de réinitialisation a été envoyé.",
});
return res.status(200).json({ message: FORGOT_PASSWORD_MESSAGE });
} catch (error) {
console.error("Erreur forgotPassword:", error);
return res.status(500).json({
@@ -284,15 +292,27 @@ const forgotPassword = async (req, res) => {
}
};
// Profil connecté (compte Google sans mot de passe) : même lien, envoyé à
// l'adresse du compte. Authentifié, donc sans captcha.
const sendPasswordSetupLink = async (req, res) => {
try {
const user = await prisma.user.findUnique({ where: { id: req.user.id } });
await sendPasswordResetEmail(user);
return res.json({ message: `Lien envoyé à ${user.email}.` });
} catch (error) {
console.error("Erreur sendPasswordSetupLink:", error);
return res.status(500).json({ message: "Erreur serveur lors de l'envoi du lien." });
}
};
const resetPassword = async (req, res) => {
try {
const { token } = req.params;
const { password, confirmPassword } = req.body;
if (!password || password.length < 8) {
return res.status(400).json({
message: "Le nouveau mot de passe doit contenir au moins 8 caractères.",
});
const passwordError = passwordRuleError(password);
if (passwordError) {
return res.status(400).json({ message: passwordError });
}
// La confirmation est vérifiée côté serveur aussi : le contrôle du
@@ -326,6 +346,14 @@ const resetPassword = async (req, res) => {
password: hashedPassword,
resetPasswordToken: null,
resetPasswordExpires: null,
// Le lien reçu par email prouve l'accès à la boîte : le mot de passe
// choisi ici remplace aussi un éventuel mot de passe provisoire, et
// l'adresse est considérée comme validée (sinon la connexion qui suit
// serait refusée pour email non vérifié).
mustChangePassword: false,
isEmailVerified: true,
emailVerifyToken: null,
emailVerifyExpires: null,
},
});
@@ -342,7 +370,8 @@ const resetPassword = async (req, res) => {
const verifyCode = async (req, res) => {
try {
const { email, code } = req.body;
const { code } = req.body;
const email = normalizeEmail(req.body.email);
if (!email || !code) {
return res.status(400).json({ message: "Email et code requis." });
@@ -424,6 +453,7 @@ const oauthCallback = (req, res) => {
lastname: user.lastname,
email: user.email,
role: user.role,
mustChangePassword: user.mustChangePassword,
})
).toString("base64");
@@ -491,6 +521,47 @@ const deleteAccount = async (req, res) => {
}
};
// Première connexion d'un employé : remplace le mot de passe provisoire reçu
// par email. L'utilisateur vient de s'authentifier avec ce mot de passe, on ne
// le redemande donc pas.
const setInitialPassword = async (req, res) => {
try {
const { password, confirmPassword } = req.body;
if (!req.user.mustChangePassword) {
return res.status(400).json({ message: "Votre mot de passe a déjà été défini." });
}
const passwordError = passwordRuleError(password);
if (passwordError) {
return res.status(400).json({ message: passwordError });
}
if (password !== confirmPassword) {
return res.status(400).json({ message: "Les deux mots de passe ne correspondent pas." });
}
const user = await prisma.user.findUnique({ where: { id: req.user.id } });
if (user.password && (await bcrypt.compare(password, user.password))) {
return res.status(400).json({
message: "Choisissez un mot de passe différent du mot de passe provisoire.",
});
}
const hashed = await bcrypt.hash(password, 10);
await prisma.user.update({
where: { id: user.id },
data: { password: hashed, mustChangePassword: false },
});
return res.json({ message: "Mot de passe enregistré." });
} catch (error) {
console.error("Erreur setInitialPassword:", error);
return res.status(500).json({ message: "Erreur serveur lors de l'enregistrement du mot de passe." });
}
};
const changePassword = async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
@@ -500,8 +571,9 @@ const changePassword = async (req, res) => {
return res.status(400).json({ message: "Les deux mots de passe sont requis." });
}
if (newPassword.length < 8) {
return res.status(400).json({ message: "Le nouveau mot de passe doit faire au moins 8 caractères." });
const passwordError = passwordRuleError(newPassword);
if (passwordError) {
return res.status(400).json({ message: passwordError });
}
const user = await prisma.user.findUnique({ where: { id: userId } });
@@ -536,7 +608,9 @@ module.exports = {
verifyCode,
resetPassword,
forgotPassword,
sendPasswordSetupLink,
changePassword,
setInitialPassword,
deleteAccount,
oauthCallback,
};