442 lines
12 KiB
JavaScript
442 lines
12 KiB
JavaScript
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;
|