const express = require('express'); const router = express.Router(); const Joi = require('joi'); const { validateRequest } = require('../middleware/validation'); // Use global test models if available, otherwise use regular models function getModels() { if (global.__TEST_MODELS__) { console.log('🔧 DEBUG: Using global test models in detectors route'); return global.__TEST_MODELS__; } return require('../models'); } const AlertService = require('../services/alertService'); const DroneTrackingService = require('../services/droneTrackingService'); const { getDroneTypeInfo, getDroneTypeName } = require('../utils/droneTypes'); // Configuration for debugging and data storage const DEBUG_CONFIG = { storeHeartbeats: process.env.STORE_HEARTBEATS === 'true', // Store heartbeat data for debugging storeNoneDetections: process.env.STORE_DRONE_TYPE0 === 'true', // Store drone_type 0 for debugging logAllDetections: process.env.LOG_ALL_DETECTIONS === 'true', // Log all detection data storeRawPayload: process.env.STORE_RAW_PAYLOAD === 'true' // Store complete raw payload for debugging }; // Initialize services const alertService = new AlertService(); const droneTracker = new DroneTrackingService(); // Handle movement alerts from the tracking service droneTracker.on('movement_alert', (alertData) => { const { io } = require('../index'); if (io) { // Emit to dashboard with detailed movement information io.emitToDashboard('drone_movement_alert', { ...alertData, timestamp: new Date().toISOString() }); // Emit to specific device room io.emitToDevice(alertData.deviceId, 'drone_movement_alert', { ...alertData, timestamp: new Date().toISOString() }); console.log(`🚨 Movement Alert: ${alertData.analysis.description} (Drone ${alertData.droneId})`); } }); // Simplified unified schema - validate based on payload type const detectorSchema = Joi.alternatives().try( // Heartbeat schema Joi.object({ type: Joi.string().valid('heartbeat').required(), key: Joi.string().required(), // Optional heartbeat fields device_id: Joi.alternatives().try( Joi.number().integer(), Joi.string() ).optional(), geo_lat: Joi.number().min(-90).max(90).optional(), geo_lon: Joi.number().min(-180).max(180).optional(), location_description: Joi.string().optional(), uptime: Joi.number().integer().min(0).optional(), memory_usage: Joi.number().integer().min(0).max(100).optional(), firmware_version: Joi.string().optional() }), // Detection schema Joi.object({ device_id: Joi.alternatives().try( Joi.number().integer(), Joi.string() ).required(), geo_lat: Joi.number().min(-90).max(90).required(), geo_lon: Joi.number().min(-180).max(180).required(), device_timestamp: Joi.number().integer().min(0).required(), drone_type: Joi.number().integer().min(0).required(), rssi: Joi.number().required(), freq: Joi.number().required(), drone_id: Joi.number().integer().required(), // Optional detection fields confidence_level: Joi.number().min(0).max(1).optional(), signal_duration: Joi.number().integer().min(0).optional() }) ); // POST /api/detectors - Unified endpoint for heartbeats and detections router.post('/', validateRequest(detectorSchema), async (req, res) => { try { // Log the full incoming payload for debugging console.log('📦 Full payload received:', JSON.stringify(req.body, null, 2)); console.log('📡 Request headers:', JSON.stringify({ 'user-agent': req.headers['user-agent'], 'content-type': req.headers['content-type'], 'content-length': req.headers['content-length'] }, null, 2)); // Determine if this is a heartbeat or detection based on payload if (req.body.type === 'heartbeat') { return await handleHeartbeat(req, res); } else if (req.body.device_id) { return await handleDetection(req, res); } else { return res.status(400).json({ success: false, error: 'Invalid payload: must be either heartbeat or detection format' }); } } catch (error) { console.error('Error in detectors endpoint:', error); res.status(500).json({ success: false, error: 'Internal server error' }); } }); // Handle heartbeat payload async function handleHeartbeat(req, res) { const { type, key, device_id, geo_lat, geo_lon, location_description, ...heartbeatData } = req.body; // Get models dynamically (use test models if available) const { Device, Heartbeat } = getModels(); console.log(`💓 Heartbeat received from device key: ${key}`); console.log('💗 Complete heartbeat data:', JSON.stringify(req.body, null, 2)); // Use device_id if provided, otherwise use key as device identifier let deviceId = device_id || key; // Convert to integer if it's a numeric string if (typeof deviceId === 'string' && /^\d+$/.test(deviceId)) { deviceId = parseInt(deviceId); } console.log(`📌 Using device ID: ${deviceId}`); // Check if device exists and is approved let device = await Device.findOne({ where: { id: deviceId } }); if (!device) { // Device not found - reject heartbeat and require manual registration console.log(`� Heartbeat rejected from unknown device ${deviceId} - device must be manually registered first`); return res.status(404).json({ success: false, error: 'Device not registered', message: 'Device not found. Please register the device manually through the UI before sending data.', device_id: deviceId, registration_required: true }); } if (!device.is_approved) { console.log(`🚫 Heartbeat rejected from unapproved device ${deviceId}`); // Emit reminder notification req.io.emit('device_approval_reminder', { device_id: deviceId, device_key: key, timestamp: new Date().toISOString(), message: `Device ${deviceId} (${key}) still awaiting approval` }); return res.status(403).json({ success: false, error: 'Device not approved', message: 'Device requires approval before it can send data', device_id: deviceId, approval_required: true }); } // Device exists and is approved - continue with heartbeat processing // Update device's last heartbeat await device.update({ last_heartbeat: new Date() }); // Create heartbeat record with all optional fields const heartbeatRecord = { device_id: deviceId, device_key: key, ...heartbeatData, received_at: new Date() }; // Add raw payload if debugging is enabled if (DEBUG_CONFIG.storeRawPayload) { heartbeatRecord.raw_payload = req.body; if (DEBUG_CONFIG.logAllDetections) { console.log(`🔍 Storing heartbeat raw payload for debugging: ${JSON.stringify(req.body)}`); } } const heartbeat = await Heartbeat.create(heartbeatRecord); // Emit real-time update via Socket.IO (from original heartbeat route) req.io.emit('device_heartbeat', { device_id: deviceId, device_key: key, timestamp: heartbeat.received_at, status: 'online', ...heartbeatData }); console.log(`✅ Heartbeat recorded for device ${deviceId}`); return res.status(200).json({ success: true, data: heartbeat, message: 'Heartbeat received' }); } // Handle detection payload async function handleDetection(req, res) { const detectionData = req.body; // Get models dynamically (use test models if available) const { Device, DroneDetection } = getModels(); // Get drone type information const droneTypeInfo = getDroneTypeInfo(detectionData.drone_type); console.log(`🚁 Drone detection received from device ${detectionData.device_id}: drone_id=${detectionData.drone_id}, type=${detectionData.drone_type} (${droneTypeInfo.name}), rssi=${detectionData.rssi}`); console.log(`🎯 Drone Type Details: ${droneTypeInfo.category} | Threat: ${droneTypeInfo.threat_level} | ${droneTypeInfo.description}`); console.log('🔍 Complete detection data:', JSON.stringify(detectionData, null, 2)); // Convert device_id to string for consistent database lookup const deviceIdString = String(detectionData.device_id); // Check if device exists and is approved console.log(`🔍 DEBUG: Looking for device with ID: ${detectionData.device_id} (type: ${typeof detectionData.device_id}) -> converted to "${deviceIdString}" (string)`); console.log(`🔍 DEBUG: Device model being used:`, Device); console.log(`🔍 DEBUG: Sequelize instance:`, Device.sequelize.constructor.name); // Get all devices to see what's actually in the database const allDevices = await Device.findAll(); console.log(`🔍 DEBUG: Total devices in database: ${allDevices.length}`); allDevices.forEach(d => { console.log(` - Device ID: ${d.id} (type: ${typeof d.id}), name: ${d.name}, approved: ${d.is_approved}, active: ${d.is_active}`); }); let device = await Device.findOne({ where: { id: deviceIdString } }); console.log(`🔍 DEBUG: Device lookup result:`, device ? `Found device ${device.id}` : 'Device not found'); if (!device) { // Device not found - reject detection and require manual registration console.log(`🚫 Detection rejected from unknown device ${detectionData.device_id} - device must be manually registered first`); return res.status(404).json({ success: false, error: 'Device not registered', message: 'Device not found. Please register the device manually through the UI before sending data.', device_id: detectionData.device_id, registration_required: true }); } if (!device.is_approved) { console.log(`🚫 Detection rejected from unapproved device ${detectionData.device_id}`); // Emit reminder notification req.io.emit('device_approval_reminder', { device_id: detectionData.device_id, timestamp: new Date().toISOString(), message: `Device ${detectionData.device_id} still awaiting approval` }); return res.status(403).json({ success: false, error: 'Device not approved', message: 'Device requires approval before it can send data', device_id: detectionData.device_id, approval_required: true }); } // Handle drone type 0 (None) - store for debugging but don't trigger alarms let isDebugDetection = false; if (detectionData.drone_type === 0) { // Check environment variable dynamically for testing const storeNoneDetections = process.env.STORE_DRONE_TYPE0 === 'true'; if (!storeNoneDetections) { // When debug mode is disabled, return early for drone_type 0 console.log(`🔍 Drone type 0 detection skipped - debug mode disabled`); return res.status(200).json({ success: true, message: 'Detection skipped - drone type 0 detections not stored when debug mode disabled', device_id: detectionData.device_id, debug_mode: false }); } if (DEBUG_CONFIG.logAllDetections) { console.log(`🔍 Debug: Drone type 0 (None) received from device ${detectionData.device_id} - storing for debug purposes`); } isDebugDetection = true; } // Create detection record with string device_id const detectionRecord = { ...detectionData, device_id: deviceIdString, // Use the string version for consistency server_timestamp: new Date() }; // Add raw payload if debugging is enabled if (DEBUG_CONFIG.storeRawPayload) { detectionRecord.raw_payload = req.body; if (DEBUG_CONFIG.logAllDetections) { console.log(`🔍 Storing raw payload for debugging: ${JSON.stringify(req.body)}`); } } // Create detection record with proper error handling let detection; try { detection = await DroneDetection.create(detectionRecord); console.log(`✅ Detection created successfully: ID ${detection.id}, Device ${deviceIdString}, Drone ${detectionData.drone_id}`); } catch (error) { console.error(`❌ Failed to create detection for device ${deviceIdString}, drone ${detectionData.drone_id}:`, error.message); // Log to admin/management for monitoring console.error(`🚨 ADMIN ALERT: Database error in detection creation - Device: ${deviceIdString}, DroneID: ${detectionData.drone_id}, Error: ${error.message}`); // Check for specific database constraint errors if (error.name === 'SequelizeValidationError') { return res.status(400).json({ success: false, error: 'Validation error', message: 'Detection data validation failed', details: error.errors?.map(e => e.message) || ['Invalid data format'], device_id: deviceIdString, drone_id: detectionData.drone_id }); } if (error.name === 'SequelizeUniqueConstraintError') { return res.status(409).json({ success: false, error: 'Duplicate detection', message: 'Detection with this combination already exists', device_id: deviceIdString, drone_id: detectionData.drone_id }); } if (error.name === 'SequelizeForeignKeyConstraintError') { return res.status(400).json({ success: false, error: 'Reference error', message: 'Invalid reference to related data', device_id: deviceIdString, drone_id: detectionData.drone_id }); } // Generic database error return res.status(500).json({ success: false, error: 'Database error', message: 'Failed to store detection data', device_id: deviceIdString, drone_id: detectionData.drone_id, logged_for_admin_review: true }); } // Process detection through tracking service for movement analysis (from original) const movementAnalysis = droneTracker.processDetection({ ...detectionData, server_timestamp: detection.server_timestamp }); // Emit real-time update via Socket.IO with movement analysis (from original) // Skip real-time updates for debug detections (drone_type 0) if (!isDebugDetection) { const detectionPayload = { id: detection.id, device_id: detection.device_id, drone_id: detection.drone_id, drone_type: detection.drone_type, rssi: detection.rssi, freq: detection.freq, geo_lat: detection.geo_lat, geo_lon: detection.geo_lon, server_timestamp: detection.server_timestamp, confidence_level: detection.confidence_level, signal_duration: detection.signal_duration, movement_analysis: movementAnalysis, device: { id: device.id, name: device.name, geo_lat: device.geo_lat, geo_lon: device.geo_lon } }; // 🔒 SECURITY: Emit only to the tenant's room to prevent cross-tenant data leakage if (device.tenant_id) { req.io.to(`tenant_${device.tenant_id}`).emit('drone_detection', detectionPayload); console.log(`🔒 Detection emitted to tenant room: tenant_${device.tenant_id}`); } else { // Fallback for devices without tenant_id (legacy support) console.warn(`⚠️ Device ${device.id} has no tenant_id - using global broadcast (security risk)`); req.io.emit('drone_detection', detectionPayload); } // Process alerts asynchronously (from original) alertService.processAlert(detection, req.io).catch(error => { console.error('Alert processing error:', error); }); console.log(`✅ Detection recorded and alert processing initiated for detection ${detection.id}`); } else { console.log(`🐛 Debug detection stored for device ${detection.device_id} (drone_type 0) - no alerts or real-time updates`); } return res.status(201).json({ success: true, data: { ...detection.toJSON(), movement_analysis: movementAnalysis }, message: 'Drone detection recorded successfully' }); } // Helper function for string hashing (if key is not numeric) String.prototype.hashCode = function() { let hash = 0; if (this.length === 0) return hash; for (let i = 0; i < this.length; i++) { const char = this.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash); }; module.exports = router;