throttling, abuse-protection & logging/monitoring pass
This commit is contained in:
342
family/tests.py
342
family/tests.py
@@ -1 +1,341 @@
|
||||
# Create your tests here.
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework.throttling import ScopedRateThrottle
|
||||
from unittest.mock import patch
|
||||
|
||||
from accounts.authentication import issue_access_token
|
||||
from accounts.models import User
|
||||
from .models import FamilyLink
|
||||
|
||||
|
||||
class FamilyLinkAcceptThrottleTests(TestCase):
|
||||
"""
|
||||
🔒 اثبات محدودیتِ جدیدِ throttle_scope='family_invite_accept' روی
|
||||
FamilyLinkAcceptView: inviteCode یک رازِ عددیِ ۶رقمی است که این
|
||||
endpoint با select_for_update() جستوجو میکنه؛ بدونِ throttle یک
|
||||
کاربرِ احرازهویتشده میتونست بینهایت کد رو حدس بزنه. این تستها هم
|
||||
رفتارِ throttle (per-user، نه IP)، هم عدمِ رگرسیون در ردیمِ موفق، هم
|
||||
عدمِ تغییرِ FamilyLink توسطِ درخواستِ بلاکشده رو تضمین میکنن.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.client = APIClient()
|
||||
self.parent = User.objects.create(device_id='device-family-parent-1', is_guest=True, name='')
|
||||
self.child = User.objects.create(
|
||||
device_id='device-family-child-1', is_guest=True, name='', phone='+989120000001',
|
||||
)
|
||||
self.child_token = issue_access_token(self.child.id)
|
||||
self.registered_user = User.objects.create(is_guest=False, name='', phone='+989120000098')
|
||||
self.registered_token = issue_access_token(self.registered_user.id)
|
||||
self.link = FamilyLink.objects.create(
|
||||
parent_user_id=self.parent.id, child_phone=self.child.phone, invite_code='123456',
|
||||
)
|
||||
|
||||
def _accept(self, code, token=None):
|
||||
headers = {}
|
||||
if token:
|
||||
headers['HTTP_AUTHORIZATION'] = f'Bearer {token}'
|
||||
return self.client.post(
|
||||
'/api/family/link/accept', {'inviteCode': code}, format='json', **headers,
|
||||
)
|
||||
|
||||
def test_unauthenticated_request_is_still_rejected(self):
|
||||
# رفتارِ IsAuthenticated موجود دستنخورده مونده؛ throttle جایگزینِ
|
||||
# authentication نیست.
|
||||
response = self._accept('123456')
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_repeated_code_guessing_is_throttled_after_configured_limit(self):
|
||||
# سناریوی brute-force: کدهای نادرستِ پشتِسرِهم.
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_accept': '3/hour'}):
|
||||
for i in range(3):
|
||||
response = self._accept(f'00000{i}', token=self.child_token)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
blocked_response = self._accept('999999', token=self.child_token)
|
||||
self.assertEqual(blocked_response.status_code, 429)
|
||||
|
||||
def test_throttle_is_scoped_per_user_not_shared_globally(self):
|
||||
# این تست مخصوصِ کاربرهای *غیرمهمان* (ثبتنامشده) است — طبقِ فیکسِ
|
||||
# GuestAwareFamilyThrottle، حسابهای مهمان دیگه bucket جداگانه
|
||||
# ندارن (نگاه کنید به FamilyLinkAcceptGuestThrottleBypassTests پایینتر)،
|
||||
# اما رفتارِ per-user برای کاربرهایِ ثبتنامشده باید دقیقاً مثلِ
|
||||
# قبل دستنخورده بمونه.
|
||||
other_user = User.objects.create(
|
||||
is_guest=False, name='', phone='+989120000099',
|
||||
)
|
||||
other_token = issue_access_token(other_user.id)
|
||||
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_accept': '1/hour'}):
|
||||
first_user_response = self._accept('000000', token=self.registered_token)
|
||||
self.assertEqual(first_user_response.status_code, 404)
|
||||
|
||||
first_user_blocked = self._accept('111111', token=self.registered_token)
|
||||
self.assertEqual(first_user_blocked.status_code, 429)
|
||||
|
||||
# کاربرِ دومِ غیرمهمانِ متفاوت هنوز باید بتونه تلاش کنه —
|
||||
# throttle برای کاربرهای ثبتنامشده هنوز per-user است، نه IP.
|
||||
second_user_response = self._accept('000000', token=other_token)
|
||||
self.assertEqual(second_user_response.status_code, 404)
|
||||
|
||||
def test_successful_accept_still_works_within_the_limit(self):
|
||||
# عدمِ رگرسیون: رفتارِ اصلیِ accept باید دقیقاً مثلِ قبل کار کنه.
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_accept': '10/hour'}):
|
||||
response = self._accept(self.link.invite_code, token=self.child_token)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['status'], 'accepted')
|
||||
self.link.refresh_from_db()
|
||||
self.assertEqual(self.link.status, 'accepted')
|
||||
self.assertEqual(self.link.child_user_id, self.child.id)
|
||||
|
||||
def test_blocked_request_does_not_mutate_family_link(self):
|
||||
# درخواستِ بلاکشده نباید FamilyLink رو تغییر بده — throttle باید
|
||||
# *قبل* از رسیدنِ به select_for_update() رد کنه.
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_accept': '1/hour'}):
|
||||
first_response = self._accept('000000', token=self.child_token)
|
||||
self.assertEqual(first_response.status_code, 404)
|
||||
|
||||
blocked_response = self._accept(self.link.invite_code, token=self.child_token)
|
||||
|
||||
self.assertEqual(blocked_response.status_code, 429)
|
||||
self.link.refresh_from_db()
|
||||
self.assertEqual(self.link.status, 'pending')
|
||||
self.assertIsNone(self.link.child_user_id)
|
||||
|
||||
|
||||
class FamilyLinkAcceptGuestThrottleBypassTests(TestCase):
|
||||
"""
|
||||
🔒 اثبات فیکسِ امنیتی: قبلاً کلیدِ throttle_scope='family_invite_accept'
|
||||
برای هر کاربرِ احرازهویتشده (مهمان یا نه) user.pk بود. چون
|
||||
GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید)
|
||||
میسازه، یک مهاجم میتونست با ساختنِ پیدرپیِ حسابهای مهمان، هر بار
|
||||
یک bucket throttle تازه برای brute-force کردنِ inviteCode ۶رقمی
|
||||
بگیره. حالا GuestAwareFamilyThrottle برای کاربرهای مهمان کلید رو بر
|
||||
اساسِ IP (همون get_ident که guest_session هم استفاده میکنه) میسازه،
|
||||
پس چرخشِ حساب مهمان دیگه bucket تازه نمیده. رفتارِ throttle برای
|
||||
کاربرِ غیرمهمان (per-user، مثلِ قبل) دستنخورده مونده.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.client = APIClient()
|
||||
|
||||
def _accept(self, code, token):
|
||||
return self.client.post(
|
||||
'/api/family/link/accept',
|
||||
{'inviteCode': code},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'Bearer {token}',
|
||||
)
|
||||
|
||||
def test_rotating_guest_accounts_share_the_same_throttle_bucket(self):
|
||||
# سه حساب مهمانِ متفاوت (شبیهسازیِ چرخشِ حساب برای دورزدنِ throttle)،
|
||||
# همه از همون کلاینت/IP — باید همه یک bucket مشترک رو مصرف کنند.
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_accept': '2/hour'}):
|
||||
guest1 = User.objects.create(device_id='device-family-guest-rot-1', is_guest=True, name='')
|
||||
response1 = self._accept('000001', issue_access_token(guest1.id))
|
||||
self.assertEqual(response1.status_code, 404)
|
||||
|
||||
guest2 = User.objects.create(device_id='device-family-guest-rot-2', is_guest=True, name='')
|
||||
response2 = self._accept('000002', issue_access_token(guest2.id))
|
||||
self.assertEqual(response2.status_code, 404)
|
||||
|
||||
# سومین حسابِ مهمانِ تازه (pk جدید) باید throttle بشه، چون
|
||||
# bucket بر اساسِ IP بهاشتراک گذاشته شده، نه user.pk — یعنی
|
||||
# سقفِ ۲/ساعت هنوز اعمال میشه، صرفنظر از تعدادِ حسابهای
|
||||
# مهمانِ ساختهشده.
|
||||
guest3 = User.objects.create(device_id='device-family-guest-rot-3', is_guest=True, name='')
|
||||
response3 = self._accept('000003', issue_access_token(guest3.id))
|
||||
|
||||
self.assertEqual(response3.status_code, 429)
|
||||
|
||||
def test_registered_users_keep_independent_per_user_buckets(self):
|
||||
# عدمِ رگرسیون: کاربرهای غیرمهمان (ثبتنامشده با شماره) باید مثلِ
|
||||
# قبل هرکدوم bucket جداگانهی خودشون رو داشته باشن — throttle
|
||||
# برای اونا هنوز بر اساسِ user.pk است، نه IP.
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_accept': '1/hour'}):
|
||||
user_a = User.objects.create(is_guest=False, name='', phone='+989120000201')
|
||||
response_a = self._accept('000001', issue_access_token(user_a.id))
|
||||
self.assertEqual(response_a.status_code, 404)
|
||||
|
||||
user_b = User.objects.create(is_guest=False, name='', phone='+989120000202')
|
||||
response_b = self._accept('000002', issue_access_token(user_b.id))
|
||||
|
||||
self.assertEqual(response_b.status_code, 404)
|
||||
|
||||
def test_blocked_guest_request_does_not_reach_or_mutate_family_link(self):
|
||||
# درخواستِ بلاکشده (چه از همون حساب، چه از حسابِ مهمانِ چرخشی)
|
||||
# نباید FamilyLink رو تغییر بده و نباید حتی به select_for_update()
|
||||
# برسه — throttle باید *قبل* از منطقِ accept رد کنه.
|
||||
parent = User.objects.create(device_id='device-family-guest-rot-parent', is_guest=True, name='')
|
||||
child = User.objects.create(
|
||||
device_id='device-family-guest-rot-child', is_guest=True, name='', phone='+989120000203',
|
||||
)
|
||||
link = FamilyLink.objects.create(
|
||||
parent_user_id=parent.id, child_phone=child.phone, invite_code='654321',
|
||||
)
|
||||
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_accept': '1/hour'}):
|
||||
guest1 = User.objects.create(device_id='device-family-guest-rot-4', is_guest=True, name='')
|
||||
first_response = self._accept('000000', issue_access_token(guest1.id))
|
||||
self.assertEqual(first_response.status_code, 404)
|
||||
|
||||
# حسابِ مهمانِ تازهی دوم، حتی با inviteCode درست، باید بلاک بشه
|
||||
# (bucket مشترکِ IP از قبل مصرف شده) و FamilyLink رو دست نزنه.
|
||||
guest2 = User.objects.create(device_id='device-family-guest-rot-5', is_guest=True, name='')
|
||||
blocked_response = self._accept(link.invite_code, issue_access_token(guest2.id))
|
||||
|
||||
self.assertEqual(blocked_response.status_code, 429)
|
||||
link.refresh_from_db()
|
||||
self.assertEqual(link.status, 'pending')
|
||||
self.assertIsNone(link.child_user_id)
|
||||
|
||||
|
||||
class FamilyLinkInviteThrottleTests(TestCase):
|
||||
"""
|
||||
🔒 اثبات محدودیتِ جدیدِ throttle_scope='family_invite_create' روی
|
||||
FamilyLinkInviteView: این endpoint قبلاً هیچ throttle مخصوصی نداشت،
|
||||
درحالیکه هر درخواستِ پذیرفتهشده یک FamilyLink ماندگار و یک کدِ
|
||||
دعوتِ ۶رقمیِ تازه میسازه. GuestAwareFamilyThrottle با pk کاربرِ
|
||||
احرازهویتشده کلید میخوره (نه IP)، پس تغییرِ IP این محدودیت رو دور
|
||||
نمیزنه.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.client = APIClient()
|
||||
self.parent = User.objects.create(device_id='device-family-invite-throttle-1', is_guest=True, name='')
|
||||
self.token = issue_access_token(self.parent.id)
|
||||
|
||||
def _invite(self, child_phone, token=None, remote_addr=None):
|
||||
headers = {}
|
||||
if token:
|
||||
headers['HTTP_AUTHORIZATION'] = f'Bearer {token}'
|
||||
if remote_addr:
|
||||
headers['REMOTE_ADDR'] = remote_addr
|
||||
return self.client.post(
|
||||
'/api/family/link/invite', {'childPhone': child_phone}, format='json', **headers,
|
||||
)
|
||||
|
||||
def test_unauthenticated_request_is_rejected(self):
|
||||
response = self._invite('+989120001001')
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_requests_within_the_limit_still_succeed(self):
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_create': '3/hour'}):
|
||||
for i in range(3):
|
||||
response = self._invite(f'+98912000110{i}', token=self.token)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('inviteCode', response.data)
|
||||
|
||||
def test_excessive_invite_requests_from_same_user_are_blocked(self):
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_create': '3/hour'}):
|
||||
for i in range(3):
|
||||
response = self._invite(f'+98912000120{i}', token=self.token)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
blocked_response = self._invite('+989120001299', token=self.token)
|
||||
self.assertEqual(blocked_response.status_code, 429)
|
||||
|
||||
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
|
||||
# ⚠️ توجه: از یک کاربرِ *غیرمهمان* استفاده میشه (نه self.parent که
|
||||
# مهمان است)، چون طبقِ فیکسِ GuestAwareFamilyThrottle حالا کلیدِ
|
||||
# bucket برای کاربرهای مهمان عمداً بر اساسِ IP است (نگاه کنید به
|
||||
# FamilyLinkInviteGuestThrottleBypassTests پایینتر) — این تست
|
||||
# فقط رفتارِ per-user (نه IP) برای کاربرهایِ ثبتنامشده رو تضمین
|
||||
# میکنه.
|
||||
registered_parent = User.objects.create(is_guest=False, name='', phone='+989120001300')
|
||||
registered_token = issue_access_token(registered_parent.id)
|
||||
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_create': '3/hour'}):
|
||||
for i in range(3):
|
||||
response = self._invite(
|
||||
f'+98912000131{i}', token=registered_token, remote_addr=f'10.5.0.{i + 1}',
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
blocked_response = self._invite('+989120001399', token=registered_token, remote_addr='10.5.0.99')
|
||||
self.assertEqual(blocked_response.status_code, 429)
|
||||
|
||||
def test_throttled_request_does_not_create_family_link(self):
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_create': '1/hour'}):
|
||||
first_response = self._invite('+989120001401', token=self.token)
|
||||
self.assertEqual(first_response.status_code, 200)
|
||||
|
||||
link_count_before = FamilyLink.objects.filter(parent_user_id=self.parent.id).count()
|
||||
blocked_response = self._invite('+989120001402', token=self.token)
|
||||
|
||||
self.assertEqual(blocked_response.status_code, 429)
|
||||
self.assertEqual(
|
||||
FamilyLink.objects.filter(parent_user_id=self.parent.id).count(), link_count_before,
|
||||
)
|
||||
self.assertFalse(
|
||||
FamilyLink.objects.filter(parent_user_id=self.parent.id, child_phone='+989120001402').exists(),
|
||||
)
|
||||
|
||||
|
||||
class FamilyLinkInviteGuestThrottleBypassTests(TestCase):
|
||||
"""
|
||||
🔒 اثبات فیکسِ امنیتی: FamilyLinkInviteView تازه throttle گرفته و از همون
|
||||
ابتدا با GuestAwareFamilyThrottle محافظت میشه (نه یک ScopedRateThrottleِ
|
||||
ساده که بعداً فیکس بشه). چون GuestSessionView با هر deviceId جدید یک
|
||||
کاربرِ مهمانِ تازه (pk جدید) میسازه، بدونِ این فیکس یک مهاجم میتونست با
|
||||
ساختنِ پیدرپیِ حسابهای مهمان، هر بار یک bucket throttle تازه برای
|
||||
ساختنِ FamilyLink/کدِ دعوتِ نامحدود بگیره. GuestAwareFamilyThrottle برای
|
||||
کاربرهای مهمان کلید رو بر اساسِ IP میسازه، پس چرخشِ حساب مهمان دیگه
|
||||
bucket تازه نمیده. رفتارِ throttle برای کاربرِ غیرمهمان (per-user)
|
||||
دستنخورده مونده.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.client = APIClient()
|
||||
|
||||
def _invite(self, child_phone, token):
|
||||
return self.client.post(
|
||||
'/api/family/link/invite', {'childPhone': child_phone}, format='json',
|
||||
HTTP_AUTHORIZATION=f'Bearer {token}',
|
||||
)
|
||||
|
||||
def test_rotating_guest_accounts_share_the_same_throttle_bucket(self):
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_create': '2/hour'}):
|
||||
guest1 = User.objects.create(device_id='device-family-invite-guest-rot-1', is_guest=True, name='')
|
||||
response1 = self._invite('+989120001501', issue_access_token(guest1.id))
|
||||
self.assertEqual(response1.status_code, 200)
|
||||
|
||||
guest2 = User.objects.create(device_id='device-family-invite-guest-rot-2', is_guest=True, name='')
|
||||
response2 = self._invite('+989120001502', issue_access_token(guest2.id))
|
||||
self.assertEqual(response2.status_code, 200)
|
||||
|
||||
# سومین حسابِ مهمانِ تازه (pk جدید) باید throttle بشه، چون
|
||||
# bucket بر اساسِ IP بهاشتراک گذاشته شده، نه user.pk.
|
||||
guest3 = User.objects.create(device_id='device-family-invite-guest-rot-3', is_guest=True, name='')
|
||||
response3 = self._invite('+989120001503', issue_access_token(guest3.id))
|
||||
|
||||
self.assertEqual(response3.status_code, 429)
|
||||
|
||||
def test_registered_users_keep_independent_per_user_buckets(self):
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_create': '1/hour'}):
|
||||
user_a = User.objects.create(is_guest=False, name='', phone='+989120001601')
|
||||
response_a = self._invite('+989120001701', issue_access_token(user_a.id))
|
||||
self.assertEqual(response_a.status_code, 200)
|
||||
|
||||
user_b = User.objects.create(is_guest=False, name='', phone='+989120001602')
|
||||
response_b = self._invite('+989120001702', issue_access_token(user_b.id))
|
||||
|
||||
self.assertEqual(response_b.status_code, 200)
|
||||
|
||||
def test_blocked_guest_request_does_not_create_family_link(self):
|
||||
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'family_invite_create': '1/hour'}):
|
||||
guest1 = User.objects.create(device_id='device-family-invite-guest-rot-4', is_guest=True, name='')
|
||||
first_response = self._invite('+989120001801', issue_access_token(guest1.id))
|
||||
self.assertEqual(first_response.status_code, 200)
|
||||
|
||||
guest2 = User.objects.create(device_id='device-family-invite-guest-rot-5', is_guest=True, name='')
|
||||
blocked_response = self._invite('+989120001802', issue_access_token(guest2.id))
|
||||
|
||||
self.assertEqual(blocked_response.status_code, 429)
|
||||
self.assertFalse(FamilyLink.objects.filter(parent_user_id=guest2.id).exists())
|
||||
|
||||
@@ -9,6 +9,7 @@ from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.exceptions import NotFound, PermissionDenied
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.throttling import ScopedRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .models import FamilyLink, ParentalSettings, WeeklyReportLog
|
||||
@@ -30,11 +31,56 @@ def _week_start(dt):
|
||||
return (dt - timedelta(days=dt.weekday())).date()
|
||||
|
||||
|
||||
class GuestAwareFamilyThrottle(ScopedRateThrottle):
|
||||
"""
|
||||
⚠️ اضافه شده: همون مشکلِ GuestAwareAIThrottle (ai_tutor/views.py) اینجا
|
||||
هم هست. ScopedRateThrottle استاندارد برای درخواستِ احرازهویتشده از
|
||||
request.user.pk بهعنوانِ کلیدِ throttle استفاده میکنه؛ چون
|
||||
GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید)
|
||||
میسازه، یک مهاجم میتونست با ساختنِ پیدرپیِ حسابهای مهمان (تا سقفِ
|
||||
guest_session=20/hour)، برای هر حساب یک bucket تازه از throttle_scope
|
||||
'family_invite_accept' بگیره و عملاً محدودیتِ ۱۰/ساعتِ brute-force
|
||||
رویِ inviteCode ۶رقمی رو دور بزنه.
|
||||
|
||||
دقیقاً طبقِ همون الگو: فقط برای کاربرهای مهمان (is_guest=True) کلیدِ
|
||||
cache از pk کاربر به IP کلاینت (get_ident) تغییر میکنه، پس همهی
|
||||
حسابهای مهمانِ ساختهشده از یک IP یک bucket مشترک دارن و چرخشِ حساب
|
||||
دیگه bucket تازه نمیده. رفتارِ throttle برای کاربرهای غیرمهمان
|
||||
(ثبتنامشده) دقیقاً مثلِ قبل (کلید بر اساسِ pk) باقی میمونه؛ منطقِ
|
||||
generation/authorization/notify اکسپت دستنخوردهست.
|
||||
"""
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
user = request.user
|
||||
if user and user.is_authenticated and getattr(user, 'is_guest', False):
|
||||
ident = f'guest-ip:{self.get_ident(request)}'
|
||||
elif user and user.is_authenticated:
|
||||
ident = user.pk
|
||||
else:
|
||||
ident = self.get_ident(request)
|
||||
|
||||
return self.cache_format % {'scope': self.scope, 'ident': ident}
|
||||
|
||||
|
||||
class FamilyLinkInviteView(APIView):
|
||||
"""POST /api/family/link/invite — دعوت فرزند توسط والد (تولید کد اتصال)"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
# ⚠️ اضافه شده: این endpoint قبلاً هیچ throttle مخصوصی نداشت، درحالیکه
|
||||
# یک FamilyLink ماندگار میسازه و یک کدِ دعوتِ ۶رقمیِ تازه تولید میکنه.
|
||||
# بدونِ throttle، یک کاربرِ احرازهویتشده میتونست با فرستادنِ
|
||||
# پیدرپیِ شمارههای مختلف، تعدادِ زیادی FamilyLink/کدِ دعوت بسازه.
|
||||
# طبقِ همون الگویِ GuestAwareFamilyThrottleِ مشترک (که
|
||||
# FamilyLinkAcceptView هم استفاده میکنه): برای کاربرهای مهمان کلید
|
||||
# بر اساسِ IP و برای کاربرهای غیرمهمان بر اساسِ user.pk است، پس
|
||||
# چرخشِ حسابِ مهمان نمیتونه bucket تازه بگیره. یک throttle_scope
|
||||
# مجزا ('family_invite_create'، نه 'family_invite_accept') استفاده
|
||||
# شده چون تولید و مصرفِ کدِ دعوت باید مستقل از هم قابلتنظیم بمونن؛
|
||||
# منطقِ generation/authorization دستنخوردهست.
|
||||
throttle_scope = 'family_invite_create'
|
||||
throttle_classes = [GuestAwareFamilyThrottle]
|
||||
|
||||
@extend_schema(
|
||||
request=FamilyLinkInviteRequestSerializer,
|
||||
responses=FamilyLinkInviteResponseSerializer,
|
||||
@@ -85,6 +131,19 @@ class FamilyLinkAcceptView(APIView):
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
# ⚠️ اضافه شده: inviteCode یک رازِ عددیِ ۶رقمی تولیدشدهی سرور است
|
||||
# (family/views.py::FamilyLinkInviteView)، و این endpoint دقیقاً همون
|
||||
# کد رو با select_for_update() جستوجو میکنه؛ بدونِ throttle، یک
|
||||
# کاربرِ احرازهویتشده میتونست بدونِ هیچ محدودیتی فضای ۶رقمی رو
|
||||
# brute-force کنه. طبقِ همون الگویِ ScopedRateThrottle که برای بقیهی
|
||||
# اندپوینتهایِ حدسپذیر پروژه اضافه شد، اینجا هم یک scope مجزا اضافه
|
||||
# شد (نه scope مشترک با discount/offer) تا نرخِ محدودسازی مستقل از اونها
|
||||
# قابلتنظیم باشه. چون DRF throttle check قبل از dispatch به post() در
|
||||
# APIView.initial() اجرا میشه، حدسهای بلاکشده اصلاً به select_for_update()
|
||||
# نمیرسن.
|
||||
throttle_scope = 'family_invite_accept'
|
||||
throttle_classes = [GuestAwareFamilyThrottle]
|
||||
|
||||
@extend_schema(
|
||||
request=FamilyLinkAcceptRequestSerializer,
|
||||
responses=FamilyLinkAcceptResponseSerializer,
|
||||
|
||||
Reference in New Issue
Block a user