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 to tenant room only if (req.io && device.tenant_id) { req.io.to(`tenant_${device.tenant_id}`).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 to tenant room only if (req.io && device.tenant_id) { req.io.to(`tenant_${device.tenant_id}`).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 to tenant room only if (req.io && device.tenant_id) { req.io.to(`tenant_${device.tenant_id}`).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; } // Validate and convert data types to match database schema const validateAndConvertDetectionData = (data) => { const errors = []; const converted = {}; // Required fields validation and conversion try { // device_id - already converted to string above converted.device_id = deviceIdString; // drone_id - must be BIGINT (convert to integer) if (data.drone_id === undefined || data.drone_id === null) { errors.push('drone_id is required'); } else { const droneId = parseInt(data.drone_id); if (isNaN(droneId)) { errors.push(`drone_id must be a number, received: ${data.drone_id}`); } else { converted.drone_id = droneId; } } // drone_type - must be INTEGER if (data.drone_type === undefined || data.drone_type === null) { errors.push('drone_type is required'); } else { const droneType = parseInt(data.drone_type); if (isNaN(droneType) || droneType < 0) { errors.push(`drone_type must be a non-negative integer, received: ${data.drone_type}`); } else { converted.drone_type = droneType; } } // rssi - must be INTEGER if (data.rssi === undefined || data.rssi === null) { errors.push('rssi is required'); } else { const rssi = parseInt(data.rssi); if (isNaN(rssi)) { errors.push(`rssi must be a number, received: ${data.rssi}`); } else { converted.rssi = rssi; } } // freq - must be INTEGER (convert from MHz decimal to MHz integer) if (data.freq === undefined || data.freq === null) { errors.push('freq is required'); } else { let freq = parseFloat(data.freq); if (isNaN(freq)) { errors.push(`freq must be a number, received: ${data.freq}`); } else { // Convert GHz to MHz if needed (e.g., 2.4 GHz = 2400 MHz) if (freq < 100) { // Likely in GHz, convert to MHz freq = freq * 1000; } converted.freq = Math.round(freq); // Round to nearest integer MHz } } // geo_lat - must be DECIMAL (latitude -90 to 90) if (data.geo_lat !== undefined && data.geo_lat !== null) { const lat = parseFloat(data.geo_lat); if (isNaN(lat) || lat < -90 || lat > 90) { errors.push(`geo_lat must be a number between -90 and 90, received: ${data.geo_lat}`); } else { converted.geo_lat = lat; } } // geo_lon - must be DECIMAL (longitude -180 to 180) if (data.geo_lon !== undefined && data.geo_lon !== null) { const lon = parseFloat(data.geo_lon); if (isNaN(lon) || lon < -180 || lon > 180) { errors.push(`geo_lon must be a number between -180 and 180, received: ${data.geo_lon}`); } else { converted.geo_lon = lon; } } // device_timestamp - must be BIGINT (Unix timestamp) if (data.device_timestamp !== undefined && data.device_timestamp !== null) { const timestamp = parseInt(data.device_timestamp); if (isNaN(timestamp) || timestamp < 0) { errors.push(`device_timestamp must be a positive integer (Unix timestamp), received: ${data.device_timestamp}`); } else { converted.device_timestamp = timestamp; } } // confidence_level - must be DECIMAL between 0 and 1 if (data.confidence_level !== undefined && data.confidence_level !== null) { const confidence = parseFloat(data.confidence_level); if (isNaN(confidence) || confidence < 0 || confidence > 1) { errors.push(`confidence_level must be a number between 0 and 1, received: ${data.confidence_level}`); } else { converted.confidence_level = confidence; } } // signal_duration - must be INTEGER (milliseconds) if (data.signal_duration !== undefined && data.signal_duration !== null) { const duration = parseInt(data.signal_duration); if (isNaN(duration) || duration < 0) { errors.push(`signal_duration must be a non-negative integer, received: ${data.signal_duration}`); } else { converted.signal_duration = duration; } } // Add server timestamp converted.server_timestamp = new Date(); } catch (error) { errors.push(`Data conversion error: ${error.message}`); } return { converted, errors }; }; // Validate and convert the detection data const { converted: validatedData, errors: validationErrors } = validateAndConvertDetectionData(detectionData); if (validationErrors.length > 0) { console.error(`❌ Validation errors for device ${deviceIdString}:`, validationErrors); return res.status(400).json({ success: false, error: 'Validation error', message: 'Detection data validation failed', details: validationErrors, device_id: deviceIdString, drone_id: detectionData.drone_id }); } console.log(`🔍 Original data:`, detectionData); console.log(`✅ Validated data:`, validatedData); // Create detection record with validated data const detectionRecord = validatedData; // 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 (req.io && 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 if (req.io) { // 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); } else { console.warn(`⚠️ Socket.IO not available - detection will not be broadcast in real-time`); } // 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;