see the real client IP behind ArvanCloud, and mark all notifications read in one call

Production is ArvanCloud -> nginx -> gunicorn, but NUM_PROXIES was 1, so DRF
took ArvanCloud's edge IP as the client: every guest and every OTP request
behind one edge shared a single throttle bucket (20 guest sessions an hour for
everyone). Default is now 2, and /api/health reports clientIp so the proxy
count can be checked from outside.

PATCH /api/notifications/read-all replaces the frontend's one-PATCH-per-item
loop, which spent the shared "read" budget 75 requests at a time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 05:03:27 +03:30
parent 147af3ac10
commit c874c186b1
8 changed files with 83 additions and 4 deletions

View File

@@ -6,7 +6,9 @@ from rest_framework.throttling import ScopedRateThrottle
from rest_framework.views import APIView
from .models import Notification
from .serializers import MarkReadResponseSerializer, NotificationListResponseSerializer
from .serializers import (
MarkAllReadResponseSerializer, MarkReadResponseSerializer, NotificationListResponseSerializer,
)
# سقفِ تعدادِ اعلانِ برگشتی؛ جلوی رشدِ نامحدودِ پاسخ برای کاربرِ قدیمی رو
# می‌گیره. صفحه‌بندیِ واقعی (cursor) وقتی فرانت infinite-scroll اضافه کنه.
@@ -60,3 +62,25 @@ class NotificationMarkReadView(APIView):
notification.save(update_fields=['is_read'])
return Response(MarkReadResponseSerializer({'success': True}).data)
class NotificationMarkAllReadView(APIView):
"""
PATCH /api/notifications/read-all — همه‌ی اعلان‌های کاربر خوانده‌شده.
دکمه‌ی «همه خوانده شد» پیش از این برای هر اعلان یک درخواستِ جدا می‌زد؛
حسابی با ۷۵ اعلان یعنی ۷۵ درخواستِ هم‌زمان که سهمِ throttleِ read را
(همان که /home و بقیه مصرف می‌کنند) یک‌جا می‌خورد.
"""
permission_classes = [IsAuthenticated]
throttle_scope = 'read'
throttle_classes = [GuestAwareReadThrottle]
@extend_schema(request=None, responses=MarkAllReadResponseSerializer,
tags=['Notifications (notifications)'])
def patch(self, request):
updated = Notification.objects.filter(
user_id=request.user.id, is_read=False,
).update(is_read=True)
return Response(MarkAllReadResponseSerializer({'success': True, 'updated': updated}).data)