mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
feat(notifications): enhance notification security and ownership checks
- Added ownership verification for user notifications to ensure only the rightful owner can mark them as read. - Implemented checks to confirm that admin notifications are correctly identified before allowing them to be marked as read. - Introduced a new method to retrieve notifications by ID in the TicketNotificationCRUD for improved data handling.
This commit is contained in:
@@ -84,12 +84,22 @@ async def mark_notification_as_read(
|
|||||||
db: AsyncSession = Depends(get_cabinet_db),
|
db: AsyncSession = Depends(get_cabinet_db),
|
||||||
):
|
):
|
||||||
"""Mark a notification as read."""
|
"""Mark a notification as read."""
|
||||||
success = await TicketNotificationCRUD.mark_as_read(db, notification_id)
|
# Security: Verify notification belongs to current user and is not an admin notification
|
||||||
if not success:
|
notification = await TicketNotificationCRUD.get_by_id(db, notification_id)
|
||||||
|
if not notification:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Notification 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}
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
@@ -154,12 +164,22 @@ async def mark_admin_notification_as_read(
|
|||||||
db: AsyncSession = Depends(get_cabinet_db),
|
db: AsyncSession = Depends(get_cabinet_db),
|
||||||
):
|
):
|
||||||
"""Mark an admin notification as read."""
|
"""Mark an admin notification as read."""
|
||||||
success = await TicketNotificationCRUD.mark_as_read(db, notification_id)
|
# Security: Verify notification exists and is an admin notification
|
||||||
if not success:
|
notification = await TicketNotificationCRUD.get_by_id(db, notification_id)
|
||||||
|
if not notification:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Notification 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}
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ class CabinetConnectionManager:
|
|||||||
|
|
||||||
async def send_to_user(self, user_id: int, message: dict) -> None:
|
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:
|
if not connections:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -78,19 +81,27 @@ class CabinetConnectionManager:
|
|||||||
disconnected.add(ws)
|
disconnected.add(ws)
|
||||||
|
|
||||||
# Cleanup disconnected
|
# Cleanup disconnected
|
||||||
|
if disconnected:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
for ws in disconnected:
|
for ws in disconnected:
|
||||||
self._user_connections.get(user_id, set()).discard(ws)
|
self._user_connections.get(user_id, set()).discard(ws)
|
||||||
|
|
||||||
async def send_to_admins(self, message: dict) -> None:
|
async def send_to_admins(self, message: dict) -> None:
|
||||||
"""Отправить сообщение всем админам."""
|
"""Отправить сообщение всем админам."""
|
||||||
|
# Snapshot connections under the lock to avoid mutation during iteration
|
||||||
|
async with self._lock:
|
||||||
if not self._admin_connections:
|
if not self._admin_connections:
|
||||||
return
|
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)
|
data = json.dumps(message, default=str, ensure_ascii=False)
|
||||||
disconnected_by_user: Dict[int, Set[WebSocket]] = {}
|
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:
|
for ws in connections:
|
||||||
try:
|
try:
|
||||||
await ws.send_text(data)
|
await ws.send_text(data)
|
||||||
@@ -101,6 +112,7 @@ class CabinetConnectionManager:
|
|||||||
disconnected_by_user[user_id].add(ws)
|
disconnected_by_user[user_id].add(ws)
|
||||||
|
|
||||||
# Cleanup disconnected
|
# Cleanup disconnected
|
||||||
|
if disconnected_by_user:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
for user_id, ws_set in disconnected_by_user.items():
|
for user_id, ws_set in disconnected_by_user.items():
|
||||||
for ws in ws_set:
|
for ws in ws_set:
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ logger = logging.getLogger(__name__)
|
|||||||
class TicketNotificationCRUD:
|
class TicketNotificationCRUD:
|
||||||
"""CRUD operations for ticket notifications in cabinet."""
|
"""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
|
@staticmethod
|
||||||
async def create(
|
async def create(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
|
|||||||
Reference in New Issue
Block a user