375 lines
10 KiB
JavaScript
375 lines
10 KiB
JavaScript
const express = require('express');
|
|
const { Op } = require('sequelize');
|
|
const models = global.__TEST_MODELS__ || require('../models');
|
|
const { DroneDetection, Device, Tenant } = 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 {
|
|
// Get tenant from authenticated user context
|
|
const tenantId = req.tenantId;
|
|
console.log('🔍 Detections - Tenant ID from request:', tenantId);
|
|
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 - Tenant lookup result:', tenant ? `Found: ${tenant.slug}` : 'Not found');
|
|
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,
|
|
start_date,
|
|
end_date,
|
|
page = 1,
|
|
limit = 50,
|
|
sort = 'server_timestamp',
|
|
order = 'desc'
|
|
} = req.query;
|
|
|
|
// Build where clause for filtering
|
|
const whereClause = {
|
|
// Exclude drone type 0 (None) from normal detection queries
|
|
drone_type: { [Op.ne]: 0 }
|
|
};
|
|
|
|
if (device_id) {
|
|
whereClause.device_id = device_id;
|
|
}
|
|
|
|
if (drone_id) {
|
|
whereClause.drone_id = drone_id;
|
|
}
|
|
|
|
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 detections with device information (filtered by tenant)
|
|
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: parseInt(limit),
|
|
offset: offset
|
|
});
|
|
|
|
// 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 / parseInt(limit));
|
|
const hasNextPage = parseInt(page) < totalPages;
|
|
const hasPrevPage = parseInt(page) > 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({
|
|
detections: enhancedDetections,
|
|
pagination: {
|
|
currentPage: parseInt(page),
|
|
totalPages,
|
|
totalCount,
|
|
limit: parseInt(limit),
|
|
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 {
|
|
// 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',
|
|
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
|
|
};
|
|
});
|
|
|
|
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 {
|
|
// 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({ error: '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(enhancedDetection);
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching detection:', error);
|
|
res.status(500).json({
|
|
error: '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 {
|
|
// 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;
|