Fix jwt-token

This commit is contained in:
2025-08-28 07:22:57 +02:00
parent 74d5114da6
commit 67ad1506b3
5 changed files with 517 additions and 14 deletions

View File

@@ -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');
}
};

View File

@@ -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']
}
]
});

View File

@@ -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({

View File

@@ -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;

293
server/routes/devices.js Normal file
View File

@@ -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;