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

102
server/.dockerignore Normal file
View File

@@ -0,0 +1,102 @@
# Node.js
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Development files
.git
.gitignore
README.md
Dockerfile
.dockerignore
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

29
server/.env.example Normal file
View File

@@ -0,0 +1,29 @@
# Environment Configuration
NODE_ENV=development
PORT=3001
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=drone_detection
DB_USER=postgres
DB_PASSWORD=password
# JWT Secret
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
# Twilio Configuration (for SMS alerts)
TWILIO_ACCOUNT_SID=your-twilio-account-sid
TWILIO_AUTH_TOKEN=your-twilio-auth-token
TWILIO_PHONE_NUMBER=your-twilio-phone-number
# Alert Configuration
SMS_ALERTS_ENABLED=true
EMAIL_ALERTS_ENABLED=false
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
# CORS Configuration
CORS_ORIGIN=http://localhost:3000

49
server/Dockerfile Normal file
View File

@@ -0,0 +1,49 @@
# Backend Dockerfile for Drone Detection System
FROM node:18-alpine AS base
# Install system dependencies
RUN apk add --no-cache \
python3 \
make \
g++ \
curl \
dumb-init
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && \
npm cache clean --force
# Copy application code
COPY . .
# Create logs directory
RUN mkdir -p logs
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Set ownership
RUN chown -R nodejs:nodejs /app
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3001
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:3001/api/health || exit 1
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
# Start the application
CMD ["npm", "start"]

95
server/index.js Normal file
View File

@@ -0,0 +1,95 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const { createServer } = require('http');
const { Server } = require('socket.io');
require('dotenv').config();
const { sequelize } = require('./models');
const routes = require('./routes');
const { initializeSocketHandlers } = require('./services/socketService');
const errorHandler = require('./middleware/errorHandler');
const app = express();
const server = createServer(app);
const io = new Server(server, {
cors: {
origin: process.env.CORS_ORIGIN || "http://localhost:3000",
methods: ["GET", "POST", "PUT", "DELETE"]
}
});
// Rate limiting
const limiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100,
message: 'Too many requests from this IP, please try again later.'
});
// Middleware
app.use(helmet());
app.use(compression());
app.use(morgan('combined'));
app.use(cors({
origin: process.env.CORS_ORIGIN || "http://localhost:3000",
credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use('/api/', limiter);
// Make io available to routes
app.use((req, res, next) => {
req.io = io;
next();
});
// Routes
app.use('/api', routes);
// Health check endpoints
app.get('/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV
});
});
app.use('/api/health', require('./routes/health'));
// Error handling
app.use(errorHandler);
// Socket.IO initialization
initializeSocketHandlers(io);
const PORT = process.env.PORT || 3001;
// Database connection and server startup
async function startServer() {
try {
await sequelize.authenticate();
console.log('Database connected successfully.');
if (process.env.NODE_ENV !== 'production') {
await sequelize.sync({ alter: true });
console.log('Database synchronized.');
}
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Environment: ${process.env.NODE_ENV}`);
});
} catch (error) {
console.error('Unable to start server:', error);
process.exit(1);
}
}
startServer();
module.exports = { app, server, io };

63
server/middleware/auth.js Normal file
View File

@@ -0,0 +1,63 @@
const jwt = require('jsonwebtoken');
const { User } = require('../models');
async function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({
success: false,
message: 'Access token required'
});
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByPk(decoded.userId, {
attributes: ['id', 'username', 'email', 'role', 'is_active']
});
if (!user || !user.is_active) {
return res.status(401).json({
success: false,
message: 'Invalid or inactive user'
});
}
req.user = user;
next();
} catch (error) {
console.error('Token verification error:', error);
return res.status(403).json({
success: false,
message: 'Invalid or expired token'
});
}
}
function requireRole(roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
const userRoles = Array.isArray(roles) ? roles : [roles];
if (!userRoles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: 'Insufficient permissions'
});
}
next();
};
}
module.exports = {
authenticateToken,
requireRole
};

View File

@@ -0,0 +1,57 @@
function errorHandler(error, req, res, next) {
console.error('Error occurred:', error);
// Default error response
let statusCode = 500;
let message = 'Internal server error';
let details = null;
// Handle specific error types
if (error.name === 'ValidationError') {
statusCode = 400;
message = 'Validation error';
details = error.details || error.message;
} else if (error.name === 'UnauthorizedError') {
statusCode = 401;
message = 'Unauthorized';
} else if (error.name === 'SequelizeValidationError') {
statusCode = 400;
message = 'Database validation error';
details = error.errors.map(err => ({
field: err.path,
message: err.message
}));
} else if (error.name === 'SequelizeUniqueConstraintError') {
statusCode = 409;
message = 'Resource already exists';
details = error.errors.map(err => ({
field: err.path,
message: err.message
}));
} else if (error.name === 'SequelizeForeignKeyConstraintError') {
statusCode = 400;
message = 'Invalid reference';
} else if (error.status) {
statusCode = error.status;
message = error.message;
}
// Send error response
const response = {
success: false,
message,
timestamp: new Date().toISOString()
};
// Include error details in development
if (process.env.NODE_ENV === 'development') {
response.error = details || error.message;
response.stack = error.stack;
} else if (details) {
response.details = details;
}
res.status(statusCode).json(response);
}
module.exports = errorHandler;

View File

@@ -0,0 +1,30 @@
function validateRequest(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
const errorDetails = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
value: detail.context.value
}));
return res.status(400).json({
success: false,
message: 'Validation error',
errors: errorDetails
});
}
// Replace req.body with validated and sanitized data
req.body = value;
next();
};
}
module.exports = {
validateRequest
};

109
server/models/AlertLog.js Normal file
View File

@@ -0,0 +1,109 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const AlertLog = sequelize.define('AlertLog', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
alert_rule_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'alert_rules',
key: 'id'
}
},
detection_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'drone_detections',
key: 'id'
}
},
alert_type: {
type: DataTypes.ENUM('sms', 'email', 'webhook', 'push'),
allowNull: false
},
recipient: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Phone number, email, or webhook URL'
},
message: {
type: DataTypes.TEXT,
allowNull: false,
comment: 'Alert message content'
},
status: {
type: DataTypes.ENUM('pending', 'sent', 'failed', 'delivered'),
defaultValue: 'pending'
},
sent_at: {
type: DataTypes.DATE,
allowNull: true
},
delivered_at: {
type: DataTypes.DATE,
allowNull: true
},
error_message: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Error message if alert failed'
},
external_id: {
type: DataTypes.STRING,
allowNull: true,
comment: 'External service message ID (Twilio SID, etc.)'
},
cost: {
type: DataTypes.DECIMAL(6, 4),
allowNull: true,
comment: 'Cost of sending the alert (for SMS, etc.)'
},
retry_count: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: 'Number of retry attempts'
},
priority: {
type: DataTypes.ENUM('low', 'medium', 'high', 'critical'),
defaultValue: 'medium'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'alert_logs',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
fields: ['alert_rule_id']
},
{
fields: ['detection_id']
},
{
fields: ['status']
},
{
fields: ['sent_at']
},
{
fields: ['alert_type', 'status']
}
]
});
return AlertLog;
};

124
server/models/AlertRule.js Normal file
View File

@@ -0,0 +1,124 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const AlertRule = sequelize.define('AlertRule', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
user_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
name: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Human-readable name for the alert rule'
},
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Description of what triggers this alert'
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
device_ids: {
type: DataTypes.JSON,
allowNull: true,
comment: 'Array of device IDs to monitor (null = all devices)'
},
drone_types: {
type: DataTypes.JSON,
allowNull: true,
comment: 'Array of drone types to alert on (null = all types)'
},
min_rssi: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Minimum RSSI threshold for alert'
},
max_rssi: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Maximum RSSI threshold for alert'
},
frequency_ranges: {
type: DataTypes.JSON,
allowNull: true,
comment: 'Array of frequency ranges to monitor [{min: 20, max: 30}]'
},
time_window: {
type: DataTypes.INTEGER,
defaultValue: 300,
comment: 'Time window in seconds to group detections'
},
min_detections: {
type: DataTypes.INTEGER,
defaultValue: 1,
comment: 'Minimum number of detections in time window to trigger alert'
},
cooldown_period: {
type: DataTypes.INTEGER,
defaultValue: 600,
comment: 'Cooldown period in seconds between alerts for same drone'
},
alert_channels: {
type: DataTypes.JSON,
defaultValue: ['sms'],
comment: 'Array of alert channels: sms, email, webhook'
},
webhook_url: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Webhook URL for custom integrations'
},
active_hours: {
type: DataTypes.JSON,
allowNull: true,
comment: 'Active hours for alerts {start: "09:00", end: "17:00"}'
},
active_days: {
type: DataTypes.JSON,
defaultValue: [1, 2, 3, 4, 5, 6, 7],
comment: 'Active days of week (1=Monday, 7=Sunday)'
},
priority: {
type: DataTypes.ENUM('low', 'medium', 'high', 'critical'),
defaultValue: 'medium',
comment: 'Alert priority level'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'alert_rules',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
fields: ['user_id']
},
{
fields: ['is_active']
},
{
fields: ['priority']
}
]
});
return AlertRule;
};

88
server/models/Device.js Normal file
View File

@@ -0,0 +1,88 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Device = sequelize.define('Device', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
allowNull: false,
comment: 'Unique device identifier from hardware'
},
name: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Human-readable device name'
},
geo_lat: {
type: DataTypes.DECIMAL(10, 8),
allowNull: true,
comment: 'Device latitude coordinate'
},
geo_lon: {
type: DataTypes.DECIMAL(11, 8),
allowNull: true,
comment: 'Device longitude coordinate'
},
location_description: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Human-readable location description'
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: 'Whether the device is currently active'
},
last_heartbeat: {
type: DataTypes.DATE,
allowNull: true,
comment: 'Timestamp of last heartbeat received'
},
heartbeat_interval: {
type: DataTypes.INTEGER,
defaultValue: 300, // 5 minutes
comment: 'Expected heartbeat interval in seconds'
},
firmware_version: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Device firmware version'
},
installation_date: {
type: DataTypes.DATE,
allowNull: true,
comment: 'When the device was installed'
},
notes: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Additional notes about the device'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'devices',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
fields: ['geo_lat', 'geo_lon']
},
{
fields: ['last_heartbeat']
},
{
fields: ['is_active']
}
]
});
return Device;
};

View File

@@ -0,0 +1,120 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const DroneDetection = sequelize.define('DroneDetection', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
device_id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'devices',
key: 'id'
},
comment: 'ID of the detecting device'
},
drone_id: {
type: DataTypes.INTEGER,
allowNull: false,
comment: 'Detected drone identifier'
},
drone_type: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Type of drone detected (0=unknown, 1=commercial, 2=racing, etc.)'
},
rssi: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Received Signal Strength Indicator'
},
freq: {
type: DataTypes.INTEGER,
allowNull: false,
comment: 'Frequency detected'
},
geo_lat: {
type: DataTypes.DECIMAL(10, 8),
allowNull: true,
comment: 'Latitude where detection occurred'
},
geo_lon: {
type: DataTypes.DECIMAL(11, 8),
allowNull: true,
comment: 'Longitude where detection occurred'
},
device_timestamp: {
type: DataTypes.BIGINT,
allowNull: true,
comment: 'Unix timestamp from the device'
},
server_timestamp: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
comment: 'When the detection was received by server'
},
confidence_level: {
type: DataTypes.DECIMAL(3, 2),
allowNull: true,
comment: 'Confidence level of detection (0.00-1.00)'
},
signal_duration: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Duration of signal in milliseconds'
},
processed: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: 'Whether this detection has been processed for alerts'
},
threat_level: {
type: DataTypes.ENUM('monitoring', 'low', 'medium', 'high', 'critical'),
allowNull: true,
comment: 'Assessed threat level based on RSSI and drone type'
},
estimated_distance: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Estimated distance to drone in meters'
},
requires_action: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: 'Whether this detection requires immediate security action'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'drone_detections',
timestamps: true,
createdAt: 'created_at',
updatedAt: false,
indexes: [
{
fields: ['device_id']
},
{
fields: ['drone_id']
},
{
fields: ['server_timestamp']
},
{
fields: ['processed']
},
{
fields: ['device_id', 'drone_id', 'server_timestamp']
}
]
});
return DroneDetection;
};

View File

@@ -0,0 +1,82 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Heartbeat = sequelize.define('Heartbeat', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
device_id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'devices',
key: 'id'
},
comment: 'ID of the device sending heartbeat'
},
device_key: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Unique key of the sensor from heartbeat message'
},
signal_strength: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Signal strength at time of heartbeat'
},
battery_level: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Battery level percentage (0-100)'
},
temperature: {
type: DataTypes.DECIMAL(4, 1),
allowNull: true,
comment: 'Device temperature in Celsius'
},
uptime: {
type: DataTypes.BIGINT,
allowNull: true,
comment: 'Device uptime in seconds'
},
memory_usage: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Memory usage percentage'
},
firmware_version: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Firmware version reported in heartbeat'
},
received_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
comment: 'When heartbeat was received by server'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'heartbeats',
timestamps: true,
createdAt: 'created_at',
updatedAt: false,
indexes: [
{
fields: ['device_id']
},
{
fields: ['received_at']
},
{
fields: ['device_id', 'received_at']
}
]
});
return Heartbeat;
};

98
server/models/User.js Normal file
View File

@@ -0,0 +1,98 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
len: [3, 50]
}
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password_hash: {
type: DataTypes.STRING,
allowNull: false
},
first_name: {
type: DataTypes.STRING,
allowNull: true
},
last_name: {
type: DataTypes.STRING,
allowNull: true
},
phone_number: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Phone number for SMS alerts (include country code)'
},
role: {
type: DataTypes.ENUM('admin', 'operator', 'viewer'),
defaultValue: 'viewer',
comment: 'User role for permission management'
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
sms_alerts_enabled: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: 'Whether user wants to receive SMS alerts'
},
email_alerts_enabled: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: 'Whether user wants to receive email alerts'
},
last_login: {
type: DataTypes.DATE,
allowNull: true
},
timezone: {
type: DataTypes.STRING,
defaultValue: 'UTC',
comment: 'User timezone for alert scheduling'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'users',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
fields: ['email']
},
{
fields: ['username']
},
{
fields: ['phone_number']
}
]
});
return User;
};

54
server/models/index.js Normal file
View File

@@ -0,0 +1,54 @@
const { Sequelize } = require('sequelize');
require('dotenv').config();
const sequelize = new Sequelize(
process.env.DB_NAME || 'drone_detection',
process.env.DB_USER || 'postgres',
process.env.DB_PASSWORD || 'password',
{
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: process.env.NODE_ENV === 'development' ? console.log : false,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
}
);
// Import models
const Device = require('./Device')(sequelize);
const DroneDetection = require('./DroneDetection')(sequelize);
const Heartbeat = require('./Heartbeat')(sequelize);
const User = require('./User')(sequelize);
const AlertRule = require('./AlertRule')(sequelize);
const AlertLog = require('./AlertLog')(sequelize);
// Define associations
Device.hasMany(DroneDetection, { foreignKey: 'device_id', as: 'detections' });
DroneDetection.belongsTo(Device, { foreignKey: 'device_id', as: 'device' });
Device.hasMany(Heartbeat, { foreignKey: 'device_id', as: 'heartbeats' });
Heartbeat.belongsTo(Device, { foreignKey: 'device_id', as: 'device' });
User.hasMany(AlertRule, { foreignKey: 'user_id', as: 'alertRules' });
AlertRule.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
AlertRule.hasMany(AlertLog, { foreignKey: 'alert_rule_id', as: 'logs' });
AlertLog.belongsTo(AlertRule, { foreignKey: 'alert_rule_id', as: 'rule' });
DroneDetection.hasMany(AlertLog, { foreignKey: 'detection_id', as: 'alerts' });
AlertLog.belongsTo(DroneDetection, { foreignKey: 'detection_id', as: 'detection' });
module.exports = {
sequelize,
Device,
DroneDetection,
Heartbeat,
User,
AlertRule,
AlertLog
};

35
server/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "drone-detection-server",
"version": "1.0.0",
"description": "Backend API for drone detection system",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"db:setup": "node scripts/setup-database.js",
"db:migrate": "node scripts/migrate.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.0.0",
"morgan": "^1.10.0",
"dotenv": "^16.3.1",
"pg": "^8.11.3",
"pg-hstore": "^2.3.4",
"sequelize": "^6.32.1",
"socket.io": "^4.7.2",
"twilio": "^4.14.0",
"joi": "^17.9.2",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.1",
"express-rate-limit": "^6.8.1",
"compression": "^1.7.4"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.6.1",
"supertest": "^6.3.3"
}
}

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;

View File

@@ -0,0 +1,17 @@
-- Database initialization script for Docker
-- This script sets up the initial database structure
-- Enable extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Set timezone
SET timezone = 'Europe/Stockholm';
-- Create indexes for better performance
-- (These will be created by Sequelize, but we can optimize here)
-- Log successful initialization
INSERT INTO pg_stat_statements_info (dealloc) VALUES (0) ON CONFLICT DO NOTHING;
-- Grant necessary permissions
GRANT ALL PRIVILEGES ON DATABASE drone_detection TO postgres;

View File

@@ -0,0 +1,331 @@
const { Sequelize } = require('sequelize');
const bcrypt = require('bcrypt');
// Import models
const Device = require('../models/Device');
const DroneDetection = require('../models/DroneDetection');
const Heartbeat = require('../models/Heartbeat');
const User = require('../models/User');
const AlertRule = require('../models/AlertRule');
const AlertLog = require('../models/AlertLog');
const setupDatabase = async () => {
try {
console.log('🚀 Starting database setup...\n');
// Load environment variables
require('dotenv').config();
// Create Sequelize instance
const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: 'postgres',
logging: console.log,
}
);
// Test database connection
console.log('📡 Testing database connection...');
await sequelize.authenticate();
console.log('✅ Database connection established successfully.\n');
// Initialize models
console.log('📋 Initializing models...');
Device.init(Device.getAttributes(), { sequelize, modelName: 'Device' });
DroneDetection.init(DroneDetection.getAttributes(), { sequelize, modelName: 'DroneDetection' });
Heartbeat.init(Heartbeat.getAttributes(), { sequelize, modelName: 'Heartbeat' });
User.init(User.getAttributes(), { sequelize, modelName: 'User' });
AlertRule.init(AlertRule.getAttributes(), { sequelize, modelName: 'AlertRule' });
AlertLog.init(AlertLog.getAttributes(), { sequelize, modelName: 'AlertLog' });
// Define associations
console.log('🔗 Setting up model associations...');
// Device associations
Device.hasMany(DroneDetection, { foreignKey: 'device_id', as: 'detections' });
Device.hasMany(Heartbeat, { foreignKey: 'device_id', as: 'heartbeats' });
// Detection associations
DroneDetection.belongsTo(Device, { foreignKey: 'device_id', as: 'device' });
// Heartbeat associations
Heartbeat.belongsTo(Device, { foreignKey: 'device_id', as: 'device' });
// User associations
User.hasMany(AlertRule, { foreignKey: 'user_id', as: 'alertRules' });
User.hasMany(AlertLog, { foreignKey: 'user_id', as: 'alertLogs' });
// Alert associations
AlertRule.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
AlertRule.hasMany(AlertLog, { foreignKey: 'alert_rule_id', as: 'logs' });
AlertLog.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
AlertLog.belongsTo(AlertRule, { foreignKey: 'alert_rule_id', as: 'alertRule' });
AlertLog.belongsTo(DroneDetection, { foreignKey: 'detection_id', as: 'detection' });
// Sync database (create tables)
console.log('🏗️ Creating database tables...');
await sequelize.sync({ force: true }); // WARNING: This will drop existing tables
console.log('✅ Database tables created successfully.\n');
// Create sample data
console.log('📊 Creating sample data...\n');
// Create admin user
console.log('👤 Creating admin user...');
const adminUser = await User.create({
username: 'admin',
email: 'admin@example.com',
password: await bcrypt.hash('admin123', 10),
role: 'admin'
});
console.log(`✅ Admin user created: ${adminUser.username}`);
// Create operator user
console.log('👤 Creating operator user...');
const operatorUser = await User.create({
username: 'operator',
email: 'operator@example.com',
password: await bcrypt.hash('operator123', 10),
role: 'operator'
});
console.log(`✅ Operator user created: ${operatorUser.username}`);
// Create sample devices
console.log('📡 Creating sample devices...');
const devices = await Device.bulkCreate([
{
device_id: 1941875381,
name: 'Drone Detector Alpha',
location: 'Stockholm Central',
geo_lat: 59.3293,
geo_lon: 18.0686,
status: 'online',
last_seen: new Date()
},
{
device_id: 1941875382,
name: 'Drone Detector Beta',
location: 'Gothenburg Port',
geo_lat: 57.7089,
geo_lon: 11.9746,
status: 'online',
last_seen: new Date()
},
{
device_id: 1941875383,
name: 'Drone Detector Gamma',
location: 'Malmö Airport',
geo_lat: 55.6050,
geo_lon: 13.0038,
status: 'offline',
last_seen: new Date(Date.now() - 2 * 60 * 60 * 1000) // 2 hours ago
}
]);
console.log(`✅ Created ${devices.length} sample devices`);
// Create sample heartbeats
console.log('💓 Creating sample heartbeats...');
const heartbeats = await Heartbeat.bulkCreate([
{
device_id: 1941875381,
battery_level: 85,
signal_strength: -45,
temperature: 22.5,
status: 'active',
timestamp: new Date()
},
{
device_id: 1941875382,
battery_level: 72,
signal_strength: -38,
temperature: 24.1,
status: 'active',
timestamp: new Date()
}
]);
console.log(`✅ Created ${heartbeats.length} sample heartbeats`);
// Create sample drone detections
console.log('🚁 Creating sample drone detections...');
const detections = await DroneDetection.bulkCreate([
{
device_id: 1941875381,
geo_lat: 59.3293,
geo_lon: 18.0686,
device_timestamp: Math.floor(Date.now() / 1000),
drone_type: 0,
rssi: -45,
freq: 20,
drone_id: 1001,
timestamp: new Date(),
threat_level: 'high',
estimated_distance: 150,
requires_action: true
},
{
device_id: 1941875382,
geo_lat: 57.7089,
geo_lon: 11.9746,
device_timestamp: Math.floor(Date.now() / 1000) - 3600,
drone_type: 1,
rssi: -52,
freq: 25,
drone_id: 1002,
timestamp: new Date(Date.now() - 60 * 60 * 1000),
threat_level: 'medium',
estimated_distance: 800,
requires_action: false
},
{
device_id: 1941875381,
geo_lat: 59.3295,
geo_lon: 18.0690,
device_timestamp: Math.floor(Date.now() / 1000) - 7200,
drone_type: 0,
rssi: -75,
freq: 22,
drone_id: 1003,
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
threat_level: 'low',
estimated_distance: 2500,
requires_action: false
}
]);
console.log(`✅ Created ${detections.length} sample drone detections`);
// Create sample alert rules
console.log('🚨 Creating sample alert rules...');
const alertRules = await AlertRule.bulkCreate([
{
user_id: adminUser.id,
name: 'Critical Security Threat',
description: 'Immediate alert for critical and high threats to government facilities',
conditions: {
min_threat_level: 'high',
rssi_threshold: -55,
max_distance: 200,
drone_types: [0, 1, 2],
device_ids: []
},
actions: {
sms: true,
phone_number: '+46701234567',
email: true,
channels: ['sms', 'email']
},
cooldown_minutes: 2,
is_active: true
},
{
user_id: operatorUser.id,
name: 'Medium Threat Monitoring',
description: 'Monitor medium threat drones in facility vicinity',
conditions: {
min_threat_level: 'medium',
rssi_threshold: -70,
max_distance: 1000,
drone_types: [1, 2],
device_ids: [1941875381, 1941875382]
},
actions: {
sms: true,
phone_number: '+46709876543',
channels: ['sms']
},
cooldown_minutes: 10,
is_active: true
},
{
user_id: adminUser.id,
name: 'Device Offline Alert',
description: 'Alert when security devices go offline',
conditions: {
device_offline: true,
device_ids: [1941875381, 1941875382, 1941875383]
},
actions: {
sms: true,
phone_number: '+46701234567',
channels: ['sms']
},
cooldown_minutes: 30,
is_active: true
}
]);
console.log(`✅ Created ${alertRules.length} sample alert rules`);
// Create sample alert logs
console.log('📝 Creating sample alert logs...');
const alertLogs = await AlertLog.bulkCreate([
{
user_id: adminUser.id,
alert_rule_id: alertRules[0].id,
detection_id: detections[0].id,
message: 'Drone detected with strong signal',
status: 'sent',
sent_at: new Date()
},
{
user_id: operatorUser.id,
alert_rule_id: alertRules[1].id,
detection_id: null,
message: 'Device 1941875383 went offline',
status: 'sent',
sent_at: new Date(Date.now() - 30 * 60 * 1000)
}
]);
console.log(`✅ Created ${alertLogs.length} sample alert logs\n`);
// Create database indexes for performance
console.log('🚀 Creating database indexes...');
await sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_drone_detections_device_timestamp
ON "DroneDetections" (device_id, timestamp);
`);
await sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_heartbeats_device_timestamp
ON "Heartbeats" (device_id, timestamp);
`);
await sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_alert_logs_user_created
ON "AlertLogs" (user_id, "createdAt");
`);
console.log('✅ Database indexes created\n');
console.log('🎉 Database setup completed successfully!\n');
console.log('📋 Summary:');
console.log(` • Users created: 2 (admin, operator)`);
console.log(` • Devices created: ${devices.length}`);
console.log(` • Heartbeats created: ${heartbeats.length}`);
console.log(` • Detections created: ${detections.length}`);
console.log(` • Alert rules created: ${alertRules.length}`);
console.log(` • Alert logs created: ${alertLogs.length}`);
console.log('\n📝 Default login credentials:');
console.log(' Admin: admin / admin123');
console.log(' Operator: operator / operator123\n');
// Close connection
await sequelize.close();
} catch (error) {
console.error('❌ Database setup failed:', error);
process.exit(1);
}
};
// Run setup if called directly
if (require.main === module) {
setupDatabase();
}
module.exports = setupDatabase;

View File

@@ -0,0 +1,499 @@
const twilio = require('twilio');
const { AlertRule, AlertLog, User, Device } = require('../models');
const { Op } = require('sequelize');
class AlertService {
constructor() {
this.twilioClient = null;
this.initializeTwilio();
}
// RSSI-based threat assessment for security installations
assessThreatLevel(rssi, droneType) {
// RSSI typically ranges from -30 (very close) to -100 (very far)
// For 15km range detection, we need to establish threat zones for sensitive facilities
let threatLevel = 'low';
let estimatedDistance = 0;
let description = '';
let actionRequired = false;
// Convert RSSI to estimated distance (rough calculation)
// Formula: Distance (m) = 10^((RSSI_at_1m - RSSI) / (10 * n))
// Where n = path loss exponent (typically 2-4 for outdoor environments)
const rssiAt1m = -30; // Typical RSSI at 1 meter
const pathLossExponent = 3; // Outdoor environment with obstacles
estimatedDistance = Math.pow(10, (rssiAt1m - rssi) / (10 * pathLossExponent));
// Threat level assessment based on distance zones for sensitive facilities
if (rssi >= -40) {
// Very close: 0-50 meters - CRITICAL THREAT
threatLevel = 'critical';
description = 'IMMEDIATE THREAT: Drone within security perimeter (0-50m)';
actionRequired = true;
} else if (rssi >= -55) {
// Close: 50-200 meters - HIGH THREAT
threatLevel = 'high';
description = 'HIGH THREAT: Drone approaching facility (50-200m)';
actionRequired = true;
} else if (rssi >= -70) {
// Medium: 200-1000 meters - MEDIUM THREAT
threatLevel = 'medium';
description = 'MEDIUM THREAT: Drone in facility vicinity (200m-1km)';
actionRequired = false;
} else if (rssi >= -85) {
// Far: 1-5 kilometers - LOW THREAT
threatLevel = 'low';
description = 'LOW THREAT: Drone detected at distance (1-5km)';
actionRequired = false;
} else {
// Very far: 5-15 kilometers - MONITORING ONLY
threatLevel = 'monitoring';
description = 'MONITORING: Drone detected at long range (5-15km)';
actionRequired = false;
}
// Adjust threat level based on drone type (if classified)
const droneTypes = {
0: 'Consumer/Hobby',
1: 'Professional/Military',
2: 'Racing/High-speed',
3: 'Unknown/Custom'
};
if (droneType === 1) {
// Military/Professional drone - escalate threat
if (threatLevel === 'low') threatLevel = 'medium';
if (threatLevel === 'medium') threatLevel = 'high';
if (threatLevel === 'high') threatLevel = 'critical';
description += ' - PROFESSIONAL/MILITARY DRONE DETECTED';
actionRequired = true;
} else if (droneType === 2) {
// Racing/Fast drone - escalate if close
if (rssi >= -55 && threatLevel !== 'critical') {
threatLevel = 'high';
description += ' - HIGH-SPEED DRONE DETECTED';
actionRequired = true;
}
}
return {
level: threatLevel,
estimatedDistance: Math.round(estimatedDistance),
rssi,
droneType: droneTypes[droneType] || 'Unknown',
description,
requiresImmediateAction: actionRequired,
priority: threatLevel === 'critical' ? 1 : threatLevel === 'high' ? 2 : threatLevel === 'medium' ? 3 : 4
};
}
initializeTwilio() {
if (process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN) {
this.twilioClient = twilio(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);
} else {
console.warn('Twilio credentials not configured. SMS alerts will be disabled.');
}
}
async processAlert(detection) {
try {
console.log(`🔍 Processing alert for detection ${detection.id}`);
// Assess threat level based on RSSI and drone type
const threatAssessment = this.assessThreatLevel(detection.rssi, detection.drone_type);
console.log('⚠️ Threat assessment:', threatAssessment);
// Update detection with threat assessment
await detection.update({
processed: true,
threat_level: threatAssessment.level,
estimated_distance: threatAssessment.estimatedDistance
});
// Get all active alert rules
const alertRules = await AlertRule.findAll({
where: { is_active: true },
include: [{
model: User,
as: 'user',
where: { is_active: true }
}]
});
for (const rule of alertRules) {
if (await this.shouldTriggerAlert(rule, detection, threatAssessment)) {
await this.triggerAlert(rule, detection, threatAssessment);
}
}
} catch (error) {
console.error('Error processing alert:', error);
throw error;
}
}
async shouldTriggerAlert(rule, detection, threatAssessment) {
try {
// SECURITY ENHANCEMENT: Check threat level requirements
if (rule.conditions.min_threat_level) {
const threatLevels = { 'monitoring': 0, 'low': 1, 'medium': 2, 'high': 3, 'critical': 4 };
const requiredLevel = threatLevels[rule.conditions.min_threat_level] || 0;
const currentLevel = threatLevels[threatAssessment.level] || 0;
if (currentLevel < requiredLevel) {
console.log(`Alert rule ${rule.name}: Threat level ${threatAssessment.level} below minimum ${rule.conditions.min_threat_level}`);
return false;
}
}
// SECURITY ENHANCEMENT: For government/sensitive sites, always alert on critical threats
if (threatAssessment.level === 'critical') {
console.log(`🚨 CRITICAL THREAT DETECTED - Force triggering alert for rule ${rule.name}`);
return true;
}
// Check device filter
if (rule.conditions.device_ids && rule.conditions.device_ids.length > 0 &&
!rule.conditions.device_ids.includes(detection.device_id)) {
return false;
}
// Check drone type filter
if (rule.conditions.drone_types && rule.conditions.drone_types.length > 0 &&
!rule.conditions.drone_types.includes(detection.drone_type)) {
return false;
}
// Check RSSI thresholds (enhanced for security)
if (rule.conditions.rssi_threshold && detection.rssi < rule.conditions.rssi_threshold) {
return false;
}
// SECURITY ENHANCEMENT: Check estimated distance
if (rule.conditions.max_distance && threatAssessment.estimatedDistance > rule.conditions.max_distance) {
console.log(`Alert rule ${rule.name}: Distance ${threatAssessment.estimatedDistance}m exceeds maximum ${rule.conditions.max_distance}m`);
return false;
}
// Check frequency ranges
if (rule.frequency_ranges && rule.frequency_ranges.length > 0) {
const inRange = rule.frequency_ranges.some(range =>
detection.freq >= range.min && detection.freq <= range.max
);
if (!inRange) {
return false;
}
}
// Check time window and minimum detections
if (rule.min_detections > 1) {
const timeWindowStart = new Date(Date.now() - rule.time_window * 1000);
const recentDetections = await DroneDetection.count({
where: {
device_id: detection.device_id,
drone_id: detection.drone_id,
server_timestamp: {
[Op.gte]: timeWindowStart
}
}
});
if (recentDetections < rule.min_detections) {
return false;
}
}
// Check cooldown period
if (rule.cooldown_period > 0) {
const cooldownStart = new Date(Date.now() - rule.cooldown_period * 1000);
const recentAlert = await AlertLog.findOne({
where: {
alert_rule_id: rule.id,
status: 'sent',
sent_at: {
[Op.gte]: cooldownStart
}
},
include: [{
model: DroneDetection,
as: 'detection',
where: {
device_id: detection.device_id,
drone_id: detection.drone_id
}
}]
});
if (recentAlert) {
return false;
}
}
// Check active hours and days
if (rule.active_hours || rule.active_days) {
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute;
const currentDay = now.getDay() || 7; // Convert Sunday from 0 to 7
if (rule.active_days && !rule.active_days.includes(currentDay)) {
return false;
}
if (rule.active_hours) {
const startTime = this.parseTime(rule.active_hours.start);
const endTime = this.parseTime(rule.active_hours.end);
if (startTime !== null && endTime !== null) {
if (startTime <= endTime) {
// Same day range
if (currentTime < startTime || currentTime > endTime) {
return false;
}
} else {
// Overnight range
if (currentTime < startTime && currentTime > endTime) {
return false;
}
}
}
}
}
return true;
} catch (error) {
console.error('Error checking alert conditions:', error);
return false;
}
}
async triggerAlert(rule, detection, threatAssessment) {
try {
const user = rule.user;
const device = await Device.findByPk(detection.device_id);
// Generate enhanced alert message with threat assessment
const message = this.generateSecurityAlertMessage(detection, device, rule, threatAssessment);
// SECURITY ENHANCEMENT: For critical threats, send to all available channels
const channels = threatAssessment.level === 'critical'
? ['sms', 'email', 'webhook'] // Force all channels for critical threats
: rule.actions.channels || ['sms'];
// Send alerts through configured channels
for (const channel of channels) {
let alertLog = null;
try {
switch (channel) {
case 'sms':
if (rule.actions.sms && rule.actions.phone_number) {
alertLog = await this.sendSMSAlert(rule.actions.phone_number, message, rule, detection, threatAssessment);
}
break;
case 'email':
if (rule.actions.email && user.email) {
alertLog = await this.sendEmailAlert(user.email, message, rule, detection, threatAssessment);
}
break;
case 'webhook':
if (rule.actions.webhook_url) {
alertLog = await this.sendWebhookAlert(rule.actions.webhook_url, detection, device, rule, threatAssessment);
}
break;
default:
console.warn(`Unknown alert channel: ${channel}`);
}
if (alertLog) {
console.log(`🚨 ${threatAssessment.level.toUpperCase()} THREAT: Alert sent via ${channel} for detection ${detection.id}`);
}
} catch (channelError) {
console.error(`Error sending ${channel} alert:`, channelError);
// Log failed alert
await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: channel,
recipient: channel === 'sms' ? user.phone_number :
channel === 'email' ? user.email : rule.webhook_url,
message: message,
status: 'failed',
error_message: channelError.message,
priority: rule.priority
});
}
}
} catch (error) {
console.error('Error triggering alert:', error);
throw error;
}
}
async sendSMSAlert(phoneNumber, message, rule, detection) {
if (!this.twilioClient) {
throw new Error('Twilio not configured');
}
const twilioMessage = await this.twilioClient.messages.create({
body: message,
from: process.env.TWILIO_PHONE_NUMBER,
to: phoneNumber
});
return await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: 'sms',
recipient: phoneNumber,
message: message,
status: 'sent',
sent_at: new Date(),
external_id: twilioMessage.sid,
priority: rule.priority
});
}
async sendEmailAlert(email, message, rule, detection) {
// Email implementation would go here
// For now, just log the alert
console.log(`Email alert would be sent to ${email}: ${message}`);
return await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: 'email',
recipient: email,
message: message,
status: 'sent',
sent_at: new Date(),
priority: rule.priority
});
}
async sendWebhookAlert(webhookUrl, detection, device, rule) {
const payload = {
event: 'drone_detection',
timestamp: new Date().toISOString(),
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,
location_description: device.location_description
},
alert_rule: {
id: rule.id,
name: rule.name,
priority: rule.priority
}
};
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'DroneDetectionSystem/1.0'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`Webhook failed with status ${response.status}`);
}
return await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: 'webhook',
recipient: webhookUrl,
message: JSON.stringify(payload),
status: 'sent',
sent_at: new Date(),
priority: rule.priority
});
}
generateAlertMessage(detection, device, rule) {
const deviceName = device.name || `Device ${device.id}`;
const location = device.location_description ||
`${device.geo_lat}, ${device.geo_lon}` ||
'Unknown location';
return `🚨 DRONE ALERT: Drone detected by ${deviceName} at ${location}. ` +
`Drone ID: ${detection.drone_id}, Frequency: ${detection.freq}MHz, ` +
`RSSI: ${detection.rssi}dBm. Time: ${new Date().toLocaleString()}`;
}
// SECURITY ENHANCEMENT: Enhanced message generation with threat assessment
generateSecurityAlertMessage(detection, device, rule, threatAssessment) {
const timestamp = new Date().toLocaleString('sv-SE', { timeZone: 'Europe/Stockholm' });
const deviceName = device ? device.name : `Device ${detection.device_id}`;
const location = device ? device.location : 'Unknown location';
// Create security-focused alert message
let message = `🚨 SECURITY ALERT 🚨\n`;
message += `THREAT LEVEL: ${threatAssessment.level.toUpperCase()}\n`;
message += `${threatAssessment.description}\n\n`;
message += `📍 LOCATION: ${location}\n`;
message += `🔧 DEVICE: ${deviceName}\n`;
message += `📏 DISTANCE: ~${threatAssessment.estimatedDistance}m\n`;
message += `📶 SIGNAL: ${detection.rssi} dBm\n`;
message += `🚁 DRONE TYPE: ${threatAssessment.droneType}\n`;
message += `⏰ TIME: ${timestamp}\n`;
if (threatAssessment.requiresImmediateAction) {
message += `\n⚠️ IMMEDIATE ACTION REQUIRED\n`;
message += `Security personnel should respond immediately.`;
}
return message;
}
parseTime(timeString) {
if (!timeString) return null;
const match = timeString.match(/^(\d{1,2}):(\d{2})$/);
if (!match) return null;
const hours = parseInt(match[1]);
const minutes = parseInt(match[2]);
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
return null;
}
return hours * 60 + minutes;
}
}
// Export singleton instance
const alertService = new AlertService();
module.exports = {
processAlert: (detection) => alertService.processAlert(detection),
AlertService
};

View File

@@ -0,0 +1,55 @@
function initializeSocketHandlers(io) {
io.on('connection', (socket) => {
console.log(`Client connected: ${socket.id}`);
// Join device-specific rooms for targeted updates
socket.on('join_device_room', (deviceId) => {
socket.join(`device_${deviceId}`);
console.log(`Client ${socket.id} joined device room: device_${deviceId}`);
});
// Join dashboard room for general updates
socket.on('join_dashboard', () => {
socket.join('dashboard');
console.log(`Client ${socket.id} joined dashboard room`);
});
// Leave rooms
socket.on('leave_device_room', (deviceId) => {
socket.leave(`device_${deviceId}`);
console.log(`Client ${socket.id} left device room: device_${deviceId}`);
});
socket.on('leave_dashboard', () => {
socket.leave('dashboard');
console.log(`Client ${socket.id} left dashboard room`);
});
// Handle client disconnect
socket.on('disconnect', () => {
console.log(`Client disconnected: ${socket.id}`);
});
// Send current status on connect
socket.emit('connection_status', {
status: 'connected',
timestamp: new Date().toISOString(),
clientId: socket.id
});
});
// Helper functions to emit events to specific rooms
io.emitToDevice = function(deviceId, event, data) {
io.to(`device_${deviceId}`).emit(event, data);
};
io.emitToDashboard = function(event, data) {
io.to('dashboard').emit(event, data);
};
console.log('Socket.IO handlers initialized');
}
module.exports = {
initializeSocketHandlers
};