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.
This commit is contained in:
parent
82ff3ecfc3
commit
4c67a79be0
@ -89,15 +89,15 @@
|
||||
"targetHandle": "html-1752550500000-left"
|
||||
},
|
||||
{
|
||||
"id": "gateway-1752550505000-notification-1752621850786",
|
||||
"data": { "condition": "todoStatus === false" },
|
||||
"id": "gateway-1752550505000-form-1752546702226-1753409198979",
|
||||
"data": {},
|
||||
"type": "custom",
|
||||
"label": "Not Completed",
|
||||
"source": "gateway-1752550505000",
|
||||
"target": "notification-1752621850786",
|
||||
"target": "form-1752546702226",
|
||||
"animated": true,
|
||||
"sourceHandle": "gateway-1752550505000-bottom",
|
||||
"targetHandle": "notification-1752621850786-left"
|
||||
"targetHandle": "form-1752546702226-top"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
@ -106,14 +106,14 @@
|
||||
"data": { "label": "Start", "description": "Process start point" },
|
||||
"type": "start",
|
||||
"label": "Start",
|
||||
"position": { "x": 300, "y": 135 }
|
||||
"position": { "x": 300, "y": 300 }
|
||||
},
|
||||
{
|
||||
"id": "form-1752546702226",
|
||||
"data": {
|
||||
"label": "Pilihan Kategori Asnaf",
|
||||
"shape": "rectangle",
|
||||
"formId": 7,
|
||||
"formId": 1,
|
||||
"formName": "Pilihan Kategori Asnaf",
|
||||
"formUuid": "d3612e05-b31a-46dc-b5e5-67e6c5bd3e78",
|
||||
"textColor": "#6b21a8",
|
||||
@ -136,7 +136,7 @@
|
||||
},
|
||||
"type": "form",
|
||||
"label": "Pilihan Kategori Asnaf",
|
||||
"position": { "x": 510, "y": 105 }
|
||||
"position": { "x": 555, "y": 270 }
|
||||
},
|
||||
{
|
||||
"id": "end-1752546716111",
|
||||
@ -170,16 +170,20 @@
|
||||
},
|
||||
"type": "api",
|
||||
"label": "API Call",
|
||||
"position": { "x": 795, "y": 105 }
|
||||
"position": { "x": 795, "y": -60 }
|
||||
},
|
||||
{
|
||||
"id": "script-1752550430989",
|
||||
"data": {
|
||||
"label": "Script Task",
|
||||
"shape": "rectangle",
|
||||
"textColor": "#374151",
|
||||
"scriptCode": "// Map API response to process variables\nconst api = processVariables.apiResponse || {};\nprocessVariables.todoTitle = api.kategori_asnaf || '';\nprocessVariables.namaAsnaf = api.nama_asnaf || '';\nprocessVariables.todoStatus = api.id > 100; // true if id > 100, otherwise false\n\n// New logic: Calculate a score\nconst katLen = (api.kategori_asnaf || '').length;\nconst namaLen = (api.nama_asnaf || '').length;\nprocessVariables.asnafScore = katLen * 10 + namaLen * 5;\n\n// New logic: Add a timestamp\nprocessVariables.resultTimestamp = new Date().toISOString();\n\n// New logic: Create a summary string\nprocessVariables.resultSummary = `Asnaf: ${processVariables.todoTitle}, Nama: ${processVariables.namaAsnaf}, Score: ${processVariables.asnafScore}, Time: ${processVariables.resultTimestamp}`;\n",
|
||||
"borderColor": "#6b7280",
|
||||
"description": "Execute JavaScript code",
|
||||
"inputVariables": ["apiResponse"],
|
||||
"scriptLanguage": "javascript",
|
||||
"backgroundColor": "#f9fafb",
|
||||
"outputVariables": [
|
||||
{
|
||||
"name": "todoTitle",
|
||||
@ -215,7 +219,7 @@
|
||||
},
|
||||
"type": "script",
|
||||
"label": "Script Task",
|
||||
"position": { "x": 1050, "y": 120 }
|
||||
"position": { "x": 1110, "y": -60 }
|
||||
},
|
||||
{
|
||||
"id": "gateway-1752550505000",
|
||||
@ -228,14 +232,6 @@
|
||||
"id": "condition-group-1",
|
||||
"output": "Completed",
|
||||
"conditions": [
|
||||
{
|
||||
"id": "condition-1",
|
||||
"value": true,
|
||||
"operator": "eq",
|
||||
"variable": "todoStatus",
|
||||
"valueType": "boolean",
|
||||
"logicalOperator": "and"
|
||||
},
|
||||
{
|
||||
"id": "condition-1753408402567",
|
||||
"value": "afiq",
|
||||
@ -254,10 +250,12 @@
|
||||
"conditions": [
|
||||
{
|
||||
"id": "condition-2",
|
||||
"value": false,
|
||||
"operator": "eq",
|
||||
"variable": "todoStatus",
|
||||
"valueType": "boolean",
|
||||
"value": "afiq",
|
||||
"maxValue": "",
|
||||
"minValue": "",
|
||||
"operator": "not_contains",
|
||||
"variable": "namaAsnaf",
|
||||
"valueType": "string",
|
||||
"logicalOperator": "and"
|
||||
}
|
||||
]
|
||||
@ -291,7 +289,7 @@
|
||||
},
|
||||
"type": "html",
|
||||
"label": "Show Result",
|
||||
"position": { "x": 1590, "y": 150 }
|
||||
"position": { "x": 1635, "y": 150 }
|
||||
},
|
||||
{
|
||||
"id": "notification-1752621850786",
|
||||
@ -302,26 +300,26 @@
|
||||
"priority": "medium",
|
||||
"expiration": { "unit": "hours", "value": 24, "enabled": false },
|
||||
"description": "Send notification to users",
|
||||
"htmlMessage": "",
|
||||
"messageFormat": "text",
|
||||
"htmlMessage": "<div style=\"font-family: Arial, sans-serif; padding: 15px;\">\n <h2 style=\"color: #3b82f6;\">Notification Title</h2>\n <p>Hello {namaAsnaf},</p>\n <p>This is a basic notification message.</p>\n <p>Thank you,<br>Process Maker</p>\n</div>",
|
||||
"messageFormat": "html",
|
||||
"recipientRole": "",
|
||||
"recipientType": "email",
|
||||
"recipientUser": "",
|
||||
"recipientEmail": "mdafiqiskandar@gmail.com",
|
||||
"recipientGroup": "",
|
||||
"deliveryOptions": { "sms": false, "email": false, "inApp": true },
|
||||
"deliveryOptions": { "sms": false, "email": true, "inApp": true },
|
||||
"richTextMessage": "",
|
||||
"notificationType": "info",
|
||||
"recipientVariable": ""
|
||||
},
|
||||
"type": "notification",
|
||||
"label": "Notification",
|
||||
"position": { "x": 1590, "y": 360 }
|
||||
"position": { "x": 1815, "y": 405 }
|
||||
}
|
||||
],
|
||||
"viewport": {
|
||||
"x": -271.3433493533669,
|
||||
"y": 163.8456958483416,
|
||||
"zoom": 0.6965142034495484
|
||||
"x": -145.563928050559,
|
||||
"y": 328.559309674283,
|
||||
"zoom": 0.7049100631988332
|
||||
}
|
||||
}
|
||||
|
@ -559,5 +559,11 @@ export default defineNuxtConfig({
|
||||
requestSizeLimiter: false,
|
||||
},
|
||||
},
|
||||
"/api/notifications/**": {
|
||||
security: {
|
||||
xssValidator: false,
|
||||
requestSizeLimiter: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -66,6 +66,7 @@
|
||||
"luxon": "^3.1.0",
|
||||
"marked": "^16.1.1",
|
||||
"maska": "^1.5.0",
|
||||
"nodemailer": "^7.0.5",
|
||||
"pinia": "^2.1.6",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-vue": "^1.1.6",
|
||||
|
@ -676,14 +676,14 @@ const copyWorkflowLink = async (processId) => {
|
||||
</button>
|
||||
|
||||
<!-- Analytics Button -->
|
||||
<button
|
||||
<!-- <button
|
||||
@click="viewProcessAnalytics(process.id)"
|
||||
class="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
title="View Analytics"
|
||||
:disabled="loading"
|
||||
>
|
||||
<Icon name="material-symbols:analytics" class="text-lg" />
|
||||
</button>
|
||||
</button> -->
|
||||
|
||||
<!-- Show restore button for deleted processes -->
|
||||
<button
|
||||
|
@ -693,29 +693,105 @@ const executeCurrentStep = async () => {
|
||||
return;
|
||||
} else if (currentNode.value?.type === 'notification') {
|
||||
console.log(`[Workflow] Sending notification: ${currentNode.value.data?.label || currentNode.value.label}`);
|
||||
// Enhanced notification node execution
|
||||
|
||||
// Extract notification configuration
|
||||
const {
|
||||
notificationType = 'info',
|
||||
recipients = [],
|
||||
message = '',
|
||||
recipientType = 'email',
|
||||
recipientUser = '',
|
||||
recipientRole = '',
|
||||
recipientVariable = '',
|
||||
recipientEmail = '',
|
||||
subject = '',
|
||||
message = '',
|
||||
messageFormat = 'text',
|
||||
htmlMessage = '',
|
||||
richTextMessage = '',
|
||||
deliveryOptions = { inApp: true, email: false, sms: false },
|
||||
priority = 'medium',
|
||||
continueOnError = true
|
||||
} = currentNode.value.data || {};
|
||||
|
||||
try {
|
||||
// Simulate notification sending
|
||||
console.log('[Workflow] Notification sent:', { type: notificationType, recipients, message, subject });
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call delay
|
||||
// Only send email if email delivery is enabled
|
||||
if (deliveryOptions.email && (subject || message)) {
|
||||
console.log('[Workflow] Sending email notification...');
|
||||
|
||||
// Prepare notification payload
|
||||
const notificationPayload = {
|
||||
notificationType,
|
||||
recipientType,
|
||||
recipientData: {
|
||||
recipientUser,
|
||||
recipientRole,
|
||||
recipientVariable,
|
||||
recipientEmail
|
||||
},
|
||||
subject: substituteVariables(subject, processVariables.value),
|
||||
message: substituteVariables(message, processVariables.value),
|
||||
htmlMessage: htmlMessage ? substituteVariables(htmlMessage, processVariables.value) : '',
|
||||
messageFormat,
|
||||
processVariables: processVariables.value,
|
||||
processName: process.value?.processName || 'Process Workflow',
|
||||
deliveryOptions,
|
||||
priority
|
||||
};
|
||||
|
||||
// Send notification via API
|
||||
const response = await $fetch('/api/notifications/send', {
|
||||
method: 'POST',
|
||||
body: notificationPayload
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
console.log('[Workflow] Email notification sent successfully:', response);
|
||||
// You could update process variables with notification results if needed
|
||||
processVariables.value.lastNotificationResult = {
|
||||
success: true,
|
||||
sent: response.sent,
|
||||
recipients: response.recipients,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.message || 'Failed to send notification');
|
||||
}
|
||||
} else {
|
||||
console.log('[Workflow] Email delivery disabled or no content - skipping email notification');
|
||||
|
||||
// Handle in-app notifications if enabled
|
||||
if (deliveryOptions.inApp) {
|
||||
console.log('[Workflow] In-app notification would be displayed here');
|
||||
// In a real implementation, you would add the notification to a queue
|
||||
// or trigger an in-app notification system
|
||||
}
|
||||
|
||||
// Set notification result in process variables
|
||||
processVariables.value.lastNotificationResult = {
|
||||
success: true,
|
||||
sent: 0,
|
||||
recipients: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
note: 'Email delivery disabled'
|
||||
};
|
||||
}
|
||||
|
||||
// Only auto-progress if there's a single outgoing edge
|
||||
if (canAutoProgress(currentNode.value)) {
|
||||
moveToNextStep();
|
||||
} else {
|
||||
console.log('[Workflow] Notification sent, multiple paths available - waiting for user choice');
|
||||
console.log('[Workflow] Notification processed, multiple paths available - waiting for user choice');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Workflow] Notification failed:', err);
|
||||
|
||||
// Set error in process variables
|
||||
processVariables.value.lastNotificationError = {
|
||||
error: err.message || String(err),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (continueOnError) {
|
||||
console.log('[Workflow] Continuing despite notification error');
|
||||
if (canAutoProgress(currentNode.value)) {
|
||||
moveToNextStep();
|
||||
} else {
|
||||
|
452
server/api/notifications/send.post.js
Normal file
452
server/api/notifications/send.post.js
Normal file
@ -0,0 +1,452 @@
|
||||
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'
|
||||
});
|
||||
}
|
||||
});
|
@ -6730,6 +6730,11 @@ node-releases@^2.0.13:
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
|
||||
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
|
||||
|
||||
nodemailer@^7.0.5:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-7.0.5.tgz#3fa6607cab42401e4bedac9297e0aaa9ff84544b"
|
||||
integrity sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==
|
||||
|
||||
nopt@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
|
||||
|
Loading…
x
Reference in New Issue
Block a user