const express = require('express'); const router = express.Router(); const Joi = require('joi'); const { validateRequest } = require('../middleware/validation'); const { authenticateToken } = require('../middleware/auth'); const MultiTenantAuth = require('../middleware/multi-tenant-auth'); const { Op } = require('sequelize'); // Dynamic model injection for testing function getModels() { if (global.__TEST_MODELS__) { console.log('🔧 DEBUG: Using global test models from models/index.js'); return global.__TEST_MODELS__; } return require('../models'); } // Initialize multi-tenant auth const multiAuth = new MultiTenantAuth(); // Validation schema for device const deviceSchema = Joi.object({ id: Joi.string().required().min(1).max(255), // Device ID is required for manual registration - can be string or number name: Joi.string().max(255).allow('').optional(), geo_lat: Joi.number().min(-90).max(90).optional(), geo_lon: Joi.number().min(-180).max(180).optional(), location_description: Joi.string().allow('').optional(), heartbeat_interval: Joi.number().integer().min(60).max(3600).optional(), firmware_version: Joi.string().allow('').optional(), installation_date: Joi.date().optional(), notes: Joi.string().allow('').optional() }); const updateDeviceSchema = Joi.object({ name: Joi.string().max(255).allow('').optional(), geo_lat: Joi.number().min(-90).max(90).optional(), geo_lon: Joi.number().min(-180).max(180).optional(), location_description: Joi.string().allow('').optional(), is_active: Joi.boolean().optional(), heartbeat_interval: Joi.number().integer().min(60).max(3600).optional(), firmware_version: Joi.string().allow('').optional(), installation_date: Joi.date().optional(), notes: Joi.string().allow('').optional() }); // GET /api/devices - Get all devices router.get('/', authenticateToken, async (req, res) => { try { const { Device, DroneDetection, Heartbeat, Tenant } = getModels(); // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } const { include_stats = false, active_only = false, limit = 100, offset = 0 } = req.query; const whereClause = { tenant_id: tenant.id }; if (active_only === 'true') { whereClause.is_active = true; } const includeOptions = []; if (include_stats === 'true') { // Include latest heartbeat and detection count includeOptions.push({ model: Heartbeat, as: 'heartbeats', limit: 1, order: [['received_at', 'DESC']], required: false, attributes: ['received_at'] }); } const devices = await Device.findAndCountAll({ where: whereClause, include: includeOptions, limit: Math.min(parseInt(limit), 1000), offset: parseInt(offset), order: [['created_at', 'DESC']] }); // If stats requested, get detection counts let devicesWithStats = devices.rows; if (include_stats === 'true') { devicesWithStats = await Promise.all(devices.rows.map(async (device) => { const detectionCount = await DroneDetection.count({ where: { device_id: device.id, server_timestamp: { [Op.gte]: new Date(Date.now() - 24 * 60 * 60 * 1000) // Last 24 hours }, drone_type: { [Op.ne]: 0 } } }); const now = new Date(); const timeSinceLastHeartbeat = device.last_heartbeat ? (now - new Date(device.last_heartbeat)) / 1000 : null; const expectedInterval = device.heartbeat_interval || 300; const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < (expectedInterval * 2); // Debug logging for device status calculation if (device.id === 1001) { // Stockholm device console.log(`🔍 DEBUG Device ${device.id} status calculation:`); console.log(` Now: ${now.toISOString()}`); console.log(` Last heartbeat: ${device.last_heartbeat}`); console.log(` Time since last: ${timeSinceLastHeartbeat} seconds`); console.log(` Expected interval: ${expectedInterval} seconds`); console.log(` Threshold: ${expectedInterval * 2} seconds`); console.log(` Is online: ${isOnline}`); console.log(` Is active: ${device.is_active}`); console.log(` Final status: ${device.is_active ? (isOnline ? 'online' : 'offline') : 'inactive'}`); } return { ...device.toJSON(), stats: { detections_24h: detectionCount, status: device.is_active ? (isOnline ? 'online' : 'offline') : 'inactive', time_since_last_heartbeat: timeSinceLastHeartbeat } }; })); } res.json({ success: true, data: devicesWithStats, pagination: { total: devices.count, limit: parseInt(limit), offset: parseInt(offset), pages: Math.ceil(devices.count / parseInt(limit)) } }); } catch (error) { console.error('Error fetching devices:', error); res.status(500).json({ success: false, message: 'Failed to fetch devices', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // GET /api/devices/map - Get devices with location data for map display router.get('/map', authenticateToken, async (req, res) => { try { const { Device, DroneDetection, Heartbeat, Tenant } = getModels(); // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Get active devices for this tenant only const devices = await Device.findAll({ where: { is_active: true, tenant_id: tenant.id }, attributes: [ 'id', 'name', 'geo_lat', 'geo_lon', 'location_description', 'last_heartbeat' ] }); // Get recent detections for each device and mark coordinate status const devicesWithDetections = await Promise.all(devices.map(async (device) => { const recentDetections = await DroneDetection.count({ where: { device_id: device.id, server_timestamp: { [Op.gte]: new Date(Date.now() - 10 * 60 * 1000) // Last 10 minutes }, drone_type: { [Op.ne]: 0 } } }); const now = new Date(); const timeSinceLastHeartbeat = device.last_heartbeat ? (now - new Date(device.last_heartbeat)) / 1000 : null; const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < 600; // 10 minutes const hasCoordinates = device.geo_lat !== null && device.geo_lon !== null; return { ...device.toJSON(), has_recent_detections: recentDetections > 0, detection_count_10m: recentDetections, status: isOnline ? 'online' : 'offline', has_coordinates: hasCoordinates, coordinate_status: hasCoordinates ? 'complete' : 'incomplete' }; })); res.json({ success: true, data: devicesWithDetections }); } catch (error) { console.error('Error fetching devices for map:', error); res.status(500).json({ success: false, message: 'Failed to fetch devices for map', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // GET /api/devices/:id - Get specific device router.get('/:id', authenticateToken, async (req, res) => { try { const { Device, DroneDetection, Heartbeat, Tenant } = getModels(); // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } const device = await Device.findOne({ where: { id: req.params.id, tenant_id: tenant.id }, include: [ { model: Heartbeat, as: 'heartbeats', limit: 5, order: [['received_at', 'DESC']] }, { model: DroneDetection, as: 'detections', limit: 10, order: [['server_timestamp', 'DESC']] } ] }); if (!device) { return res.status(404).json({ success: false, message: 'Device not found in your tenant' }); } res.json({ success: true, data: device }); } catch (error) { console.error('Error fetching device:', error); res.status(500).json({ success: false, message: 'Failed to fetch device', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // POST /api/devices - Create new device (admin only) router.post('/', authenticateToken, validateRequest(deviceSchema), async (req, res) => { try { const { Device, DroneDetection, Heartbeat, Tenant } = getModels(); // Check admin role if (req.user.role !== 'admin') { return res.status(403).json({ success: false, message: 'Admin role required for device creation' }); } // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Check if device ID already exists in this tenant const existingDevice = await Device.findOne({ where: { id: req.body.id, tenant_id: tenant.id } }); if (existingDevice) { return res.status(409).json({ success: false, message: 'Device with this ID already exists in your tenant' }); } // Create device with tenant association const deviceData = { ...req.body, tenant_id: tenant.id, is_approved: true, // Manually created devices are automatically approved is_active: true }; const device = await Device.create(deviceData); console.log(`✅ Device ${device.id} created in tenant "${tenantId}" by user "${req.user.username}"`); res.status(201).json({ success: true, data: device, message: 'Device created successfully' }); } catch (error) { console.error('Error creating device:', error); if (error.name === 'SequelizeUniqueConstraintError') { return res.status(409).json({ success: false, message: 'Device with this ID already exists' }); } res.status(500).json({ success: false, message: 'Failed to create device', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // PUT /api/devices/:id - Update device router.put('/:id', authenticateToken, validateRequest(updateDeviceSchema), async (req, res) => { try { const { Device, DroneDetection, Heartbeat, Tenant } = getModels(); // Check admin role if (req.user.role !== 'admin') { return res.status(403).json({ success: false, message: 'Admin role required for device updates' }); } const device = await Device.findByPk(req.params.id); if (!device) { return res.status(404).json({ success: false, message: 'Device not found' }); } // Check if device belongs to user's tenant const tenantId = await multiAuth.determineTenant(req); const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (device.tenant_id !== tenant.id) { return res.status(404).json({ success: false, message: 'Device not found' }); } console.log(`📝 Device ${req.params.id} update requested by user ${req.user.id} (${req.user.username})`); console.log('Update data:', req.body); await device.update(req.body); // Emit real-time update to tenant room only if (req.io && device.tenant_id) { req.io.to(`tenant_${device.tenant_id}`).emit('device_updated', device); } console.log(`✅ Device ${req.params.id} updated successfully`); res.json({ success: true, data: device, message: 'Device updated successfully' }); } catch (error) { console.error('Error updating device:', error); res.status(500).json({ success: false, message: 'Failed to update device', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // DELETE /api/devices/:id - Delete device (admin only) router.delete('/:id', authenticateToken, async (req, res) => { try { const { Device, DroneDetection, Heartbeat, Tenant } = getModels(); // Check admin role if (req.user.role !== 'admin') { return res.status(403).json({ success: false, message: 'Admin role required for device deletion' }); } const device = await Device.findByPk(req.params.id); if (!device) { return res.status(404).json({ success: false, message: 'Device not found' }); } // Check if device belongs to user's tenant const tenantId = await multiAuth.determineTenant(req); const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (device.tenant_id !== tenant.id) { return res.status(404).json({ success: false, message: 'Device not found' }); } // Actually delete the device await device.destroy(); res.json({ success: true, message: 'Device deactivated successfully' }); } catch (error) { console.error('Error deleting device:', error); res.status(500).json({ success: false, message: 'Failed to delete device', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // GET /api/devices/pending - List devices pending approval router.get('/pending', authenticateToken, async (req, res) => { try { const { Device, DroneDetection, Heartbeat, Tenant } = getModels(); const pendingDevices = await Device.findAll({ where: { is_approved: false }, attributes: [ 'id', 'name', 'geo_lat', 'geo_lon', 'last_heartbeat', 'created_at', 'firmware_version', 'is_approved' ], order: [['created_at', 'DESC']] }); res.json({ success: true, data: pendingDevices, count: pendingDevices.length }); } catch (error) { console.error('Error fetching pending devices:', error); res.status(500).json({ success: false, message: 'Failed to fetch pending devices', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // POST /api/devices/:id/approve - Approve or reject a device router.post('/:id/approve', async (req, res) => { try { const { Device, DroneDetection, Heartbeat, Tenant } = getModels(); const deviceId = req.params.id; // Keep device ID as string - don't parse as integer const { approved } = req.body; if (typeof approved !== 'boolean') { return res.status(400).json({ success: false, message: 'approved field must be a boolean' }); } const device = await Device.findByPk(deviceId); if (!device) { return res.status(404).json({ success: false, message: 'Device not found' }); } await device.update({ is_approved: approved, is_active: approved // Set device as active when approved, inactive when unapproved }); // Emit real-time notification to tenant room only const { io } = require('../index'); if (io && device.tenant_id) { io.to(`tenant_${device.tenant_id}`).emit('device_approval_updated', { device_id: deviceId, approved: approved, timestamp: new Date().toISOString(), message: approved ? `Device ${deviceId} has been approved` : `Device ${deviceId} approval has been revoked` }); } console.log(`${approved ? '✅' : '❌'} Device ${deviceId} approval ${approved ? 'granted' : 'revoked'}`); res.json({ success: true, data: device, message: approved ? 'Device approved successfully' : 'Device approval revoked' }); } catch (error) { console.error('Error updating device approval:', error); res.status(500).json({ success: false, message: 'Failed to update device approval', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); module.exports = router;