Whole new user interface and user system

This commit is contained in:
2025-09-30 15:47:57 -06:00
parent 75fefb20cb
commit acd2234db5
26 changed files with 4125 additions and 134 deletions

View File

@@ -48,6 +48,24 @@ class AuthController {
});
}
// Check if temp user has expired
const userType = user['User Type'] || user.UserType || user.userType || 'user';
if (userType === 'temp') {
const expiration = user.ExpiresAt || user.expiresAt || user.Expiration || user.expiration;
if (expiration) {
const expirationDate = new Date(expiration);
const now = new Date();
if (now > expirationDate) {
console.warn(`Expired temp user attempted login: ${email}, expired: ${expiration}`);
return res.status(401).json({
success: false,
error: 'Account has expired. Please contact an administrator.'
});
}
}
}
// Update last login time
try {
// Debug: Log user object structure
@@ -78,6 +96,7 @@ class AuthController {
req.session.userEmail = user.Email || user.email;
req.session.userName = user.Name || user.name;
req.session.isAdmin = user.Admin || user.admin || false;
req.session.userType = userType;
console.log('User logged in successfully:', {
email: req.session.userEmail,
@@ -100,7 +119,8 @@ class AuthController {
id: req.session.userId,
email: req.session.userEmail,
name: req.session.userName,
isAdmin: req.session.isAdmin
isAdmin: req.session.isAdmin,
userType: req.session.userType
}
});
});
@@ -151,7 +171,8 @@ class AuthController {
id: req.session.userId,
email: req.session.userEmail,
name: req.session.userName,
isAdmin: req.session.isAdmin
isAdmin: req.session.isAdmin,
userType: req.session.userType || 'user'
}
});
} else {

View File

@@ -3,6 +3,29 @@ const emailService = require('../services/email');
const representAPI = require('../services/represent-api');
const { generateSlug, validateSlug } = require('../utils/validators');
const VALID_CAMPAIGN_STATUSES = ['draft', 'active', 'paused', 'archived'];
const normalizeTargetLevels = (rawLevels) => {
if (Array.isArray(rawLevels)) {
return rawLevels;
}
if (typeof rawLevels === 'string' && rawLevels.length > 0) {
return rawLevels.split(',').map(level => level.trim()).filter(Boolean);
}
return [];
};
const normalizeStatus = (rawStatus, fallback = 'draft') => {
if (!rawStatus) {
return fallback;
}
const status = String(rawStatus).toLowerCase();
return VALID_CAMPAIGN_STATUSES.includes(status) ? status : fallback;
};
// Helper function to cache representatives
async function cacheRepresentatives(postalCode, representatives, representData) {
try {
@@ -40,6 +63,9 @@ class CampaignsController {
if (id != null) {
emailCount = await nocoDB.getCampaignEmailCount(id);
}
const rawTargetLevels = campaign['Target Government Levels'] || campaign.target_government_levels;
const normalizedTargetLevels = normalizeTargetLevels(rawTargetLevels);
// Normalize campaign data structure for frontend
return {
@@ -50,22 +76,42 @@ class CampaignsController {
email_subject: campaign['Email Subject'] || campaign.email_subject,
email_body: campaign['Email Body'] || campaign.email_body,
call_to_action: campaign['Call to Action'] || campaign.call_to_action,
status: campaign['Status'] || campaign.status,
status: normalizeStatus(campaign['Status'] || campaign.status),
allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email,
allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link,
collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info,
show_email_count: campaign['Show Email Count'] || campaign.show_email_count,
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
target_government_levels: campaign['Target Government Levels'] || campaign.target_government_levels,
target_government_levels: normalizedTargetLevels,
created_at: campaign.CreatedAt || campaign.created_at,
updated_at: campaign.UpdatedAt || campaign.updated_at,
created_by_user_id: campaign['Created By User ID'] || campaign.created_by_user_id || null,
created_by_user_email: campaign['Created By User Email'] || campaign.created_by_user_email || null,
created_by_user_name: campaign['Created By User Name'] || campaign.created_by_user_name || null,
emailCount
};
}));
let filteredCampaigns = campaignsWithCounts;
if (req.user && !req.user.isAdmin) {
const sessionUserId = req.user.id != null ? String(req.user.id) : (req.session?.userId != null ? String(req.session.userId) : null);
const sessionUserEmail = req.user.email ? String(req.user.email).toLowerCase() : (req.session?.userEmail ? String(req.session.userEmail).toLowerCase() : null);
filteredCampaigns = campaignsWithCounts.filter((campaign) => {
const createdById = campaign.created_by_user_id != null ? String(campaign.created_by_user_id) : null;
const createdByEmail = campaign.created_by_user_email ? String(campaign.created_by_user_email).toLowerCase() : null;
return (
(sessionUserId && createdById && createdById === sessionUserId) ||
(sessionUserEmail && createdByEmail && createdByEmail === sessionUserEmail)
);
});
}
res.json({
success: true,
campaigns: campaignsWithCounts
campaigns: filteredCampaigns
});
} catch (error) {
console.error('Get campaigns error:', error);
@@ -111,15 +157,18 @@ class CampaignsController {
email_subject: campaign['Email Subject'] || campaign.email_subject,
email_body: campaign['Email Body'] || campaign.email_body,
call_to_action: campaign['Call to Action'] || campaign.call_to_action,
status: campaign['Status'] || campaign.status,
status: normalizeStatus(campaign['Status'] || campaign.status),
allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email,
allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link,
collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info,
show_email_count: campaign['Show Email Count'] || campaign.show_email_count,
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
target_government_levels: campaign['Target Government Levels'] || campaign.target_government_levels,
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
created_at: campaign.CreatedAt || campaign.created_at,
updated_at: campaign.UpdatedAt || campaign.updated_at,
created_by_user_id: campaign['Created By User ID'] || campaign.created_by_user_id || null,
created_by_user_email: campaign['Created By User Email'] || campaign.created_by_user_email || null,
created_by_user_name: campaign['Created By User Name'] || campaign.created_by_user_name || null,
emailCount
}
});
@@ -147,8 +196,8 @@ class CampaignsController {
});
}
const campaignStatus = campaign['Status'] || campaign.status;
if (campaignStatus !== 'active') {
const campaignStatus = normalizeStatus(campaign['Status'] || campaign.status);
if (campaignStatus !== 'active') {
return res.status(403).json({
success: false,
error: 'Campaign is not currently active'
@@ -180,11 +229,7 @@ class CampaignsController {
collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info,
show_email_count: campaign['Show Email Count'] || campaign.show_email_count,
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
target_government_levels: Array.isArray(campaign['Target Government Levels'] || campaign.target_government_levels)
? (campaign['Target Government Levels'] || campaign.target_government_levels)
: (typeof (campaign['Target Government Levels'] || campaign.target_government_levels) === 'string' && (campaign['Target Government Levels'] || campaign.target_government_levels).length > 0
? (campaign['Target Government Levels'] || campaign.target_government_levels).split(',').map(s => s.trim())
: []),
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
emailCount
}
});
@@ -217,7 +262,10 @@ class CampaignsController {
target_government_levels = ['Federal', 'Provincial', 'Municipal']
} = req.body;
// Generate slug from title
const ownerUserId = req.user?.id ?? req.session?.userId ?? null;
const ownerEmail = req.user?.email ?? req.session?.userEmail ?? null;
const ownerName = req.user?.name ?? req.session?.userName ?? null;
const normalizedStatus = normalizeStatus(status, 'draft');
let slug = generateSlug(title);
// Ensure slug is unique
@@ -235,18 +283,18 @@ class CampaignsController {
email_subject,
email_body,
call_to_action,
status,
status: normalizedStatus,
allow_smtp_email,
allow_mailto_link,
collect_user_info,
show_email_count,
allow_email_editing,
// NocoDB MultiSelect expects an array of values
target_government_levels: Array.isArray(target_government_levels)
? target_government_levels
: (typeof target_government_levels === 'string' && target_government_levels.length > 0
? target_government_levels.split(',').map(s => s.trim())
: [])
target_government_levels: normalizeTargetLevels(target_government_levels),
// Add user ownership data
created_by_user_id: ownerUserId,
created_by_user_email: ownerEmail,
created_by_user_name: ownerName
};
const campaign = await nocoDB.createCampaign(campaignData);
@@ -262,15 +310,18 @@ class CampaignsController {
email_subject: campaign['Email Subject'] || campaign.email_subject,
email_body: campaign['Email Body'] || campaign.email_body,
call_to_action: campaign['Call to Action'] || campaign.call_to_action,
status: campaign['Status'] || campaign.status,
status: normalizeStatus(campaign['Status'] || campaign.status),
allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email,
allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link,
collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info,
show_email_count: campaign['Show Email Count'] || campaign.show_email_count,
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
target_government_levels: campaign['Target Government Levels'] || campaign.target_government_levels,
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
created_at: campaign.CreatedAt || campaign.created_at,
updated_at: campaign.UpdatedAt || campaign.updated_at
updated_at: campaign.UpdatedAt || campaign.updated_at,
created_by_user_id: campaign['Created By User ID'] || campaign.created_by_user_id || ownerUserId,
created_by_user_email: campaign['Created By User Email'] || campaign.created_by_user_email || ownerEmail,
created_by_user_name: campaign['Created By User Name'] || campaign.created_by_user_name || ownerName
}
});
} catch (error) {
@@ -288,18 +339,45 @@ class CampaignsController {
async updateCampaign(req, res, next) {
try {
const { id } = req.params;
const updates = req.body;
const updates = { ...req.body };
const existingCampaign = await nocoDB.getCampaignById(id);
if (!existingCampaign) {
return res.status(404).json({
success: false,
error: 'Campaign not found'
});
}
const isAdmin = (req.user && req.user.isAdmin) || req.session?.isAdmin || false;
const sessionUserId = req.user?.id ?? req.session?.userId ?? null;
const sessionUserEmail = req.user?.email ?? req.session?.userEmail ?? null;
if (!isAdmin) {
const createdById = existingCampaign['Created By User ID'] ?? existingCampaign.created_by_user_id ?? null;
const createdByEmail = existingCampaign['Created By User Email'] ?? existingCampaign.created_by_user_email ?? null;
const ownsCampaign = (
(createdById != null && sessionUserId != null && String(createdById) === String(sessionUserId)) ||
(createdByEmail && sessionUserEmail && String(createdByEmail).toLowerCase() === String(sessionUserEmail).toLowerCase())
);
if (!ownsCampaign) {
return res.status(403).json({
success: false,
error: 'Access denied'
});
}
}
// If title is being updated, regenerate slug
if (updates.title) {
let slug = generateSlug(updates.title);
// Ensure slug is unique (but allow current campaign to keep its slug)
const existingCampaign = await nocoDB.getCampaignBySlug(slug);
const existingId = existingCampaign ? (existingCampaign.ID || existingCampaign.Id || existingCampaign.id) : null;
if (existingCampaign && String(existingId) !== String(id)) {
const campaignWithSlug = await nocoDB.getCampaignBySlug(slug);
const existingId = campaignWithSlug ? (campaignWithSlug.ID || campaignWithSlug.Id || campaignWithSlug.id) : null;
if (campaignWithSlug && String(existingId) !== String(id)) {
let counter = 1;
let originalSlug = slug;
const originalSlug = slug;
while (await nocoDB.getCampaignBySlug(slug)) {
slug = `${originalSlug}-${counter}`;
counter++;
@@ -308,20 +386,33 @@ class CampaignsController {
updates.slug = slug;
}
// Ensure target_government_levels remains an array for MultiSelect
if (updates.target_government_levels) {
updates.target_government_levels = Array.isArray(updates.target_government_levels)
? updates.target_government_levels
: (typeof updates.target_government_levels === 'string'
? updates.target_government_levels.split(',').map(s => s.trim())
: []);
if (updates.target_government_levels !== undefined) {
updates.target_government_levels = normalizeTargetLevels(updates.target_government_levels);
}
updates.updated_at = new Date().toISOString();
if (updates.status !== undefined) {
const sanitizedStatus = normalizeStatus(updates.status, null);
if (!sanitizedStatus) {
return res.status(400).json({
success: false,
error: 'Invalid campaign status'
});
}
updates.status = sanitizedStatus;
}
// Remove user ownership fields from updates to prevent unauthorized changes
delete updates.created_by_user_id;
delete updates.created_by_user_email;
delete updates.created_by_user_name;
// Remove auto-generated fields that NocoDB handles
delete updates.updated_at;
delete updates.UpdatedAt;
delete updates.created_at;
delete updates.CreatedAt;
const campaign = await nocoDB.updateCampaign(id, updates);
// Normalize the updated campaign data
res.json({
success: true,
campaign: {
@@ -332,14 +423,18 @@ class CampaignsController {
email_subject: campaign['Email Subject'] || campaign.email_subject,
email_body: campaign['Email Body'] || campaign.email_body,
call_to_action: campaign['Call to Action'] || campaign.call_to_action,
status: campaign['Status'] || campaign.status,
status: normalizeStatus(campaign['Status'] || campaign.status),
allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email,
allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link,
collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info,
show_email_count: campaign['Show Email Count'] || campaign.show_email_count,
target_government_levels: campaign['Target Government Levels'] || campaign.target_government_levels,
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
created_at: campaign.CreatedAt || campaign.created_at,
updated_at: campaign.UpdatedAt || campaign.updated_at
updated_at: campaign.UpdatedAt || campaign.updated_at,
created_by_user_id: campaign['Created By User ID'] || campaign.created_by_user_id || null,
created_by_user_email: campaign['Created By User Email'] || campaign.created_by_user_email || null,
created_by_user_name: campaign['Created By User Name'] || campaign.created_by_user_name || null
}
});
} catch (error) {
@@ -393,17 +488,16 @@ class CampaignsController {
} = req.body;
// Get campaign
const campaign = await nocoDB.getCampaignBySlug(slug);
const campaign = await nocoDB.getCampaignBySlug(slug);
if (!campaign) {
return res.status(404).json({
success: false,
error: 'Campaign not found'
});
}
if ((campaign['Status'] || campaign.status) !== 'active') {
const campaignStatus = normalizeStatus(campaign['Status'] || campaign.status);
if (campaignStatus !== 'active') {
return res.status(403).json({
success: false,
error: 'Campaign is not currently active'
@@ -425,14 +519,14 @@ class CampaignsController {
});
}
// Use custom email content if provided (when email editing is enabled), otherwise use campaign defaults
const allowEmailEditing = campaign['Allow Email Editing'] || campaign.allow_email_editing;
const subject = (allowEmailEditing && customEmailSubject)
? customEmailSubject
: (campaign['Email Subject'] || campaign.email_subject);
const message = (allowEmailEditing && customEmailBody)
? customEmailBody
: (campaign['Email Body'] || campaign.email_body);
// Use custom email content if provided (when email editing is enabled), otherwise use campaign defaults
const allowEmailEditing = campaign['Allow Email Editing'] || campaign.allow_email_editing;
const subject = (allowEmailEditing && customEmailSubject)
? customEmailSubject
: (campaign['Email Subject'] || campaign.email_subject);
const message = (allowEmailEditing && customEmailBody)
? customEmailBody
: (campaign['Email Body'] || campaign.email_body);
let emailResult = { success: true };
@@ -515,7 +609,7 @@ class CampaignsController {
});
}
const campaignStatus = campaign['Status'] || campaign.status;
const campaignStatus = normalizeStatus(campaign['Status'] || campaign.status);
if (campaignStatus !== 'active') {
return res.status(403).json({
success: false,
@@ -570,7 +664,7 @@ class CampaignsController {
});
}
const campaignStatus = campaign['Status'] || campaign.status;
const campaignStatus = normalizeStatus(campaign['Status'] || campaign.status);
if (campaignStatus !== 'active') {
return res.status(403).json({
success: false,
@@ -687,6 +781,36 @@ class CampaignsController {
async getCampaignAnalytics(req, res, next) {
try {
const { id } = req.params;
const campaign = await nocoDB.getCampaignById(id);
if (!campaign) {
return res.status(404).json({
success: false,
error: 'Campaign not found'
});
}
const sessionUserId = req.user?.id ?? req.session?.userId ?? null;
const sessionUserEmail = req.user?.email ?? req.session?.userEmail ?? null;
const isAdmin = (req.user && req.user.isAdmin) || req.session?.isAdmin || false;
if (!isAdmin) {
const createdById = campaign['Created By User ID'] ?? campaign.created_by_user_id ?? null;
const createdByEmail = campaign['Created By User Email'] ?? campaign.created_by_user_email ?? null;
const ownsCampaign = (
(createdById != null && sessionUserId != null && String(createdById) === String(sessionUserId)) ||
(createdByEmail && sessionUserEmail && String(createdByEmail).toLowerCase() === String(sessionUserEmail).toLowerCase())
);
if (!ownsCampaign) {
return res.status(403).json({
success: false,
error: 'Access denied'
});
}
}
const analytics = await nocoDB.getCampaignAnalytics(id);

View File

@@ -0,0 +1,338 @@
const nocodbService = require('../services/nocodb');
const { sendLoginDetails } = require('../services/email');
const { sanitizeUser, extractId } = require('../utils/helpers');
class UsersController {
async getAll(req, res) {
try {
console.log('UsersController.getAll called');
console.log('Users table ID:', nocodbService.tableIds.users);
if (!nocodbService.tableIds.users) {
console.error('Users table not configured in environment');
return res.status(500).json({
success: false,
error: 'Users table not configured. Please set NOCODB_TABLE_USERS in your environment variables.'
});
}
console.log('Fetching users from NocoDB...');
const response = await nocodbService.getAll(nocodbService.tableIds.users, {
limit: 100
});
const users = response.list || [];
console.log(`Retrieved ${users.length} users from database`);
// Remove password field from response for security
const safeUsers = users.map(sanitizeUser);
res.json({
success: true,
users: safeUsers
});
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch users: ' + error.message
});
}
}
async create(req, res) {
try {
const { email, password, name, phone, isAdmin, userType, expireDays } = req.body;
if (!email || !password) {
return res.status(400).json({
success: false,
error: 'Email and password are required'
});
}
if (!nocodbService.tableIds.users) {
return res.status(500).json({
success: false,
error: 'Users table not configured'
});
}
// Check if user already exists
console.log(`Checking if user exists with email: ${email}`);
let existingUser = null;
try {
existingUser = await nocodbService.getUserByEmail(email);
console.log('Existing user check result:', existingUser ? 'User exists' : 'User does not exist');
} catch (error) {
console.error('Error checking for existing user:', error.message);
// Continue with creation if check fails - NocoDB will handle the unique constraint
}
if (existingUser) {
console.log('Existing user found:', { id: existingUser.ID || existingUser.Id || existingUser.id, email: existingUser.Email || existingUser.email });
return res.status(400).json({
success: false,
error: 'User with this email already exists'
});
}
// Calculate expiration date for temp users
let expiresAt = null;
if (userType === 'temp' && expireDays) {
const expirationDate = new Date();
expirationDate.setDate(expirationDate.getDate() + expireDays);
expiresAt = expirationDate.toISOString();
}
// Create new user - use the exact column titles from NocoDB schema
const userData = {
Email: email,
Name: name || '',
Password: password,
Phone: phone || '',
Admin: isAdmin === true,
'User Type': userType || 'user',
ExpiresAt: expiresAt,
ExpireDays: userType === 'temp' ? expireDays : null,
'Last Login': null
};
const response = await nocodbService.create(
nocodbService.tableIds.users,
userData
);
res.status(201).json({
success: true,
message: 'User created successfully',
user: {
id: extractId(response),
email: email,
name: name,
phone: phone,
admin: isAdmin,
userType: userType,
expiresAt: expiresAt
}
});
} catch (error) {
console.error('Error creating user:', error);
// Check if it's a unique constraint violation (email already exists)
if (error.response?.data?.code === '23505' ||
error.response?.data?.message?.includes('already exists') ||
error.message?.includes('already exists')) {
return res.status(400).json({
success: false,
error: 'A user with this email address already exists'
});
}
res.status(500).json({
success: false,
error: 'Failed to create user: ' + (error.message || 'Unknown error')
});
}
}
async delete(req, res) {
try {
const userId = req.params.id;
if (!nocodbService.tableIds.users) {
return res.status(500).json({
success: false,
error: 'Users table not configured'
});
}
// Don't allow admins to delete themselves
if (userId === req.session.userId) {
return res.status(400).json({
success: false,
error: 'Cannot delete your own account'
});
}
await nocodbService.deleteUser(userId);
res.json({
success: true,
message: 'User deleted successfully'
});
} catch (error) {
console.error('Error deleting user:', error);
res.status(500).json({
success: false,
error: 'Failed to delete user'
});
}
}
async sendLoginDetails(req, res) {
try {
const userId = req.params.id;
if (!nocodbService.tableIds.users) {
return res.status(500).json({
success: false,
error: 'Users table not configured'
});
}
// Get user data from database
const user = await nocodbService.getById(
nocodbService.tableIds.users,
userId
);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
// Send login details email
await sendLoginDetails(user);
res.json({
success: true,
message: 'Login details sent successfully'
});
} catch (error) {
console.error('Error sending login details:', error);
res.status(500).json({
success: false,
error: 'Failed to send login details'
});
}
}
async emailAllUsers(req, res) {
try {
const { subject, content } = req.body;
if (!subject || !content) {
return res.status(400).json({
success: false,
error: 'Subject and content are required'
});
}
if (!nocodbService.tableIds.users) {
return res.status(500).json({
success: false,
error: 'Users table not configured'
});
}
// Get all users
const response = await nocodbService.getAll(nocodbService.tableIds.users, {
limit: 1000
});
const users = response.list || [];
if (users.length === 0) {
return res.status(400).json({
success: false,
error: 'No users found to email'
});
}
// Import email service
const { sendEmail } = require('../services/email');
const emailTemplates = require('../services/emailTemplates');
// Convert rich text content to plain text for the text version
const stripHtmlTags = (html) => {
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
};
// Prepare base template variables
const baseTemplateVariables = {
APP_NAME: 'BNKops Influence - User Broadcast',
EMAIL_SUBJECT: subject,
EMAIL_CONTENT: content,
EMAIL_CONTENT_TEXT: stripHtmlTags(content),
SENDER_NAME: req.session.userName || req.session.userEmail || 'Administrator',
TIMESTAMP: new Date().toLocaleString()
};
// Send emails to all users
const emailResults = [];
const failedEmails = [];
for (const user of users) {
try {
const userVariables = {
...baseTemplateVariables,
USER_NAME: user.Name || user.name || user.Email || user.email || 'User',
USER_EMAIL: user.Email || user.email
};
const emailContent = await emailTemplates.render('user-broadcast', userVariables);
await sendEmail({
to: user.Email || user.email,
subject: subject,
text: emailContent.text,
html: emailContent.html
});
emailResults.push({
email: user.Email || user.email,
name: user.Name || user.name || user.Email || user.email,
success: true
});
console.log(`Sent broadcast email to: ${user.Email || user.email}`);
} catch (emailError) {
console.error(`Failed to send broadcast email to ${user.Email || user.email}:`, emailError);
failedEmails.push({
email: user.Email || user.email,
name: user.Name || user.name || user.Email || user.email,
error: emailError.message
});
}
}
const successCount = emailResults.length;
const failCount = failedEmails.length;
if (successCount === 0) {
return res.status(500).json({
success: false,
error: 'Failed to send any emails',
details: failedEmails
});
}
res.json({
success: true,
message: `Sent email to ${successCount} user${successCount !== 1 ? 's' : ''}${failCount > 0 ? `, ${failCount} failed` : ''}`,
results: {
successful: emailResults,
failed: failedEmails,
total: users.length,
subject: subject
}
});
} catch (error) {
console.error('Error sending broadcast email:', error);
res.status(500).json({
success: false,
error: 'Failed to send broadcast email'
});
}
}
}
module.exports = new UsersController();