const express = require('express'); const { Op } = require('sequelize'); // Dynamic model injection for testing function getModels() { if (global.__TEST_MODELS__) { return global.__TEST_MODELS__; } return require('../models'); } const { authenticateToken } = require('../middleware/auth'); const { getDroneTypeInfo } = require('../utils/droneTypes'); const MultiTenantAuth = require('../middleware/multi-tenant-auth'); const router = express.Router(); // Initialize multi-tenant auth const multiAuth = new MultiTenantAuth(); /** * GET /api/detections * Get all drone detections with filtering and pagination (tenant-filtered) */ router.get('/', authenticateToken, async (req, res) => { try { const models = getModels(); const { DroneDetection, Device, Tenant } = models; // Get tenant from authenticated user context const tenantId = req.tenantId; console.log(`🔍 Detections query - tenantId from req: ${tenantId}`); console.log(`🔍 Detections query - req.user.tenant_id: ${req.user?.tenant_id}`); if (!tenantId) { return res.status(400).json({ success: false, message: 'No tenant context available' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); console.log(`🔍 Detections query - found tenant:`, tenant ? { id: tenant.id, slug: tenant.slug, name: tenant.name } : 'null'); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } if (!tenant.is_active) { return res.status(403).json({ success: false, message: 'Tenant is inactive' }); } const { device_id, drone_id, drone_type, start_date, end_date, page = 1, limit = 50, sort = 'server_timestamp', order = 'desc' } = req.query; // Validate and sanitize pagination parameters const parsedPage = parseInt(page); const parsedLimit = parseInt(limit); // Use defaults for invalid values const validatedPage = (parsedPage > 0) ? parsedPage : 1; const validatedLimit = (parsedLimit > 0 && parsedLimit <= 100) ? parsedLimit : 50; // Build where clause for filtering const whereClause = {}; // Only exclude drone type 0 if no specific drone_type is requested if (!drone_type) { whereClause.drone_type = { [Op.ne]: 0 }; } if (device_id) { whereClause.device_id = device_id; } if (drone_id) { whereClause.drone_id = drone_id; } if (drone_type) { whereClause.drone_type = parseInt(drone_type); } if (start_date) { whereClause.server_timestamp = { ...whereClause.server_timestamp, [Op.gte]: new Date(start_date) }; } if (end_date) { whereClause.server_timestamp = { ...whereClause.server_timestamp, [Op.lte]: new Date(end_date) }; } // Calculate offset for pagination const offset = (validatedPage - 1) * validatedLimit; // Query detections with device information (filtered by tenant) console.log(`🔍 Detections query - filtering devices by tenant_id: ${tenant.id}`); // Debug: Show all devices and their tenant assignments const allDevices = await Device.findAll({ attributes: ['id', 'name', 'tenant_id'] }); console.log(`🔍 All devices in database:`, allDevices.map(d => ({ id: d.id, name: d.name, tenant_id: d.tenant_id }))); const detections = await DroneDetection.findAll({ where: whereClause, include: [{ model: Device, as: 'device', where: { tenant_id: tenant.id }, // Filter by tenant attributes: ['id', 'name', 'geo_lat', 'geo_lon', 'location_description', 'is_approved'] }], order: [[sort, order.toUpperCase()]], limit: validatedLimit, offset: offset }); console.log(`🔍 Detections query - found ${detections.length} detections for tenant ${tenant.slug}`); console.log(`🔍 Detection data being returned:`, JSON.stringify(detections.map(d => ({ id: d.id, device_id: d.device_id, drone_id: d.drone_id, drone_type: d.drone_type, server_timestamp: d.server_timestamp, device: d.device ? { id: d.device.id, name: d.device.name } : null })), null, 2)); // Get total count for pagination (also filtered by tenant) const totalCount = await DroneDetection.count({ where: whereClause, include: [{ model: Device, as: 'device', where: { tenant_id: tenant.id } }] }); // Calculate pagination info const totalPages = Math.ceil(totalCount / validatedLimit); const hasNextPage = validatedPage < totalPages; const hasPrevPage = validatedPage > 1; // Enhance detections with drone type information const enhancedDetections = detections.map(detection => { const droneTypeInfo = getDroneTypeInfo(detection.drone_type); return { ...detection.toJSON(), drone_type_info: droneTypeInfo }; }); res.json({ success: true, data: { detections: enhancedDetections, pagination: { currentPage: validatedPage, totalPages, totalCount, limit: validatedLimit, hasNextPage, hasPrevPage } } }); } catch (error) { console.error('Error fetching detections:', error); res.status(500).json({ error: 'Failed to fetch detections', details: error.message }); } }); /** * GET /api/detections/debug * Get all detections including drone type 0 (None) for debugging purposes * Admin access only */ router.get('/debug', authenticateToken, async (req, res) => { try { const models = getModels(); const { DroneDetection, Device } = models; // Check if user is admin if (req.user.role !== 'admin') { return res.status(403).json({ success: false, error: 'Access denied', message: 'Admin access required for debug data' }); } const { device_id, drone_id, drone_type, start_date, end_date, page = 1, limit = 100, sort = 'server_timestamp', order = 'desc' } = req.query; // Build where clause for debugging (includes all drone types) const whereClause = {}; if (device_id) { whereClause.device_id = device_id; } if (drone_id) { whereClause.drone_id = drone_id; } if (drone_type !== undefined) { whereClause.drone_type = parseInt(drone_type); } if (start_date) { whereClause.server_timestamp = { ...whereClause.server_timestamp, [Op.gte]: new Date(start_date) }; } if (end_date) { whereClause.server_timestamp = { ...whereClause.server_timestamp, [Op.lte]: new Date(end_date) }; } // Calculate offset for pagination const offset = (parseInt(page) - 1) * parseInt(limit); // Query ALL detections including type 0 for debugging (with tenant filtering) const detections = await DroneDetection.findAndCountAll({ where: whereClause, include: [{ model: Device, as: 'device', where: { tenant_id: req.user.tenant_id // 🔒 SECURITY: Filter by user's tenant }, attributes: ['id', 'name', 'location_description', 'geo_lat', 'geo_lon', 'tenant_id'] }], limit: parseInt(limit), offset: offset, order: [[sort, order.toUpperCase()]] }); // Add drone type information to each detection const enhancedDetections = detections.rows.map(detection => { const droneTypeInfo = getDroneTypeInfo(detection.drone_type); return { ...detection.toJSON(), drone_type_info: droneTypeInfo, is_debug_data: detection.drone_type === 0 }; }); console.log(`🔒 Admin debug: Retrieved ${detections.count} detections for tenant ${req.user.tenant_id}`); res.json({ success: true, data: enhancedDetections, pagination: { total: detections.count, page: parseInt(page), limit: parseInt(limit), pages: Math.ceil(detections.count / parseInt(limit)) }, debug_info: { includes_none_detections: true, total_none_detections: await DroneDetection.count({ where: { drone_type: 0 } }), message: "Debug data includes drone type 0 (None) detections" } }); } catch (error) { console.error('Error fetching debug detections:', error); res.status(500).json({ success: false, error: 'Failed to fetch debug detections', details: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); /** * GET /api/detections/:id * Get a specific detection by ID (tenant-filtered) */ router.get('/:id', authenticateToken, async (req, res) => { try { const models = getModels(); const { DroneDetection, Device, Tenant } = models; // Get tenant from authenticated user context const tenantId = req.tenantId; if (!tenantId) { return res.status(400).json({ success: false, message: 'No tenant context available' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } if (!tenant.is_active) { return res.status(403).json({ success: false, message: 'Tenant is inactive' }); } const { id } = req.params; const detection = await DroneDetection.findByPk(id, { include: [{ model: Device, as: 'device', where: { tenant_id: tenant.id }, // Filter by tenant attributes: ['id', 'name', 'geo_lat', 'geo_lon', 'location_description', 'is_approved'] }] }); if (!detection) { return res.status(404).json({ success: false, message: 'Detection not found in your tenant' }); } // Enhance detection with drone type information const droneTypeInfo = getDroneTypeInfo(detection.drone_type); const enhancedDetection = { ...detection.toJSON(), drone_type_info: droneTypeInfo }; res.json({ success: true, data: enhancedDetection }); } catch (error) { console.error('Error fetching detection:', error); res.status(500).json({ success: false, message: 'Failed to fetch detection', details: error.message }); } }); /** * DELETE /api/detections/:id * Delete a specific detection (admin only) */ router.delete('/:id', authenticateToken, async (req, res) => { try { const models = getModels(); const { DroneDetection, Device, Tenant } = models; // Check if user is admin if (req.user.role !== 'admin') { return res.status(403).json({ success: false, message: 'Admin access required' }); } // Get tenant from authenticated user context const tenantId = req.tenantId; if (!tenantId) { return res.status(400).json({ success: false, message: 'No tenant context available' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } const { id } = req.params; // Find detection with tenant filtering const detection = await DroneDetection.findOne({ where: { id }, include: [{ model: Device, as: 'device', where: { tenant_id: tenant.id }, // Ensure detection belongs to user's tenant attributes: ['id', 'tenant_id'] }] }); if (!detection) { return res.status(404).json({ success: false, message: 'Detection not found or not accessible' }); } await detection.destroy(); res.json({ success: true, message: 'Detection deleted successfully' }); } catch (error) { console.error('Error deleting detection:', error); res.status(500).json({ success: false, message: 'Failed to delete detection', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); module.exports = router;