changement
This commit is contained in:
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user