implementation de nodemailer for email validation

This commit is contained in:
2026-06-26 14:01:38 +02:00
parent 45535e2b21
commit baa1b8ccc0
14 changed files with 5235 additions and 24 deletions

View File

@@ -1,12 +1,12 @@
const express = require("express");
const cors = require("cors");
require("dotenv").config();
const authRoutes = require("./routes/auth.routes");
const gameRoutes = require("./routes/game.routes");
const employeeRoutes = require("./routes/employee.routes");
const adminRoutes = require("./routes/admin.routes");
const swaggerUi = require("swagger-ui-express");
const swaggerSpec = require("./docs/swagger");
@@ -15,24 +15,18 @@ const app = express();
app.use(cors());
app.use(express.json());
// routes
app.use("/api/auth", authRoutes);
app.use("/api/game", gameRoutes);
app.use("/api/employee", employeeRoutes);
app.use("/api/admin", adminRoutes);
app.use(
"/api-docs",
swaggerUi.serve,
swaggerUi.setup(swaggerSpec)
);
// swagger
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// test route
app.get("/", (req, res) => {
res.json({
message: "API Thé Tip Top opérationnelle",
});
res.json({ message: "API Thé Tip Top opérationnelle" });
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Serveur lancé sur http://localhost:${PORT}`);
});
module.exports = app;

View File

@@ -0,0 +1,13 @@
const nodemailer = require("nodemailer");
const transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST,
port: Number(process.env.MAIL_PORT),
secure: false,
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
},
});
module.exports = transporter;

View File

@@ -1,6 +1,8 @@
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const prisma = require("../config/prisma");
const transporter = require("../config/mailer");
const register = async (req, res) => {
try {
@@ -32,6 +34,9 @@ const register = async (req, res) => {
const hashedPassword = await bcrypt.hash(password, 10);
const emailVerifyToken = crypto.randomBytes(32).toString("hex");
const emailVerifyExpires = new Date(Date.now() + 1000 * 60 * 60); // 1h
const user = await prisma.user.create({
data: {
firstname,
@@ -41,6 +46,9 @@ const register = async (req, res) => {
birthdate: birthdate ? new Date(birthdate) : null,
gender,
marketingOptIn: marketingOptIn || false,
isEmailVerified: false,
emailVerifyToken,
emailVerifyExpires,
},
select: {
id: true,
@@ -49,12 +57,28 @@ const register = async (req, res) => {
email: true,
role: true,
marketingOptIn: true,
isEmailVerified: true,
createdAt: true,
},
});
const verifyUrl = `${process.env.APP_URL}/api/auth/verify-email/${emailVerifyToken}`;
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: email,
subject: "Validation de votre compte Thé Tip Top",
html: `
<h2>Bienvenue ${firstname}</h2>
<p>Merci pour votre inscription.</p>
<p>Veuillez cliquer sur le lien ci-dessous pour valider votre compte :</p>
<a href="${verifyUrl}">Valider mon compte</a>
<p>Ce lien expire dans 1 heure.</p>
`,
});
return res.status(201).json({
message: "Inscription réussie.",
message: "Inscription réussie. Un email de validation a été envoyé.",
user,
});
} catch (error) {
@@ -65,6 +89,45 @@ const register = async (req, res) => {
}
};
const verifyEmail = async (req, res) => {
try {
const { token } = req.params;
const user = await prisma.user.findFirst({
where: {
emailVerifyToken: token,
emailVerifyExpires: {
gt: new Date(),
},
},
});
if (!user) {
return res.status(400).json({
message: "Lien de validation invalide ou expiré.",
});
}
await prisma.user.update({
where: { id: user.id },
data: {
isEmailVerified: true,
emailVerifyToken: null,
emailVerifyExpires: null,
},
});
return res.status(200).json({
message: "Email validé avec succès. Vous pouvez maintenant vous connecter.",
});
} catch (error) {
console.error("Erreur verifyEmail:", error);
return res.status(500).json({
message: "Erreur serveur lors de la validation de l'email.",
});
}
};
const login = async (req, res) => {
try {
const { email, password } = req.body;
@@ -85,6 +148,12 @@ const login = async (req, res) => {
});
}
if (!user.isEmailVerified) {
return res.status(403).json({
message: "Veuillez valider votre adresse email avant de vous connecter.",
});
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
@@ -121,6 +190,114 @@ const login = async (req, res) => {
}
};
const forgotPassword = async (req, res) => {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({
message: "Email requis.",
});
}
const user = await prisma.user.findUnique({
where: { email },
});
// 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é.",
});
}
const resetPasswordToken = crypto.randomBytes(32).toString("hex");
const resetPasswordExpires = new Date(Date.now() + 1000 * 60 * 30); // 30 min
await prisma.user.update({
where: { id: user.id },
data: {
resetPasswordToken,
resetPasswordExpires,
},
});
const resetUrl = `${process.env.APP_URL}/api/auth/reset-password/${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é.",
});
} catch (error) {
console.error("Erreur forgotPassword:", error);
return res.status(500).json({
message: "Erreur serveur lors de la demande de réinitialisation.",
});
}
};
const resetPassword = async (req, res) => {
try {
const { token } = req.params;
const { password } = 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 user = await prisma.user.findFirst({
where: {
resetPasswordToken: token,
resetPasswordExpires: {
gt: new Date(),
},
},
});
if (!user) {
return res.status(400).json({
message: "Lien de réinitialisation invalide ou expiré.",
});
}
const hashedPassword = await bcrypt.hash(password, 10);
await prisma.user.update({
where: { id: user.id },
data: {
password: hashedPassword,
resetPasswordToken: null,
resetPasswordExpires: null,
},
});
return res.status(200).json({
message: "Mot de passe réinitialisé avec succès. Vous pouvez maintenant vous connecter.",
});
} catch (error) {
console.error("Erreur resetPassword:", error);
return res.status(500).json({
message: "Erreur serveur lors de la réinitialisation du mot de passe.",
});
}
};
const me = async (req, res) => {
return res.json({
user: req.user,
@@ -131,4 +308,7 @@ module.exports = {
register,
login,
me,
verifyEmail,
resetPassword,
forgotPassword
};

View File

@@ -1,5 +1,5 @@
const express = require("express");
const { register, login, me } = require("../controllers/auth.controller");
const { register, login, verifyEmail, forgotPassword, resetPassword, me } = require("../controllers/auth.controller");
const authMiddleware = require("../middlewares/auth.middleware");
const validate = require("../middlewares/validate.middleware");
const { registerSchema, loginSchema } = require("../validations/auth.validation");
@@ -63,7 +63,99 @@ router.post("/login", validate(loginSchema), login);
* 200:
* description: Profil utilisateur
*/
router.get("/me", authMiddleware, me);
/**
* @swagger
* /api/auth/verify-email/{token}:
* get:
* summary: Vérification de l'adresse email
* tags:
* - Auth
* parameters:
* - in: path
* name: token
* required: true
* schema:
* type: string
* description: Token de validation envoyé par email
* responses:
* 200:
* description: Email vérifié avec succès.
* 400:
* description: Lien de validation invalide ou expiré.
* 500:
* description: Erreur serveur.
*/
router.get("/verify-email/:token",verifyEmail);
/**
* @swagger
* /api/auth/forgot-password:
* post:
* summary: Demande de réinitialisation du mot de passe
* tags:
* - Auth
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - email
* properties:
* email:
* type: string
* format: email
* example: jean.dupont@gmail.com
* responses:
* 200:
* description: Si un compte existe avec cet email, un lien de réinitialisation a été envoyé.
* 400:
* description: Email requis.
* 500:
* description: Erreur serveur.
*/
router.post("/forgot-password", forgotPassword);
/**
* @swagger
* /api/auth/reset-password/{token}:
* post:
* summary: Réinitialisation du mot de passe
* tags:
* - Auth
* parameters:
* - in: path
* name: token
* required: true
* schema:
* type: string
* description: Token de réinitialisation reçu par email
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - password
* properties:
* password:
* type: string
* format: password
* example: NouveauPassword123
* responses:
* 200:
* description: Mot de passe réinitialisé avec succès.
* 400:
* description: Lien invalide, expiré ou mot de passe invalide.
* 500:
* description: Erreur serveur.
*/
router.post("/reset-password/:token",resetPassword);
module.exports = router;