Initial commit

This commit is contained in:
2025-08-16 19:43:44 +02:00
commit ea9a2627b4
64 changed files with 9232 additions and 0 deletions

311
server/routes/alert.js Normal file
View 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
View 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
View 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;

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