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

@@ -146,3 +146,29 @@ class NotificationPayloadShapeTests(TestCase):
r = self.client.get('/api/notifications', HTTP_AUTHORIZATION=f'Bearer {self.token}')
self.assertEqual(r.data['items'][0]['body'], '')
class MarkAllReadTests(TestCase):
"""دکمه‌ی «همه خوانده شد» با یک درخواست، نه یکی برای هر اعلان."""
def setUp(self):
from django.core.cache import cache
cache.clear()
from accounts.authentication import issue_access_token
from accounts.models import User
self.me = User.objects.create(phone='09120000021')
self.other = User.objects.create(phone='09120000022')
self.auth = {'HTTP_AUTHORIZATION': f'Bearer {issue_access_token(self.me.id)}'}
for user in (self.me, self.me, self.other):
Notification.objects.create(user_id=user.id, type='xp_earned', title='x')
def test_marks_only_my_unread_notifications(self):
response = self.client.patch('/api/notifications/read-all', **self.auth)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {'success': True, 'updated': 2})
self.assertFalse(Notification.objects.filter(user_id=self.me.id, is_read=False).exists())
self.assertTrue(Notification.objects.filter(user_id=self.other.id, is_read=False).exists())
def test_requires_sign_in(self):
self.assertEqual(self.client.patch('/api/notifications/read-all').status_code, 401)