Md Afiq Iskandar 4c67a79be0 Implement Email Notification System and Update Notification API
- Introduced a new API endpoint for sending email notifications using Nodemailer, allowing for customizable email content and recipient management.
- Enhanced the notification configuration to support various recipient types (email, user, role, variable) and added error handling for recipient resolution.
- Updated the process builder to include HTML message formatting and improved logging for notification processing.
- Modified the Nuxt configuration to include Nodemailer as a dependency and adjusted security settings for the notifications API.
- Refactored process definition JSON to accommodate changes in notification handling and updated UI components accordingly.
2025-07-25 11:12:58 +08:00

452 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import nodemailer from 'nodemailer';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Email templates for different notification types
const emailTemplates = {
info: {
color: '#3b82f6',
backgroundColor: '#eff6ff',
icon: ''
},
success: {
color: '#10b981',
backgroundColor: '#ecfdf5',
icon: '✅'
},
warning: {
color: '#f59e0b',
backgroundColor: '#fffbeb',
icon: '⚠️'
},
error: {
color: '#ef4444',
backgroundColor: '#fef2f2',
icon: '❌'
},
reminder: {
color: '#8b5cf6',
backgroundColor: '#f3e8ff',
icon: '⏰'
},
status: {
color: '#6366f1',
backgroundColor: '#eef2ff',
icon: '📊'
}
};
// Create Nodemailer transporter with configurable email provider settings
function createTransporter() {
// Default configuration for common providers
const providerConfigs = {
gmail: {
host: 'smtp.gmail.com',
port: 587,
secure: false
},
outlook: {
host: 'smtp-mail.outlook.com',
port: 587,
secure: false
},
yahoo: {
host: 'smtp.mail.yahoo.com',
port: 587,
secure: false
},
proton: {
host: 'mail.protonmail.ch',
port: 587,
secure: false,
tls: {
ciphers: 'SSLv3',
rejectUnauthorized: false
}
}
};
// Get provider from environment or default to custom
const provider = process.env.EMAIL_PROVIDER?.toLowerCase() || 'custom';
// Use provider-specific config or custom config
let config;
if (providerConfigs[provider]) {
config = {
...providerConfigs[provider],
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
}
};
} else {
// Custom SMTP configuration
config = {
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === 'true', // true for SSL (port 465), false for STARTTLS (port 587)
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
}
};
// Add custom TLS settings if provided
if (process.env.SMTP_TLS_CIPHERS || process.env.SMTP_TLS_REJECT_UNAUTHORIZED) {
config.tls = {};
if (process.env.SMTP_TLS_CIPHERS) {
config.tls.ciphers = process.env.SMTP_TLS_CIPHERS;
}
if (process.env.SMTP_TLS_REJECT_UNAUTHORIZED) {
config.tls.rejectUnauthorized = process.env.SMTP_TLS_REJECT_UNAUTHORIZED === 'true';
}
}
}
return nodemailer.createTransport(config);
}
// Generate HTML email template
function generateEmailTemplate(notificationType, subject, message, processName) {
const template = emailTemplates[notificationType] || emailTemplates.info;
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${subject}</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
.container { max-width: 600px; margin: 0 auto; background-color: white; }
.header { background-color: ${template.backgroundColor}; padding: 20px; border-bottom: 3px solid ${template.color}; }
.header h1 { color: ${template.color}; margin: 0; font-size: 24px; }
.icon { font-size: 32px; margin-bottom: 10px; }
.content { padding: 30px; }
.message { font-size: 16px; line-height: 1.6; color: #333; margin-bottom: 20px; }
.footer { background-color: #f8f9fa; padding: 20px; text-align: center; font-size: 12px; color: #6c757d; border-top: 1px solid #e9ecef; }
.process-info { background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid ${template.color}; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="icon">${template.icon}</div>
<h1>${subject}</h1>
</div>
<div class="content">
<div class="message">
${message.replace(/\n/g, '<br>')}
</div>
${processName ? `
<div class="process-info">
<strong>Process:</strong> ${processName}
</div>
` : ''}
</div>
<div class="footer">
<p>This is an automated notification from the Corrad Process Management System.</p>
<p>Please do not reply to this email.</p>
</div>
</div>
</body>
</html>
`;
}
// Resolve recipients based on recipient type
async function resolveRecipients(recipientType, recipientData, processVariables) {
const recipients = [];
try {
switch (recipientType) {
case 'email':
// Direct email address
if (recipientData.recipientEmail && isValidEmail(recipientData.recipientEmail)) {
recipients.push({
email: recipientData.recipientEmail,
name: recipientData.recipientEmail
});
}
break;
case 'user':
// Look up user from database
if (recipientData.recipientUser) {
const user = await prisma.user.findUnique({
where: { id: recipientData.recipientUser },
select: { email: true, name: true, firstname: true, lastname: true }
});
if (user && user.email && isValidEmail(user.email)) {
const displayName = user.name || `${user.firstname || ''} ${user.lastname || ''}`.trim() || user.email;
recipients.push({
email: user.email,
name: displayName
});
}
}
break;
case 'role':
// Look up users by role
if (recipientData.recipientRole) {
const usersWithRole = await prisma.user.findMany({
where: {
userrole: {
some: {
roleid: recipientData.recipientRole
}
}
},
select: { email: true, name: true, firstname: true, lastname: true }
});
usersWithRole.forEach(user => {
if (user.email && isValidEmail(user.email)) {
const displayName = user.name || `${user.firstname || ''} ${user.lastname || ''}`.trim() || user.email;
recipients.push({
email: user.email,
name: displayName
});
}
});
}
break;
case 'variable':
// Get recipient from process variable
if (recipientData.recipientVariable && processVariables[recipientData.recipientVariable]) {
const variableValue = processVariables[recipientData.recipientVariable];
if (typeof variableValue === 'string' && isValidEmail(variableValue)) {
recipients.push({
email: variableValue,
name: variableValue
});
} else if (typeof variableValue === 'object' && variableValue.email && isValidEmail(variableValue.email)) {
recipients.push({
email: variableValue.email,
name: variableValue.name || variableValue.email
});
}
}
break;
case 'group':
// For future implementation - group-based recipients
console.warn('Group-based recipients not yet implemented');
break;
default:
console.warn(`Unknown recipient type: ${recipientType}`);
}
} catch (error) {
console.error('Error resolving recipients:', error);
}
return recipients;
}
// Validate email address
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// Substitute variables in text
function substituteVariables(text, variables) {
if (typeof text !== 'string') return text;
// Replace {{variable}} format
text = text.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (match, varName) => {
const value = variables[varName];
if (value === undefined || value === null) return '';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
});
// Replace {variable} format
text = text.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, varName) => {
const value = variables[varName];
if (value === undefined || value === null) return '';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
});
return text;
}
export default defineEventHandler(async (event) => {
try {
// Only allow POST requests
if (event.node.req.method !== 'POST') {
throw createError({
statusCode: 405,
statusMessage: 'Method Not Allowed'
});
}
// Parse request body
const body = await readBody(event);
const {
notificationType = 'info',
recipientType = 'email',
recipientData = {},
subject = '',
message = '',
processVariables = {},
processName = '',
deliveryOptions = { email: true },
priority = 'medium',
htmlMessage = '',
messageFormat = 'text'
} = body;
// Validate required fields
if (!subject && !message) {
throw createError({
statusCode: 400,
statusMessage: 'Subject or message is required'
});
}
// Check if email delivery is enabled
if (!deliveryOptions.email) {
return {
success: true,
message: 'Email delivery disabled for this notification',
sent: 0
};
}
// Check environment variables
if (!process.env.EMAIL_USER || !process.env.EMAIL_PASSWORD) {
throw createError({
statusCode: 500,
statusMessage: 'Email configuration missing. Please set EMAIL_USER and EMAIL_PASSWORD environment variables.'
});
}
// For custom SMTP, also check required SMTP settings
const provider = process.env.EMAIL_PROVIDER?.toLowerCase() || 'custom';
if (provider === 'custom' && !process.env.SMTP_HOST) {
throw createError({
statusCode: 500,
statusMessage: 'SMTP configuration missing. Please set SMTP_HOST for custom email provider.'
});
}
// Substitute variables in content
const processedSubject = substituteVariables(subject, processVariables);
const processedMessage = substituteVariables(message, processVariables);
const processedHtmlMessage = htmlMessage ? substituteVariables(htmlMessage, processVariables) : '';
// Resolve recipients
const recipients = await resolveRecipients(recipientType, recipientData, processVariables);
if (recipients.length === 0) {
throw createError({
statusCode: 400,
statusMessage: 'No valid recipients found'
});
}
// Create transporter
const transporter = createTransporter();
// Verify connection
try {
await transporter.verify();
} catch (error) {
console.error('SMTP connection failed:', error);
throw createError({
statusCode: 500,
statusMessage: 'Failed to connect to email server'
});
}
// Prepare email content
let emailHtml;
if (messageFormat === 'html' && processedHtmlMessage) {
emailHtml = processedHtmlMessage;
} else {
emailHtml = generateEmailTemplate(notificationType, processedSubject, processedMessage, processName);
}
// Send emails
const results = [];
let successCount = 0;
let failureCount = 0;
for (const recipient of recipients) {
try {
const mailOptions = {
from: {
name: process.env.EMAIL_FROM_NAME || 'Corrad Process System',
address: process.env.EMAIL_USER
},
to: {
name: recipient.name,
address: recipient.email
},
subject: processedSubject,
text: processedMessage,
html: emailHtml,
priority: priority === 'high' ? 'high' : priority === 'low' ? 'low' : 'normal'
};
const info = await transporter.sendMail(mailOptions);
results.push({
recipient: recipient.email,
success: true,
messageId: info.messageId
});
successCount++;
console.log(`Email sent successfully to ${recipient.email}:`, info.messageId);
} catch (error) {
results.push({
recipient: recipient.email,
success: false,
error: error.message
});
failureCount++;
console.error(`Failed to send email to ${recipient.email}:`, error.message);
}
}
// Close transporter
transporter.close();
// Return results
return {
success: successCount > 0,
message: `Email notification processed. Sent: ${successCount}, Failed: ${failureCount}`,
sent: successCount,
failed: failureCount,
recipients: recipients.length,
details: results
};
} catch (error) {
console.error('Notification API Error:', error);
// Handle createError objects
if (error.statusCode) {
throw error;
}
// Handle other errors
throw createError({
statusCode: 500,
statusMessage: error.message || 'Failed to send notification'
});
}
});