465 lines
14 KiB
JavaScript
465 lines
14 KiB
JavaScript
const nodemailer = require('nodemailer');
|
|
const twilio = require('twilio');
|
|
const logger = require('../utils/logger');
|
|
const database = require('../config/database');
|
|
const redis = require('../config/redis');
|
|
|
|
class NotificationService {
|
|
constructor() {
|
|
this.emailTransporter = null;
|
|
this.twilioClient = null;
|
|
this.isInitialized = false;
|
|
}
|
|
|
|
async initialize() {
|
|
try {
|
|
await this.setupEmailTransporter();
|
|
await this.setupTwilioClient();
|
|
this.isInitialized = true;
|
|
logger.info('Notification service initialized successfully');
|
|
} catch (error) {
|
|
logger.error('Failed to initialize Notification service:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async setupEmailTransporter() {
|
|
try {
|
|
this.emailTransporter = nodemailer.createTransporter({
|
|
host: process.env.SMTP_HOST,
|
|
port: process.env.SMTP_PORT,
|
|
secure: process.env.SMTP_PORT === '465',
|
|
auth: {
|
|
user: process.env.SMTP_USER,
|
|
pass: process.env.SMTP_PASS
|
|
}
|
|
});
|
|
|
|
// Verify connection
|
|
await this.emailTransporter.verify();
|
|
logger.info('Email transporter configured successfully');
|
|
} catch (error) {
|
|
logger.error('Failed to setup email transporter:', error);
|
|
this.emailTransporter = null;
|
|
}
|
|
}
|
|
|
|
async setupTwilioClient() {
|
|
try {
|
|
if (process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN) {
|
|
this.twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
|
|
logger.info('Twilio client configured successfully');
|
|
} else {
|
|
logger.warn('Twilio credentials not provided, SMS notifications disabled');
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to setup Twilio client:', error);
|
|
this.twilioClient = null;
|
|
}
|
|
}
|
|
|
|
async sendNotification(userId, notification) {
|
|
try {
|
|
const startTime = Date.now();
|
|
|
|
// Get user notification preferences
|
|
const user = await this.getUserNotificationPreferences(userId);
|
|
if (!user) {
|
|
logger.warn(`User ${userId} not found for notification`);
|
|
return false;
|
|
}
|
|
|
|
// Store notification in database
|
|
const notificationId = await this.storeNotification(userId, notification);
|
|
|
|
// Send notifications based on user preferences
|
|
const results = {
|
|
email: false,
|
|
sms: false,
|
|
inApp: true // Always store in-app
|
|
};
|
|
|
|
// Send email if enabled
|
|
if (user.email_enabled && user.email) {
|
|
results.email = await this.sendEmail(user.email, notification);
|
|
}
|
|
|
|
// Send SMS if enabled
|
|
if (user.sms_enabled && user.phone && this.twilioClient) {
|
|
results.sms = await this.sendSMS(user.phone, notification);
|
|
}
|
|
|
|
// Update notification status
|
|
await this.updateNotificationStatus(notificationId, results);
|
|
|
|
const processingTime = Date.now() - startTime;
|
|
logger.logNotification('multi_channel', userId, notification.title, results);
|
|
logger.logPerformance('notification_sending', processingTime, {
|
|
userId,
|
|
channels: Object.keys(results).filter(k => results[k]).length
|
|
});
|
|
|
|
return results;
|
|
} catch (error) {
|
|
logger.error('Error sending notification:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async sendEmail(recipients, notification) {
|
|
try {
|
|
if (!this.emailTransporter) {
|
|
logger.warn('Email transporter not configured');
|
|
return false;
|
|
}
|
|
|
|
const emailContent = this.formatEmailContent(notification);
|
|
|
|
const mailOptions = {
|
|
from: process.env.SMTP_USER,
|
|
to: Array.isArray(recipients) ? recipients.join(',') : recipients,
|
|
subject: emailContent.subject,
|
|
html: emailContent.html,
|
|
text: emailContent.text
|
|
};
|
|
|
|
const result = await this.emailTransporter.sendMail(mailOptions);
|
|
|
|
logger.logNotification('email', recipients, notification.title, 'sent');
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('Error sending email:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async sendSMS(recipients, notification) {
|
|
try {
|
|
if (!this.twilioClient) {
|
|
logger.warn('Twilio client not configured');
|
|
return false;
|
|
}
|
|
|
|
const message = this.formatSMSContent(notification);
|
|
const phoneNumbers = Array.isArray(recipients) ? recipients : [recipients];
|
|
|
|
const results = await Promise.allSettled(
|
|
phoneNumbers.map(phone =>
|
|
this.twilioClient.messages.create({
|
|
body: message,
|
|
from: process.env.TWILIO_PHONE_NUMBER,
|
|
to: phone
|
|
})
|
|
)
|
|
);
|
|
|
|
const successCount = results.filter(r => r.status === 'fulfilled').length;
|
|
const success = successCount === phoneNumbers.length;
|
|
|
|
if (success) {
|
|
logger.logNotification('sms', recipients, notification.title, 'sent');
|
|
} else {
|
|
logger.error('Some SMS messages failed to send');
|
|
}
|
|
|
|
return success;
|
|
} catch (error) {
|
|
logger.error('Error sending SMS:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async sendWebhook(url, data) {
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const success = response.ok;
|
|
|
|
if (success) {
|
|
logger.logNotification('webhook', url, 'Webhook notification', 'sent');
|
|
} else {
|
|
logger.error(`Webhook failed with status: ${response.status}`);
|
|
}
|
|
|
|
return success;
|
|
} catch (error) {
|
|
logger.error('Error sending webhook:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
formatEmailContent(notification) {
|
|
const severityColors = {
|
|
critical: '#dc3545',
|
|
warning: '#ffc107',
|
|
info: '#17a2b8',
|
|
success: '#28a745'
|
|
};
|
|
|
|
const color = severityColors[notification.severity] || '#6c757d';
|
|
|
|
const html = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>${notification.title}</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background-color: ${color}; color: white; padding: 20px; text-align: center; }
|
|
.content { padding: 20px; background-color: #f8f9fa; }
|
|
.footer { text-align: center; padding: 20px; color: #6c757d; font-size: 12px; }
|
|
.severity { display: inline-block; padding: 4px 8px; border-radius: 4px; color: white; font-size: 12px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>${notification.title}</h1>
|
|
<span class="severity" style="background-color: ${color};">${notification.severity.toUpperCase()}</span>
|
|
</div>
|
|
<div class="content">
|
|
<p>${notification.message}</p>
|
|
${notification.deviceId ? `<p><strong>Device ID:</strong> ${notification.deviceId}</p>` : ''}
|
|
${notification.data ? `<p><strong>Details:</strong> ${JSON.stringify(notification.data, null, 2)}</p>` : ''}
|
|
<p><strong>Timestamp:</strong> ${new Date().toLocaleString()}</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>This is an automated notification from the AI Agent IoT Dashboard.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
const text = `
|
|
${notification.title}
|
|
Severity: ${notification.severity.toUpperCase()}
|
|
|
|
${notification.message}
|
|
|
|
${notification.deviceId ? `Device ID: ${notification.deviceId}` : ''}
|
|
${notification.data ? `Details: ${JSON.stringify(notification.data)}` : ''}
|
|
Timestamp: ${new Date().toLocaleString()}
|
|
|
|
This is an automated notification from the AI Agent IoT Dashboard.
|
|
`;
|
|
|
|
return {
|
|
subject: `[${notification.severity.toUpperCase()}] ${notification.title}`,
|
|
html: html,
|
|
text: text
|
|
};
|
|
}
|
|
|
|
formatSMSContent(notification) {
|
|
let message = `${notification.severity.toUpperCase()}: ${notification.title}`;
|
|
|
|
if (notification.message) {
|
|
message += `\n${notification.message}`;
|
|
}
|
|
|
|
if (notification.deviceId) {
|
|
message += `\nDevice: ${notification.deviceId}`;
|
|
}
|
|
|
|
return message.substring(0, 160); // SMS character limit
|
|
}
|
|
|
|
async getUserNotificationPreferences(userId) {
|
|
try {
|
|
const [users] = await database.query(
|
|
`SELECT id, username, email, phone, email_enabled, sms_enabled, notification_preferences
|
|
FROM users WHERE id = ? AND status = "active"`,
|
|
[userId]
|
|
);
|
|
|
|
if (users.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const user = users[0];
|
|
const preferences = JSON.parse(user.notification_preferences || '{}');
|
|
|
|
return {
|
|
...user,
|
|
notification_preferences: preferences
|
|
};
|
|
} catch (error) {
|
|
logger.error('Error getting user notification preferences:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async storeNotification(userId, notification) {
|
|
try {
|
|
const result = await database.query(
|
|
`INSERT INTO notifications
|
|
(user_id, type, title, message, severity, device_id, data, status, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
|
[
|
|
userId,
|
|
notification.type,
|
|
notification.title,
|
|
notification.message,
|
|
notification.severity,
|
|
notification.deviceId,
|
|
JSON.stringify(notification.data || {}),
|
|
'sent'
|
|
]
|
|
);
|
|
|
|
return result.insertId;
|
|
} catch (error) {
|
|
logger.error('Failed to store notification:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async updateNotificationStatus(notificationId, results) {
|
|
try {
|
|
const status = results.email || results.sms ? 'delivered' : 'failed';
|
|
const deliveryInfo = JSON.stringify(results);
|
|
|
|
await database.query(
|
|
'UPDATE notifications SET status = ?, delivery_info = ?, updated_at = NOW() WHERE id = ?',
|
|
[status, deliveryInfo, notificationId]
|
|
);
|
|
} catch (error) {
|
|
logger.error('Failed to update notification status:', error);
|
|
}
|
|
}
|
|
|
|
async getUserNotifications(userId, limit = 50, offset = 0) {
|
|
try {
|
|
const notifications = await database.query(
|
|
`SELECT * FROM notifications
|
|
WHERE user_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[userId, limit, offset]
|
|
);
|
|
|
|
return notifications;
|
|
} catch (error) {
|
|
logger.error('Error getting user notifications:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async markNotificationAsRead(notificationId, userId) {
|
|
try {
|
|
await database.query(
|
|
'UPDATE notifications SET read_at = NOW() WHERE id = ? AND user_id = ?',
|
|
[notificationId, userId]
|
|
);
|
|
|
|
logger.info(`Notification ${notificationId} marked as read by user ${userId}`);
|
|
} catch (error) {
|
|
logger.error('Error marking notification as read:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async markAllNotificationsAsRead(userId) {
|
|
try {
|
|
await database.query(
|
|
'UPDATE notifications SET read_at = NOW() WHERE user_id = ? AND read_at IS NULL',
|
|
[userId]
|
|
);
|
|
|
|
logger.info(`All notifications marked as read for user ${userId}`);
|
|
} catch (error) {
|
|
logger.error('Error marking all notifications as read:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async deleteNotification(notificationId, userId) {
|
|
try {
|
|
await database.query(
|
|
'DELETE FROM notifications WHERE id = ? AND user_id = ?',
|
|
[notificationId, userId]
|
|
);
|
|
|
|
logger.info(`Notification ${notificationId} deleted by user ${userId}`);
|
|
} catch (error) {
|
|
logger.error('Error deleting notification:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getNotificationStatistics(userId = null) {
|
|
try {
|
|
let query = `
|
|
SELECT
|
|
type,
|
|
severity,
|
|
status,
|
|
COUNT(*) as count,
|
|
DATE(created_at) as date
|
|
FROM notifications
|
|
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
|
`;
|
|
|
|
const params = [];
|
|
|
|
if (userId) {
|
|
query += ' AND user_id = ?';
|
|
params.push(userId);
|
|
}
|
|
|
|
query += ' GROUP BY type, severity, status, DATE(created_at) ORDER BY date DESC';
|
|
|
|
const stats = await database.query(query, params);
|
|
return stats;
|
|
} catch (error) {
|
|
logger.error('Error getting notification statistics:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async cleanupOldNotifications(daysToKeep = 30) {
|
|
try {
|
|
const result = await database.query(
|
|
'DELETE FROM notifications WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)',
|
|
[daysToKeep]
|
|
);
|
|
|
|
logger.info(`Cleaned up ${result.affectedRows} old notifications`);
|
|
return result.affectedRows;
|
|
} catch (error) {
|
|
logger.error('Error cleaning up old notifications:', error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
async healthCheck() {
|
|
try {
|
|
const emailStatus = this.emailTransporter ? 'configured' : 'not_configured';
|
|
const smsStatus = this.twilioClient ? 'configured' : 'not_configured';
|
|
|
|
return {
|
|
status: this.isInitialized ? 'healthy' : 'not_initialized',
|
|
message: this.isInitialized ? 'Notification service is healthy' : 'Service not initialized',
|
|
email: emailStatus,
|
|
sms: smsStatus
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
status: 'unhealthy',
|
|
message: 'Notification service health check failed',
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new NotificationService();
|