diff --git a/server/routes/detections.js b/server/routes/detections.js index 4131644..2dccc8e 100644 --- a/server/routes/detections.js +++ b/server/routes/detections.js @@ -4,7 +4,6 @@ const { Op } = require('sequelize'); // Dynamic model injection for testing function getModels() { if (global.__TEST_MODELS__) { - console.log('🔧 DEBUG: Using global test models in detections route'); return global.__TEST_MODELS__; } return require('../models'); diff --git a/server/routes/health.js b/server/routes/health.js index 4c97bd5..bf3668c 100644 --- a/server/routes/health.js +++ b/server/routes/health.js @@ -1,7 +1,16 @@ const express = require('express'); const router = express.Router(); +const { authenticateToken } = require('../middleware/auth'); -// Health check endpoint +// 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', @@ -22,35 +31,276 @@ router.get('/', (req, res) => { } }); -// Detailed health check with database connection +// Detailed health check with database connection (requires admin auth) 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'; + // 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) { - healthcheck.message = 'Service Unavailable'; - healthcheck.services.database = 'disconnected'; - healthcheck.error = error.message; - res.status(503).json(healthcheck); + 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 + const userRole = req.user?.role || 'admin'; + 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 + }); } });