Initial commit
This commit is contained in:
311
server/routes/alert.js
Normal file
311
server/routes/alert.js
Normal file
@@ -0,0 +1,311 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
const { AlertRule, AlertLog, User } = require('../models');
|
||||
const { validateRequest } = require('../middleware/validation');
|
||||
const { authenticateToken, requireRole } = require('../middleware/auth');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// Validation schemas
|
||||
const alertRuleSchema = Joi.object({
|
||||
name: Joi.string().required(),
|
||||
description: Joi.string().optional(),
|
||||
device_ids: Joi.array().items(Joi.number().integer()).optional(),
|
||||
drone_types: Joi.array().items(Joi.number().integer()).optional(),
|
||||
min_rssi: Joi.number().integer().optional(),
|
||||
max_rssi: Joi.number().integer().optional(),
|
||||
frequency_ranges: Joi.array().items(Joi.object({
|
||||
min: Joi.number().integer().required(),
|
||||
max: Joi.number().integer().required()
|
||||
})).optional(),
|
||||
time_window: Joi.number().integer().min(60).max(3600).default(300),
|
||||
min_detections: Joi.number().integer().min(1).default(1),
|
||||
cooldown_period: Joi.number().integer().min(0).default(600),
|
||||
alert_channels: Joi.array().items(Joi.string().valid('sms', 'email', 'webhook')).default(['sms']),
|
||||
webhook_url: Joi.string().uri().optional(),
|
||||
active_hours: Joi.object({
|
||||
start: Joi.string().pattern(/^\d{2}:\d{2}$/).optional(),
|
||||
end: Joi.string().pattern(/^\d{2}:\d{2}$/).optional()
|
||||
}).optional(),
|
||||
active_days: Joi.array().items(Joi.number().integer().min(1).max(7)).default([1,2,3,4,5,6,7]),
|
||||
priority: Joi.string().valid('low', 'medium', 'high', 'critical').default('medium')
|
||||
});
|
||||
|
||||
// GET /api/alerts/rules - Get alert rules for current user
|
||||
router.get('/rules', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { limit = 50, offset = 0, is_active } = req.query;
|
||||
|
||||
const whereClause = { user_id: req.user.id };
|
||||
if (is_active !== undefined) whereClause.is_active = is_active === 'true';
|
||||
|
||||
const alertRules = await AlertRule.findAndCountAll({
|
||||
where: whereClause,
|
||||
limit: Math.min(parseInt(limit), 100),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: alertRules.rows,
|
||||
pagination: {
|
||||
total: alertRules.count,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
pages: Math.ceil(alertRules.count / parseInt(limit))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching alert rules:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch alert rules',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/alerts/rules - Create new alert rule
|
||||
router.post('/rules', authenticateToken, validateRequest(alertRuleSchema), async (req, res) => {
|
||||
try {
|
||||
const alertRule = await AlertRule.create({
|
||||
...req.body,
|
||||
user_id: req.user.id
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: alertRule,
|
||||
message: 'Alert rule created successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating alert rule:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to create alert rule',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/alerts/rules/:id - Update alert rule
|
||||
router.put('/rules/:id', authenticateToken, validateRequest(alertRuleSchema), async (req, res) => {
|
||||
try {
|
||||
const alertRule = await AlertRule.findOne({
|
||||
where: {
|
||||
id: req.params.id,
|
||||
user_id: req.user.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!alertRule) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Alert rule not found'
|
||||
});
|
||||
}
|
||||
|
||||
await alertRule.update(req.body);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: alertRule,
|
||||
message: 'Alert rule updated successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating alert rule:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to update alert rule',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/alerts/rules/:id - Delete alert rule
|
||||
router.delete('/rules/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const alertRule = await AlertRule.findOne({
|
||||
where: {
|
||||
id: req.params.id,
|
||||
user_id: req.user.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!alertRule) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Alert rule not found'
|
||||
});
|
||||
}
|
||||
|
||||
await alertRule.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Alert rule deleted successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting alert rule:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to delete alert rule',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/alerts/logs - Get alert logs for current user
|
||||
router.get('/logs', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
status,
|
||||
alert_type,
|
||||
start_date,
|
||||
end_date
|
||||
} = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
if (status) whereClause.status = status;
|
||||
if (alert_type) whereClause.alert_type = alert_type;
|
||||
|
||||
if (start_date || end_date) {
|
||||
whereClause.created_at = {};
|
||||
if (start_date) whereClause.created_at[Op.gte] = new Date(start_date);
|
||||
if (end_date) whereClause.created_at[Op.lte] = new Date(end_date);
|
||||
}
|
||||
|
||||
const alertLogs = await AlertLog.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [{
|
||||
model: AlertRule,
|
||||
as: 'rule',
|
||||
where: { user_id: req.user.id },
|
||||
attributes: ['id', 'name', 'priority']
|
||||
}],
|
||||
limit: Math.min(parseInt(limit), 200),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: alertLogs.rows,
|
||||
pagination: {
|
||||
total: alertLogs.count,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
pages: Math.ceil(alertLogs.count / parseInt(limit))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching alert logs:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch alert logs',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/alerts/stats - Get alert statistics for current user
|
||||
router.get('/stats', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { hours = 24 } = req.query;
|
||||
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
|
||||
|
||||
// Get user's alert rules
|
||||
const userRuleIds = await AlertRule.findAll({
|
||||
where: { user_id: req.user.id },
|
||||
attributes: ['id']
|
||||
}).then(rules => rules.map(rule => rule.id));
|
||||
|
||||
if (userRuleIds.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total_alerts: 0,
|
||||
sent_alerts: 0,
|
||||
failed_alerts: 0,
|
||||
pending_alerts: 0,
|
||||
by_type: {},
|
||||
by_status: {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const [totalAlerts, alertsByStatus, alertsByType] = await Promise.all([
|
||||
AlertLog.count({
|
||||
where: {
|
||||
alert_rule_id: { [Op.in]: userRuleIds },
|
||||
created_at: { [Op.gte]: timeWindow }
|
||||
}
|
||||
}),
|
||||
AlertLog.findAll({
|
||||
where: {
|
||||
alert_rule_id: { [Op.in]: userRuleIds },
|
||||
created_at: { [Op.gte]: timeWindow }
|
||||
},
|
||||
attributes: [
|
||||
'status',
|
||||
[sequelize.fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: ['status'],
|
||||
raw: true
|
||||
}),
|
||||
AlertLog.findAll({
|
||||
where: {
|
||||
alert_rule_id: { [Op.in]: userRuleIds },
|
||||
created_at: { [Op.gte]: timeWindow }
|
||||
},
|
||||
attributes: [
|
||||
'alert_type',
|
||||
[sequelize.fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: ['alert_type'],
|
||||
raw: true
|
||||
})
|
||||
]);
|
||||
|
||||
const statusCounts = alertsByStatus.reduce((acc, item) => {
|
||||
acc[item.status] = parseInt(item.count);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const typeCounts = alertsByType.reduce((acc, item) => {
|
||||
acc[item.alert_type] = parseInt(item.count);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total_alerts: totalAlerts,
|
||||
sent_alerts: statusCounts.sent || 0,
|
||||
failed_alerts: statusCounts.failed || 0,
|
||||
pending_alerts: statusCounts.pending || 0,
|
||||
by_type: typeCounts,
|
||||
by_status: statusCounts,
|
||||
time_window_hours: hours
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching alert statistics:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch alert statistics',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
270
server/routes/dashboard.js
Normal file
270
server/routes/dashboard.js
Normal file
@@ -0,0 +1,270 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { DroneDetection, Device, Heartbeat } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
const { sequelize } = require('../models');
|
||||
|
||||
// GET /api/dashboard/overview - Get dashboard overview statistics
|
||||
router.get('/overview', async (req, res) => {
|
||||
try {
|
||||
const { hours = 24 } = req.query;
|
||||
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
|
||||
|
||||
// Get basic statistics
|
||||
const [
|
||||
totalDevices,
|
||||
activeDevices,
|
||||
totalDetections,
|
||||
recentDetections,
|
||||
uniqueDronesDetected
|
||||
] = await Promise.all([
|
||||
Device.count(),
|
||||
Device.count({ where: { is_active: true } }),
|
||||
DroneDetection.count(),
|
||||
DroneDetection.count({
|
||||
where: { server_timestamp: { [Op.gte]: timeWindow } }
|
||||
}),
|
||||
DroneDetection.count({
|
||||
where: { server_timestamp: { [Op.gte]: timeWindow } },
|
||||
distinct: true,
|
||||
col: 'drone_id'
|
||||
})
|
||||
]);
|
||||
|
||||
// Get device status breakdown
|
||||
const devices = await Device.findAll({
|
||||
attributes: ['id', 'last_heartbeat', 'heartbeat_interval', 'is_active']
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
let onlineDevices = 0;
|
||||
let offlineDevices = 0;
|
||||
|
||||
devices.forEach(device => {
|
||||
if (!device.is_active) return;
|
||||
|
||||
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);
|
||||
|
||||
if (isOnline) {
|
||||
onlineDevices++;
|
||||
} else {
|
||||
offlineDevices++;
|
||||
}
|
||||
});
|
||||
|
||||
// Get recent alerts count
|
||||
// This would require AlertLog model which we haven't imported yet
|
||||
// const recentAlerts = await AlertLog.count({
|
||||
// where: { created_at: { [Op.gte]: timeWindow } }
|
||||
// });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
summary: {
|
||||
total_devices: totalDevices,
|
||||
active_devices: activeDevices,
|
||||
online_devices: onlineDevices,
|
||||
offline_devices: offlineDevices,
|
||||
total_detections: totalDetections,
|
||||
recent_detections: recentDetections,
|
||||
unique_drones_detected: uniqueDronesDetected,
|
||||
// recent_alerts: recentAlerts || 0,
|
||||
time_window_hours: hours
|
||||
},
|
||||
device_status: {
|
||||
total: totalDevices,
|
||||
active: activeDevices,
|
||||
online: onlineDevices,
|
||||
offline: offlineDevices,
|
||||
inactive: totalDevices - activeDevices
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard overview:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch dashboard overview',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/activity - Get recent activity feed
|
||||
router.get('/activity', async (req, res) => {
|
||||
try {
|
||||
const { limit = 50, hours = 24 } = req.query;
|
||||
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
|
||||
|
||||
// Get recent detections with device info
|
||||
const recentDetections = await DroneDetection.findAll({
|
||||
where: { server_timestamp: { [Op.gte]: timeWindow } },
|
||||
include: [{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['id', 'name', 'geo_lat', 'geo_lon', 'location_description']
|
||||
}],
|
||||
limit: Math.min(parseInt(limit), 200),
|
||||
order: [['server_timestamp', 'DESC']]
|
||||
});
|
||||
|
||||
// Get recent heartbeats
|
||||
const recentHeartbeats = await Heartbeat.findAll({
|
||||
where: { received_at: { [Op.gte]: timeWindow } },
|
||||
include: [{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['id', 'name', 'geo_lat', 'geo_lon']
|
||||
}],
|
||||
limit: Math.min(parseInt(limit), 50),
|
||||
order: [['received_at', 'DESC']]
|
||||
});
|
||||
|
||||
// Combine and sort activities
|
||||
const activities = [
|
||||
...recentDetections.map(detection => ({
|
||||
type: 'detection',
|
||||
timestamp: detection.server_timestamp,
|
||||
data: {
|
||||
detection_id: detection.id,
|
||||
device_id: detection.device_id,
|
||||
device_name: detection.device.name || `Device ${detection.device_id}`,
|
||||
drone_id: detection.drone_id,
|
||||
drone_type: detection.drone_type,
|
||||
rssi: detection.rssi,
|
||||
freq: detection.freq,
|
||||
location: detection.device.location_description ||
|
||||
`${detection.device.geo_lat}, ${detection.device.geo_lon}`
|
||||
}
|
||||
})),
|
||||
...recentHeartbeats.map(heartbeat => ({
|
||||
type: 'heartbeat',
|
||||
timestamp: heartbeat.received_at,
|
||||
data: {
|
||||
device_id: heartbeat.device_id,
|
||||
device_name: heartbeat.device.name || `Device ${heartbeat.device_id}`,
|
||||
battery_level: heartbeat.battery_level,
|
||||
signal_strength: heartbeat.signal_strength,
|
||||
temperature: heartbeat.temperature
|
||||
}
|
||||
}))
|
||||
];
|
||||
|
||||
// Sort by timestamp descending
|
||||
activities.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: activities.slice(0, parseInt(limit))
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard activity:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch dashboard activity',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/charts/detections - Get detection chart data
|
||||
router.get('/charts/detections', async (req, res) => {
|
||||
try {
|
||||
const { hours = 24, interval = 'hour' } = req.query;
|
||||
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
|
||||
|
||||
let groupBy;
|
||||
switch (interval) {
|
||||
case 'minute':
|
||||
groupBy = "DATE_TRUNC('minute', server_timestamp)";
|
||||
break;
|
||||
case 'hour':
|
||||
groupBy = "DATE_TRUNC('hour', server_timestamp)";
|
||||
break;
|
||||
case 'day':
|
||||
groupBy = "DATE_TRUNC('day', server_timestamp)";
|
||||
break;
|
||||
default:
|
||||
groupBy = "DATE_TRUNC('hour', server_timestamp)";
|
||||
}
|
||||
|
||||
const detectionCounts = await DroneDetection.findAll({
|
||||
where: { server_timestamp: { [Op.gte]: timeWindow } },
|
||||
attributes: [
|
||||
[sequelize.fn('DATE_TRUNC', interval, sequelize.col('server_timestamp')), 'time_bucket'],
|
||||
[sequelize.fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: ['time_bucket'],
|
||||
order: [['time_bucket', 'ASC']],
|
||||
raw: true
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: detectionCounts.map(item => ({
|
||||
timestamp: item.time_bucket,
|
||||
count: parseInt(item.count)
|
||||
}))
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching detection chart data:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch detection chart data',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/charts/devices - Get device activity chart data
|
||||
router.get('/charts/devices', async (req, res) => {
|
||||
try {
|
||||
const { hours = 24 } = req.query;
|
||||
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
|
||||
|
||||
const deviceActivity = await DroneDetection.findAll({
|
||||
where: { server_timestamp: { [Op.gte]: timeWindow } },
|
||||
attributes: [
|
||||
'device_id',
|
||||
[sequelize.fn('COUNT', '*'), 'detection_count']
|
||||
],
|
||||
include: [{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['name', 'location_description']
|
||||
}],
|
||||
group: ['device_id', 'device.id', 'device.name', 'device.location_description'],
|
||||
order: [[sequelize.fn('COUNT', '*'), 'DESC']],
|
||||
raw: false
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: deviceActivity.map(item => ({
|
||||
device_id: item.device_id,
|
||||
device_name: item.device.name || `Device ${item.device_id}`,
|
||||
location: item.device.location_description,
|
||||
detection_count: parseInt(item.dataValues.detection_count)
|
||||
}))
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching device chart data:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch device chart data',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
318
server/routes/device.js
Normal file
318
server/routes/device.js
Normal file
@@ -0,0 +1,318 @@
|
||||
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 (admin only)
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
await device.update(req.body);
|
||||
|
||||
// Emit real-time update
|
||||
req.io.emit('device_updated', device);
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
228
server/routes/droneDetection.js
Normal file
228
server/routes/droneDetection.js
Normal file
@@ -0,0 +1,228 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
const { DroneDetection, Device } = require('../models');
|
||||
const { processAlert } = require('../services/alertService');
|
||||
const { validateRequest } = require('../middleware/validation');
|
||||
|
||||
// Validation schema for drone detection
|
||||
const droneDetectionSchema = Joi.object({
|
||||
device_id: Joi.number().integer().required(),
|
||||
geo_lat: Joi.number().min(-90).max(90).default(0),
|
||||
geo_lon: Joi.number().min(-180).max(180).default(0),
|
||||
device_timestamp: Joi.number().integer().min(0).default(0),
|
||||
drone_type: Joi.number().integer().min(0).default(0),
|
||||
rssi: Joi.number().integer().default(0),
|
||||
freq: Joi.number().integer().required(),
|
||||
drone_id: Joi.number().integer().required(),
|
||||
confidence_level: Joi.number().min(0).max(1).optional(),
|
||||
signal_duration: Joi.number().integer().min(0).optional()
|
||||
});
|
||||
|
||||
// POST /api/detections - Receive drone detection data
|
||||
router.post('/', validateRequest(droneDetectionSchema), async (req, res) => {
|
||||
try {
|
||||
const detectionData = req.body;
|
||||
|
||||
// Ensure device exists or create it
|
||||
const [device] = await Device.findOrCreate({
|
||||
where: { id: detectionData.device_id },
|
||||
defaults: {
|
||||
id: detectionData.device_id,
|
||||
geo_lat: detectionData.geo_lat || 0,
|
||||
geo_lon: detectionData.geo_lon || 0,
|
||||
last_heartbeat: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// Create the detection record
|
||||
const detection = await DroneDetection.create({
|
||||
...detectionData,
|
||||
server_timestamp: new Date()
|
||||
});
|
||||
|
||||
// Emit real-time update via Socket.IO
|
||||
req.io.emit('drone_detection', {
|
||||
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,
|
||||
device: {
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
geo_lat: device.geo_lat,
|
||||
geo_lon: device.geo_lon
|
||||
}
|
||||
});
|
||||
|
||||
// Process alerts asynchronously
|
||||
processAlert(detection).catch(error => {
|
||||
console.error('Alert processing error:', error);
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: detection,
|
||||
message: 'Drone detection recorded successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating drone detection:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to record drone detection',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/detections - Get drone detections with filtering
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
device_id,
|
||||
drone_id,
|
||||
start_date,
|
||||
end_date,
|
||||
limit = 100,
|
||||
offset = 0,
|
||||
order = 'DESC'
|
||||
} = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
|
||||
if (device_id) whereClause.device_id = device_id;
|
||||
if (drone_id) whereClause.drone_id = drone_id;
|
||||
|
||||
if (start_date || end_date) {
|
||||
whereClause.server_timestamp = {};
|
||||
if (start_date) whereClause.server_timestamp[Op.gte] = new Date(start_date);
|
||||
if (end_date) whereClause.server_timestamp[Op.lte] = new Date(end_date);
|
||||
}
|
||||
|
||||
const detections = await DroneDetection.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['id', 'name', 'geo_lat', 'geo_lon', 'location_description']
|
||||
}],
|
||||
limit: Math.min(parseInt(limit), 1000), // Max 1000 records
|
||||
offset: parseInt(offset),
|
||||
order: [['server_timestamp', order]]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: detections.rows,
|
||||
pagination: {
|
||||
total: detections.count,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
pages: Math.ceil(detections.count / parseInt(limit))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching drone detections:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch drone detections',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/detections/stats - Get detection statistics
|
||||
router.get('/stats', async (req, res) => {
|
||||
try {
|
||||
const { device_id, hours = 24 } = req.query;
|
||||
|
||||
const whereClause = {
|
||||
server_timestamp: {
|
||||
[Op.gte]: new Date(Date.now() - hours * 60 * 60 * 1000)
|
||||
}
|
||||
};
|
||||
|
||||
if (device_id) whereClause.device_id = device_id;
|
||||
|
||||
const [totalDetections, uniqueDrones, uniqueDevices, avgRssi] = await Promise.all([
|
||||
DroneDetection.count({ where: whereClause }),
|
||||
DroneDetection.count({
|
||||
where: whereClause,
|
||||
distinct: true,
|
||||
col: 'drone_id'
|
||||
}),
|
||||
DroneDetection.count({
|
||||
where: whereClause,
|
||||
distinct: true,
|
||||
col: 'device_id'
|
||||
}),
|
||||
DroneDetection.findAll({
|
||||
where: whereClause,
|
||||
attributes: [
|
||||
[sequelize.fn('AVG', sequelize.col('rssi')), 'avg_rssi']
|
||||
]
|
||||
})
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total_detections: totalDetections,
|
||||
unique_drones: uniqueDrones,
|
||||
active_devices: uniqueDevices,
|
||||
average_rssi: Math.round(avgRssi[0]?.dataValues?.avg_rssi || 0),
|
||||
time_period_hours: hours
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching detection stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch detection statistics',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/detections/:id - Get specific detection
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const detection = await DroneDetection.findByPk(req.params.id, {
|
||||
include: [{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['id', 'name', 'geo_lat', 'geo_lon', 'location_description']
|
||||
}]
|
||||
});
|
||||
|
||||
if (!detection) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Detection not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: detection
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching detection:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch detection',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
54
server/routes/health.js
Normal file
54
server/routes/health.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Health check endpoint
|
||||
router.get('/', (req, res) => {
|
||||
const healthcheck = {
|
||||
uptime: process.uptime(),
|
||||
message: 'OK',
|
||||
timestamp: Date.now(),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
version: process.env.npm_package_version || '1.0.0'
|
||||
};
|
||||
|
||||
try {
|
||||
res.status(200).json(healthcheck);
|
||||
} catch (error) {
|
||||
healthcheck.message = error;
|
||||
res.status(503).json(healthcheck);
|
||||
}
|
||||
});
|
||||
|
||||
// Detailed health check with database connection
|
||||
router.get('/detailed', async (req, res) => {
|
||||
const healthcheck = {
|
||||
uptime: process.uptime(),
|
||||
message: 'OK',
|
||||
timestamp: Date.now(),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
services: {}
|
||||
};
|
||||
|
||||
try {
|
||||
// Check database connection
|
||||
const { sequelize } = require('../models');
|
||||
await sequelize.authenticate();
|
||||
healthcheck.services.database = 'connected';
|
||||
|
||||
// Check Redis connection (if configured)
|
||||
if (process.env.REDIS_HOST) {
|
||||
// Add Redis check if implemented
|
||||
healthcheck.services.redis = 'not_implemented';
|
||||
}
|
||||
|
||||
res.status(200).json(healthcheck);
|
||||
} catch (error) {
|
||||
healthcheck.message = 'Service Unavailable';
|
||||
healthcheck.services.database = 'disconnected';
|
||||
healthcheck.error = error.message;
|
||||
res.status(503).json(healthcheck);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
199
server/routes/heartbeat.js
Normal file
199
server/routes/heartbeat.js
Normal file
@@ -0,0 +1,199 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
const { Heartbeat, Device } = require('../models');
|
||||
const { validateRequest } = require('../middleware/validation');
|
||||
|
||||
// Validation schema for heartbeat
|
||||
const heartbeatSchema = Joi.object({
|
||||
type: Joi.string().valid('heartbeat').required(),
|
||||
key: Joi.string().required(),
|
||||
device_id: Joi.number().integer().optional(),
|
||||
signal_strength: Joi.number().integer().optional(),
|
||||
battery_level: Joi.number().integer().min(0).max(100).optional(),
|
||||
temperature: Joi.number().optional(),
|
||||
uptime: Joi.number().integer().min(0).optional(),
|
||||
memory_usage: Joi.number().integer().min(0).max(100).optional(),
|
||||
firmware_version: Joi.string().optional()
|
||||
});
|
||||
|
||||
// POST /api/heartbeat - Receive heartbeat from devices
|
||||
router.post('/', validateRequest(heartbeatSchema), async (req, res) => {
|
||||
try {
|
||||
const { type, key, device_id, ...heartbeatData } = req.body;
|
||||
|
||||
// If device_id is not provided, try to find device by key
|
||||
let deviceId = device_id;
|
||||
if (!deviceId) {
|
||||
// Try to extract device ID from key or use key as identifier
|
||||
// This is a fallback for devices that only send key
|
||||
const keyMatch = key.match(/device[_-]?(\d+)/i);
|
||||
deviceId = keyMatch ? parseInt(keyMatch[1]) : key.hashCode(); // Simple hash if no pattern
|
||||
}
|
||||
|
||||
// Ensure device exists or create it
|
||||
const [device] = await Device.findOrCreate({
|
||||
where: { id: deviceId },
|
||||
defaults: {
|
||||
id: deviceId,
|
||||
name: `Device ${deviceId}`,
|
||||
last_heartbeat: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// Update device's last heartbeat
|
||||
await device.update({ last_heartbeat: new Date() });
|
||||
|
||||
// Create heartbeat record
|
||||
const heartbeat = await Heartbeat.create({
|
||||
device_id: deviceId,
|
||||
device_key: key,
|
||||
...heartbeatData,
|
||||
received_at: new Date()
|
||||
});
|
||||
|
||||
// Emit real-time update via Socket.IO
|
||||
req.io.emit('device_heartbeat', {
|
||||
device_id: deviceId,
|
||||
device_key: key,
|
||||
timestamp: heartbeat.received_at,
|
||||
status: 'online',
|
||||
...heartbeatData
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: heartbeat,
|
||||
message: 'Heartbeat recorded successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing heartbeat:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to process heartbeat',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/heartbeat/status - Get device status overview
|
||||
router.get('/status', async (req, res) => {
|
||||
try {
|
||||
const devices = await Device.findAll({
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
'geo_lat',
|
||||
'geo_lon',
|
||||
'last_heartbeat',
|
||||
'heartbeat_interval',
|
||||
'is_active'
|
||||
],
|
||||
include: [{
|
||||
model: Heartbeat,
|
||||
as: 'heartbeats',
|
||||
limit: 1,
|
||||
order: [['received_at', 'DESC']],
|
||||
attributes: ['battery_level', 'signal_strength', 'temperature', 'firmware_version']
|
||||
}]
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const deviceStatus = devices.map(device => {
|
||||
const timeSinceLastHeartbeat = device.last_heartbeat
|
||||
? (now - new Date(device.last_heartbeat)) / 1000
|
||||
: null;
|
||||
|
||||
const expectedInterval = device.heartbeat_interval || 300; // 5 minutes default
|
||||
const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < (expectedInterval * 2);
|
||||
|
||||
return {
|
||||
device_id: device.id,
|
||||
name: device.name,
|
||||
geo_lat: device.geo_lat,
|
||||
geo_lon: device.geo_lon,
|
||||
status: device.is_active ? (isOnline ? 'online' : 'offline') : 'inactive',
|
||||
last_heartbeat: device.last_heartbeat,
|
||||
time_since_last_heartbeat: timeSinceLastHeartbeat,
|
||||
latest_data: device.heartbeats[0] || null
|
||||
};
|
||||
});
|
||||
|
||||
const summary = {
|
||||
total_devices: devices.length,
|
||||
online: deviceStatus.filter(d => d.status === 'online').length,
|
||||
offline: deviceStatus.filter(d => d.status === 'offline').length,
|
||||
inactive: deviceStatus.filter(d => d.status === 'inactive').length
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
summary,
|
||||
devices: deviceStatus
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching device status:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch device status',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/heartbeat/device/:deviceId - Get heartbeat history for specific device
|
||||
router.get('/device/:deviceId', async (req, res) => {
|
||||
try {
|
||||
const { deviceId } = req.params;
|
||||
const { limit = 50, offset = 0 } = req.query;
|
||||
|
||||
const heartbeats = await Heartbeat.findAndCountAll({
|
||||
where: { device_id: deviceId },
|
||||
limit: Math.min(parseInt(limit), 1000),
|
||||
offset: parseInt(offset),
|
||||
order: [['received_at', 'DESC']],
|
||||
include: [{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['id', 'name', 'geo_lat', 'geo_lon']
|
||||
}]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: heartbeats.rows,
|
||||
pagination: {
|
||||
total: heartbeats.count,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
pages: Math.ceil(heartbeats.count / parseInt(limit))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching device heartbeats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch device heartbeats',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to generate simple hash from string
|
||||
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;
|
||||
45
server/routes/index.js
Normal file
45
server/routes/index.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Import route modules
|
||||
const droneDetectionRoutes = require('./droneDetection');
|
||||
const heartbeatRoutes = require('./heartbeat');
|
||||
const deviceRoutes = require('./device');
|
||||
const userRoutes = require('./user');
|
||||
const alertRoutes = require('./alert');
|
||||
const dashboardRoutes = require('./dashboard');
|
||||
|
||||
// API versioning
|
||||
router.use('/v1/detections', droneDetectionRoutes);
|
||||
router.use('/v1/heartbeat', heartbeatRoutes);
|
||||
router.use('/v1/devices', deviceRoutes);
|
||||
router.use('/v1/users', userRoutes);
|
||||
router.use('/v1/alerts', alertRoutes);
|
||||
router.use('/v1/dashboard', dashboardRoutes);
|
||||
|
||||
// Default routes (no version prefix for backward compatibility)
|
||||
router.use('/detections', droneDetectionRoutes);
|
||||
router.use('/heartbeat', heartbeatRoutes);
|
||||
router.use('/devices', deviceRoutes);
|
||||
router.use('/users', userRoutes);
|
||||
router.use('/alerts', alertRoutes);
|
||||
router.use('/dashboard', dashboardRoutes);
|
||||
|
||||
// API documentation endpoint
|
||||
router.get('/', (req, res) => {
|
||||
res.json({
|
||||
message: 'Drone Detection System API',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
detections: '/api/detections',
|
||||
heartbeat: '/api/heartbeat',
|
||||
devices: '/api/devices',
|
||||
users: '/api/users',
|
||||
alerts: '/api/alerts',
|
||||
dashboard: '/api/dashboard'
|
||||
},
|
||||
documentation: '/api/docs'
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
214
server/routes/user.js
Normal file
214
server/routes/user.js
Normal file
@@ -0,0 +1,214 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { User } = require('../models');
|
||||
const { validateRequest } = require('../middleware/validation');
|
||||
const { authenticateToken, requireRole } = require('../middleware/auth');
|
||||
|
||||
// Validation schemas
|
||||
const registerSchema = Joi.object({
|
||||
username: Joi.string().min(3).max(50).required(),
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().min(6).required(),
|
||||
first_name: Joi.string().optional(),
|
||||
last_name: Joi.string().optional(),
|
||||
phone_number: Joi.string().optional(),
|
||||
role: Joi.string().valid('admin', 'operator', 'viewer').default('viewer')
|
||||
});
|
||||
|
||||
const loginSchema = Joi.object({
|
||||
username: Joi.string().required(),
|
||||
password: Joi.string().required()
|
||||
});
|
||||
|
||||
const updateProfileSchema = Joi.object({
|
||||
first_name: Joi.string().optional(),
|
||||
last_name: Joi.string().optional(),
|
||||
phone_number: Joi.string().optional(),
|
||||
sms_alerts_enabled: Joi.boolean().optional(),
|
||||
email_alerts_enabled: Joi.boolean().optional(),
|
||||
timezone: Joi.string().optional()
|
||||
});
|
||||
|
||||
// POST /api/users/register - Register new user
|
||||
router.post('/register', validateRequest(registerSchema), async (req, res) => {
|
||||
try {
|
||||
const { password, ...userData } = req.body;
|
||||
|
||||
// Hash password
|
||||
const saltRounds = 12;
|
||||
const password_hash = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// Create user
|
||||
const user = await User.create({
|
||||
...userData,
|
||||
password_hash
|
||||
});
|
||||
|
||||
// Remove password hash from response
|
||||
const { password_hash: _, ...userResponse } = user.toJSON();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: userResponse,
|
||||
message: 'User registered successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error registering user:', error);
|
||||
|
||||
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: 'Username or email already exists'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to register user',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/users/login - User login
|
||||
router.post('/login', validateRequest(loginSchema), async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
// Find user by username or email
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ username: username },
|
||||
{ email: username }
|
||||
],
|
||||
is_active: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user || !await bcrypt.compare(password, user.password_hash)) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
// Update last login
|
||||
await user.update({ last_login: new Date() });
|
||||
|
||||
// Generate JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id, username: user.username, role: user.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
// Remove password hash from response
|
||||
const { password_hash: _, ...userResponse } = user.toJSON();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: userResponse,
|
||||
token,
|
||||
expires_in: '24h'
|
||||
},
|
||||
message: 'Login successful'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during login:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Login failed',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/users/profile - Get current user profile
|
||||
router.get('/profile', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { password_hash: _, ...userProfile } = req.user.toJSON();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: userProfile
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching user profile:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch user profile',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/users/profile - Update user profile
|
||||
router.put('/profile', authenticateToken, validateRequest(updateProfileSchema), async (req, res) => {
|
||||
try {
|
||||
await req.user.update(req.body);
|
||||
|
||||
const { password_hash: _, ...userResponse } = req.user.toJSON();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: userResponse,
|
||||
message: 'Profile updated successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating user profile:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to update profile',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/users - Get all users (admin only)
|
||||
router.get('/', authenticateToken, requireRole(['admin']), async (req, res) => {
|
||||
try {
|
||||
const { limit = 50, offset = 0, role, is_active } = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
if (role) whereClause.role = role;
|
||||
if (is_active !== undefined) whereClause.is_active = is_active === 'true';
|
||||
|
||||
const users = await User.findAndCountAll({
|
||||
where: whereClause,
|
||||
attributes: { exclude: ['password_hash'] },
|
||||
limit: Math.min(parseInt(limit), 100),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: users.rows,
|
||||
pagination: {
|
||||
total: users.count,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
pages: Math.ceil(users.count / parseInt(limit))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch users',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user