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 emailCampaignListRoutes = require('./routes/emailCampaignsAdmin');
|
||||
const subscribersAdminRoutes = require('./routes/subscribersAdmin');
|
||||
const subscribersRoutes = require('./routes/subscribers');
|
||||
|
||||
// Create Express app
|
||||
const app = express();
|
||||
|
|
@ -321,6 +322,8 @@ app.delete('/api/image/product/:filename', adminAuthMiddleware(pool, query), (re
|
|||
// Use routes
|
||||
app.use('/api/admin/settings', settingsAdminRoutes(pool, query, adminAuthMiddleware(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/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