From 67ad1506b32199671a7791d195d413f9eeea0e72 Mon Sep 17 00:00:00 2001 From: Alexander Borg Date: Thu, 28 Aug 2025 07:22:57 +0200 Subject: [PATCH] Fix jwt-token --- .../20250828000001-add-device-approval.js | 41 +++ server/models/Device.js | 8 + server/routes/detectors.js | 109 ++++++- server/routes/device.js | 80 +++++ server/routes/devices.js | 293 ++++++++++++++++++ 5 files changed, 517 insertions(+), 14 deletions(-) create mode 100644 server/migrations/20250828000001-add-device-approval.js create mode 100644 server/routes/devices.js diff --git a/server/migrations/20250828000001-add-device-approval.js b/server/migrations/20250828000001-add-device-approval.js new file mode 100644 index 0000000..6eb0a31 --- /dev/null +++ b/server/migrations/20250828000001-add-device-approval.js @@ -0,0 +1,41 @@ +/** + * Migration: Add is_approved field to devices table + * This migration adds device approval functionality to the system + */ + +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + // Add is_approved column to devices table + await queryInterface.addColumn('devices', 'is_approved', { + type: Sequelize.BOOLEAN, + defaultValue: false, + allowNull: false, + comment: 'Whether the device is approved to send data' + }); + + // Add index for is_approved for better query performance + await queryInterface.addIndex('devices', ['is_approved'], { + name: 'devices_is_approved_idx' + }); + + // Approve all existing devices by default (backward compatibility) + await queryInterface.sequelize.query( + 'UPDATE devices SET is_approved = true WHERE created_at < NOW()' + ); + + console.log('✅ Added is_approved field to devices table'); + console.log('✅ Approved all existing devices for backward compatibility'); + }, + + async down(queryInterface, Sequelize) { + // Remove index first + await queryInterface.removeIndex('devices', 'devices_is_approved_idx'); + + // Remove the column + await queryInterface.removeColumn('devices', 'is_approved'); + + console.log('✅ Removed is_approved field from devices table'); + } +}; diff --git a/server/models/Device.js b/server/models/Device.js index 28ca653..24b8a7a 100644 --- a/server/models/Device.js +++ b/server/models/Device.js @@ -34,6 +34,11 @@ module.exports = (sequelize) => { defaultValue: true, comment: 'Whether the device is currently active' }, + is_approved: { + type: DataTypes.BOOLEAN, + defaultValue: false, + comment: 'Whether the device is approved to send data' + }, last_heartbeat: { type: DataTypes.DATE, allowNull: true, @@ -81,6 +86,9 @@ module.exports = (sequelize) => { }, { fields: ['is_active'] + }, + { + fields: ['is_approved'] } ] }); diff --git a/server/routes/detectors.js b/server/routes/detectors.js index f854fe1..a2193c6 100644 --- a/server/routes/detectors.js +++ b/server/routes/detectors.js @@ -98,15 +98,56 @@ async function handleHeartbeat(req, res) { deviceId = keyMatch ? parseInt(keyMatch[1]) : key.hashCode(); } - // Ensure device exists or create it - const [device] = await Device.findOrCreate({ - where: { id: deviceId }, - defaults: { + // Check if device exists and is approved + let device = await Device.findOne({ where: { id: deviceId } }); + + if (!device) { + // Create new device as unapproved + device = await Device.create({ id: deviceId, name: `Device ${deviceId}`, - last_heartbeat: new Date() - } - }); + last_heartbeat: new Date(), + is_approved: false + }); + + // Emit notification for new device requiring approval + req.io.emit('new_device_pending', { + device_id: deviceId, + device_key: key, + timestamp: new Date().toISOString(), + message: `New device ${deviceId} (${key}) requires approval` + }); + + console.log(`⚠️ New unapproved device ${deviceId} created, awaiting approval`); + + return res.status(202).json({ + success: false, + error: 'Device not approved', + message: 'Device has been registered but requires approval before it can send data', + device_id: deviceId, + approval_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 + }); + } // Update device's last heartbeat await device.update({ last_heartbeat: new Date() }); @@ -143,16 +184,56 @@ async function handleDetection(req, res) { console.log(`🚁 Drone detection received from device ${detectionData.device_id}: drone_id=${detectionData.drone_id}, type=${detectionData.drone_type}, rssi=${detectionData.rssi}`); - // Ensure device exists or create it (from original detection route) - const [device] = await Device.findOrCreate({ - where: { id: detectionData.device_id }, - defaults: { + // Check if device exists and is approved + let device = await Device.findOne({ where: { id: detectionData.device_id } }); + + if (!device) { + // Create new device as unapproved + device = await Device.create({ id: detectionData.device_id, + name: `Device ${detectionData.device_id}`, geo_lat: detectionData.geo_lat || 0, geo_lon: detectionData.geo_lon || 0, - last_heartbeat: new Date() - } - }); + last_heartbeat: new Date(), + is_approved: false + }); + + // Emit notification for new device requiring approval + req.io.emit('new_device_pending', { + device_id: detectionData.device_id, + timestamp: new Date().toISOString(), + message: `New device ${detectionData.device_id} requires approval` + }); + + console.log(`⚠️ New unapproved device ${detectionData.device_id} created, awaiting approval`); + + return res.status(202).json({ + success: false, + error: 'Device not approved', + message: 'Device has been registered but requires approval before it can send data', + device_id: detectionData.device_id, + approval_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 + }); + } // Create detection record const detection = await DroneDetection.create({ diff --git a/server/routes/device.js b/server/routes/device.js index bffdbb2..0b6e8d5 100644 --- a/server/routes/device.js +++ b/server/routes/device.js @@ -315,4 +315,84 @@ router.delete('/:id', authenticateToken, async (req, res) => { } }); +// GET /api/devices/pending - List devices pending approval +router.get('/pending', async (req, res) => { + try { + 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 deviceId = parseInt(req.params.id); + 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 }); + + // Emit real-time notification + const { io } = require('../index'); + if (io) { + io.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; diff --git a/server/routes/devices.js b/server/routes/devices.js new file mode 100644 index 0000000..b1b15db --- /dev/null +++ b/server/routes/devices.js @@ -0,0 +1,293 @@ +const express = require('express'); +const router = express.Router(); +const Joi = require('joi'); +const { validateRequest } = require('../middleware/validation'); +const { Device, Heartbeat, DroneDetection } = require('../models'); +const { Op } = require('sequelize'); + +// Validation schemas +const approveDeviceSchema = Joi.object({ + device_id: Joi.number().integer().required(), + approved: Joi.boolean().required() +}); + +const updateDeviceSchema = Joi.object({ + name: 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(), + is_active: Joi.boolean().optional(), + heartbeat_interval: Joi.number().integer().min(30).optional(), + notes: Joi.string().allow('').optional() +}); + +// GET /api/devices - List all devices with approval status +router.get('/', async (req, res) => { + try { + const devices = await Device.findAll({ + attributes: [ + 'id', 'name', 'geo_lat', 'geo_lon', 'location_description', + 'is_active', 'is_approved', 'last_heartbeat', 'heartbeat_interval', + 'firmware_version', 'installation_date', 'notes', 'created_at' + ], + order: [['created_at', 'DESC']] + }); + + // Add status information + const devicesWithStatus = devices.map(device => { + const lastHeartbeat = device.last_heartbeat; + const heartbeatInterval = device.heartbeat_interval || 300; + const now = new Date(); + const timeSinceHeartbeat = lastHeartbeat ? + (now - new Date(lastHeartbeat)) / 1000 : null; + + let status = 'unknown'; + if (!device.is_approved) { + status = 'pending_approval'; + } else if (!device.is_active) { + status = 'inactive'; + } else if (timeSinceHeartbeat && timeSinceHeartbeat > heartbeatInterval * 2) { + status = 'offline'; + } else if (timeSinceHeartbeat && timeSinceHeartbeat <= heartbeatInterval * 2) { + status = 'online'; + } + + return { + ...device.toJSON(), + status, + time_since_heartbeat: timeSinceHeartbeat + }; + }); + + res.json({ + success: true, + data: devicesWithStatus + }); + } catch (error) { + console.error('Error fetching devices:', error); + res.status(500).json({ + success: false, + error: 'Failed to fetch devices' + }); + } +}); + +// GET /api/devices/pending - List devices pending approval +router.get('/pending', async (req, res) => { + try { + const pendingDevices = await Device.findAll({ + where: { is_approved: false }, + attributes: [ + 'id', 'name', 'geo_lat', 'geo_lon', 'last_heartbeat', + 'created_at', 'firmware_version' + ], + order: [['created_at', 'DESC']] + }); + + res.json({ + success: true, + data: pendingDevices + }); + } catch (error) { + console.error('Error fetching pending devices:', error); + res.status(500).json({ + success: false, + error: 'Failed to fetch pending devices' + }); + } +}); + +// POST /api/devices/:id/approve - Approve or reject a device +router.post('/:id/approve', validateRequest(approveDeviceSchema), async (req, res) => { + try { + const deviceId = parseInt(req.params.id); + const { approved } = req.body; + + const device = await Device.findByPk(deviceId); + if (!device) { + return res.status(404).json({ + success: false, + error: 'Device not found' + }); + } + + await device.update({ is_approved: approved }); + + // Emit real-time notification + const { io } = require('../index'); + if (io) { + io.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, + error: 'Failed to update device approval' + }); + } +}); + +// PUT /api/devices/:id - Update device information +router.put('/:id', validateRequest(updateDeviceSchema), async (req, res) => { + try { + const deviceId = parseInt(req.params.id); + const updateData = req.body; + + const device = await Device.findByPk(deviceId); + if (!device) { + return res.status(404).json({ + success: false, + error: 'Device not found' + }); + } + + await device.update(updateData); + + // Emit real-time notification + const { io } = require('../index'); + if (io) { + io.emit('device_updated', { + device_id: deviceId, + updates: updateData, + timestamp: new Date().toISOString() + }); + } + + console.log(`📝 Device ${deviceId} updated`); + + res.json({ + success: true, + data: device, + message: 'Device updated successfully' + }); + } catch (error) { + console.error('Error updating device:', error); + res.status(500).json({ + success: false, + error: 'Failed to update device' + }); + } +}); + +// DELETE /api/devices/:id - Delete a device and all its data +router.delete('/:id', async (req, res) => { + try { + const deviceId = parseInt(req.params.id); + + const device = await Device.findByPk(deviceId); + if (!device) { + return res.status(404).json({ + success: false, + error: 'Device not found' + }); + } + + // Delete related data first (due to foreign key constraints) + await DroneDetection.destroy({ where: { device_id: deviceId } }); + await Heartbeat.destroy({ where: { device_id: deviceId } }); + + // Delete the device + await device.destroy(); + + // Emit real-time notification + const { io } = require('../index'); + if (io) { + io.emit('device_deleted', { + device_id: deviceId, + timestamp: new Date().toISOString(), + message: `Device ${deviceId} has been deleted` + }); + } + + console.log(`🗑️ Device ${deviceId} and all related data deleted`); + + res.json({ + success: true, + message: 'Device deleted successfully' + }); + } catch (error) { + console.error('Error deleting device:', error); + res.status(500).json({ + success: false, + error: 'Failed to delete device' + }); + } +}); + +// GET /api/devices/:id/stats - Get device statistics +router.get('/:id/stats', async (req, res) => { + try { + const deviceId = parseInt(req.params.id); + const days = parseInt(req.query.days) || 7; + + const device = await Device.findByPk(deviceId); + if (!device) { + return res.status(404).json({ + success: false, + error: 'Device not found' + }); + } + + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + // Get heartbeat count + const heartbeatCount = await Heartbeat.count({ + where: { + device_id: deviceId, + received_at: { [Op.gte]: startDate } + } + }); + + // Get detection count + const detectionCount = await DroneDetection.count({ + where: { + device_id: deviceId, + server_timestamp: { [Op.gte]: startDate } + } + }); + + // Get latest heartbeat + const latestHeartbeat = await Heartbeat.findOne({ + where: { device_id: deviceId }, + order: [['received_at', 'DESC']], + attributes: ['received_at', 'signal_strength', 'battery_level', 'temperature'] + }); + + res.json({ + success: true, + data: { + device_id: deviceId, + period_days: days, + heartbeat_count: heartbeatCount, + detection_count: detectionCount, + latest_heartbeat: latestHeartbeat, + uptime_percentage: heartbeatCount > 0 ? + Math.min(100, (heartbeatCount / (days * 24 * 12)) * 100) : 0 // Assuming 5min intervals + } + }); + } catch (error) { + console.error('Error fetching device stats:', error); + res.status(500).json({ + success: false, + error: 'Failed to fetch device statistics' + }); + } +}); + +module.exports = router;