/** * Newsletter Controller * Handles newsletter subscriptions and management */ const NewsletterSubscription = require('../models/NewsletterSubscription.model'); const emailService = require('../services/email.service'); const logger = require('../utils/logger.util'); /** * Subscribe to newsletter (public) * POST /api/newsletter/subscribe */ exports.subscribe = async (req, res) => { try { const { email, name, source, interests } = req.body; // Validate email if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return res.status(400).json({ success: false, error: 'Valid email address is required' }); } // Capture metadata const metadata = { ip_address: req.ip || req.connection.remoteAddress, user_agent: req.get('user-agent'), referrer: req.get('referer') }; const subscription = await NewsletterSubscription.subscribe({ email, name, source: source || 'blog', interests: interests || [], ...metadata }); logger.info('Newsletter subscription created', { email: subscription.email, source: subscription.source, verified: subscription.verified }); // In a real system, send verification email here // For now, we'll auto-verify (remove this in production) await NewsletterSubscription.verify(subscription.verification_token); res.status(201).json({ success: true, message: 'Successfully subscribed to newsletter', subscription: { email: subscription.email, verified: true } }); } catch (subscribeError) { console.error('Newsletter subscription error:', subscribeError); res.status(500).json({ success: false, error: 'Failed to subscribe to newsletter', details: process.env.NODE_ENV === 'development' ? subscribeError.message : undefined }); } }; /** * Verify email subscription * GET /api/newsletter/verify/:token */ exports.verify = async (req, res) => { try { const { token } = req.params; const verified = await NewsletterSubscription.verify(token); if (!verified) { return res.status(404).json({ success: false, error: 'Invalid or expired verification token' }); } res.json({ success: true, message: 'Email verified successfully' }); } catch (error) { logger.error('Newsletter verification error:', error); res.status(500).json({ success: false, error: 'Failed to verify email' }); } }; /** * Unsubscribe from newsletter (public) * POST /api/newsletter/unsubscribe */ exports.unsubscribe = async (req, res) => { try { const { email, token } = req.body; if (!email && !token) { return res.status(400).json({ success: false, error: 'Email or token is required' }); } const unsubscribed = await NewsletterSubscription.unsubscribe(email, token); if (!unsubscribed) { return res.status(404).json({ success: false, error: 'Subscription not found' }); } logger.info('Newsletter unsubscribe', { email: email || 'via token' }); res.json({ success: true, message: 'Successfully unsubscribed from newsletter' }); } catch (error) { logger.error('Newsletter unsubscribe error:', error); res.status(500).json({ success: false, error: 'Failed to unsubscribe' }); } }; /** * Update subscription preferences (public) * PUT /api/newsletter/preferences */ exports.updatePreferences = async (req, res) => { try { const { email, name, interests } = req.body; if (!email) { return res.status(400).json({ success: false, error: 'Email is required' }); } const updated = await NewsletterSubscription.updatePreferences(email, { name, interests }); if (!updated) { return res.status(404).json({ success: false, error: 'Active subscription not found' }); } res.json({ success: true, message: 'Preferences updated successfully' }); } catch (error) { logger.error('Newsletter preferences update error:', error); res.status(500).json({ success: false, error: 'Failed to update preferences' }); } }; /** * Get newsletter statistics (admin only) * GET /api/admin/newsletter/stats */ exports.getStats = async (req, res) => { try { const stats = await NewsletterSubscription.getStats(); res.json({ success: true, stats }); } catch (error) { logger.error('Newsletter stats error:', error); res.status(500).json({ success: false, error: 'Failed to retrieve statistics' }); } }; /** * List all subscriptions (admin only) * GET /api/admin/newsletter/subscriptions */ exports.listSubscriptions = async (req, res) => { try { const { limit = 100, skip = 0, active = 'true', verified = null, source = null } = req.query; const options = { limit: parseInt(limit), skip: parseInt(skip), active: active === 'true', verified: verified === null ? null : verified === 'true', source: source || null }; const subscriptions = await NewsletterSubscription.list(options); const total = await NewsletterSubscription.count({ active: options.active, ...(options.verified !== null && { verified: options.verified }), ...(options.source && { source: options.source }) }); // Convert ObjectId to string for JSON serialization const serializedSubscriptions = subscriptions.map(sub => ({ ...sub, _id: sub._id.toString() })); res.json({ success: true, subscriptions: serializedSubscriptions, pagination: { total, limit: options.limit, skip: options.skip, has_more: skip + subscriptions.length < total } }); } catch (error) { logger.error('Newsletter list error:', error); res.status(500).json({ success: false, error: 'Failed to list subscriptions' }); } }; /** * Export subscriptions as CSV (admin only) * GET /api/admin/newsletter/export */ exports.exportSubscriptions = async (req, res) => { try { const { active = 'true' } = req.query; const subscriptions = await NewsletterSubscription.list({ active: active === 'true', limit: 10000 }); // Generate CSV const csv = [ 'Email,Name,Source,Verified,Subscribed At', ...subscriptions.map(sub => `${sub.email},"${sub.name || ''}",${sub.source},${sub.verified},${sub.subscribed_at.toISOString()}` ) ].join('\n'); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename="newsletter-subscriptions-${Date.now()}.csv"`); res.send(csv); } catch (error) { logger.error('Newsletter export error:', error); res.status(500).json({ success: false, error: 'Failed to export subscriptions' }); } }; /** * Delete subscription (admin only) * DELETE /api/admin/newsletter/subscriptions/:id */ exports.deleteSubscription = async (req, res) => { try { const { id } = req.params; const deleted = await NewsletterSubscription.delete(id); if (!deleted) { return res.status(404).json({ success: false, error: 'Subscription not found' }); } logger.info('Newsletter subscription deleted', { id }); res.json({ success: true, message: 'Subscription deleted successfully' }); } catch (error) { logger.error('Newsletter delete error:', error); res.status(500).json({ success: false, error: 'Failed to delete subscription' }); } }; /** * Send newsletter (admin only) * POST /api/newsletter/admin/send */ exports.send = async (req, res) => { try { const { tier, subject, previewText, variables, testMode, testEmail } = req.body; // Validation if (!tier || !subject || !variables) { return res.status(400).json({ success: false, error: 'Missing required fields: tier, subject, variables' }); } if (testMode && !testEmail) { return res.status(400).json({ success: false, error: 'Test email required when testMode is true' }); } // Send newsletter const result = await emailService.sendNewsletter({ tier, subject, previewText, variables, testMode: testMode === true, testEmail }); logger.info('[Newsletter] Newsletter sent', { tier, sent: result.sent, failed: result.failed }); res.json(result); } catch (error) { logger.error('[Newsletter] Send error:', error); res.status(500).json({ success: false, error: 'Failed to send newsletter', details: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }; /** * Preview newsletter (admin only) * POST /api/newsletter/admin/preview */ exports.preview = async (req, res) => { try { const { tier, variables } = req.body; if (!tier || !variables) { return res.status(400).json({ success: false, error: 'Missing required fields: tier, variables' }); } const { html } = await emailService.renderEmail(tier, { name: 'Preview User', tier_name: emailService.getTierDisplayName(tier), unsubscribe_link: '#', preferences_link: '#', ...variables }); res.send(html); } catch (error) { logger.error('[Newsletter] Preview error:', error); res.status(500).json({ success: false, error: 'Failed to generate preview', details: process.env.NODE_ENV === 'development' ? error.message : undefined }); } };