diff --git a/client/src/contexts/SocketContext.jsx b/client/src/contexts/SocketContext.jsx index 60aaafb..c19a5b9 100644 --- a/client/src/contexts/SocketContext.jsx +++ b/client/src/contexts/SocketContext.jsx @@ -1,6 +1,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; import { io } from 'socket.io-client'; import { useAuth } from './AuthContext'; +import { useMultiTenantAuth } from './MultiTenantAuthContext'; import toast from 'react-hot-toast'; import APP_CONFIG from '../config/app'; @@ -17,6 +18,7 @@ export const SocketProvider = ({ children }) => { localStorage.getItem('notificationsEnabled') !== 'false' // Default to enabled ); const { isAuthenticated } = useAuth(); + const { state: { tenant } } = useMultiTenantAuth(); // Mobile notification management const [notificationCooldown, setNotificationCooldown] = useState(new Map()); @@ -135,6 +137,12 @@ export const SocketProvider = ({ children }) => { // Join dashboard room for general updates newSocket.emit('join_dashboard'); + // 🔒 SECURITY: Join tenant-specific room for isolated updates + if (tenant) { + newSocket.emit('join_tenant_room', tenant); + console.log(`🔒 Joined tenant room: ${tenant}`); + } + toast.success('Connected to real-time updates'); }); @@ -224,7 +232,7 @@ export const SocketProvider = ({ children }) => { setConnected(false); } } - }, [isAuthenticated]); + }, [isAuthenticated, tenant]); const joinDeviceRoom = (deviceId) => { if (socket) { diff --git a/server/routes/detectors.js b/server/routes/detectors.js index b411444..e79ad5c 100644 --- a/server/routes/detectors.js +++ b/server/routes/detectors.js @@ -378,7 +378,7 @@ async function handleDetection(req, res) { // Emit real-time update via Socket.IO with movement analysis (from original) // Skip real-time updates for debug detections (drone_type 0) if (!isDebugDetection) { - req.io.emit('drone_detection', { + const detectionPayload = { id: detection.id, device_id: detection.device_id, drone_id: detection.drone_id, @@ -397,7 +397,17 @@ async function handleDetection(req, res) { geo_lat: device.geo_lat, geo_lon: device.geo_lon } - }); + }; + + // 🔒 SECURITY: Emit only to the tenant's room to prevent cross-tenant data leakage + if (device.tenant_id) { + req.io.to(`tenant_${device.tenant_id}`).emit('drone_detection', detectionPayload); + console.log(`🔒 Detection emitted to tenant room: tenant_${device.tenant_id}`); + } else { + // Fallback for devices without tenant_id (legacy support) + console.warn(`⚠️ Device ${device.id} has no tenant_id - using global broadcast (security risk)`); + req.io.emit('drone_detection', detectionPayload); + } // Process alerts asynchronously (from original) alertService.processAlert(detection, req.io).catch(error => { diff --git a/server/services/socketService.js b/server/services/socketService.js index 2869a76..e6dd8fe 100644 --- a/server/services/socketService.js +++ b/server/services/socketService.js @@ -14,6 +14,12 @@ function initializeSocketHandlers(io) { console.log(`Client ${socket.id} (IP: ${clientIP}) joined device room: device_${deviceId}`); }); + // 🔒 SECURITY: Join tenant-specific room for multi-tenant isolation + socket.on('join_tenant_room', (tenantId) => { + socket.join(`tenant_${tenantId}`); + console.log(`Client ${socket.id} (IP: ${clientIP}) joined tenant room: tenant_${tenantId}`); + }); + // Join dashboard room for general updates socket.on('join_dashboard', () => { socket.join('dashboard'); @@ -26,6 +32,11 @@ function initializeSocketHandlers(io) { console.log(`Client ${socket.id} (IP: ${clientIP}) left device room: device_${deviceId}`); }); + socket.on('leave_tenant_room', (tenantId) => { + socket.leave(`tenant_${tenantId}`); + console.log(`Client ${socket.id} (IP: ${clientIP}) left tenant room: tenant_${tenantId}`); + }); + socket.on('leave_dashboard', () => { socket.leave('dashboard'); console.log(`Client ${socket.id} (IP: ${clientIP}) left dashboard room`); @@ -49,6 +60,10 @@ function initializeSocketHandlers(io) { io.to(`device_${deviceId}`).emit(event, data); }; + io.emitToTenant = function(tenantId, event, data) { + io.to(`tenant_${tenantId}`).emit(event, data); + }; + io.emitToDashboard = function(event, data) { io.to('dashboard').emit(event, data); }; diff --git a/test_enhanced_detection.py b/test_enhanced_detection.py index 07b6ce6..1e23038 100644 --- a/test_enhanced_detection.py +++ b/test_enhanced_detection.py @@ -144,13 +144,14 @@ def send_detection(drone_type=2, drone_id=None, geo_lat=0, geo_lon=0, rssi=-45, print(f"❌ Connection error: {e}") return False -def simulate_drone_approach(drone_type=2, drone_id=None, steps=10): +def simulate_drone_approach(drone_type=2, drone_id=None, device_id=None, steps=10): """ Simulate a drone approaching from distance Args: drone_type: Type of drone to simulate drone_id: Specific drone ID (auto-generated if None) + device_id: Device ID to use (uses global DEVICE_ID if None) steps: Number of detection steps """ @@ -189,6 +190,7 @@ def simulate_drone_approach(drone_type=2, drone_id=None, steps=10): geo_lon=lon, rssi=rssi, freq=2400, + device_id=device_id, show_response=True )