From 4759ce99e74f402112674078b1705b185e70a92a Mon Sep 17 00:00:00 2001 From: soufiane Date: Mon, 24 Nov 2025 00:07:44 +0100 Subject: [PATCH] feat: add newsletter subscription feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- NEWSLETTER_SETUP.md | 126 +++++++++++++++ database/migrations/add-newsletter-table.sql | 26 ++++ index.js | 2 + public/test-newsletter.html | 80 ++++++++++ scripts/apply-newsletter-migration.js | 47 ++++++ src/controllers/newsletter.controller.js | 154 +++++++++++++++++++ src/routes/newsletter.routes.js | 31 ++++ src/services/newsletter.service.js | 131 ++++++++++++++++ src/validations/newsletter.validation.js | 31 ++++ 9 files changed, 628 insertions(+) create mode 100644 NEWSLETTER_SETUP.md create mode 100644 database/migrations/add-newsletter-table.sql create mode 100644 public/test-newsletter.html create mode 100644 scripts/apply-newsletter-migration.js create mode 100644 src/controllers/newsletter.controller.js create mode 100644 src/routes/newsletter.routes.js create mode 100644 src/services/newsletter.service.js create mode 100644 src/validations/newsletter.validation.js diff --git a/NEWSLETTER_SETUP.md b/NEWSLETTER_SETUP.md new file mode 100644 index 00000000..0e13b007 --- /dev/null +++ b/NEWSLETTER_SETUP.md @@ -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 diff --git a/database/migrations/add-newsletter-table.sql b/database/migrations/add-newsletter-table.sql new file mode 100644 index 00000000..3ddf42f4 --- /dev/null +++ b/database/migrations/add-newsletter-table.sql @@ -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é'; diff --git a/index.js b/index.js index c6dc7233..41c6a210 100644 --- a/index.js +++ b/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); diff --git a/public/test-newsletter.html b/public/test-newsletter.html new file mode 100644 index 00000000..8ae80b7f --- /dev/null +++ b/public/test-newsletter.html @@ -0,0 +1,80 @@ + + + + + + Test Newsletter + + + +

Test Newsletter API

+
+ + +
+
+ + + + diff --git a/scripts/apply-newsletter-migration.js b/scripts/apply-newsletter-migration.js new file mode 100644 index 00000000..387cf8be --- /dev/null +++ b/scripts/apply-newsletter-migration.js @@ -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(); diff --git a/src/controllers/newsletter.controller.js b/src/controllers/newsletter.controller.js new file mode 100644 index 00000000..c251e46a --- /dev/null +++ b/src/controllers/newsletter.controller.js @@ -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, +}; diff --git a/src/routes/newsletter.routes.js b/src/routes/newsletter.routes.js new file mode 100644 index 00000000..b2a5a6d6 --- /dev/null +++ b/src/routes/newsletter.routes.js @@ -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; diff --git a/src/services/newsletter.service.js b/src/services/newsletter.service.js new file mode 100644 index 00000000..3439abd1 --- /dev/null +++ b/src/services/newsletter.service.js @@ -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 = ` + + + + + + +
+
+

🍵 Bienvenue dans notre newsletter!

+
+
+

Merci de votre abonnement

+

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.

+

Restez connecté avec Thé Tip Top pour découvrir :

+
    +
  • 🍵 Nos nouveaux thés et infusions
  • +
  • 🎁 Des offres spéciales réservées aux abonnés
  • +
  • 📰 Les actualités de nos boutiques
  • +
  • 🎉 Les résultats de nos jeux-concours
  • +
+
+

+ Vous ne souhaitez plus recevoir nos emails ? Se désabonner +

+
+
+ +
+ + + `; + + 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, +}; diff --git a/src/validations/newsletter.validation.js b/src/validations/newsletter.validation.js new file mode 100644 index 00000000..c7fce9d2 --- /dev/null +++ b/src/validations/newsletter.validation.js @@ -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, +};