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 { const { firstname, lastname, email, password, birthdate, gender, marketingOptIn, } = req.body; if (!firstname || !lastname || !email || !password) { return res.status(400).json({ message: "Tous les champs obligatoires doivent être renseignés.", }); } const existingUser = await prisma.user.findUnique({ where: { email }, }); if (existingUser) { return res.status(409).json({ message: "Un compte existe déjà avec cet email.", }); } const hashedPassword = await bcrypt.hash(password, 10); 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: { firstname, lastname, email, password: hashedPassword, birthdate: birthdate ? new Date(birthdate) : null, gender, marketingOptIn: marketingOptIn || false, isEmailVerified: false, emailVerifyToken, emailVerifyExpires, }, select: { id: true, firstname: true, lastname: true, email: true, role: true, marketingOptIn: true, isEmailVerified: true, createdAt: true, }, }); // Le code de validation est le seul moyen d'activer le compte. Si l'envoi // échoue, laisser l'utilisateur en base le bloquerait définitivement : il // ne pourrait ni se connecter (email non vérifié) ni se réinscrire (email // déjà pris). On annule donc la création pour que la tentative reste // rejouable. try { await transporter.sendMail({ from: process.env.MAIL_FROM, to: email, subject: "Votre code de validation Thé Tip Top", html: `
Merci pour votre inscription sur Thé Tip Top.
Voici votre code de validation à 4 chiffres :
Ce code expire dans 15 minutes.
Bonjour ${user.firstname},
Vous avez demandé la réinitialisation de votre mot de passe.
Cliquez sur le lien ci-dessous pour définir un nouveau mot de passe :
Réinitialiser mon mot de passeCe lien expire dans 30 minutes.
Si vous n'êtes pas à l'origine de cette demande, ignorez cet email.
`, }); 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, 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.", }); } // La confirmation est vérifiée côté serveur aussi : le contrôle du // navigateur ne protège pas un appel direct à l'API. if (confirmPassword !== undefined && password !== confirmPassword) { return res.status(400).json({ message: "Les deux mots de passe ne correspondent pas.", }); } 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 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." }); } }; // Fin du parcours OAuth : passport a posé l'utilisateur sur req.user, on émet // le même JWT que la connexion classique et on renvoie le navigateur vers le // front, qui récupère le token dans l'URL. const oauthCallback = (req, res) => { const frontendUrl = process.env.FRONTEND_URL || "http://localhost:5173"; try { const user = req.user; if (!user) { return res.redirect( `${frontendUrl}/auth/callback?error=${encodeURIComponent( "Connexion externe refusée." )}` ); } const token = jwt.sign( { id: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: "24h" } ); const payload = Buffer.from( JSON.stringify({ id: user.id, firstname: user.firstname, lastname: user.lastname, email: user.email, role: user.role, }) ).toString("base64"); return res.redirect(`${frontendUrl}/auth/callback?token=${token}&user=${payload}`); } catch (error) { console.error("Erreur oauthCallback:", error); return res.redirect( `${frontendUrl}/auth/callback?error=${encodeURIComponent( "Erreur serveur lors de la connexion externe." )}` ); } }; 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. Détacher les tickets du compte sans les remettre en jeu : un code // figure sur un ticket de caisse physique et n'est jouable qu'une // fois. Le réinitialiser permettrait de rejouer indéfiniment le même // code en supprimant puis recréant un compte. await tx.ticket.updateMany({ where: { userId }, data: { userId: 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 } }); if (!user.password) { return res.status(400).json({ message: "Ce compte utilise une connexion externe et n'a pas de mot de passe à modifier.", }); } 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, changePassword, deleteAccount, oauthCallback, };