diff --git a/app/cabinet/routes/ticket_notifications.py b/app/cabinet/routes/ticket_notifications.py index 7b76685f..a8420d44 100644 --- a/app/cabinet/routes/ticket_notifications.py +++ b/app/cabinet/routes/ticket_notifications.py @@ -84,12 +84,22 @@ async def mark_notification_as_read( db: AsyncSession = Depends(get_cabinet_db), ): """Mark a notification as read.""" - success = await TicketNotificationCRUD.mark_as_read(db, notification_id) - if not success: + # Security: Verify notification belongs to current user and is not an admin notification + notification = await TicketNotificationCRUD.get_by_id(db, notification_id) + if not notification: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Notification not found", ) + + # Check ownership: notification must belong to user and not be an admin notification + if notification.user_id != user.id or notification.is_for_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You don't have permission to mark this notification as read", + ) + + await TicketNotificationCRUD.mark_as_read(db, notification_id) return {"success": True} @@ -154,12 +164,22 @@ async def mark_admin_notification_as_read( db: AsyncSession = Depends(get_cabinet_db), ): """Mark an admin notification as read.""" - success = await TicketNotificationCRUD.mark_as_read(db, notification_id) - if not success: + # Security: Verify notification exists and is an admin notification + notification = await TicketNotificationCRUD.get_by_id(db, notification_id) + if not notification: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Notification not found", ) + + # Check that this is actually an admin notification + if not notification.is_for_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="This is not an admin notification", + ) + + await TicketNotificationCRUD.mark_as_read(db, notification_id) return {"success": True} diff --git a/app/cabinet/routes/websocket.py b/app/cabinet/routes/websocket.py index 62f309fb..442ea9ba 100644 --- a/app/cabinet/routes/websocket.py +++ b/app/cabinet/routes/websocket.py @@ -63,7 +63,10 @@ class CabinetConnectionManager: async def send_to_user(self, user_id: int, message: dict) -> None: """Отправить сообщение конкретному пользователю.""" - connections = self._user_connections.get(user_id, set()) + # Snapshot connections under the lock to avoid mutation during iteration + async with self._lock: + connections = list(self._user_connections.get(user_id, set())) + if not connections: return @@ -78,19 +81,27 @@ class CabinetConnectionManager: disconnected.add(ws) # Cleanup disconnected - async with self._lock: - for ws in disconnected: - self._user_connections.get(user_id, set()).discard(ws) + if disconnected: + async with self._lock: + for ws in disconnected: + self._user_connections.get(user_id, set()).discard(ws) async def send_to_admins(self, message: dict) -> None: """Отправить сообщение всем админам.""" - if not self._admin_connections: - return + # Snapshot connections under the lock to avoid mutation during iteration + async with self._lock: + if not self._admin_connections: + return + # Create a snapshot: list of (user_id, list of websockets) + admin_snapshot = [ + (user_id, list(connections)) + for user_id, connections in self._admin_connections.items() + ] data = json.dumps(message, default=str, ensure_ascii=False) disconnected_by_user: Dict[int, Set[WebSocket]] = {} - for user_id, connections in self._admin_connections.items(): + for user_id, connections in admin_snapshot: for ws in connections: try: await ws.send_text(data) @@ -101,10 +112,11 @@ class CabinetConnectionManager: disconnected_by_user[user_id].add(ws) # Cleanup disconnected - async with self._lock: - for user_id, ws_set in disconnected_by_user.items(): - for ws in ws_set: - self._admin_connections.get(user_id, set()).discard(ws) + if disconnected_by_user: + async with self._lock: + for user_id, ws_set in disconnected_by_user.items(): + for ws in ws_set: + self._admin_connections.get(user_id, set()).discard(ws) # Глобальный менеджер подключений diff --git a/app/database/crud/ticket_notification.py b/app/database/crud/ticket_notification.py index fa7c1f01..3623ae68 100644 --- a/app/database/crud/ticket_notification.py +++ b/app/database/crud/ticket_notification.py @@ -17,6 +17,13 @@ logger = logging.getLogger(__name__) class TicketNotificationCRUD: """CRUD operations for ticket notifications in cabinet.""" + @staticmethod + async def get_by_id(db: AsyncSession, notification_id: int) -> Optional[TicketNotification]: + """Get notification by ID.""" + query = select(TicketNotification).where(TicketNotification.id == notification_id) + result = await db.execute(query) + return result.scalar_one_or_none() + @staticmethod async def create( db: AsyncSession,