moved maling unsubscribed from behiond auth
This commit is contained in:
parent
fdcf390d48
commit
8da0206f10
2 changed files with 410 additions and 0 deletions
|
|
@ -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)));
|
||||||
|
|
||||||
|
|
|
||||||
407
backend/src/routes/subscribers.js
Normal file
407
backend/src/routes/subscribers.js
Normal 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;
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue