feat: add newsletter subscription feature
- Add newsletter database table migration - Create newsletter controller with subscribe/unsubscribe endpoints - Add newsletter routes and validation - Implement newsletter service with email validation - Add setup documentation and migration scripts - Include test page for newsletter functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3e08a647a5
commit
4759ce99e7
126
NEWSLETTER_SETUP.md
Normal file
126
NEWSLETTER_SETUP.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Newsletter Feature - Installation Guide
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
La fonctionnalité newsletter a été ajoutée au backend et frontend. Elle permet aux utilisateurs de s'abonner à la newsletter depuis le footer du site.
|
||||
|
||||
## Backend
|
||||
|
||||
### Fichiers créés :
|
||||
|
||||
1. **Migration de base de données** : `database/migrations/add-newsletter-table.sql`
|
||||
2. **Controller** : `src/controllers/newsletter.controller.js`
|
||||
3. **Service** : `src/services/newsletter.service.js`
|
||||
4. **Routes** : `src/routes/newsletter.routes.js`
|
||||
5. **Validation** : `src/validations/newsletter.validation.js`
|
||||
|
||||
### Endpoints API :
|
||||
|
||||
- `POST /api/newsletter/subscribe` - S'abonner (public)
|
||||
- `POST /api/newsletter/unsubscribe` - Se désabonner (public)
|
||||
- `GET /api/newsletter/subscribers` - Liste des abonnés (Admin seulement)
|
||||
- `GET /api/newsletter/count` - Nombre d'abonnés actifs (Admin seulement)
|
||||
|
||||
## Frontend
|
||||
|
||||
### Fichiers modifiés/créés :
|
||||
|
||||
1. **Service** : `services/newsletter.service.ts`
|
||||
2. **Constants** : `utils/constants.ts` (ajout des endpoints newsletter)
|
||||
3. **Footer** : `components/Footer.tsx` (ajout du formulaire d'inscription)
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Appliquer la migration de base de données
|
||||
|
||||
Exécutez le script SQL pour créer la table `newsletters` :
|
||||
|
||||
```bash
|
||||
# Depuis le répertoire backend
|
||||
psql -h 51.75.24.29 -U postgres -d thetiptop_dev -p 5433 -f database/migrations/add-newsletter-table.sql
|
||||
```
|
||||
|
||||
Ou connectez-vous à votre base de données et exécutez manuellement le contenu du fichier `database/migrations/add-newsletter-table.sql`.
|
||||
|
||||
### 2. Redémarrer le backend
|
||||
|
||||
```bash
|
||||
# Dans le répertoire backend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. Redémarrer le frontend
|
||||
|
||||
```bash
|
||||
# Dans le répertoire frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Pour les utilisateurs :
|
||||
|
||||
- Formulaire d'inscription à la newsletter dans le footer
|
||||
- Validation d'email en temps réel
|
||||
- Messages de confirmation/erreur
|
||||
- Email de confirmation d'abonnement automatique
|
||||
- Possibilité de se désabonner
|
||||
|
||||
### Pour les administrateurs :
|
||||
|
||||
- Voir la liste complète des abonnés
|
||||
- Voir le nombre d'abonnés actifs
|
||||
- API endpoints protégés par authentification et rôle ADMIN
|
||||
|
||||
## Emails
|
||||
|
||||
Les emails de confirmation sont envoyés automatiquement lors de l'inscription. Configuration SMTP requise dans le fichier `.env` :
|
||||
|
||||
```env
|
||||
SMTP_HOST=your-smtp-host
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-smtp-user
|
||||
SMTP_PASS=your-smtp-password
|
||||
EMAIL_FROM=noreply@thetiptop.fr
|
||||
```
|
||||
|
||||
En mode développement sans configuration SMTP, les emails sont affichés dans la console.
|
||||
|
||||
## Test
|
||||
|
||||
### Test manuel :
|
||||
|
||||
1. Ouvrez le frontend (http://localhost:3000)
|
||||
2. Scrollez jusqu'au footer
|
||||
3. Entrez votre email dans le champ "Newsletter"
|
||||
4. Cliquez sur "S'inscrire"
|
||||
5. Vérifiez le message de confirmation
|
||||
|
||||
### Test API avec curl :
|
||||
|
||||
```bash
|
||||
# S'abonner
|
||||
curl -X POST http://localhost:4000/api/newsletter/subscribe \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "test@example.com"}'
|
||||
|
||||
# Nombre d'abonnés (nécessite un token admin)
|
||||
curl -X GET http://localhost:4000/api/newsletter/count \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_TOKEN"
|
||||
```
|
||||
|
||||
## Sécurité
|
||||
|
||||
- Les endpoints publics (subscribe/unsubscribe) ne nécessitent pas d'authentification
|
||||
- Les endpoints admin (subscribers/count) nécessitent un token JWT et le rôle ADMIN
|
||||
- Validation Zod sur les entrées
|
||||
- Protection contre les doublons d'email
|
||||
- Sanitisation des données
|
||||
|
||||
## Prochaines étapes possibles
|
||||
|
||||
- Ajout d'une page de gestion de newsletter pour les admins
|
||||
- Export de la liste des abonnés en CSV
|
||||
- Système de campagnes email
|
||||
- Segmentation des abonnés
|
||||
- Statistiques d'engagement
|
||||
26
database/migrations/add-newsletter-table.sql
Normal file
26
database/migrations/add-newsletter-table.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- ============================================
|
||||
-- MIGRATION: Add Newsletter Table
|
||||
-- ============================================
|
||||
|
||||
-- Create newsletters table
|
||||
CREATE TABLE IF NOT EXISTS newsletters (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
subscribed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
unsubscribed_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_newsletters_email ON newsletters(email);
|
||||
CREATE INDEX idx_newsletters_is_active ON newsletters(is_active);
|
||||
|
||||
-- Add trigger for updated_at
|
||||
CREATE TRIGGER update_newsletters_updated_at BEFORE UPDATE ON newsletters
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Add comment
|
||||
COMMENT ON TABLE newsletters IS 'Table des abonnements à la newsletter';
|
||||
COMMENT ON COLUMN newsletters.is_active IS 'TRUE si abonné, FALSE si désabonné';
|
||||
2
index.js
2
index.js
|
|
@ -14,6 +14,7 @@ import gameRoutes from "./src/routes/game.routes.js";
|
|||
import employeeRoutes from "./src/routes/employee.routes.js";
|
||||
import adminRoutes from "./src/routes/admin.routes.js";
|
||||
import drawRoutes from "./src/routes/draw.routes.js";
|
||||
import newsletterRoutes from "./src/routes/newsletter.routes.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
|
|
@ -89,6 +90,7 @@ app.use("/api/game", gameRoutes);
|
|||
app.use("/api/employee", employeeRoutes);
|
||||
app.use("/api/admin", adminRoutes);
|
||||
app.use("/api/draw", drawRoutes);
|
||||
app.use("/api/newsletter", newsletterRoutes);
|
||||
|
||||
// Error handler (doit être après les routes)
|
||||
app.use(errorHandler);
|
||||
|
|
|
|||
80
public/test-newsletter.html
Normal file
80
public/test-newsletter.html
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Newsletter</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
input, button {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
button {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
#result {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Newsletter API</h1>
|
||||
<form id="newsletterForm">
|
||||
<input type="email" id="email" placeholder="Votre email" required>
|
||||
<button type="submit">S'inscrire</button>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
document.getElementById('newsletterForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const email = document.getElementById('email').value;
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
try {
|
||||
console.log('🔄 Envoi de la requête...');
|
||||
const response = await fetch('http://localhost:4000/api/newsletter/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('✅ Réponse:', data);
|
||||
|
||||
resultDiv.className = response.ok ? 'success' : 'error';
|
||||
resultDiv.textContent = data.message || 'Réponse reçue';
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur:', error);
|
||||
resultDiv.className = 'error';
|
||||
resultDiv.textContent = 'Erreur: ' + error.message;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📍 Test depuis:', window.location.origin);
|
||||
console.log('🎯 API cible: http://localhost:4000/api/newsletter/subscribe');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
47
scripts/apply-newsletter-migration.js
Normal file
47
scripts/apply-newsletter-migration.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Script pour appliquer la migration newsletter
|
||||
*/
|
||||
import { pool } from '../db.js';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
async function applyMigration() {
|
||||
try {
|
||||
console.log('📦 Application de la migration newsletter...');
|
||||
|
||||
// Lire le fichier SQL
|
||||
const migrationPath = join(__dirname, '../database/migrations/add-newsletter-table.sql');
|
||||
const sql = fs.readFileSync(migrationPath, 'utf8');
|
||||
|
||||
console.log('📄 Fichier SQL lu:', migrationPath);
|
||||
|
||||
// Exécuter la migration
|
||||
await pool.query(sql);
|
||||
|
||||
console.log('✅ Migration appliquée avec succès!');
|
||||
console.log('📊 Table newsletters créée');
|
||||
|
||||
// Vérifier que la table existe
|
||||
const result = await pool.query(
|
||||
"SELECT table_name FROM information_schema.tables WHERE table_name = 'newsletters'"
|
||||
);
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
console.log('✅ Vérification: Table newsletters trouvée dans la base de données');
|
||||
} else {
|
||||
console.log('❌ Erreur: Table newsletters non trouvée après migration');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors de l\'application de la migration:', error.message);
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
applyMigration();
|
||||
154
src/controllers/newsletter.controller.js
Normal file
154
src/controllers/newsletter.controller.js
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* Controller Newsletter
|
||||
*/
|
||||
import { pool } from '../../db.js';
|
||||
import { AppError, asyncHandler } from '../middleware/errorHandler.js';
|
||||
import { sendNewsletterConfirmationEmail } from '../services/newsletter.service.js';
|
||||
|
||||
/**
|
||||
* S'abonner à la newsletter
|
||||
* POST /api/newsletter/subscribe
|
||||
*/
|
||||
export const subscribe = asyncHandler(async (req, res, next) => {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return next(new AppError('Email requis', 400));
|
||||
}
|
||||
|
||||
// Vérifier si l'email existe déjà
|
||||
const existingSubscription = await pool.query(
|
||||
'SELECT * FROM newsletters WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (existingSubscription.rows.length > 0) {
|
||||
const subscription = existingSubscription.rows[0];
|
||||
|
||||
// Si déjà abonné et actif
|
||||
if (subscription.is_active) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Vous êtes déjà abonné à notre newsletter',
|
||||
});
|
||||
}
|
||||
|
||||
// Si désabonné, réactiver l'abonnement
|
||||
await pool.query(
|
||||
'UPDATE newsletters SET is_active = TRUE, unsubscribed_at = NULL, updated_at = NOW() WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
// Envoyer l'email de confirmation
|
||||
try {
|
||||
await sendNewsletterConfirmationEmail(email);
|
||||
} catch (error) {
|
||||
console.error('Erreur envoi email confirmation:', error);
|
||||
// On continue même si l'email échoue
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Votre abonnement à la newsletter a été réactivé avec succès',
|
||||
});
|
||||
}
|
||||
|
||||
// Créer un nouvel abonnement
|
||||
await pool.query(
|
||||
'INSERT INTO newsletters (email, is_active) VALUES ($1, TRUE)',
|
||||
[email]
|
||||
);
|
||||
|
||||
// Envoyer l'email de confirmation
|
||||
try {
|
||||
await sendNewsletterConfirmationEmail(email);
|
||||
} catch (error) {
|
||||
console.error('Erreur envoi email confirmation:', error);
|
||||
// On continue même si l'email échoue
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Merci ! Vous êtes maintenant abonné à notre newsletter',
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Se désabonner de la newsletter
|
||||
* POST /api/newsletter/unsubscribe
|
||||
*/
|
||||
export const unsubscribe = asyncHandler(async (req, res, next) => {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return next(new AppError('Email requis', 400));
|
||||
}
|
||||
|
||||
// Vérifier si l'abonnement existe
|
||||
const subscription = await pool.query(
|
||||
'SELECT * FROM newsletters WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (subscription.rows.length === 0) {
|
||||
return next(new AppError('Aucun abonnement trouvé pour cet email', 404));
|
||||
}
|
||||
|
||||
// Désactiver l'abonnement
|
||||
await pool.query(
|
||||
'UPDATE newsletters SET is_active = FALSE, unsubscribed_at = NOW(), updated_at = NOW() WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Vous avez été désabonné de notre newsletter',
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Obtenir tous les abonnés (Admin seulement)
|
||||
* GET /api/newsletter/subscribers
|
||||
*/
|
||||
export const getSubscribers = asyncHandler(async (req, res) => {
|
||||
const result = await pool.query(
|
||||
`SELECT id, email, is_active, subscribed_at, unsubscribed_at, created_at
|
||||
FROM newsletters
|
||||
ORDER BY created_at DESC`
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: result.rows.length,
|
||||
data: result.rows.map(row => ({
|
||||
id: row.id,
|
||||
email: row.email,
|
||||
isActive: row.is_active,
|
||||
subscribedAt: row.subscribed_at,
|
||||
unsubscribedAt: row.unsubscribed_at,
|
||||
createdAt: row.created_at,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Obtenir le nombre d'abonnés actifs (Admin seulement)
|
||||
* GET /api/newsletter/count
|
||||
*/
|
||||
export const getActiveCount = asyncHandler(async (req, res) => {
|
||||
const result = await pool.query(
|
||||
'SELECT COUNT(*) as count FROM newsletters WHERE is_active = TRUE'
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: parseInt(result.rows[0].count),
|
||||
});
|
||||
});
|
||||
|
||||
export default {
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
getSubscribers,
|
||||
getActiveCount,
|
||||
};
|
||||
31
src/routes/newsletter.routes.js
Normal file
31
src/routes/newsletter.routes.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Routes newsletter
|
||||
*/
|
||||
import { Router } from 'express';
|
||||
import * as newsletterController from '../controllers/newsletter.controller.js';
|
||||
import { authenticateToken, authorizeRoles } from '../middleware/auth.js';
|
||||
import { validate } from '../middleware/validate.js';
|
||||
import { subscribeSchema, unsubscribeSchema } from '../validations/newsletter.validation.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Routes publiques
|
||||
router.post('/subscribe', validate(subscribeSchema), newsletterController.subscribe);
|
||||
router.post('/unsubscribe', validate(unsubscribeSchema), newsletterController.unsubscribe);
|
||||
|
||||
// Routes protégées (Admin seulement)
|
||||
router.get(
|
||||
'/subscribers',
|
||||
authenticateToken,
|
||||
authorizeRoles('ADMIN'),
|
||||
newsletterController.getSubscribers
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/count',
|
||||
authenticateToken,
|
||||
authorizeRoles('ADMIN'),
|
||||
newsletterController.getActiveCount
|
||||
);
|
||||
|
||||
export default router;
|
||||
131
src/services/newsletter.service.js
Normal file
131
src/services/newsletter.service.js
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* Service Newsletter
|
||||
*/
|
||||
import nodemailer from 'nodemailer';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Configuration du transporteur email (réutilise la config du service email)
|
||||
const createTransporter = () => {
|
||||
if (process.env.NODE_ENV === 'development' && !process.env.SMTP_HOST) {
|
||||
console.log('⚠️ Mode développement: Les emails seront affichés dans la console');
|
||||
return null;
|
||||
}
|
||||
|
||||
return nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: parseInt(process.env.SMTP_PORT) || 587,
|
||||
secure: process.env.SMTP_PORT === '465',
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const transporter = createTransporter();
|
||||
|
||||
/**
|
||||
* Fonction générique pour envoyer un email
|
||||
*/
|
||||
const sendEmail = async ({ to, subject, html, text }) => {
|
||||
try {
|
||||
if (!transporter) {
|
||||
console.log('📧 Email qui aurait été envoyé:');
|
||||
console.log('To:', to);
|
||||
console.log('Subject:', subject);
|
||||
console.log('Content:', text || html);
|
||||
return { success: true, mode: 'dev' };
|
||||
}
|
||||
|
||||
const mailOptions = {
|
||||
from: process.env.EMAIL_FROM || 'noreply@thetiptop.fr',
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
};
|
||||
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
console.log('✅ Email envoyé:', info.messageId);
|
||||
return { success: true, messageId: info.messageId };
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur envoi email:', error);
|
||||
throw new Error('Erreur lors de l\'envoi de l\'email');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Envoie un email de confirmation d'abonnement à la newsletter
|
||||
*/
|
||||
export const sendNewsletterConfirmationEmail = async (email) => {
|
||||
const unsubscribeUrl = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/newsletter/unsubscribe?email=${encodeURIComponent(email)}`;
|
||||
|
||||
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; }
|
||||
.footer { text-align: center; margin-top: 20px; color: #666; font-size: 12px; }
|
||||
.unsubscribe { margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; }
|
||||
.unsubscribe a { color: #999; text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🍵 Bienvenue dans notre newsletter!</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Merci de votre abonnement</h2>
|
||||
<p>Nous sommes ravis de vous compter parmi nos abonnés !</p>
|
||||
<p>Vous recevrez désormais nos dernières actualités, offres exclusives et nouveautés directement dans votre boîte mail.</p>
|
||||
<p>Restez connecté avec Thé Tip Top pour découvrir :</p>
|
||||
<ul>
|
||||
<li>🍵 Nos nouveaux thés et infusions</li>
|
||||
<li>🎁 Des offres spéciales réservées aux abonnés</li>
|
||||
<li>📰 Les actualités de nos boutiques</li>
|
||||
<li>🎉 Les résultats de nos jeux-concours</li>
|
||||
</ul>
|
||||
<div class="unsubscribe">
|
||||
<p style="font-size: 12px; color: #999;">
|
||||
Vous ne souhaitez plus recevoir nos emails ? <a href="${unsubscribeUrl}">Se désabonner</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© 2025 Thé Tip Top - Tous droits réservés</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const text = `
|
||||
Bienvenue dans notre newsletter!
|
||||
|
||||
Nous sommes ravis de vous compter parmi nos abonnés !
|
||||
|
||||
Vous recevrez désormais nos dernières actualités, offres exclusives et nouveautés directement dans votre boîte mail.
|
||||
|
||||
Pour vous désabonner: ${unsubscribeUrl}
|
||||
|
||||
© 2025 Thé Tip Top - Tous droits réservés
|
||||
`;
|
||||
|
||||
return sendEmail({
|
||||
to: email,
|
||||
subject: '🍵 Bienvenue dans la newsletter Thé Tip Top',
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
sendNewsletterConfirmationEmail,
|
||||
};
|
||||
31
src/validations/newsletter.validation.js
Normal file
31
src/validations/newsletter.validation.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Schémas de validation avec Zod pour la newsletter
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
// Schéma pour l'abonnement à la newsletter
|
||||
export const subscribeSchema = z.object({
|
||||
body: z.object({
|
||||
email: z
|
||||
.string({
|
||||
required_error: 'L\'email est requis',
|
||||
})
|
||||
.email('Format d\'email invalide'),
|
||||
}),
|
||||
});
|
||||
|
||||
// Schéma pour le désabonnement de la newsletter
|
||||
export const unsubscribeSchema = z.object({
|
||||
body: z.object({
|
||||
email: z
|
||||
.string({
|
||||
required_error: 'L\'email est requis',
|
||||
})
|
||||
.email('Format d\'email invalide'),
|
||||
}),
|
||||
});
|
||||
|
||||
export default {
|
||||
subscribeSchema,
|
||||
unsubscribeSchema,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user