feat: add email notifications for registration, account deletion, and draw winner
- Add welcome email sent on user registration - Add account deletion confirmation email - Add draw winner notification email with celebratory design - Remove email verification requirement on registration - All emails have HTML templates with responsive design 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c31480886c
commit
7f4d4c35be
|
|
@ -5,10 +5,7 @@ import bcrypt from 'bcrypt';
|
||||||
import { pool } from '../../db.js';
|
import { pool } from '../../db.js';
|
||||||
import { AppError, asyncHandler } from '../middleware/errorHandler.js';
|
import { AppError, asyncHandler } from '../middleware/errorHandler.js';
|
||||||
import { generateToken, generateJWT, getTokenExpiry, isExpired } from '../utils/helpers.js';
|
import { generateToken, generateJWT, getTokenExpiry, isExpired } from '../utils/helpers.js';
|
||||||
import {
|
import { sendResetPasswordEmail, sendWelcomeEmail } from '../services/email.service.js';
|
||||||
sendVerificationEmail,
|
|
||||||
sendResetPasswordEmail,
|
|
||||||
} from '../services/email.service.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inscription d'un nouvel utilisateur
|
* Inscription d'un nouvel utilisateur
|
||||||
|
|
@ -27,30 +24,27 @@ export const register = asyncHandler(async (req, res, next) => {
|
||||||
// Hasher le mot de passe
|
// Hasher le mot de passe
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
// Générer un token de vérification
|
// Créer l'utilisateur (vérifié directement, sans vérification email)
|
||||||
const verificationToken = generateToken();
|
|
||||||
|
|
||||||
// Créer l'utilisateur
|
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`INSERT INTO users (email, password, first_name, last_name, phone, address, city, postal_code, verification_token)
|
`INSERT INTO users (email, password, first_name, last_name, phone, address, city, postal_code, is_verified)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, TRUE)
|
||||||
RETURNING id, email, first_name, last_name, role, created_at`,
|
RETURNING id, email, first_name, last_name, role, created_at`,
|
||||||
[email, hashedPassword, firstName, lastName, phone, address, city, postalCode, verificationToken]
|
[email, hashedPassword, firstName, lastName, phone, address, city, postalCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const user = result.rows[0];
|
const user = result.rows[0];
|
||||||
|
|
||||||
// Envoyer l'email de vérification
|
// Envoyer l'email de bienvenue
|
||||||
try {
|
try {
|
||||||
await sendVerificationEmail(email, verificationToken);
|
await sendWelcomeEmail(email, firstName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur envoi email:', error);
|
console.error('Erreur envoi email de bienvenue:', error);
|
||||||
// On ne bloque pas l'inscription si l'email échoue
|
// On ne bloque pas l'inscription si l'email échoue
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Inscription réussie. Veuillez vérifier votre email pour activer votre compte.',
|
message: 'Inscription réussie.',
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import { pool } from '../../db.js';
|
import { pool } from '../../db.js';
|
||||||
import { AppError, asyncHandler } from '../middleware/errorHandler.js';
|
import { AppError, asyncHandler } from '../middleware/errorHandler.js';
|
||||||
|
import { sendDrawWinnerEmail } from '../services/email.service.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupérer la liste des participants éligibles
|
* Récupérer la liste des participants éligibles
|
||||||
|
|
@ -179,6 +180,14 @@ export const conductDraw = asyncHandler(async (req, res) => {
|
||||||
console.log(` Participants éligibles: ${eligibleParticipants.length}`);
|
console.log(` Participants éligibles: ${eligibleParticipants.length}`);
|
||||||
console.log(` Total participants: ${totalParticipants}`);
|
console.log(` Total participants: ${totalParticipants}`);
|
||||||
|
|
||||||
|
// Envoyer l'email de notification au gagnant
|
||||||
|
try {
|
||||||
|
await sendDrawWinnerEmail(winner.email, winner.first_name, prizeName || 'Un an de thé d\'une valeur de 360€');
|
||||||
|
console.log(` 📧 Email de notification envoyé à ${winner.email}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur envoi email au gagnant:', error);
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Tirage au sort effectué avec succès!',
|
message: 'Tirage au sort effectué avec succès!',
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { pool } from '../../db.js';
|
import { pool } from '../../db.js';
|
||||||
import { AppError, asyncHandler } from '../middleware/errorHandler.js';
|
import { AppError, asyncHandler } from '../middleware/errorHandler.js';
|
||||||
|
import { sendAccountDeletedEmail } from '../services/email.service.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupérer le profil de l'utilisateur connecté
|
* Récupérer le profil de l'utilisateur connecté
|
||||||
|
|
@ -167,9 +168,23 @@ export const changePassword = asyncHandler(async (req, res, next) => {
|
||||||
export const deleteAccount = asyncHandler(async (req, res) => {
|
export const deleteAccount = asyncHandler(async (req, res) => {
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// Récupérer les infos de l'utilisateur avant suppression
|
||||||
|
const userResult = await pool.query(
|
||||||
|
'SELECT email, first_name FROM users WHERE id = $1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
const user = userResult.rows[0];
|
||||||
|
|
||||||
// Supprimer l'utilisateur (les tickets seront supprimés en cascade)
|
// Supprimer l'utilisateur (les tickets seront supprimés en cascade)
|
||||||
await pool.query('DELETE FROM users WHERE id = $1', [userId]);
|
await pool.query('DELETE FROM users WHERE id = $1', [userId]);
|
||||||
|
|
||||||
|
// Envoyer l'email de confirmation de suppression
|
||||||
|
try {
|
||||||
|
await sendAccountDeletedEmail(user.email, user.first_name);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur envoi email de suppression:', error);
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Compte supprimé avec succès',
|
message: 'Compte supprimé avec succès',
|
||||||
|
|
|
||||||
|
|
@ -320,9 +320,235 @@ export const sendContactEmail = async ({ fullName, email, subject, message }) =>
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un email de bienvenue à l'inscription
|
||||||
|
*/
|
||||||
|
export const sendWelcomeEmail = async (email, firstName) => {
|
||||||
|
const loginUrl = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/login`;
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }
|
||||||
|
.content { background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px; }
|
||||||
|
.button { display: inline-block; padding: 12px 30px; background: #667eea; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0; }
|
||||||
|
.features { background: white; border-radius: 10px; padding: 20px; margin: 20px 0; }
|
||||||
|
.feature { padding: 10px 0; border-bottom: 1px solid #eee; }
|
||||||
|
.feature:last-child { border-bottom: none; }
|
||||||
|
.footer { text-align: center; margin-top: 20px; color: #666; font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🍵 Bienvenue chez Thé Tip Top!</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2>Bonjour ${firstName}!</h2>
|
||||||
|
<p>Nous sommes ravis de vous accueillir dans notre jeu-concours Thé Tip Top!</p>
|
||||||
|
|
||||||
|
<div class="features">
|
||||||
|
<div class="feature">🎟️ <strong>Participez au jeu</strong> - Entrez vos codes tickets pour tenter de gagner</div>
|
||||||
|
<div class="feature">🎁 <strong>Gagnez des lots</strong> - 100% des tickets sont gagnants!</div>
|
||||||
|
<div class="feature">🏆 <strong>Gros lot</strong> - Tentez de remporter un an de thé d'une valeur de 360€</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Vous pouvez dès maintenant participer au jeu en vous connectant à votre compte:</p>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="${loginUrl}" class="button">Accéder à mon compte</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top: 30px; color: #666; font-size: 14px;">
|
||||||
|
Bonne chance et merci de votre participation!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© 2025 Thé Tip Top - Tous droits réservés</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const text = `
|
||||||
|
Bienvenue chez Thé Tip Top!
|
||||||
|
|
||||||
|
Bonjour ${firstName}!
|
||||||
|
|
||||||
|
Nous sommes ravis de vous accueillir dans notre jeu-concours Thé Tip Top!
|
||||||
|
|
||||||
|
- Participez au jeu en entrant vos codes tickets
|
||||||
|
- 100% des tickets sont gagnants!
|
||||||
|
- Tentez de remporter le gros lot: un an de thé d'une valeur de 360€
|
||||||
|
|
||||||
|
Connectez-vous pour participer: ${loginUrl}
|
||||||
|
|
||||||
|
Bonne chance!
|
||||||
|
`;
|
||||||
|
|
||||||
|
return sendEmail({
|
||||||
|
to: email,
|
||||||
|
subject: '🍵 Bienvenue au jeu-concours Thé Tip Top!',
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un email de confirmation de suppression de compte
|
||||||
|
*/
|
||||||
|
export const sendAccountDeletedEmail = async (email, firstName) => {
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { background: linear-gradient(135deg, #6c757d 0%, #495057 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }
|
||||||
|
.content { background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px; }
|
||||||
|
.footer { text-align: center; margin-top: 20px; color: #666; font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>👋 Au revoir ${firstName}!</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2>Votre compte a été supprimé</h2>
|
||||||
|
<p>Nous vous confirmons que votre compte Thé Tip Top a bien été supprimé.</p>
|
||||||
|
|
||||||
|
<p>Toutes vos données personnelles ont été effacées de notre système conformément à la réglementation RGPD.</p>
|
||||||
|
|
||||||
|
<p>Nous espérons vous revoir bientôt parmi nous!</p>
|
||||||
|
|
||||||
|
<p style="margin-top: 30px; color: #666; font-size: 14px;">
|
||||||
|
Si vous n'êtes pas à l'origine de cette demande, veuillez nous contacter immédiatement.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© 2025 Thé Tip Top - Tous droits réservés</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const text = `
|
||||||
|
Au revoir ${firstName}!
|
||||||
|
|
||||||
|
Votre compte a été supprimé.
|
||||||
|
|
||||||
|
Nous vous confirmons que votre compte Thé Tip Top a bien été supprimé.
|
||||||
|
Toutes vos données personnelles ont été effacées de notre système.
|
||||||
|
|
||||||
|
Nous espérons vous revoir bientôt parmi nous!
|
||||||
|
|
||||||
|
Si vous n'êtes pas à l'origine de cette demande, veuillez nous contacter immédiatement.
|
||||||
|
`;
|
||||||
|
|
||||||
|
return sendEmail({
|
||||||
|
to: email,
|
||||||
|
subject: '👋 Confirmation de suppression de compte - Thé Tip Top',
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un email au gagnant du tirage au sort
|
||||||
|
*/
|
||||||
|
export const sendDrawWinnerEmail = async (email, firstName, prizeName) => {
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { background: linear-gradient(135deg, #FFD700 0%, #FF6B35 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }
|
||||||
|
.content { background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px; }
|
||||||
|
.prize-box { background: linear-gradient(135deg, #fff9e6 0%, #fff3cd 100%); border: 3px solid #FFD700; border-radius: 15px; padding: 30px; margin: 20px 0; text-align: center; }
|
||||||
|
.confetti { font-size: 30px; }
|
||||||
|
.footer { text-align: center; margin-top: 20px; color: #666; font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🎊 FÉLICITATIONS ${firstName.toUpperCase()}! 🎊</h1>
|
||||||
|
<p style="font-size: 18px;">Vous êtes le GRAND GAGNANT du tirage au sort!</p>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="confetti">🎉🎈🎁🏆🎈🎉</div>
|
||||||
|
|
||||||
|
<div class="prize-box">
|
||||||
|
<h2 style="color: #FF6B35; margin: 0;">Vous avez remporté:</h2>
|
||||||
|
<h1 style="color: #FFD700; font-size: 28px; margin: 15px 0;">🏆 ${prizeName} 🏆</h1>
|
||||||
|
<p style="font-size: 18px; color: #666;">D'une valeur de 360€!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size: 16px;">Vous avez été sélectionné parmi tous les participants au jeu-concours Thé Tip Top pour remporter notre gros lot!</p>
|
||||||
|
|
||||||
|
<p><strong>Pour récupérer votre gain:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Nous vous contacterons très prochainement pour organiser la remise de votre lot</li>
|
||||||
|
<li>Veuillez vous munir d'une pièce d'identité</li>
|
||||||
|
<li>Vous pouvez également nous contacter directement</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p style="margin-top: 30px; color: #666; font-size: 14px;">
|
||||||
|
Merci d'avoir participé au jeu-concours Thé Tip Top!<br>
|
||||||
|
Toute l'équipe vous félicite chaleureusement! 🍵
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© 2025 Thé Tip Top - Tous droits réservés</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const text = `
|
||||||
|
🎊 FÉLICITATIONS ${firstName.toUpperCase()}! 🎊
|
||||||
|
|
||||||
|
Vous êtes le GRAND GAGNANT du tirage au sort Thé Tip Top!
|
||||||
|
|
||||||
|
Vous avez remporté: ${prizeName}
|
||||||
|
D'une valeur de 360€!
|
||||||
|
|
||||||
|
Vous avez été sélectionné parmi tous les participants pour remporter notre gros lot!
|
||||||
|
|
||||||
|
Pour récupérer votre gain:
|
||||||
|
- Nous vous contacterons très prochainement
|
||||||
|
- Veuillez vous munir d'une pièce d'identité
|
||||||
|
- Vous pouvez également nous contacter directement
|
||||||
|
|
||||||
|
Merci d'avoir participé au jeu-concours Thé Tip Top!
|
||||||
|
Toute l'équipe vous félicite chaleureusement!
|
||||||
|
`;
|
||||||
|
|
||||||
|
return sendEmail({
|
||||||
|
to: email,
|
||||||
|
subject: '🎊 FÉLICITATIONS! Vous êtes le GRAND GAGNANT du tirage Thé Tip Top!',
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
sendVerificationEmail,
|
sendVerificationEmail,
|
||||||
sendResetPasswordEmail,
|
sendResetPasswordEmail,
|
||||||
sendPrizeWinEmail,
|
sendPrizeWinEmail,
|
||||||
sendContactEmail,
|
sendContactEmail,
|
||||||
|
sendWelcomeEmail,
|
||||||
|
sendAccountDeletedEmail,
|
||||||
|
sendDrawWinnerEmail,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user