diff --git a/server/index.js b/server/index.js index 4532c23..05b6fb8 100644 --- a/server/index.js +++ b/server/index.js @@ -116,6 +116,17 @@ async function startServer() { console.log('πŸ“± SMS Alerts: ⚠️ Disabled (no Twilio credentials)'); } + // Start periodic clear alert checking (every 2 minutes) + setInterval(async () => { + try { + await alertService.checkClearedAlerts(); + } catch (error) { + console.error('Error checking cleared alerts:', error); + } + }, 2 * 60 * 1000); // Check every 2 minutes + + console.log('πŸ”„ Clear alert monitoring: βœ… Started'); + console.log(`πŸ“Š Health check: http://localhost:${PORT}/health`); console.log(`🌐 API endpoint: http://localhost:${PORT}/api`); console.log('================================================\n'); diff --git a/server/models/AlertRule.js b/server/models/AlertRule.js index 3d95fff..99baed3 100644 --- a/server/models/AlertRule.js +++ b/server/models/AlertRule.js @@ -74,6 +74,11 @@ module.exports = (sequelize) => { defaultValue: ['sms'], comment: 'Array of alert channels: sms, email, webhook' }, + sms_phone_number: { + type: DataTypes.STRING, + allowNull: true, + comment: 'Phone number for SMS alerts' + }, webhook_url: { type: DataTypes.STRING, allowNull: true, diff --git a/server/seedDatabase.js b/server/seedDatabase.js index 7e77544..ddeace1 100644 --- a/server/seedDatabase.js +++ b/server/seedDatabase.js @@ -1,5 +1,5 @@ -const bcrypt = require('bcryptjs'); -const { User, Device } = require('./models'); +const bcrypt = require('bcrypt'); +const { User, Device, AlertRule } = require('./models'); async function seedDatabase() { try { @@ -74,6 +74,65 @@ async function seedDatabase() { console.log('βœ… Devices already exist'); } + // Get admin user for alert rules + const adminUser = await User.findOne({ where: { username: 'admin' } }); + + // Create location-specific alert rules + const alertRuleCount = await AlertRule.count(); + if (alertRuleCount === 0) { + await AlertRule.bulkCreate([ + { + user_id: adminUser.id, + name: 'Arlanda Airport Security Alert', + description: 'High-priority drone detection alert for Arlanda Airport', + is_active: true, + device_ids: [1], // Arlanda detector only + min_rssi: -85, // Alert on any detection + priority: 'critical', + alert_channels: ['sms'], + sms_phone_number: '0736419592', + cooldown_period: 300, // 5 minutes between alerts + min_detections: 1, + time_window: 60 // 1 minute window + }, + { + user_id: adminUser.id, + name: 'MuskΓΆ Naval Base Security Alert', + description: 'High-priority drone detection alert for MuskΓΆ Naval Base', + is_active: true, + device_ids: [2], // MuskΓΆ detector only + min_rssi: -85, // Alert on any detection + priority: 'critical', + alert_channels: ['sms'], + sms_phone_number: '0739999999', + cooldown_period: 300, // 5 minutes between alerts + min_detections: 1, + time_window: 60 // 1 minute window + }, + { + user_id: adminUser.id, + name: 'Royal Castle Security Alert', + description: 'High-priority drone detection alert for Royal Castle', + is_active: true, + device_ids: [3], // Royal Castle detector only + min_rssi: -85, // Alert on any detection + priority: 'critical', + alert_channels: ['sms'], + sms_phone_number: '0739999999', + cooldown_period: 300, // 5 minutes between alerts + min_detections: 1, + time_window: 60 // 1 minute window + } + ]); + + console.log('βœ… Location-specific alert rules created:'); + console.log(' - Arlanda Airport β†’ 0736419592'); + console.log(' - MuskΓΆ Naval Base β†’ 073999999'); + console.log(' - Royal Castle β†’ 073999999'); + } else { + console.log('βœ… Alert rules already exist'); + } + console.log('🌱 Database seeding completed'); } catch (error) { console.error('❌ Database seeding failed:', error); diff --git a/server/services/alertService.js b/server/services/alertService.js index 2217a63..0074955 100644 --- a/server/services/alertService.js +++ b/server/services/alertService.js @@ -7,6 +7,7 @@ class AlertService { this.twilioClient = null; this.twilioPhone = null; this.twilioEnabled = false; + this.activeAlerts = new Map(); // Track active alerts for clear notifications this.initializeTwilio(); } @@ -156,6 +157,15 @@ class AlertService { for (const rule of alertRules) { if (await this.shouldTriggerAlert(rule, detection, threatAssessment)) { await this.triggerAlert(rule, detection, threatAssessment); + + // Track active alert for potential clear notification + const alertKey = `${rule.id}-${detection.device_id}`; + this.activeAlerts.set(alertKey, { + rule, + detection, + threatAssessment, + alertTime: new Date() + }); } } @@ -313,7 +323,7 @@ class AlertService { // SECURITY ENHANCEMENT: For critical threats, send to all available channels const channels = threatAssessment.level === 'critical' ? ['sms', 'email', 'webhook'] // Force all channels for critical threats - : rule.actions.channels || ['sms']; + : rule.alert_channels || ['sms']; // Send alerts through configured channels for (const channel of channels) { @@ -322,8 +332,8 @@ class AlertService { try { switch (channel) { case 'sms': - if (rule.actions.sms && rule.actions.phone_number) { - alertLog = await this.sendSMSAlert(rule.actions.phone_number, message, rule, detection, threatAssessment); + if (rule.alert_channels.includes('sms') && rule.sms_phone_number) { + alertLog = await this.sendSMSAlert(rule.sms_phone_number, message, rule, detection, threatAssessment); } break; @@ -375,7 +385,8 @@ class AlertService { // Check if Twilio is enabled if (!this.twilioEnabled || !this.twilioClient) { console.log('πŸ“± SMS alert skipped - Twilio not configured'); - console.log(`πŸ“± Would have sent to ${phoneNumber}: ${message}`); + console.log(`πŸ“± Would have sent to ${phoneNumber}:`); + console.log(`πŸ“± Message: ${message}`); return await AlertLog.create({ alert_rule_id: rule.id, @@ -392,7 +403,8 @@ class AlertService { } try { - console.log(`πŸ“± Sending SMS alert to ${phoneNumber}`); + console.log(`πŸ“± Sending SMS alert to ${phoneNumber} for rule: ${rule.name}`); + console.log(`πŸ“± Message: ${message}`); const twilioMessage = await this.twilioClient.messages.create({ body: message, @@ -400,7 +412,7 @@ class AlertService { to: phoneNumber }); - console.log(`βœ… SMS sent successfully: ${twilioMessage.sid}`); + console.log(`βœ… SMS sent successfully to ${phoneNumber}: ${twilioMessage.sid}`); return await AlertLog.create({ alert_rule_id: rule.id, @@ -414,7 +426,7 @@ class AlertService { priority: rule.priority }); } catch (error) { - console.error('❌ Failed to send SMS:', error.message); + console.error(`❌ Failed to send SMS to ${phoneNumber}:`, error.message); return await AlertLog.create({ alert_rule_id: rule.id, @@ -553,6 +565,59 @@ class AlertService { return hours * 60 + minutes; } + + // Check for cleared alerts (call this periodically) + async checkClearedAlerts() { + try { + const now = new Date(); + const clearThreshold = 5 * 60 * 1000; // 5 minutes without detection = cleared + + for (const [alertKey, alertData] of this.activeAlerts.entries()) { + const timeSinceAlert = now - alertData.alertTime; + + if (timeSinceAlert > clearThreshold) { + // Check if there are any recent detections from this device + const recentDetections = await DroneDetection.count({ + where: { + device_id: alertData.detection.device_id, + device_timestamp: { + [Op.gte]: new Date(now - clearThreshold) + } + } + }); + + if (recentDetections === 0) { + await this.sendClearAlert(alertData); + this.activeAlerts.delete(alertKey); + } + } + } + } catch (error) { + console.error('Error checking cleared alerts:', error); + } + } + + async sendClearAlert(alertData) { + const { rule, detection } = alertData; + + if (rule.alert_channels.includes('sms') && rule.sms_phone_number) { + const device = await Device.findByPk(detection.device_id); + const clearMessage = this.generateClearMessage(device, rule); + + console.log(`🟒 ALERT CLEARED - Sending clear notification for ${device.name} to ${rule.sms_phone_number}`); + + await this.sendSMSAlert(rule.sms_phone_number, clearMessage, rule, detection, null); + } + } + + generateClearMessage(device, rule) { + return `🟒 ALL CLEAR 🟒\n\n` + + `πŸ“ LOCATION: ${device.location_description || device.name}\n` + + `πŸ”§ DEVICE: ${device.name}\n` + + `⏰ TIME: ${new Date().toLocaleString('sv-SE')}\n\n` + + `βœ… No drone activity detected for 5+ minutes.\n` + + `πŸ›‘οΈ Area is secure.`; + } } // Export the class directly (not a singleton instance)