diff --git a/backend/app/routers/connections.py b/backend/app/routers/connections.py index 430c015..2f90b3b 100644 --- a/backend/app/routers/connections.py +++ b/backend/app/routers/connections.py @@ -9,6 +9,7 @@ Security: - Audit logging for all connection events """ import asyncio +import logging from datetime import date as date_type, datetime, timedelta, timezone from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Path, Query, Request @@ -49,6 +50,7 @@ from app.services.connection import ( from app.services.notification import create_notification router = APIRouter() +logger = logging.getLogger(__name__) # ── Helpers ────────────────────────────────────────────────────────── @@ -355,6 +357,24 @@ async def respond_to_request( current_user: User = Depends(get_current_user), ): """Accept or reject a connection request. Atomic via UPDATE...WHERE status='pending'.""" + try: + return await _respond_to_request_inner(body, request, background_tasks, request_id, db, current_user) + except HTTPException: + raise + except Exception: + # get_db middleware auto-rollbacks on unhandled exceptions + logger.exception("Unhandled error in respond_to_request (request_id=%s, user=%s)", request_id, current_user.id) + raise HTTPException(status_code=500, detail=f"Internal server error while processing connection response (request {request_id})") + + +async def _respond_to_request_inner( + body: RespondRequest, + request: Request, + background_tasks: BackgroundTasks, + request_id: int, + db: AsyncSession, + current_user: User, +) -> RespondAcceptResponse | RespondRejectResponse: now = datetime.now() # Atomic update — only succeeds if status is still 'pending' and receiver is current user diff --git a/frontend/src/components/notifications/NotificationToaster.tsx b/frontend/src/components/notifications/NotificationToaster.tsx index 4ebd234..53f6d0c 100644 --- a/frontend/src/components/notifications/NotificationToaster.tsx +++ b/frontend/src/components/notifications/NotificationToaster.tsx @@ -16,6 +16,9 @@ export default function NotificationToaster() { const prevUnreadRef = useRef(0); // Track in-flight request IDs so repeated clicks are blocked const respondingRef = useRef>(new Set()); + // Always call the latest respond — Sonner toasts capture closures at creation time + const respondRef = useRef(respond); + respondRef.current = respond; const handleConnectionRespond = useCallback( async (requestId: number, action: 'accept' | 'reject', toastId: string | number) => { @@ -30,7 +33,7 @@ export default function NotificationToaster() { ); try { - await respond({ requestId, action }); + await respondRef.current({ requestId, action }); toast.dismiss(loadingId); toast.success(action === 'accept' ? 'Connection accepted' : 'Request declined'); } catch (err) { @@ -40,7 +43,7 @@ export default function NotificationToaster() { respondingRef.current.delete(requestId); } }, - [respond], + [], ); // Track unread count changes to force-refetch the list