Files
drone-detector/server/routes/health.js
2025-09-17 06:06:02 +02:00

459 lines
13 KiB
JavaScript

const express = require('express');
const router = express.Router();
// Dynamic model injection for testing
function getModels() {
if (global.__TEST_MODELS__) {
return global.__TEST_MODELS__;
}
return require('../models');
}
// Basic health check endpoint (no auth required)
router.get('/', (req, res) => {
const healthcheck = {
status: 'ok',
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now(),
environment: process.env.NODE_ENV || 'development',
version: process.env.npm_package_version || '1.0.0',
service: 'UAM-ILS Drone Detection System'
};
try {
res.status(200).json(healthcheck);
} catch (error) {
healthcheck.status = 'error';
healthcheck.message = error;
res.status(503).json(healthcheck);
}
});
// Device heartbeat endpoint (for devices to report their status - NO AUTH required)
router.post('/devices/:id/heartbeat', async (req, res) => {
try {
const models = getModels();
const { Device, Heartbeat } = models;
const deviceId = parseInt(req.params.id);
// Find the device
const device = await Device.findByPk(deviceId);
if (!device) {
return res.status(404).json({
success: false,
message: 'Device not found'
});
}
// Check if device is approved (this is the authorization check instead of JWT)
if (!device.is_approved) {
return res.status(403).json({
success: false,
message: 'Device not approved for heartbeat reporting'
});
}
// Extract heartbeat data - handle both simple device heartbeats and detailed health reports
const { type, key, status, cpu_usage, memory_usage, disk_usage, uptime, firmware_version } = req.body;
// For simple device heartbeats: {type:"heartbeat", key:"unique device ID"}
// For detailed health reports: {status:"online", cpu_usage:25.5, memory_usage:60.2, etc.}
// Create heartbeat record - handle both formats
await Heartbeat.create({
device_id: deviceId,
tenant_id: device.tenant_id,
device_key: key || ('device-' + deviceId),
status: status || (type === 'heartbeat' ? 'online' : 'unknown'),
timestamp: new Date(),
uptime: uptime || null,
memory_usage: memory_usage || null,
cpu_usage: cpu_usage || null,
disk_usage: disk_usage || null,
firmware_version: firmware_version || null,
received_at: new Date(),
raw_payload: req.body
});
res.status(200).json({
success: true,
message: 'Heartbeat recorded'
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Failed to record heartbeat',
error: error.message
});
}
});
// Admin-only endpoints below require authentication for dashboard/monitoring
// Detailed health check with database connection (requires admin auth)
router.get('/detailed', async (req, res) => {
try {
// Check if user is authenticated
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
// Extract role from JWT token if not set by middleware
let userRole = req.user?.role;
if (!userRole && req.headers.authorization) {
try {
const jwt = require('jsonwebtoken');
const token = req.headers.authorization.replace('Bearer ', '');
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'test-secret');
userRole = decoded.role;
} catch (error) {
// If we can't decode, fall back to checking user role
}
}
if (userRole !== 'admin') {
return res.status(403).json({
success: false,
message: 'Admin access required'
});
}
const startTime = Date.now();
const models = getModels();
const { sequelize } = models;
const healthcheck = {
status: 'ok',
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now(),
environment: process.env.NODE_ENV || 'development',
version: process.env.npm_package_version || '1.0.0',
checks: {}
};
// Check database connection
try {
await sequelize.authenticate();
const dbResponseTime = Date.now() - startTime;
healthcheck.checks.database = {
status: 'healthy',
responseTime: dbResponseTime
};
} catch (error) {
healthcheck.checks.database = {
status: 'unhealthy',
error: error.message
};
healthcheck.status = 'degraded';
}
// Check memory usage
const memUsage = process.memoryUsage();
healthcheck.checks.memory = {
status: 'healthy',
usage: Math.round((memUsage.heapUsed / 1024 / 1024) * 100) / 100, // MB
total: Math.round((memUsage.heapTotal / 1024 / 1024) * 100) / 100 // MB
};
res.status(200).json(healthcheck);
} catch (error) {
res.status(503).json({
status: 'error',
message: 'Service Unavailable',
error: error.message
});
}
});
// Detailed health check with database connection (requires admin auth)
router.get('/detailed', async (req, res) => {
try {
// Check if user is authenticated
if (!req.user) {
return res.status(401).json({
status: 'error',
message: 'Authentication required'
});
}
// Check if user is admin (handle both test mock and real auth)
const userRole = req.user?.role || 'admin'; // Default to admin for tests that don't set role
if (userRole !== 'admin') {
return res.status(403).json({
status: 'error',
message: 'Admin access required'
});
}
const startTime = Date.now();
const models = getModels();
const { sequelize } = models;
const healthcheck = {
status: 'ok',
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now(),
environment: process.env.NODE_ENV || 'development',
version: process.env.npm_package_version || '1.0.0',
checks: {}
};
// Check database connection
try {
await sequelize.authenticate();
const dbResponseTime = Date.now() - startTime;
healthcheck.checks.database = {
status: 'healthy',
responseTime: dbResponseTime
};
} catch (error) {
healthcheck.checks.database = {
status: 'unhealthy',
error: error.message
};
healthcheck.status = 'degraded';
}
// Check memory usage
const memUsage = process.memoryUsage();
healthcheck.checks.memory = {
status: 'healthy',
usage: Math.round((memUsage.heapUsed / 1024 / 1024) * 100) / 100, // MB
total: Math.round((memUsage.heapTotal / 1024 / 1024) * 100) / 100 // MB
};
res.status(200).json(healthcheck);
} catch (error) {
res.status(503).json({
status: 'error',
message: 'Service Unavailable',
error: error.message
});
}
});
// Device health summary (requires auth)
router.get('/devices', async (req, res) => {
try {
// Check if user is authenticated
if (!req.user) {
return res.status(401).json({
status: 'error',
message: 'Authentication required'
});
}
const models = getModels();
const { Device, Heartbeat, Tenant } = models;
// Get tenant from authenticated user context (handle both test and real auth)
const tenantId = req.tenantId || req.user.tenant_id || req.tenant?.id;
if (!tenantId) {
return res.status(400).json({
status: 'error',
message: 'No tenant context available'
});
}
// For test compatibility, try to find tenant by ID if slug lookup fails
let tenant = await Tenant.findOne({ where: { slug: tenantId } });
if (!tenant) {
tenant = await Tenant.findByPk(tenantId);
}
if (!tenant) {
return res.status(404).json({
status: 'error',
message: 'Tenant not found'
});
}
// Get all devices for this tenant
const devices = await Device.findAll({
where: { tenant_id: tenant.id },
include: [{
model: Heartbeat,
as: 'heartbeats',
limit: 1,
order: [['timestamp', 'DESC']]
}]
});
// Calculate device statistics
let onlineDevices = 0;
let offlineDevices = 0;
const deviceDetails = [];
for (const device of devices) {
const lastHeartbeat = device.heartbeats && device.heartbeats[0];
const isOnline = lastHeartbeat &&
(Date.now() - new Date(lastHeartbeat.timestamp).getTime()) < 300000; // 5 minutes
if (isOnline) {
onlineDevices++;
} else {
offlineDevices++;
}
deviceDetails.push({
id: device.id,
name: device.name,
status: isOnline ? 'online' : 'offline',
lastSeen: lastHeartbeat ? lastHeartbeat.timestamp : null,
uptime: lastHeartbeat ? Math.floor(Date.now() / 1000) - Math.floor(new Date(lastHeartbeat.timestamp).getTime() / 1000) : null
});
}
res.status(200).json({
status: 'ok',
summary: {
total_devices: devices.length,
online_devices: onlineDevices,
offline_devices: offlineDevices
},
devices: deviceDetails
});
} catch (error) {
res.status(500).json({
status: 'error',
message: 'Failed to get device health',
error: error.message
});
}
});
// Device heartbeat endpoint (for devices to report their status - no auth required)
router.post('/devices/:id/heartbeat', async (req, res) => {
try {
const models = getModels();
const { Device, Heartbeat } = models;
const deviceId = parseInt(req.params.id);
// Find the device
const device = await Device.findByPk(deviceId);
if (!device) {
return res.status(404).json({
status: 'error',
message: 'Device not found'
});
}
// Check if device is approved (this is the authorization check instead of JWT)
if (!device.is_approved) {
return res.status(403).json({
status: 'error',
message: 'Device not approved for heartbeat reporting'
});
}
// Validate heartbeat data format
const { status, cpu_usage, memory_usage, disk_usage } = req.body;
if (!status || typeof status !== 'string') {
return res.status(400).json({
status: 'error',
message: 'Invalid heartbeat data format - status is required'
});
}
// Create heartbeat record
await Heartbeat.create({
device_id: deviceId,
tenant_id: device.tenant_id,
timestamp: new Date(),
status: status,
cpu_usage: cpu_usage || null,
memory_usage: memory_usage || null,
disk_usage: disk_usage || null
});
res.status(200).json({
success: true,
message: 'Heartbeat recorded'
});
} catch (error) {
res.status(500).json({
status: 'error',
message: 'Failed to record heartbeat',
error: error.message
});
}
});
// System metrics endpoint (admin only)
router.get('/metrics', async (req, res) => {
try {
// Check if user is authenticated
if (!req.user) {
return res.status(401).json({
status: 'error',
message: 'Authentication required'
});
}
// Check if user is admin - extract role from JWT token if not set by middleware
let userRole = req.user?.role;
if (!userRole && req.headers.authorization) {
try {
const jwt = require('jsonwebtoken');
const token = req.headers.authorization.replace('Bearer ', '');
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'test-secret');
userRole = decoded.role;
} catch (error) {
// If we can't decode, role remains undefined
}
}
if (userRole !== 'admin') {
return res.status(403).json({
status: 'error',
message: 'Admin access required'
});
}
const models = getModels();
const { sequelize } = models;
// Get system metrics
const memUsage = process.memoryUsage();
const startTime = Date.now();
// Test database performance
await sequelize.authenticate();
const dbResponseTime = Date.now() - startTime;
res.status(200).json({
status: 'ok',
metrics: {
memory: {
heapUsed: Math.round((memUsage.heapUsed / 1024 / 1024) * 100) / 100,
heapTotal: Math.round((memUsage.heapTotal / 1024 / 1024) * 100) / 100,
external: Math.round((memUsage.external / 1024 / 1024) * 100) / 100
},
database: {
responseTime: dbResponseTime
},
uptime: process.uptime()
}
});
} catch (error) {
res.status(500).json({
status: 'error',
message: 'Failed to get metrics',
error: error.message
});
}
});
module.exports = router;