422 lines
11 KiB
JavaScript
Executable File
422 lines
11 KiB
JavaScript
Executable File
require("dotenv").config();
|
|
|
|
const bcrypt = require("bcrypt");
|
|
const jwt = require("jsonwebtoken");
|
|
const Joi = require("joi");
|
|
const nodemailer = require("nodemailer");
|
|
const tables = require("../tables");
|
|
|
|
const secretKey = process.env.APP_SECRET;
|
|
const saltRounds = 10;
|
|
|
|
const passwordSchema = Joi.string()
|
|
.min(8)
|
|
.regex(/[A-Z]/)
|
|
.message("Le mot de passe doit contenir au moins une majuscule.")
|
|
.regex(/\d/)
|
|
.message("Le mot de passe doit contenir au moins un chiffre.")
|
|
.regex(/[!@#$%^&*()_+{}[\]:;<>,.?~\\/-]/)
|
|
.message("Le mot de passe doit contenir au moins un caractère spécial.");
|
|
|
|
const nicknameSchema = Joi.string()
|
|
.alphanum()
|
|
.min(3)
|
|
.message("Le pseudo doit contenir au moins 3 caractères alphanumériques.");
|
|
|
|
const emailSchema = Joi.string().email().message("Format d'e-mail invalide.");
|
|
|
|
const validatePasswordComplexity = async (password) => {
|
|
await passwordSchema.validateAsync(password);
|
|
};
|
|
|
|
const validateEmail = async (email) => {
|
|
await emailSchema.validateAsync(email);
|
|
};
|
|
|
|
const validateUniqueNickname = async (nickname) => {
|
|
await nicknameSchema.validateAsync(nickname);
|
|
const existingUserByNickname = await tables.users.getByNickname(
|
|
nickname
|
|
);
|
|
|
|
if (existingUserByNickname) {
|
|
throw new Error("Ce pseudo est déjà pris.");
|
|
}
|
|
};
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
service: "gmail",
|
|
auth: {
|
|
user: process.env.EMAIL_USER,
|
|
pass: process.env.EMAIL_PASS,
|
|
},
|
|
});
|
|
|
|
const generateResetToken = (user) => {
|
|
const resetToken = jwt.sign({ user: user.id }, resetTokenSecret, {
|
|
expiresIn: "1h",
|
|
});
|
|
|
|
const base64Token = Buffer.from(resetToken).toString("base64");
|
|
|
|
return base64Token;
|
|
};
|
|
|
|
const sendPasswordResetEmail = async (user, resetToken) => {
|
|
const resetLink = `${process.env.FRONTEND_URL}/reset-password/${resetToken}`;
|
|
const mailOptions = {
|
|
from: "INSERT_YOUR_EMAIL_ADDRESS_HERE",
|
|
to: user.mail,
|
|
subject: "Réinitialisation de mot de passe",
|
|
html: `
|
|
<p>Bonjour ${user.firstname},</p>
|
|
<p>Vous avez demandé une réinitialisation de mot de passe pour votre compte.</p>
|
|
<p>Cliquez sur le lien ci-dessous pour réinitialiser votre mot de passe :</p>
|
|
<a href="${resetLink}">Réinitialiser le mot de passe</a>
|
|
<p>Si vous n'avez pas demandé cette réinitialisation, veuillez ignorer ce message.</p>
|
|
<p>Merci,</p>
|
|
`,
|
|
};
|
|
|
|
await transporter.sendMail(mailOptions);
|
|
};
|
|
|
|
const forgottenPassword = async (req, res) => {
|
|
const { mail } = req.body;
|
|
|
|
try {
|
|
const user = await tables.users.getByMail(mail);
|
|
if (!user) {
|
|
return res.status(404).json({ message: "Utilisateur non trouvé" });
|
|
}
|
|
|
|
const resetToken = generateResetToken(user);
|
|
|
|
await sendPasswordResetEmail(user, resetToken);
|
|
|
|
return res.status(200).json({
|
|
message: "Envoi d'un e-mail de réinitialisation du mot de passe",
|
|
});
|
|
} catch (error) {
|
|
console.error(
|
|
"Erreur dans l'envoi de l'e-mail de réinitialisation du mot de passe:",
|
|
error
|
|
);
|
|
return res.status(500).json({
|
|
message:
|
|
"Erreur dans l'envoi de l'e-mail de réinitialisation du mot de passe",
|
|
});
|
|
}
|
|
};
|
|
const resetPassword = async (req, res) => {
|
|
const { password } = req.body;
|
|
|
|
const resetToken = decodeURIComponent(req.params.token);
|
|
|
|
try {
|
|
const decodedToken = jwt.verify(resetToken, resetTokenSecret);
|
|
|
|
const user = await tables.users.read(decodedToken.user);
|
|
|
|
if (!user) {
|
|
return res.status(404).json({ message: "Utilisateur non trouvé" });
|
|
}
|
|
|
|
if (!password) {
|
|
return res.status(400).json({ message: "Nouveau mot de passe manquant" });
|
|
}
|
|
|
|
const { error } = Joi.string()
|
|
.min(8)
|
|
.regex(/[A-Z]/)
|
|
.message("Le mot de passe doit contenir au moins une majuscule.")
|
|
.regex(/\d/)
|
|
.message("Le mot de passe doit contenir au moins un chiffre.")
|
|
.regex(/[!@#$%^&*()_+{}[\]:;<>,.?~\\/-]/)
|
|
.message("Le mot de passe doit contenir au moins un caractère spécial.")
|
|
.validate(password);
|
|
|
|
if (error) {
|
|
return res.status(400).json({ message: error.details[0].message });
|
|
}
|
|
|
|
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
|
await tables.users.edit(user.id, { password: hashedPassword });
|
|
|
|
return res
|
|
.status(200)
|
|
.json({ message: "Réinitialisation du mot de passe réussie" });
|
|
} catch (error) {
|
|
console.error("Erreur de réinitialisation du mot de passe:", error);
|
|
return res.status(500).json({
|
|
message: "Erreur de réinitialisation du mot de passe",
|
|
error,
|
|
});
|
|
}
|
|
};
|
|
|
|
const login = async (req, res) => {
|
|
const { mail, password } = req.body;
|
|
|
|
try {
|
|
const user = await tables.users.getByMail(mail);
|
|
|
|
if (!user) {
|
|
return res.status(401).json({ message: "Adresse email introuvable" });
|
|
}
|
|
|
|
const passwordMatch = await bcrypt.compare(password, user.password);
|
|
|
|
if (!passwordMatch) {
|
|
return res.status(401).json({ message: "Mot de passe incorrect" });
|
|
}
|
|
|
|
const token = jwt.sign({ user: user.id }, secretKey);
|
|
|
|
return res.status(200).json({
|
|
message: "Connexion réussie",
|
|
user: { ...user, credits: user.credits },
|
|
token,
|
|
});
|
|
} catch (error) {
|
|
console.error("Erreur lors de la connexion :", error);
|
|
return res.status(500).json({ message: "Erreur interne du serveur" });
|
|
}
|
|
};
|
|
|
|
const browse = async (req, res, next) => {
|
|
try {
|
|
const users = await tables.users.readAll();
|
|
res.json(users);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
const read = async (req, res, next) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { field } = req.query;
|
|
|
|
const user = await tables.users.read(id);
|
|
|
|
if (field && user && user[field]) {
|
|
res.json({ [field]: user[field] });
|
|
} else if (user) {
|
|
res.json(user);
|
|
} else {
|
|
res.sendStatus(404);
|
|
}
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
const edit = async (req, res) => {
|
|
const userId = req.params.id;
|
|
|
|
try {
|
|
if (!req.body) {
|
|
return res.status(400).json({ message: "Corps de la requête vide" });
|
|
}
|
|
|
|
const {
|
|
nickname,
|
|
mail,
|
|
newPassword,
|
|
tetardcoin,
|
|
currentPassword,
|
|
} = req.body;
|
|
|
|
const user = await tables.users.read(userId);
|
|
|
|
if (!user) {
|
|
return res.status(404).json({ message: "Utilisateur non trouvé" });
|
|
}
|
|
|
|
const isCurrentPasswordCorrect = await bcrypt.compare(
|
|
currentPassword,
|
|
user.password
|
|
);
|
|
|
|
if (!isCurrentPasswordCorrect) {
|
|
return res.status(401).json({ message: "Mot de passe actuel incorrect" });
|
|
}
|
|
|
|
const errors = {};
|
|
|
|
if (nickname !== undefined) {
|
|
if (nickname.trim() === "") {
|
|
errors.nickname = "Le pseudo ne peut pas être vide";
|
|
} else if (nickname.trim() !== user.nickname.trim()) {
|
|
try {
|
|
await validateUniqueNickname(nickname);
|
|
} catch (error) {
|
|
errors.nickname = error.message;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mail !== undefined && mail.trim() === "") {
|
|
errors.mail = "L'e-mail ne peut pas être vide";
|
|
}
|
|
|
|
if (newPassword !== undefined && newPassword.trim() !== "") {
|
|
try {
|
|
await validatePasswordComplexity(newPassword.trim());
|
|
} catch (error) {
|
|
errors.newPassword = error.message;
|
|
}
|
|
}
|
|
|
|
if (Object.keys(errors).length > 0) {
|
|
return res.status(400).json({ errors });
|
|
}
|
|
|
|
const updatedFields = {};
|
|
|
|
if (nickname !== undefined) {
|
|
updatedFields.nickname = nickname;
|
|
}
|
|
|
|
if (mail !== undefined) {
|
|
updatedFields.mail = mail;
|
|
}
|
|
|
|
if (newPassword !== undefined && newPassword.trim() !== "") {
|
|
updatedFields.password = await bcrypt.hash(
|
|
newPassword.trim(),
|
|
saltRounds
|
|
);
|
|
}
|
|
|
|
const affectedRows = await tables.users.edit(userId, updatedFields);
|
|
|
|
if (affectedRows === 0) {
|
|
return res.status(500).json({ message: "Échec de la mise à jour" });
|
|
}
|
|
|
|
const editedUser = await tables.users.read(userId);
|
|
|
|
return res.json({ message: "Mis à jour", user: editedUser });
|
|
} catch (error) {
|
|
return res.status(500).json({
|
|
message: "Erreur lors de la mise à jour de l'utilisateur",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
const add = async (req, res, next) => {
|
|
try {
|
|
const { nickname, mail, password, confirmPassword } =
|
|
req.body;
|
|
|
|
const existingUserByMail = await tables.users.getByMail(mail);
|
|
if (existingUserByMail) {
|
|
return res
|
|
.status(400)
|
|
.json({ message: "Cette adresse e-mail est déjà enregistrée." });
|
|
}
|
|
|
|
const existingUserByNickname = await tables.users.getByNickname(
|
|
nickname
|
|
);
|
|
if (existingUserByNickname) {
|
|
return res.status(400).json({ message: "Ce pseudo est déjà pris." });
|
|
}
|
|
|
|
await validateEmail(mail);
|
|
|
|
if (password !== confirmPassword) {
|
|
return res
|
|
.status(400)
|
|
.json({ message: "Les mots de passe ne correspondent pas." });
|
|
}
|
|
|
|
try {
|
|
await passwordSchema.validateAsync(password);
|
|
} catch (validationError) {
|
|
return res.status(400).json({ message: validationError.message });
|
|
}
|
|
|
|
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
|
|
|
const user = {
|
|
firstname,
|
|
lastname,
|
|
nickname,
|
|
mail,
|
|
password: hashedPassword,
|
|
tetardcoin: 1000,
|
|
};
|
|
|
|
const insertId = await tables.users.create(user);
|
|
|
|
const token = jwt.sign({ user: user.id }, secretKey);
|
|
|
|
res.status(201).json({ insertId, token });
|
|
return insertId;
|
|
} catch (err) {
|
|
console.error("Erreur lors de l'enregistrement de l'utilisateur :", err);
|
|
return next(err);
|
|
}
|
|
};
|
|
|
|
const destroy = async (req, res) => {
|
|
try {
|
|
const userId = req.params.id;
|
|
const { currentPassword } = req.body;
|
|
|
|
if (!currentPassword) {
|
|
return res.status(400).json({
|
|
errors: {
|
|
currentPassword:
|
|
"Le mot de passe actuel est requis pour la suppression du compte.",
|
|
},
|
|
});
|
|
}
|
|
|
|
const user = await tables.users.getById(userId);
|
|
|
|
const isCurrentPasswordCorrect = await bcrypt.compare(
|
|
currentPassword,
|
|
user.password
|
|
);
|
|
|
|
if (!isCurrentPasswordCorrect) {
|
|
return res.status(401).json({
|
|
errors: {
|
|
currentPassword: "Le mot de passe actuel est incorrect.",
|
|
},
|
|
});
|
|
}
|
|
|
|
await tables.users.delete(userId);
|
|
|
|
res.sendStatus(204);
|
|
} catch (err) {
|
|
console.error("Erreur lors de la suppression du compte :", err);
|
|
|
|
res.status(500).json({
|
|
errors: {
|
|
general: "Une erreur s'est produite lors de la suppression du compte.",
|
|
},
|
|
});
|
|
}
|
|
|
|
return Promise.resolve();
|
|
};
|
|
|
|
|
|
module.exports = {
|
|
browse,
|
|
read,
|
|
edit,
|
|
add,
|
|
destroy,
|
|
login,
|
|
forgottenPassword,
|
|
resetPassword,
|
|
};
|