tractatus/src/controllers/newsletter.controller.js
TheFlow 6bcda34665 fix(newsletter): serialize ObjectId to string in API response
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>
2025-10-24 20:29:12 +13:00

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'
});
}
};