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); // Validate heartbeat payload const { type, key, status, cpu_usage, memory_usage, disk_usage, uptime, firmware_version, timestamp } = req.body; // Check for unexpected fields const allowedFields = ['type', 'key', 'status', 'cpu_usage', 'memory_usage', 'disk_usage', 'uptime', 'firmware_version', 'timestamp']; const receivedFields = Object.keys(req.body); const unexpectedFields = receivedFields.filter(field => !allowedFields.includes(field)); if (unexpectedFields.length > 0) { return res.status(400).json({ success: false, message: `Unexpected fields: ${unexpectedFields.join(', ')}` }); } // Validate status if provided if (status && !['online', 'offline', 'error', 'maintenance'].includes(status)) { return res.status(400).json({ success: false, message: 'Invalid status. Must be one of: online, offline, error, maintenance' }); } // Validate percentage fields const percentageFields = ['cpu_usage', 'memory_usage', 'disk_usage']; for (const field of percentageFields) { if (req.body[field] !== undefined) { const value = parseFloat(req.body[field]); if (isNaN(value) || value < 0 || value > 100) { return res.status(400).json({ success: false, message: `${field} must be a number between 0 and 100` }); } } } // Validate timestamp if provided if (timestamp && isNaN(Date.parse(timestamp))) { return res.status(400).json({ success: false, message: 'Invalid timestamp format' }); } // 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', approval_required: true }); } // Extract heartbeat data - handle both simple device heartbeats and detailed health reports // Variables already destructured above: type, key, status, cpu_usage, memory_usage, disk_usage, uptime, firmware_version // 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) { // Get latest heartbeat metrics for this device const latestHeartbeat = await Heartbeat.findOne({ where: { device_id: device.id }, order: [['timestamp', 'DESC']] }); const isOnline = latestHeartbeat && (Date.now() - new Date(latestHeartbeat.timestamp).getTime()) < 300000; // 5 minutes if (isOnline) { onlineDevices++; } else { offlineDevices++; } // Calculate uptime (time since first heartbeat) const firstHeartbeat = await Heartbeat.findOne({ where: { device_id: device.id }, order: [['timestamp', 'ASC']] }); let uptime = null; if (firstHeartbeat && latestHeartbeat) { uptime = Math.floor((new Date(latestHeartbeat.timestamp).getTime() - new Date(firstHeartbeat.timestamp).getTime()) / 1000); } deviceDetails.push({ id: device.id, name: device.name, status: isOnline ? 'online' : 'offline', lastSeen: latestHeartbeat ? latestHeartbeat.timestamp : null, uptime: uptime, metrics: latestHeartbeat ? { cpu_usage: latestHeartbeat.cpu_usage, memory_usage: latestHeartbeat.memory_usage, disk_usage: latestHeartbeat.disk_usage, uptime: latestHeartbeat.uptime } : 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', approval_required: true }); } // Validate heartbeat data format const { timestamp, 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 and must be a string' }); } // Validate status values const validStatuses = ['online', 'offline', 'error', 'maintenance']; if (!validStatuses.includes(status)) { return res.status(400).json({ status: 'error', message: `Invalid status value. Must be one of: ${validStatuses.join(', ')}` }); } // Validate timestamp if provided if (timestamp && isNaN(new Date(timestamp).getTime())) { return res.status(400).json({ status: 'error', message: 'Invalid timestamp format' }); } // Validate percentage fields const percentageFields = { cpu_usage, memory_usage, disk_usage }; for (const [field, value] of Object.entries(percentageFields)) { if (value !== undefined && value !== null && (typeof value !== 'number' || value < 0 || value > 100)) { return res.status(400).json({ status: 'error', message: `Invalid ${field} value. Must be a number between 0 and 100` }); } } // Check for unexpected fields const allowedFields = ['timestamp', 'status', 'cpu_usage', 'memory_usage', 'disk_usage', 'uptime']; const receivedFields = Object.keys(req.body); const invalidFields = receivedFields.filter(field => !allowedFields.includes(field)); if (invalidFields.length > 0) { return res.status(400).json({ status: 'error', message: `Invalid fields in heartbeat data: ${invalidFields.join(', ')}` }); } // 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 user tenant ID with proper fallback const tenantId = req.user?.tenant_id || req.tenantId || req.tenant?.id; if (!tenantId) { return res.status(400).json({ status: 'error', message: 'No tenant context available' }); } // Get system metrics const memUsage = process.memoryUsage(); const startTime = Date.now(); // Test database performance await sequelize.authenticate(); const dbResponseTime = Date.now() - startTime; // Get CPU usage const cpuUsage = process.cpuUsage(); // Get database statistics const [deviceCount] = await sequelize.query( 'SELECT COUNT(*) as count FROM "Devices" WHERE tenant_id = :tenantId', { replacements: { tenantId: tenantId }, type: sequelize.QueryTypes.SELECT } ); const [detectionCount] = await sequelize.query( 'SELECT COUNT(*) as count FROM "DroneDetections" WHERE tenant_id = :tenantId', { replacements: { tenantId: tenantId }, type: sequelize.QueryTypes.SELECT } ); res.status(200).json({ status: 'ok', system: { 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 }, cpu: { user: cpuUsage.user, system: cpuUsage.system }, uptime: process.uptime() }, database: { responseTime: dbResponseTime, connection_pool: { active: sequelize.connectionManager.pool._count || 0, idle: sequelize.connectionManager.pool._idle?.length || 0, total: sequelize.connectionManager.pool.options.max || 10 } }, statistics: { total_devices: parseInt(deviceCount.count), total_detections: parseInt(detectionCount.count) } }); } catch (error) { res.status(500).json({ status: 'error', message: 'Failed to get metrics', error: error.message }); } }); module.exports = router;