diff --git a/.gitignore b/.gitignore
index b347d25..4ec5b20 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,4 @@ node_modules
npm-debug.log
yarn-error.log
.DS_Store
-public/uploads/*
\ No newline at end of file
+backend/public/uploads/*
\ No newline at end of file
diff --git a/backend/src/index.js b/backend/src/index.js
index cd63ee1..a1fb07d 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -11,9 +11,8 @@ const settingsAdminRoutes = require('./routes/settingsAdmin');
const SystemSettings = require('./models/SystemSettings');
const fs = require('fs');
// services
-
const notificationService = require('./services/notificationService');
-
+const emailService = require('./services/emailService');
// routes
const stripePaymentRoutes = require('./routes/stripePayment');
@@ -32,6 +31,8 @@ const blogAdminRoutes = require('./routes/blogAdmin');
const blogCommentsAdminRoutes = require('./routes/blogCommentsAdmin');
const productReviewsRoutes = require('./routes/productReviews');
const productReviewsAdminRoutes = require('./routes/productReviewsAdmin');
+const emailTemplatesAdminRoutes = require('./routes/emailTemplatesAdmin');
+const publicSettingsRoutes = require('./routes/publicSettings');
// Create Express app
const app = express();
@@ -180,6 +181,8 @@ app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', message: 'API is running' });
});
+app.use('/api/settings', publicSettingsRoutes(pool, query));
+
// Upload endpoints
// Public upload endpoint (basic)
app.post('/api/image/upload', upload.single('image'), (req, res) => {
@@ -197,6 +200,21 @@ app.post('/api/image/upload', upload.single('image'), (req, res) => {
});
});
+app.get('/api/public-file/:filename', (req, res) => {
+ const { filename } = req.params;
+
+ // Prevent path traversal attacks
+ if (filename.includes('..') || filename.includes('/')) {
+ return res.status(400).json({
+ error: true,
+ message: 'Invalid filename'
+ });
+ }
+
+ // Serve files from public uploads folder
+ res.sendFile(path.join(__dirname, '../public/uploads', filename));
+});
+
app.use('/api/product-reviews', productReviewsRoutes(pool, query, authMiddleware(pool, query)));
app.use('/api/admin/product-reviews', productReviewsAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
@@ -205,6 +223,7 @@ app.use('/api/admin/coupons', couponsAdminRoutes(pool, query, adminAuthMiddlewar
app.use('/api/admin/orders', ordersAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/blog', blogAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/blog-comments', blogCommentsAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
+app.use('/api/admin/email-templates', emailTemplatesAdminRoutes(pool, query, adminAuthMiddleware(pool, query))); // Add new route
// Admin-only product image upload
app.post('/api/image/product', adminAuthMiddleware(pool, query), upload.single('image'), (req, res) => {
diff --git a/backend/src/middleware/adminAuth.js b/backend/src/middleware/adminAuth.js
index 52db624..2527697 100644
--- a/backend/src/middleware/adminAuth.js
+++ b/backend/src/middleware/adminAuth.js
@@ -27,7 +27,7 @@ module.exports = (pool, query) => {
});
}
- // Check if user is admin
+
if (!result.rows[0].is_admin) {
return res.status(403).json({
error: true,
@@ -38,7 +38,6 @@ module.exports = (pool, query) => {
// Add user to request object
req.user = result.rows[0];
- // Continue to next middleware/route handler
next();
} catch (error) {
return res.status(500).json({
diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js
index a009b32..2b75427 100644
--- a/backend/src/middleware/auth.js
+++ b/backend/src/middleware/auth.js
@@ -30,7 +30,6 @@ module.exports = (pool, query) => {
// Add user to request object
req.user = result.rows[0];
- // Continue to next middleware/route handler
next();
} catch (error) {
return res.status(500).json({
diff --git a/backend/src/middleware/upload.js b/backend/src/middleware/upload.js
index 679f1a2..96c09bf 100644
--- a/backend/src/middleware/upload.js
+++ b/backend/src/middleware/upload.js
@@ -47,7 +47,6 @@ const fileFilter = (req, file, cb) => {
}
};
-// Create the multer instance
const upload = multer({
storage,
fileFilter,
diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js
index e6eb385..a27cdac 100644
--- a/backend/src/routes/auth.js
+++ b/backend/src/routes/auth.js
@@ -1,21 +1,9 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
-const nodemailer = require('nodemailer');
const config = require('../config');
+const emailService = require('../services/emailService');
const router = express.Router();
-const createTransporter = () => {
- return nodemailer.createTransport({
- host: config.email.host,
- port: config.email.port,
- auth: {
- user: config.email.user,
- pass: config.email.pass
- }
- });
-};
-
-const transporter = createTransporter();
module.exports = (pool, query) => {
// Register new user
@@ -95,18 +83,15 @@ module.exports = (pool, query) => {
const loginLink = `${config.site.protocol}://${config.site.domain}/verify?code=${authCode}&email=${encodeURIComponent(email)}`;
-
-
- await transporter.sendMail({
- from: 'noreply@2many.ca',
- to: email,
- subject: 'Your Login Code',
- html: `
-
Your login code is: ${authCode}
- This code will expire in 15 minutes.
- Or click here to log in directly.
- `
- });
+ try {
+ await emailService.sendLoginCodeEmail({
+ to: email,
+ code: authCode,
+ loginLink: loginLink
+ });
+ } catch (emailError) {
+ console.error('Failed to send login code email:', emailError);
+ }
let retObj = {
message: 'Login code sent to address: ' + email
}
diff --git a/backend/src/routes/blog.js b/backend/src/routes/blog.js
index 038a70a..731f316 100644
--- a/backend/src/routes/blog.js
+++ b/backend/src/routes/blog.js
@@ -152,7 +152,6 @@ module.exports = (pool, query, authMiddleware) => {
const commentThreads = [];
const commentMap = {};
- // First, create a map of all comments
commentsResult.rows.forEach(comment => {
commentMap[comment.id] = {
...comment,
@@ -173,7 +172,6 @@ module.exports = (pool, query, authMiddleware) => {
}
});
- // Return the post with images and comments
res.json({
...post,
images: imagesResult.rows,
@@ -245,7 +243,6 @@ module.exports = (pool, query, authMiddleware) => {
}
}
- // Determine if comment needs moderation
const isApproved = req.user.is_admin ? true : false;
// Insert comment
diff --git a/backend/src/routes/blogAdmin.js b/backend/src/routes/blogAdmin.js
index f2bee22..4653e69 100644
--- a/backend/src/routes/blogAdmin.js
+++ b/backend/src/routes/blogAdmin.js
@@ -10,7 +10,7 @@ module.exports = (pool, query, authMiddleware) => {
// Get all blog posts (admin)
router.get('/', async (req, res, next) => {
try {
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -47,7 +47,7 @@ module.exports = (pool, query, authMiddleware) => {
try {
const { id } = req.params;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -112,7 +112,6 @@ module.exports = (pool, query, authMiddleware) => {
tags, featuredImagePath, status, publishNow
} = req.body;
- // Check if user is admin
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -135,7 +134,6 @@ module.exports = (pool, query, authMiddleware) => {
remove: /[*+~.()'"!:@]/g // regex to remove characters
});
- // Check if slug already exists
const slugCheck = await query(
'SELECT id FROM blog_posts WHERE slug = $1',
[slug]
@@ -221,7 +219,7 @@ module.exports = (pool, query, authMiddleware) => {
tags, featuredImagePath, status, publishNow
} = req.body;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -365,7 +363,7 @@ module.exports = (pool, query, authMiddleware) => {
try {
const { id } = req.params;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -406,7 +404,7 @@ module.exports = (pool, query, authMiddleware) => {
const { postId } = req.params;
const { imagePath, caption, displayOrder } = req.body;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -457,7 +455,7 @@ module.exports = (pool, query, authMiddleware) => {
try {
const { postId, imageId } = req.params;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
diff --git a/backend/src/routes/blogCommentsAdmin.js b/backend/src/routes/blogCommentsAdmin.js
index 0563310..13c77cd 100644
--- a/backend/src/routes/blogCommentsAdmin.js
+++ b/backend/src/routes/blogCommentsAdmin.js
@@ -8,7 +8,7 @@ module.exports = (pool, query, authMiddleware) => {
// Get all pending comments (admin)
router.get('/pending', async (req, res, next) => {
try {
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -41,7 +41,7 @@ module.exports = (pool, query, authMiddleware) => {
try {
const { postId } = req.params;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -120,7 +120,7 @@ module.exports = (pool, query, authMiddleware) => {
try {
const { commentId } = req.params;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -169,7 +169,7 @@ module.exports = (pool, query, authMiddleware) => {
try {
const { commentId } = req.params;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
diff --git a/backend/src/routes/couponAdmin.js b/backend/src/routes/couponAdmin.js
index 37c437e..c87d9ad 100644
--- a/backend/src/routes/couponAdmin.js
+++ b/backend/src/routes/couponAdmin.js
@@ -12,7 +12,7 @@ module.exports = (pool, query, authMiddleware) => {
*/
router.get('/', async (req, res, next) => {
try {
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -68,7 +68,7 @@ module.exports = (pool, query, authMiddleware) => {
try {
const { id } = req.params;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -151,7 +151,7 @@ module.exports = (pool, query, authMiddleware) => {
blacklistedProducts
} = req.body;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -343,7 +343,7 @@ module.exports = (pool, query, authMiddleware) => {
blacklistedProducts
} = req.body;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -605,7 +605,7 @@ module.exports = (pool, query, authMiddleware) => {
try {
const { id } = req.params;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
@@ -648,7 +648,7 @@ module.exports = (pool, query, authMiddleware) => {
try {
const { id } = req.params;
- // Check if user is admin
+
if (!req.user.is_admin) {
return res.status(403).json({
error: true,
diff --git a/backend/src/routes/emailTemplatesAdmin.js b/backend/src/routes/emailTemplatesAdmin.js
new file mode 100644
index 0000000..e458279
--- /dev/null
+++ b/backend/src/routes/emailTemplatesAdmin.js
@@ -0,0 +1,602 @@
+const express = require('express');
+const { v4: uuidv4 } = require('uuid');
+const router = express.Router();
+const nodemailer = require('nodemailer');
+const config = require('../config');
+
+// Create email transporter
+const createTransporter = () => {
+ return nodemailer.createTransport({
+ host: config.email.host,
+ port: config.email.port,
+ auth: {
+ user: config.email.user,
+ pass: config.email.pass
+ }
+ });
+};
+
+module.exports = (pool, query, authMiddleware) => {
+ // Apply authentication middleware to all routes
+ router.use(authMiddleware);
+
+ /**
+ * Get all email templates
+ * GET /api/admin/email-templates
+ */
+ router.get('/', async (req, res, next) => {
+ try {
+
+ if (!req.user.is_admin) {
+ return res.status(403).json({
+ error: true,
+ message: 'Admin access required'
+ });
+ }
+
+ // Get all settings with 'email_templates' category
+ const result = await query(
+ 'SELECT * FROM system_settings WHERE category = $1 ORDER BY key',
+ ['email_templates']
+ );
+
+ // Transform settings into template objects
+ const templates = result.rows.map(setting => {
+ try {
+ // Parse the template data from the JSON value
+ const templateData = JSON.parse(setting.value);
+ return {
+ id: setting.key,
+ ...templateData
+ };
+ } catch (e) {
+ console.error(`Failed to parse template setting: ${setting.key}`, e);
+ return null;
+ }
+ }).filter(Boolean); // Remove any null entries
+
+ res.json(templates);
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * Get templates by type
+ * GET /api/admin/email-templates/type/:type
+ */
+ router.get('/type/:type', async (req, res, next) => {
+ try {
+ const { type } = req.params;
+
+ if (!req.user.is_admin) {
+ return res.status(403).json({
+ error: true,
+ message: 'Admin access required'
+ });
+ }
+
+ // Get all settings with 'email_templates' category
+ const result = await query(
+ 'SELECT * FROM system_settings WHERE category = $1 ORDER BY key',
+ ['email_templates']
+ );
+
+ // Transform settings into template objects and filter by type
+ const templates = result.rows
+ .map(setting => {
+ try {
+ // Parse the template data from the JSON value
+ const templateData = JSON.parse(setting.value);
+ if (templateData.type === type) {
+ return {
+ id: setting.key,
+ ...templateData
+ };
+ }
+ return null;
+ } catch (e) {
+ console.error(`Failed to parse template setting: ${setting.key}`, e);
+ return null;
+ }
+ })
+ .filter(Boolean); // Remove any null entries
+
+ res.json(templates);
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * Get default template for a type
+ * GET /api/admin/email-templates/default/:type
+ */
+ router.get('/default/:type', async (req, res, next) => {
+ try {
+ const { type } = req.params;
+
+ if (!req.user.is_admin) {
+ return res.status(403).json({
+ error: true,
+ message: 'Admin access required'
+ });
+ }
+
+ // Get all settings with 'email_templates' category
+ const result = await query(
+ 'SELECT * FROM system_settings WHERE category = $1 ORDER BY key',
+ ['email_templates']
+ );
+
+ // Find the default template for the specified type
+ let defaultTemplate = null;
+
+ for (const setting of result.rows) {
+ try {
+ const templateData = JSON.parse(setting.value);
+ if (templateData.type === type && templateData.isDefault) {
+ defaultTemplate = {
+ id: setting.key,
+ ...templateData
+ };
+ break;
+ }
+ } catch (e) {
+ console.error(`Failed to parse template setting: ${setting.key}`, e);
+ }
+ }
+
+ if (defaultTemplate) {
+ res.json(defaultTemplate);
+ } else {
+ res.status(404).json({
+ error: true,
+ message: `No default template found for type: ${type}`
+ });
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * Get a single template by ID
+ * GET /api/admin/email-templates/:id
+ */
+ router.get('/:id', async (req, res, next) => {
+ try {
+ const { id } = req.params;
+
+
+ if (!req.user.is_admin) {
+ return res.status(403).json({
+ error: true,
+ message: 'Admin access required'
+ });
+ }
+
+ // Get the setting by key
+ const result = await query(
+ 'SELECT * FROM system_settings WHERE key = $1',
+ [id]
+ );
+
+ if (result.rows.length === 0) {
+ return res.status(404).json({
+ error: true,
+ message: 'Template not found'
+ });
+ }
+
+ try {
+ // Parse the template data from the JSON value
+ const templateData = JSON.parse(result.rows[0].value);
+ res.json({
+ id: result.rows[0].key,
+ ...templateData
+ });
+ } catch (e) {
+ console.error(`Failed to parse template setting: ${id}`, e);
+ return res.status(500).json({
+ error: true,
+ message: 'Failed to parse template data'
+ });
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * Create a new template
+ * POST /api/admin/email-templates
+ */
+ router.post('/', async (req, res, next) => {
+ try {
+ const { name, type, subject, content, isDefault } = req.body;
+
+
+ if (!req.user.is_admin) {
+ return res.status(403).json({
+ error: true,
+ message: 'Admin access required'
+ });
+ }
+
+ // Validate required fields
+ if (!name || !type || !subject || !content) {
+ return res.status(400).json({
+ error: true,
+ message: 'Name, type, subject, and content are required'
+ });
+ }
+
+ // Begin transaction for potential default template updates
+ const client = await pool.connect();
+
+ try {
+ await client.query('BEGIN');
+
+ // Generate a unique key for the setting
+ const templateKey = `email_template_${Date.now()}`;
+
+ // Create the template object
+ const templateData = {
+ name,
+ type,
+ subject,
+ content,
+ isDefault: isDefault || false,
+ createdAt: new Date().toISOString()
+ };
+
+ // If this template should be the default, unset any existing defaults
+ if (templateData.isDefault) {
+ // Get all settings with 'email_templates' category
+ const existingTemplates = await client.query(
+ 'SELECT * FROM system_settings WHERE category = $1',
+ ['email_templates']
+ );
+
+ // Find and update any existing default templates of the same type
+ for (const setting of existingTemplates.rows) {
+ try {
+ const existingData = JSON.parse(setting.value);
+ if (existingData.type === type && existingData.isDefault) {
+ existingData.isDefault = false;
+ existingData.updatedAt = new Date().toISOString();
+
+ await client.query(
+ 'UPDATE system_settings SET value = $1, updated_at = NOW() WHERE key = $2',
+ [JSON.stringify(existingData), setting.key]
+ );
+ }
+ } catch (e) {
+ console.error(`Failed to parse template setting: ${setting.key}`, e);
+ }
+ }
+ }
+
+ // Insert the new template
+ await client.query(
+ 'INSERT INTO system_settings (key, value, category) VALUES ($1, $2, $3)',
+ [templateKey, JSON.stringify(templateData), 'email_templates']
+ );
+
+ await client.query('COMMIT');
+
+ res.status(201).json({
+ id: templateKey,
+ ...templateData
+ });
+ } catch (error) {
+ await client.query('ROLLBACK');
+ throw error;
+ } finally {
+ client.release();
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * Update a template
+ * PUT /api/admin/email-templates/:id
+ */
+ router.put('/:id', async (req, res, next) => {
+ try {
+ const { id } = req.params;
+ const { name, type, subject, content, isDefault } = req.body;
+
+
+ if (!req.user.is_admin) {
+ return res.status(403).json({
+ error: true,
+ message: 'Admin access required'
+ });
+ }
+
+ // Validate required fields
+ if (!name || !type || !subject || !content) {
+ return res.status(400).json({
+ error: true,
+ message: 'Name, type, subject, and content are required'
+ });
+ }
+
+ // Begin transaction for potential default template updates
+ const client = await pool.connect();
+
+ try {
+ await client.query('BEGIN');
+
+ // Check if the template exists
+ const templateCheck = await client.query(
+ 'SELECT * FROM system_settings WHERE key = $1',
+ [id]
+ );
+
+ if (templateCheck.rows.length === 0) {
+ await client.query('ROLLBACK');
+ return res.status(404).json({
+ error: true,
+ message: 'Template not found'
+ });
+ }
+
+ // Parse the existing template data
+ let existingData;
+ try {
+ existingData = JSON.parse(templateCheck.rows[0].value);
+ } catch (e) {
+ await client.query('ROLLBACK');
+ return res.status(500).json({
+ error: true,
+ message: 'Failed to parse existing template data'
+ });
+ }
+
+ // Create the updated template object
+ const templateData = {
+ name,
+ type,
+ subject,
+ content,
+ isDefault: isDefault !== undefined ? isDefault : existingData.isDefault,
+ createdAt: existingData.createdAt,
+ updatedAt: new Date().toISOString()
+ };
+
+ // If this template should be the default, unset any existing defaults
+ if (templateData.isDefault && (!existingData.isDefault || existingData.type !== type)) {
+ // Get all settings with 'email_templates' category
+ const existingTemplates = await client.query(
+ 'SELECT * FROM system_settings WHERE category = $1',
+ ['email_templates']
+ );
+
+ // Find and update any existing default templates of the same type
+ for (const setting of existingTemplates.rows) {
+ if (setting.key === id) continue; // Skip the current template
+
+ try {
+ const otherData = JSON.parse(setting.value);
+ if (otherData.type === type && otherData.isDefault) {
+ otherData.isDefault = false;
+ otherData.updatedAt = new Date().toISOString();
+
+ await client.query(
+ 'UPDATE system_settings SET value = $1, updated_at = NOW() WHERE key = $2',
+ [JSON.stringify(otherData), setting.key]
+ );
+ }
+ } catch (e) {
+ console.error(`Failed to parse template setting: ${setting.key}`, e);
+ }
+ }
+ }
+
+ // Update the template
+ await client.query(
+ 'UPDATE system_settings SET value = $1, updated_at = NOW() WHERE key = $2',
+ [JSON.stringify(templateData), id]
+ );
+
+ await client.query('COMMIT');
+
+ res.json({
+ id,
+ ...templateData
+ });
+ } catch (error) {
+ await client.query('ROLLBACK');
+ throw error;
+ } finally {
+ client.release();
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * Delete a template
+ * DELETE /api/admin/email-templates/:id
+ */
+ router.delete('/:id', async (req, res, next) => {
+ try {
+ const { id } = req.params;
+
+
+ if (!req.user.is_admin) {
+ return res.status(403).json({
+ error: true,
+ message: 'Admin access required'
+ });
+ }
+
+ // Check if the template exists and is not a default template
+ const templateCheck = await query(
+ 'SELECT * FROM system_settings WHERE key = $1',
+ [id]
+ );
+
+ if (templateCheck.rows.length === 0) {
+ return res.status(404).json({
+ error: true,
+ message: 'Template not found'
+ });
+ }
+
+ // Parse the template data to check if it's a default template
+ try {
+ const templateData = JSON.parse(templateCheck.rows[0].value);
+ if (templateData.isDefault) {
+ return res.status(400).json({
+ error: true,
+ message: 'Cannot delete a default template. Please set another template as default first.'
+ });
+ }
+ } catch (e) {
+ console.error(`Failed to parse template setting: ${id}`, e);
+ }
+
+ // Delete the template
+ await query(
+ 'DELETE FROM system_settings WHERE key = $1',
+ [id]
+ );
+
+ res.json({
+ success: true,
+ message: 'Template deleted successfully'
+ });
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * Send a test email using a template
+ * POST /api/admin/email-templates/:id/test
+ */
+ router.post('/:id/test', async (req, res, next) => {
+ try {
+ const { id } = req.params;
+ const { email, variables } = req.body;
+
+
+ if (!req.user.is_admin) {
+ return res.status(403).json({
+ error: true,
+ message: 'Admin access required'
+ });
+ }
+
+ // Validate email address
+ if (!email) {
+ return res.status(400).json({
+ error: true,
+ message: 'Email address is required'
+ });
+ }
+
+ // Check if the template exists
+ const templateCheck = await query(
+ 'SELECT * FROM system_settings WHERE key = $1',
+ [id]
+ );
+
+ if (templateCheck.rows.length === 0) {
+ return res.status(404).json({
+ error: true,
+ message: 'Template not found'
+ });
+ }
+
+ // Parse the template data
+ let templateData;
+ try {
+ templateData = JSON.parse(templateCheck.rows[0].value);
+ } catch (e) {
+ console.error(`Failed to parse template setting: ${id}`, e);
+ return res.status(500).json({
+ error: true,
+ message: 'Failed to parse template data'
+ });
+ }
+
+ // Replace variables in template
+ let emailContent = templateData.content;
+ let emailSubject = templateData.subject;
+
+ if (variables) {
+ for (const [key, value] of Object.entries(variables)) {
+ const placeholder = `{{${key}}}`;
+ emailContent = emailContent.replace(new RegExp(placeholder, 'g'), value);
+ emailSubject = emailSubject.replace(new RegExp(placeholder, 'g'), value);
+ }
+ }
+
+ // Create a transporter
+ const transporter = createTransporter();
+
+ // Send the test email
+ await transporter.sendMail({
+ from: config.email.reply,
+ to: email,
+ subject: `[TEST] ${emailSubject}`,
+ html: emailContent
+ });
+
+ res.json({
+ success: true,
+ message: `Test email sent to ${email}`
+ });
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * Utility function to get a template by type
+ * @param {string} type - Template type
+ * @returns {Promise