From 62a13a9f4540feb2d8d0fe1a1b2602e7edeaf698 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Sat, 13 Dec 2025 14:29:08 +0500 Subject: [PATCH] public share UI fix --- app/routers/map_share.py | 2 +- app/services/map_share_service.py | 6 +- app/websocket/connection_manager.py | 32 ++++++ public/src/hooks/useMapWebSocket.ts | 7 +- public/src/pages/SharedMap.tsx | 163 ++++++++++++++++++---------- 5 files changed, 149 insertions(+), 61 deletions(-) diff --git a/app/routers/map_share.py b/app/routers/map_share.py index d3240f1..e28fb97 100644 --- a/app/routers/map_share.py +++ b/app/routers/map_share.py @@ -98,5 +98,5 @@ async def delete_map_share_link( current_user: User = Depends(get_current_user) ): """Delete a share link.""" - map_share_service.delete_share_link(db, map_id, link_id, current_user) + await map_share_service.delete_share_link(db, map_id, link_id, current_user) return None diff --git a/app/services/map_share_service.py b/app/services/map_share_service.py index 4806128..25a9ead 100644 --- a/app/services/map_share_service.py +++ b/app/services/map_share_service.py @@ -223,7 +223,7 @@ def get_share_links(db: Session, map_id: UUID, current_user: User) -> List[MapSh return links -def delete_share_link( +async def delete_share_link( db: Session, map_id: UUID, link_id: UUID, @@ -252,6 +252,10 @@ def delete_share_link( db.delete(link) db.commit() + # Disconnect all guest users from this map + from app.websocket.connection_manager import manager + await manager.disconnect_guests(map_id) + def get_map_by_share_token(db: Session, token: str) -> tuple[Map, SharePermission]: """Get map by share token (for guest access).""" diff --git a/app/websocket/connection_manager.py b/app/websocket/connection_manager.py index e99052d..cc926f2 100644 --- a/app/websocket/connection_manager.py +++ b/app/websocket/connection_manager.py @@ -82,6 +82,38 @@ class ConnectionManager: if not self.active_connections[map_key]: del self.active_connections[map_key] + async def disconnect_guests(self, map_id: UUID): + """Disconnect all guest connections (no user_id) on a specific map.""" + map_key = str(map_id) + + if map_key not in self.active_connections: + return + + # Find all guest websockets (uid is None) + connections_to_close = [ + websocket for websocket, uid in self.active_connections[map_key] + if uid is None + ] + + # Close each connection + for websocket in connections_to_close: + try: + await websocket.close(code=1008, reason="Share link revoked") + logger.info(f"Closed WebSocket for guest on map {map_id}") + except Exception as e: + logger.error(f"Error closing WebSocket for guest: {e}") + + # Remove from active connections + self.active_connections[map_key] = { + conn for conn in self.active_connections[map_key] + if conn[0] != websocket + } + self.websocket_to_map.pop(websocket, None) + + # Clean up empty map entry + if not self.active_connections[map_key]: + del self.active_connections[map_key] + async def broadcast_to_map(self, map_id: UUID, message: dict): """Broadcast a message to all clients connected to a specific map.""" map_key = str(map_id) diff --git a/public/src/hooks/useMapWebSocket.ts b/public/src/hooks/useMapWebSocket.ts index e16b7c9..bf0fabd 100644 --- a/public/src/hooks/useMapWebSocket.ts +++ b/public/src/hooks/useMapWebSocket.ts @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { authService } from '../services/authService'; interface WebSocketMessage { - type: 'connected' | 'item_created' | 'item_updated' | 'item_deleted'; + type: 'connected' | 'item_created' | 'item_updated' | 'item_deleted' | 'access_revoked'; data: any; } @@ -104,6 +104,11 @@ export function useMapWebSocket({ onItemDeletedRef.current?.(message.data.id); break; + case 'access_revoked': + console.log('Access revoked, closing connection'); + ws.close(); + break; + default: console.log('Unknown message type:', message.type); } diff --git a/public/src/pages/SharedMap.tsx b/public/src/pages/SharedMap.tsx index 054e658..fa1a0aa 100644 --- a/public/src/pages/SharedMap.tsx +++ b/public/src/pages/SharedMap.tsx @@ -1,13 +1,15 @@ import { useState, useEffect } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import { MapContainer, TileLayer, useMap } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; import { LayerSwitcher } from '../components/map/LayerSwitcher'; import { DrawingHandler } from '../components/map/DrawingHandler'; import { MapItemsLayer } from '../components/map/MapItemsLayer'; import { Toolbar } from '../components/map/Toolbar'; +import { MapView } from '../components/map/MapView'; import { useMapWebSocket } from '../hooks/useMapWebSocket'; import { apiClient } from '../services/api'; +import { useUIStore } from '../stores/uiStore'; type MapLayer = 'osm' | 'google' | 'esri'; @@ -47,11 +49,14 @@ function MapController() { export function SharedMap() { const { token } = useParams<{ token: string }>(); + const navigate = useNavigate(); + const { darkMode, toggleDarkMode } = useUIStore(); const [activeLayer, setActiveLayer] = useState('osm'); const [refreshTrigger, setRefreshTrigger] = useState(0); const [mapData, setMapData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); // Load map data using share token useEffect(() => { @@ -59,7 +64,6 @@ export function SharedMap() { if (!token) return; try { - // Make an unauthenticated request with share token const response = await apiClient.get(`/api/maps/shared/${token}`); setMapData(response.data); setLoading(false); @@ -99,9 +103,9 @@ export function SharedMap() { if (loading) { return ( -
+
-
Loading shared map...
+
Loading shared map...
); @@ -109,10 +113,10 @@ export function SharedMap() { if (error) { return ( -
+
-
Error
-
{error}
+
Error
+
{error}
); @@ -120,80 +124,123 @@ export function SharedMap() { if (!mapData) { return ( -
+
-
Map not found
+
Map not found
); } - const layer = MAP_LAYERS[activeLayer]; const isReadOnly = permission === 'read'; return ( -
+
{/* Header */} -
-
-
-

{mapData.name}

-

- {isReadOnly ? 'View-only access' : 'Edit access'} • Shared map -

+
+
+
+ {/* Mobile hamburger button */} + +
+

{mapData.name}

+

+ {isReadOnly ? 'View-only' : 'Edit access'} • Shared map +

+
-
+ +
{isConnected && ( -
-
- Live +
+
+ Live
)} + +
-
+
- {/* Map */} -
- {/* Toolbar */} - {!isReadOnly && ( -
- {}} /> -
+
+ {/* Mobile overlay */} + {isSidebarOpen && ( +
setIsSidebarOpen(false)} + /> )} - {/* Layer switcher */} -
- + {/* Left sidebar with toolbar */} +
+
+ +
+ + {/* Map Style section */} +
+

Map Style

+ +
- - - + {/* Main map view */} +
+ + + - {/* Drawing handler for edit access */} - {!isReadOnly && ( - - )} + {/* Drawing handler for edit access */} + {!isReadOnly && ( + + )} - {/* Render existing map items */} - - + {/* Render existing map items */} + + +
);