Files
drone-detector/server/routes/alert.js
2025-08-28 13:17:37 +02:00

323 lines
9.3 KiB
JavaScript

const express = require('express');
const router = express.Router();
const Joi = require('joi');
const { AlertRule, AlertLog, User, sequelize } = 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().allow('').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(),
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().allow('').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 {
// Custom validation for webhook URL when webhook channel is selected
if (req.body.alert_channels && req.body.alert_channels.includes('webhook')) {
if (!req.body.webhook_url || req.body.webhook_url.trim() === '') {
return res.status(400).json({
success: false,
message: 'Validation error',
errors: [{
field: 'webhook_url',
message: 'Webhook URL is required when webhook channel is selected',
value: req.body.webhook_url || ''
}]
});
}
}
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;