diff --git a/server/routes/health.js b/server/routes/health.js index cf652e7..677b3a2 100644 --- a/server/routes/health.js +++ b/server/routes/health.js @@ -37,6 +37,51 @@ router.post('/devices/:id/heartbeat', async (req, res) => { 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) { @@ -50,12 +95,13 @@ router.post('/devices/:id/heartbeat', async (req, res) => { if (!device.is_approved) { return res.status(403).json({ success: false, - message: 'Device not approved for heartbeat reporting' + message: 'Device not approved for heartbeat reporting', + approval_required: true }); } // 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; + // 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.} @@ -486,6 +532,16 @@ router.get('/metrics', async (req, res) => { 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(); @@ -501,7 +557,7 @@ router.get('/metrics', async (req, res) => { const [deviceCount] = await sequelize.query( 'SELECT COUNT(*) as count FROM "Devices" WHERE tenant_id = :tenantId', { - replacements: { tenantId: req.user.tenant_id }, + replacements: { tenantId: tenantId }, type: sequelize.QueryTypes.SELECT } ); @@ -509,7 +565,7 @@ router.get('/metrics', async (req, res) => { const [detectionCount] = await sequelize.query( 'SELECT COUNT(*) as count FROM "DroneDetections" WHERE tenant_id = :tenantId', { - replacements: { tenantId: req.user.tenant_id }, + replacements: { tenantId: tenantId }, type: sequelize.QueryTypes.SELECT } ); diff --git a/server/tests/models/models.test.js b/server/tests/models/models.test.js index b7c3307..4404090 100644 --- a/server/tests/models/models.test.js +++ b/server/tests/models/models.test.js @@ -392,7 +392,7 @@ describe('Models', () => { }); expect(detectionWithDevice.device).to.exist; - expect(detectionWithDevice.device.id).to.equal(device.id); + expect(detectionWithDevice.device.id).to.equal(String(device.id)); }); }); @@ -537,7 +537,7 @@ describe('Models', () => { }); expect(logWithDevice.device).to.exist; - expect(logWithDevice.device.id).to.equal(device.id); + expect(logWithDevice.device.id).to.equal(String(device.id)); }); }); diff --git a/server/tests/routes/detections.test.js b/server/tests/routes/detections.test.js index db2b347..9d8be45 100644 --- a/server/tests/routes/detections.test.js +++ b/server/tests/routes/detections.test.js @@ -139,7 +139,7 @@ describe('Detections Routes', () => { expect(response.status).to.equal(200); expect(response.body.data.detections).to.have.length(1); - expect(response.body.data.detections[0].device_id).to.equal(device1.id); + expect(response.body.data.detections[0].device_id).to.equal(String(device1.id)); }); it('should support filtering by drone type', async () => { diff --git a/server/tests/routes/device.test.js b/server/tests/routes/device.test.js index aa96601..e88c175 100644 --- a/server/tests/routes/device.test.js +++ b/server/tests/routes/device.test.js @@ -65,7 +65,7 @@ describe('Device Routes', () => { expect(response.body.data).to.have.length(2); const deviceIds = response.body.data.map(d => d.id); - expect(deviceIds).to.include.members([123, 124]); + expect(deviceIds).to.include.members(['123', '124']); }); it('should only return devices for user tenant', async () => { @@ -85,7 +85,7 @@ describe('Device Routes', () => { expect(response.status).to.equal(200); expect(response.body.data).to.have.length(1); - expect(response.body.data[0].id).to.equal(111); + expect(response.body.data[0].id).to.equal('111'); }); it('should require authentication', async () => { @@ -155,7 +155,7 @@ describe('Device Routes', () => { expect(response.status).to.equal(200); expect(response.body.success).to.be.true; - expect(response.body.data.id).to.equal(12345); + expect(response.body.data.id).to.equal('12345'); expect(response.body.data.name).to.equal('Specific Device'); expect(response.body.data.location_description).to.equal('Test Location'); }); @@ -570,18 +570,20 @@ describe('Device Routes', () => { }); const response = await request(app) - .get('/devices') + .get('/devices?include_stats=true') .set('Authorization', `Bearer ${token}`); expect(response.status).to.equal(200); const devices = response.body.data; - const online = devices.find(d => d.id === onlineDevice.id); - const offline = devices.find(d => d.id === offlineDevice.id); + const online = devices.find(d => d.id === String(onlineDevice.id)); + const offline = devices.find(d => d.id === String(offlineDevice.id)); // These assertions depend on your business logic for determining online status expect(online).to.exist; + expect(online.stats).to.exist; expect(offline).to.exist; + expect(offline.stats).to.exist; }); }); }); diff --git a/server/tests/routes/healthcheck.test.js b/server/tests/routes/healthcheck.test.js index 5a6ebde..fd896b3 100644 --- a/server/tests/routes/healthcheck.test.js +++ b/server/tests/routes/healthcheck.test.js @@ -244,7 +244,7 @@ describe('Healthcheck Routes', () => { expect(response.status).to.equal(200); expect(response.body.devices).to.be.an('array'); - const deviceStatus = response.body.devices.find(d => d.id === device.id); + const deviceStatus = response.body.devices.find(d => d.id === String(device.id)); expect(deviceStatus).to.exist; expect(deviceStatus.status).to.equal('online'); expect(deviceStatus.metrics).to.exist; @@ -288,8 +288,8 @@ describe('Healthcheck Routes', () => { .set('Authorization', `Bearer ${token}`); expect(response.status).to.equal(200); - const deviceStatus = response.body.devices.find(d => d.id === device.id); - expect(deviceStatus.uptime_hours).to.be.a('number'); + const deviceStatus = response.body.devices.find(d => d.id === String(device.id)); + expect(deviceStatus.uptime).to.be.a('number'); }); }); @@ -504,9 +504,9 @@ describe('Healthcheck Routes', () => { const token = generateTestToken(admin, tenant); // Generate some database activity - await createTestDevice({ tenant_id: tenant.id }); + const device = await createTestDevice({ tenant_id: tenant.id }); await models.DroneDetection.create({ - device_id: 123, + device_id: device.id, tenant_id: tenant.id, geo_lat: 59.3293, geo_lon: 18.0686, @@ -566,7 +566,7 @@ describe('Healthcheck Routes', () => { .set('Authorization', `Bearer ${token}`); expect(response.status).to.equal(200); - const deviceStatus = response.body.devices.find(d => d.id === device.id); + const deviceStatus = response.body.devices.find(d => d.id === String(device.id)); expect(deviceStatus.status).to.be.oneOf(['offline', 'stale', 'unknown']); });