404 lines
11 KiB
JavaScript
404 lines
11 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const Joi = require('joi');
|
|
const { Device, DroneDetection, Heartbeat } = require('../models');
|
|
const { validateRequest } = require('../middleware/validation');
|
|
const { authenticateToken } = require('../middleware/auth');
|
|
const { Op } = require('sequelize');
|
|
|
|
// Validation schema for device
|
|
const deviceSchema = Joi.object({
|
|
id: Joi.number().integer().required(),
|
|
name: Joi.string().max(255).optional(),
|
|
geo_lat: Joi.number().min(-90).max(90).optional(),
|
|
geo_lon: Joi.number().min(-180).max(180).optional(),
|
|
location_description: Joi.string().optional(),
|
|
heartbeat_interval: Joi.number().integer().min(60).max(3600).optional(),
|
|
firmware_version: Joi.string().optional(),
|
|
installation_date: Joi.date().optional(),
|
|
notes: Joi.string().optional()
|
|
});
|
|
|
|
const updateDeviceSchema = Joi.object({
|
|
name: Joi.string().max(255).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(60).max(3600).optional(),
|
|
firmware_version: Joi.string().optional(),
|
|
installation_date: Joi.date().optional(),
|
|
notes: Joi.string().optional()
|
|
});
|
|
|
|
// GET /api/devices - Get all devices
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const {
|
|
include_stats = false,
|
|
active_only = false,
|
|
limit = 100,
|
|
offset = 0
|
|
} = req.query;
|
|
|
|
const whereClause = {};
|
|
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', 'battery_level', 'signal_strength', 'temperature']
|
|
});
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
});
|
|
|
|
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);
|
|
|
|
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', async (req, res) => {
|
|
try {
|
|
const devices = await Device.findAll({
|
|
where: {
|
|
is_active: true,
|
|
geo_lat: { [Op.ne]: null },
|
|
geo_lon: { [Op.ne]: null }
|
|
},
|
|
attributes: [
|
|
'id',
|
|
'name',
|
|
'geo_lat',
|
|
'geo_lon',
|
|
'location_description',
|
|
'last_heartbeat'
|
|
]
|
|
});
|
|
|
|
// Get recent detections for each device
|
|
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
|
|
}
|
|
}
|
|
});
|
|
|
|
const now = new Date();
|
|
const timeSinceLastHeartbeat = device.last_heartbeat
|
|
? (now - new Date(device.last_heartbeat)) / 1000
|
|
: null;
|
|
|
|
const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < 600; // 10 minutes
|
|
|
|
return {
|
|
...device.toJSON(),
|
|
has_recent_detections: recentDetections > 0,
|
|
detection_count_10m: recentDetections,
|
|
status: isOnline ? 'online' : 'offline'
|
|
};
|
|
}));
|
|
|
|
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', async (req, res) => {
|
|
try {
|
|
const device = await Device.findByPk(req.params.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'
|
|
});
|
|
}
|
|
|
|
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 = await Device.create(req.body);
|
|
|
|
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 = await Device.findByPk(req.params.id);
|
|
|
|
if (!device) {
|
|
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
|
|
req.io.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 = await Device.findByPk(req.params.id);
|
|
|
|
if (!device) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Device not found'
|
|
});
|
|
}
|
|
|
|
// Soft delete by setting is_active to false
|
|
await device.update({ is_active: false });
|
|
|
|
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', 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;
|