Root cause: MongoDB ObjectId objects were being sent to frontend as-is, which JSON.stringify converts to '[object Object]' string in data attributes. Fix: Convert _id to string on server-side before sending to client. This is the actual fix - previous attempts were client-side workarounds. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
315 lines
7.6 KiB
JavaScript
315 lines
7.6 KiB
JavaScript
/**
|
|
* Newsletter Controller
|
|
* Handles newsletter subscriptions and management
|
|
*/
|
|
|
|
const NewsletterSubscription = require('../models/NewsletterSubscription.model');
|
|
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'
|
|
});
|
|
}
|
|
};
|