merge: integrate Alexis latest changes

This commit is contained in:
2026-08-17 11:46:41 +00:00
60 changed files with 5959 additions and 686 deletions

View File

@@ -4,9 +4,10 @@ DATABASE_URL="postgresql://postgres:password@localhost:5432/the_tip_top_db"
JWT_SECRET="your_secret_key"
APP_URL=http://localhost:5000
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USER=yourmail_adress@gmail.com
MAIL_PASS=_______________________
MAIL_FROM="Thé Tip Top <yourmail_adress@gmail.com>"
APP_URL="http://localhost:5000"
MAIL_HOST="smtp.gmail.com"
MAIL_PORT="587"
MAIL_USER="engongomo.emma27@gmail.com"
MAIL_PASS="rczo zqps hsnm msdm"
MAIL_FROM="Thé Tip Top <engongomo.emma27@gmail.com>"

View File

@@ -14,7 +14,7 @@
"cors": "^2.8.6",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"nodemailer": "^9.0.1",
"nodemailer": "^9.0.3",
"prisma": "^6.19.3",
"swagger-jsdoc": "^6.3.0",
"swagger-ui-express": "^5.0.1",
@@ -109,6 +109,7 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -604,31 +605,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@emnapi/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
@@ -636,7 +612,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -1988,6 +1963,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -2306,6 +2282,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.38",
"caniuse-lite": "^1.0.30001799",
@@ -3172,6 +3149,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -5487,9 +5465,9 @@
}
},
"node_modules/nodemailer": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz",
"integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==",
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz",
"integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
@@ -5924,6 +5902,7 @@
"integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/config": "6.19.3",
"@prisma/engines": "6.19.3"

View File

@@ -21,7 +21,7 @@
"cors": "^2.8.6",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"nodemailer": "^9.0.1",
"nodemailer": "^9.0.3",
"prisma": "^6.19.3",
"swagger-jsdoc": "^6.3.0",
"swagger-ui-express": "^5.0.1",

View File

@@ -34,8 +34,8 @@ 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 emailVerifyToken = String(Math.floor(1000 + Math.random() * 9000));
const emailVerifyExpires = new Date(Date.now() + 1000 * 60 * 15); // 15 min
const user = await prisma.user.create({
data: {
@@ -62,23 +62,25 @@ const register = async (req, res) => {
},
});
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",
subject: "Votre code de validation 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>
<div style="font-family:sans-serif;max-width:480px;margin:auto">
<h2 style="color:#006B6B">Bienvenue ${firstname} !</h2>
<p>Merci pour votre inscription sur Thé Tip Top.</p>
<p>Voici votre code de validation à 4 chiffres :</p>
<div style="font-size:36px;font-weight:bold;letter-spacing:12px;color:#006B6B;text-align:center;padding:20px 0">
${emailVerifyToken}
</div>
<p style="color:#888;font-size:13px">Ce code expire dans <strong>15 minutes</strong>.</p>
</div>
`,
});
return res.status(201).json({
message: "Inscription réussie. Un email de validation a été envoyé.",
message: "Inscription réussie. Un code de validation a été envoyé par email.",
user,
});
} catch (error) {
@@ -148,7 +150,7 @@ const login = async (req, res) => {
});
}
if (!user.isEmailVerified) {
if (!user.isEmailVerified && user.role === "CLIENT") {
return res.status(403).json({
message: "Veuillez valider votre adresse email avant de vous connecter.",
});
@@ -298,17 +300,144 @@ const resetPassword = async (req, res) => {
}
};
const verifyCode = async (req, res) => {
try {
const { email, code } = req.body;
if (!email || !code) {
return res.status(400).json({ message: "Email et code requis." });
}
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
return res.status(404).json({ message: "Compte introuvable." });
}
if (user.isEmailVerified) {
return res.status(400).json({ message: "Ce compte est déjà vérifié." });
}
if (!user.emailVerifyToken || user.emailVerifyToken !== String(code)) {
return res.status(400).json({ message: "Code invalide." });
}
if (new Date() > user.emailVerifyExpires) {
return res.status(400).json({ message: "Code expiré. Veuillez vous réinscrire." });
}
await prisma.user.update({
where: { id: user.id },
data: { isEmailVerified: true, emailVerifyToken: null, emailVerifyExpires: null },
});
const token = jwt.sign(
{ id: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "24h" }
);
return res.json({
message: "Compte vérifié avec succès.",
token,
user: {
id: user.id,
firstname: user.firstname,
lastname: user.lastname,
email: user.email,
role: user.role,
},
});
} catch (error) {
console.error("Erreur verifyCode:", error);
return res.status(500).json({ message: "Erreur serveur lors de la vérification." });
}
};
const me = async (req, res) => {
return res.json({
user: req.user,
});
};
const deleteAccount = async (req, res) => {
try {
const userId = req.user.id;
await prisma.$transaction(async (tx) => {
// 1. Supprimer le grand gagnant si c'est lui
await tx.grandPrizeWinner.deleteMany({ where: { userId } });
// 2. Récupérer les participations pour supprimer leurs claims
const participations = await tx.participation.findMany({
where: { userId },
select: { id: true },
});
const participationIds = participations.map((p) => p.id);
// 3. Supprimer les claims
if (participationIds.length > 0) {
await tx.claim.deleteMany({ where: { participationId: { in: participationIds } } });
}
// 4. Supprimer les participations
await tx.participation.deleteMany({ where: { userId } });
// 5. Libérer les tickets utilisés par ce user
await tx.ticket.updateMany({
where: { userId },
data: { userId: null, isUsed: false, usedAt: null },
});
// 6. Supprimer l'utilisateur
await tx.user.delete({ where: { id: userId } });
});
return res.json({ message: "Compte supprimé avec succès." });
} catch (error) {
console.error("Erreur deleteAccount:", error);
return res.status(500).json({ message: "Erreur serveur lors de la suppression du compte." });
}
};
const changePassword = async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
const userId = req.user.id;
if (!currentPassword || !newPassword) {
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 user = await prisma.user.findUnique({ where: { id: userId } });
const isValid = await bcrypt.compare(currentPassword, user.password);
if (!isValid) {
return res.status(401).json({ message: "Mot de passe actuel incorrect." });
}
const hashed = await bcrypt.hash(newPassword, 10);
await prisma.user.update({ where: { id: userId }, data: { password: hashed } });
return res.json({ message: "Mot de passe modifié avec succès." });
} catch (error) {
console.error("Erreur changePassword:", error);
return res.status(500).json({ message: "Erreur serveur lors du changement de mot de passe." });
}
};
module.exports = {
register,
login,
me,
verifyEmail,
verifyCode,
resetPassword,
forgotPassword
forgotPassword,
changePassword,
deleteAccount,
};

View File

@@ -1,5 +1,5 @@
const express = require("express");
const { register, login, verifyEmail, forgotPassword, resetPassword, me } = require("../controllers/auth.controller");
const { register, login, verifyEmail, verifyCode, forgotPassword, resetPassword, me, changePassword, deleteAccount } = require("../controllers/auth.controller");
const authMiddleware = require("../middlewares/auth.middleware");
const validate = require("../middlewares/validate.middleware");
const { registerSchema, loginSchema } = require("../validations/auth.validation");
@@ -155,7 +155,10 @@ router.post("/forgot-password", forgotPassword);
* 500:
* description: Erreur serveur.
*/
router.post("/reset-password/:token",resetPassword);
router.post("/reset-password/:token", resetPassword);
router.post("/verify-code", verifyCode);
router.patch("/change-password", authMiddleware, changePassword);
router.delete("/account", authMiddleware, deleteAccount);
module.exports = router;

View File

@@ -0,0 +1,39 @@
const bcrypt = require('bcrypt')
const prisma = require('../config/prisma')
async function createAdmin() {
const email = 'admin@thetiptop.fr'
const password = 'Admin@TipTop2026!'
const existing = await prisma.user.findUnique({ where: { email } })
if (existing) {
await prisma.user.update({
where: { email },
data: { isEmailVerified: true, role: 'ADMIN' },
})
console.log('Admin mis à jour :', email)
return
}
const hashed = await bcrypt.hash(password, 10)
await prisma.user.create({
data: {
firstname: 'Admin',
lastname: 'TipTop',
email,
password: hashed,
role: 'ADMIN',
isEmailVerified: true,
},
})
console.log('Admin créé avec succès')
console.log(' Email :', email)
console.log(' Mot de passe:', password)
}
createAdmin()
.catch((e) => { console.error('Erreur :', e.message); process.exit(1) })
.finally(() => prisma.$disconnect())