moved maling unsubscribed from behiond auth

This commit is contained in:
2ManyProjects 2025-05-01 22:51:25 -05:00
parent fdcf390d48
commit 8da0206f10
2 changed files with 410 additions and 0 deletions

View file

@ -36,6 +36,7 @@ const publicSettingsRoutes = require('./routes/publicSettings');
const mailingListRoutes = require('./routes/mailingListAdmin'); const mailingListRoutes = require('./routes/mailingListAdmin');
const emailCampaignListRoutes = require('./routes/emailCampaignsAdmin'); const emailCampaignListRoutes = require('./routes/emailCampaignsAdmin');
const subscribersAdminRoutes = require('./routes/subscribersAdmin'); const subscribersAdminRoutes = require('./routes/subscribersAdmin');
const subscribersRoutes = require('./routes/subscribers');
// Create Express app // Create Express app
const app = express(); const app = express();
@ -321,6 +322,8 @@ app.delete('/api/image/product/:filename', adminAuthMiddleware(pool, query), (re
// Use routes // Use routes
app.use('/api/admin/settings', settingsAdminRoutes(pool, query, adminAuthMiddleware(pool, query))); app.use('/api/admin/settings', settingsAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/products', productRoutes(pool, query)); app.use('/api/products', productRoutes(pool, query));
app.use('/api/subscribers', subscribersRoutes(pool, query));
app.use('/api/auth', authRoutes(pool, query)); app.use('/api/auth', authRoutes(pool, query));
app.use('/api/user/orders', userOrdersRoutes(pool, query, authMiddleware(pool, query))); app.use('/api/user/orders', userOrdersRoutes(pool, query, authMiddleware(pool, query)));

View file

@ -0,0 +1,407 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const router = express.Router();
const emailService = require('../services/emailService');
module.exports = (pool, query, authMiddleware) => {
/**
* Public route to handle subscription
* POST /api/subscribers/subscribe
*/
router.post('/subscribe', async (req, res, next) => {
try {
const { email, firstName, lastName, listId } = req.body;
// Validate required fields
if (!email || !listId) {
return res.status(400).json({
error: true,
message: 'Email and list ID are required'
});
}
// Check if list exists
const listCheck = await query(
'SELECT id FROM mailing_lists WHERE id = $1',
[listId]
);
if (listCheck.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Mailing list not found'
});
}
// Begin transaction
const client = await pool.connect();
try {
await client.query('BEGIN');
// Check if subscriber already exists
let subscriberId;
const subscriberCheck = await client.query(
'SELECT id, status FROM subscribers WHERE email = $1',
[email]
);
if (subscriberCheck.rows.length > 0) {
// Update existing subscriber
subscriberId = subscriberCheck.rows[0].id;
const currentStatus = subscriberCheck.rows[0].status;
// Only update if not already unsubscribed or complained
if (currentStatus !== 'unsubscribed' && currentStatus !== 'complained') {
await client.query(
`UPDATE subscribers
SET first_name = COALESCE($1, first_name),
last_name = COALESCE($2, last_name),
status = 'active',
updated_at = NOW()
WHERE id = $3`,
[firstName, lastName, subscriberId]
);
} else {
await client.query('ROLLBACK');
return res.status(400).json({
error: true,
message: 'This email has previously unsubscribed and cannot be resubscribed without consent'
});
}
} else {
// Create new subscriber
subscriberId = uuidv4();
await client.query(
`INSERT INTO subscribers (id, email, first_name, last_name)
VALUES ($1, $2, $3, $4)`,
[subscriberId, email, firstName, lastName]
);
}
// Check if subscriber is already on this list
const listSubscriberCheck = await client.query(
'SELECT * FROM mailing_list_subscribers WHERE list_id = $1 AND subscriber_id = $2',
[listId, subscriberId]
);
if (listSubscriberCheck.rows.length === 0) {
// Add subscriber to list
await client.query(
`INSERT INTO mailing_list_subscribers (list_id, subscriber_id)
VALUES ($1, $2)`,
[listId, subscriberId]
);
}
await client.query('COMMIT');
// Generate a confirmation token for double opt-in
const confirmationToken = uuidv4();
// Store the confirmation token
await query(
`INSERT INTO subscription_confirmations (subscriber_id, token, expires_at)
VALUES ($1, $2, NOW() + INTERVAL '7 days')`,
[subscriberId, confirmationToken]
);
// Send confirmation email
try {
await emailService.sendSubscriptionConfirmation({
to: email,
firstName: firstName || '',
confirmationLink: `${process.env.SITE_URL}/confirm-subscription?token=${confirmationToken}`
});
} catch (emailError) {
console.error('Failed to send confirmation email:', emailError);
}
res.status(200).json({
success: true,
message: 'Subscription request received. Please check your email to confirm your subscription.'
});
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
} catch (error) {
next(error);
}
});
/**
* Confirm subscription (double opt-in)
* GET /api/subscribers/confirm
*/
router.get('/confirm', async (req, res, next) => {
try {
const { token } = req.query;
if (!token) {
return res.status(400).json({
error: true,
message: 'Confirmation token is required'
});
}
// Check if token exists and is valid
const confirmationQuery = `
SELECT
sc.subscriber_id,
s.email,
s.first_name
FROM subscription_confirmations sc
JOIN subscribers s ON sc.subscriber_id = s.id
WHERE sc.token = $1 AND sc.expires_at > NOW() AND sc.confirmed_at IS NULL
`;
const confirmationResult = await query(confirmationQuery, [token]);
if (confirmationResult.rows.length === 0) {
return res.status(400).json({
error: true,
message: 'Invalid or expired confirmation token'
});
}
const { subscriber_id, email, first_name } = confirmationResult.rows[0];
// Mark subscription as confirmed
await query(
`UPDATE subscription_confirmations
SET confirmed_at = NOW()
WHERE token = $1`,
[token]
);
// Ensure subscriber is marked as active
await query(
`UPDATE subscribers
SET status = 'active', updated_at = NOW()
WHERE id = $1`,
[subscriber_id]
);
// Send welcome email
try {
await emailService.sendWelcomeEmail({
to: email,
firstName: first_name || ''
});
} catch (emailError) {
console.error('Failed to send welcome email:', emailError);
}
res.status(200).json({
success: true,
message: 'Your subscription has been confirmed. Thank you!'
});
} catch (error) {
next(error);
}
});
/**
* Handle unsubscribe request
* GET /api/subscribers/unsubscribe
*/
router.get('/unsubscribe', async (req, res, next) => {
try {
const { email, token, listId } = req.query;
if (!email && !token) {
return res.status(400).json({
error: true,
message: 'Email or token is required'
});
}
// Begin transaction
const client = await pool.connect();
try {
await client.query('BEGIN');
let subscriberId;
if (token) {
// Verify the unsubscribe token
const tokenCheck = await client.query(
'SELECT subscriber_id FROM unsubscribe_tokens WHERE token = $1 AND expires_at > NOW()',
[token]
);
if (tokenCheck.rows.length === 0) {
await client.query('ROLLBACK');
return res.status(400).json({
error: true,
message: 'Invalid or expired unsubscribe token'
});
}
subscriberId = tokenCheck.rows[0].subscriber_id;
} else {
// Look up subscriber by email
const subscriberCheck = await client.query(
'SELECT id FROM subscribers WHERE email = $1',
[email]
);
if (subscriberCheck.rows.length === 0) {
await client.query('ROLLBACK');
return res.status(404).json({
error: true,
message: 'Email not found in our database'
});
}
subscriberId = subscriberCheck.rows[0].id;
}
// Update subscriber status
await client.query(
`UPDATE subscribers
SET status = 'unsubscribed', updated_at = NOW()
WHERE id = $1`,
[subscriberId]
);
// If list ID is provided, only remove from that list
if (listId) {
await client.query(
'DELETE FROM mailing_list_subscribers WHERE subscriber_id = $1 AND list_id = $2',
[subscriberId, listId]
);
}
// Otherwise remove from all lists
else {
await client.query(
'DELETE FROM mailing_list_subscribers WHERE subscriber_id = $1',
[subscriberId]
);
}
// Record unsubscribe activity
await client.query(
`INSERT INTO subscriber_activity (subscriber_id, type, details)
VALUES ($1, 'unsubscribe', $2)`,
[subscriberId, listId ? `Unsubscribed from list ${listId}` : 'Unsubscribed from all lists']
);
await client.query('COMMIT');
res.status(200).json({
success: true,
message: 'You have been successfully unsubscribed'
});
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
} catch (error) {
next(error);
}
});
/**
* Public route to handle bounce and complaint notifications
* POST /api/subscribers/webhook
*/
router.post('/webhook', async (req, res, next) => {
try {
const { type, email, message, campaignId, reason, timestamp } = req.body;
// Validate required fields
if (!type || !email) {
return res.status(400).json({
error: true,
message: 'Type and email are required'
});
}
// Verify webhook signature
// In production, implement proper verification of webhook authenticity
// Find subscriber by email
const subscriberCheck = await query(
'SELECT id FROM subscribers WHERE email = $1',
[email]
);
if (subscriberCheck.rows.length === 0) {
return res.status(404).json({
error: true,
message: 'Subscriber not found'
});
}
const subscriberId = subscriberCheck.rows[0].id;
// Handle event based on type
switch (type) {
case 'bounce':
// Update subscriber status
await query(
`UPDATE subscribers
SET status = 'bounced', updated_at = NOW()
WHERE id = $1`,
[subscriberId]
);
// Record bounce activity
await query(
`INSERT INTO subscriber_activity (subscriber_id, campaign_id, type, timestamp, details)
VALUES ($1, $2, 'bounce', $3, $4)`,
[subscriberId, campaignId, timestamp || new Date(), reason || message || 'Email bounced']
);
break;
case 'complaint':
// Update subscriber status
await query(
`UPDATE subscribers
SET status = 'complained', updated_at = NOW()
WHERE id = $1`,
[subscriberId]
);
// Remove from all lists
await query(
'DELETE FROM mailing_list_subscribers WHERE subscriber_id = $1',
[subscriberId]
);
// Record complaint activity
await query(
`INSERT INTO subscriber_activity (subscriber_id, campaign_id, type, timestamp, details)
VALUES ($1, $2, 'complaint', $3, $4)`,
[subscriberId, campaignId, timestamp || new Date(), reason || message || 'Spam complaint received']
);
break;
default:
return res.status(400).json({
error: true,
message: 'Unsupported event type'
});
}
res.json({
success: true,
message: `Processed ${type} for ${email}`
});
} catch (error) {
next(error);
}
});
return router;
};