Remove the guest system entirely: no is_guest/device_id, no guest throttles, phone login is the only way in

accounts.0008 drops the columns and refuses to run while guest rows exist.
Docs describe the current state; tests that only proved guest behaviour are gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 18:20:10 +03:30
parent 8835e01595
commit 663252a260
61 changed files with 357 additions and 1518 deletions

View File

@@ -152,7 +152,7 @@ class BazaarVerifyPurchaseThrottleTests(TestCase):
(throttle_scope='bazaar_verify'): هر درخواستِ پذیرفته‌شده یک تماسِ
سرور-به-سرورِ واقعی با API بازار می‌زنه؛ بدونِ throttle مخصوص، یک
کاربرِ احرازهویت‌شده می‌تونست با ارسالِ مکررِ purchaseToken بارِ زیادی
روی سرویسِ بازار/بک‌اند بندازه. GuestAwareBazaarThrottle با pk کاربرِ
روی سرویسِ بازار/بک‌اند بندازه. throttle با pk کاربرِ
احرازهویت‌شده کلید می‌خوره (نه IP)، پس تغییرِ IP این محدودیت رو دور
نمی‌زنه.
"""
@@ -160,7 +160,7 @@ class BazaarVerifyPurchaseThrottleTests(TestCase):
def setUp(self):
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-bazaar-throttle-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000007', name='')
self.token = issue_access_token(self.user.id)
def _make_order(self, key):
@@ -211,7 +211,7 @@ class BazaarVerifyPurchaseThrottleTests(TestCase):
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
# کاربرِ غیرمهمان (نه self.user که مهمانه)، چون کلیدِ throttleِ
# مهمان‌ها عمداً بر اساسِ IP است؛ این تست فقط رفتارِ per-user رو می‌سنجه.
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000751')
registered_user = User.objects.create(name='', phone='+989120000751')
registered_token = issue_access_token(registered_user.id)
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'bazaar_verify': '3/hour'}):
@@ -256,29 +256,13 @@ class BazaarVerifyPurchaseThrottleTests(TestCase):
self.assertFalse(Transaction.objects.filter(order=second_order).exists())
class BazaarVerifyPurchaseGuestThrottleBypassTests(TestCase):
"""
🔒 اثبات فیکسِ امنیتی: قبلاً کلیدِ throttle_scope='bazaar_verify' برای
هر کاربرِ احرازهویت‌شده (مهمان یا نه) user.pk بود. چون GuestSessionView
با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید) می‌سازه، یک مهاجم
می‌تونست با ساختنِ پی‌درپیِ حساب‌های مهمان، هر بار یک bucket throttle
تازه برای تریگر کردنِ تماسِ سرور-به-سرورِ verify_purchase بگیره. حالا
GuestAwareBazaarThrottle برای کاربرهای مهمان کلید رو بر اساسِ IP
می‌سازه، پس چرخشِ حساب مهمان دیگه bucket تازه نمی‌ده. رفتارِ throttle
برای کاربرِ غیرمهمان (per-user، مثلِ قبل) دست‌نخورده مونده.
"""
class BazaarVerifyPurchasePerUserThrottleTests(TestCase):
"""هر کاربرِ ثبت‌نام‌شده سطلِ throttleِ جدای خودش را دارد؛ مصرفِ حسابِ دیگر به او نمی‌رسد."""
def setUp(self):
cache.clear()
self.client = APIClient()
def _make_guest_with_order(self, device_id, key):
guest = User.objects.create(device_id=device_id, is_guest=True, name='')
order = Order.objects.create(
user_id=guest.id, amount=10000, provider='bazaar',
bazaar_product_sku='sku_one_time', idempotency_key=key,
)
return guest, order
def _verify(self, order_id, purchase_token, token):
return self.client.post(
@@ -288,32 +272,13 @@ class BazaarVerifyPurchaseGuestThrottleBypassTests(TestCase):
HTTP_AUTHORIZATION=f'Bearer {token}',
)
def test_rotating_guest_accounts_share_the_same_throttle_bucket(self):
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'bazaar_verify': '2/hour'}):
with patch('payment.views.bazaar.verify_purchase') as mock_verify:
mock_verify.return_value = BazaarPurchaseResult(is_valid=True, purchase_state=0)
guest1, order1 = self._make_guest_with_order('device-bazaar-guest-rot-1', 'order-bazaar-rot-1')
response1 = self._verify(order1.id, 'token-rot-1', issue_access_token(guest1.id))
self.assertEqual(response1.status_code, 200)
guest2, order2 = self._make_guest_with_order('device-bazaar-guest-rot-2', 'order-bazaar-rot-2')
response2 = self._verify(order2.id, 'token-rot-2', issue_access_token(guest2.id))
self.assertEqual(response2.status_code, 200)
# سومین حسابِ مهمانِ تازه (pk جدید) باید throttle بشه، چون
# bucket بر اساسِ IP به‌اشتراک گذاشته شده، نه user.pk.
guest3, order3 = self._make_guest_with_order('device-bazaar-guest-rot-3', 'order-bazaar-rot-3')
response3 = self._verify(order3.id, 'token-rot-3', 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, {'bazaar_verify': '1/hour'}):
with patch('payment.views.bazaar.verify_purchase') as mock_verify:
mock_verify.return_value = BazaarPurchaseResult(is_valid=True, purchase_state=0)
user_a = User.objects.create(is_guest=False, name='', phone='+989120000761')
user_a = User.objects.create(name='', phone='+989120000761')
order_a = Order.objects.create(
user_id=user_a.id, amount=10000, provider='bazaar',
bazaar_product_sku='sku_one_time', idempotency_key='order-bazaar-reg-a',
@@ -321,7 +286,7 @@ class BazaarVerifyPurchaseGuestThrottleBypassTests(TestCase):
response_a = self._verify(order_a.id, 'token-reg-a', issue_access_token(user_a.id))
self.assertEqual(response_a.status_code, 200)
user_b = User.objects.create(is_guest=False, name='', phone='+989120000762')
user_b = User.objects.create(name='', phone='+989120000762')
order_b = Order.objects.create(
user_id=user_b.id, amount=10000, provider='bazaar',
bazaar_product_sku='sku_one_time', idempotency_key='order-bazaar-reg-b',
@@ -330,26 +295,6 @@ class BazaarVerifyPurchaseGuestThrottleBypassTests(TestCase):
self.assertEqual(response_b.status_code, 200)
def test_blocked_guest_request_does_not_call_bazaar_or_mutate_order(self):
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'bazaar_verify': '1/hour'}):
with patch('payment.views.bazaar.verify_purchase') as mock_verify:
mock_verify.return_value = BazaarPurchaseResult(is_valid=True, purchase_state=0)
guest1, order1 = self._make_guest_with_order('device-bazaar-guest-rot-4', 'order-bazaar-rot-4')
first_response = self._verify(order1.id, 'token-rot-4', issue_access_token(guest1.id))
self.assertEqual(first_response.status_code, 200)
mock_verify.reset_mock()
guest2, order2 = self._make_guest_with_order('device-bazaar-guest-rot-5', 'order-bazaar-rot-5')
blocked_response = self._verify(order2.id, 'token-rot-5', issue_access_token(guest2.id))
self.assertEqual(blocked_response.status_code, 429)
mock_verify.assert_not_called()
order2.refresh_from_db()
self.assertEqual(order2.status, 'pending')
self.assertEqual(order2.bazaar_purchase_token, '')
self.assertFalse(Transaction.objects.filter(order=order2).exists())
class ZarinpalClientTests(TestCase):
"""

View File

@@ -23,7 +23,7 @@ from subscription.models import Plan, Subscription
class _Base(TestCase):
def setUp(self):
cache.clear()
self.user = User.objects.create(phone='09120007001', is_guest=False)
self.user = User.objects.create(phone='09120007001')
self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
self.month = Plan.objects.create(title='ماهانه', price=49000, duration_days=30)
@@ -137,7 +137,7 @@ class ReconcileTests(_Base):
class AffiliateTests(_Base):
def setUp(self):
super().setUp()
self.referrer = User.objects.create(phone='09120007002', is_guest=False)
self.referrer = User.objects.create(phone='09120007002')
self.account = AffiliateAccount.objects.create(user_id=self.referrer.id, referral_code='SARA10',
commission_rate=0.1)
@@ -157,11 +157,3 @@ class AffiliateTests(_Base):
me = referrer.get('/api/affiliate/me').json()
self.assertEqual((me['referredCount'], me['paidCount'], me['totalCommission']), (1, 1, 4900))
def test_guests_take_no_part(self):
guest = User.objects.create(device_id='dev-aff', is_guest=True)
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(guest.id)}')
self.assertEqual(client.post('/api/affiliate/track', {'referralCode': 'SARA10'},
format='json').status_code, 403)
self.assertEqual(client.get('/api/affiliate/me').status_code, 403)
self.assertFalse(Conversion.objects.exists())

View File

@@ -26,7 +26,7 @@ from .models import Order
class BazaarPackTests(TestCase):
def setUp(self):
cache.clear()
self.user = User.objects.create(phone='09120007101', is_guest=False)
self.user = User.objects.create(phone='09120007101')
self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
self.pack = Plan.objects.get(kind='pack', question_credits=20)
@@ -74,7 +74,7 @@ class BazaarSkuComesFromTheOrderTests(TestCase):
def setUp(self):
cache.clear()
self.user = User.objects.create(phone='09120007102', is_guest=False)
self.user = User.objects.create(phone='09120007102')
self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
@@ -105,7 +105,7 @@ class BazaarPurchaseFromTheAppTests(TestCase):
def setUp(self):
cache.clear()
self.user = User.objects.create(phone='09120007103', is_guest=False)
self.user = User.objects.create(phone='09120007103')
self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
self.pack = Plan.objects.get(kind='pack', question_credits=20)

View File

@@ -25,7 +25,7 @@ def _client(user):
class InvoiceTests(TestCase):
def setUp(self):
cache.clear()
self.user = User.objects.create(phone='09120007001', is_guest=False)
self.user = User.objects.create(phone='09120007001')
self.client = _client(self.user)
self.plan = Plan.objects.create(title='سالانه', price=299000, duration_days=365)
@@ -80,7 +80,7 @@ class InvoiceTests(TestCase):
def test_someone_else_sees_nothing(self):
self.buy()
stranger = User.objects.create(phone='09120007002', is_guest=False)
stranger = User.objects.create(phone='09120007002')
self.assertEqual(self.invoices(_client(stranger)), [])
def test_orders_from_before_the_snapshot_have_no_list_price(self):

View File

@@ -27,7 +27,7 @@ from .services import OrderStateError, apply_order_payment_result, cancel_pendin
class _Base(TestCase):
def setUp(self):
cache.clear()
self.user = User.objects.create(phone='09120007401', is_guest=False)
self.user = User.objects.create(phone='09120007401')
self.client_app = APIClient()
self.client_app.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
self.pack = Plan.objects.get(kind='pack', question_credits=20)
@@ -107,7 +107,7 @@ class RefundServiceTests(_Base):
self.assertEqual(sub.status, 'canceled')
def test_the_referrers_commission_for_this_order_is_undone(self):
referrer = User.objects.create(phone='09120007402', is_guest=False)
referrer = User.objects.create(phone='09120007402')
account = AffiliateAccount.objects.create(user_id=referrer.id, referral_code='REF1', commission_rate=0.1)
Conversion.objects.create(affiliate_account=account, referred_user_id=self.user.id)
order = self.paid_pack()

View File

@@ -30,7 +30,7 @@ def client_for(user):
class _Base(TestCase):
def setUp(self):
cache.clear()
self.user = User.objects.create(phone='09120009901', is_guest=False)
self.user = User.objects.create(phone='09120009901')
self.app = client_for(self.user)
self.pack = Plan.objects.get(kind='pack', question_credits=20)

View File

@@ -48,24 +48,6 @@ def _redirect_to_configured_url(url: str) -> HttpResponseRedirect:
return redirect_cls(url)
class GuestAwareBazaarThrottle(ScopedRateThrottle):
"""همون الگوی GuestAwareAIThrottle/GuestAwareDiscountThrottle/etc: چرخشِ
حساب مهمان (هر deviceId یه pk تازه) نباید bucket تازه‌ی bazaar_verify
بده، چون هر درخواستِ پذیرفته‌شده یه فراخوانیِ سرور-به-سرور به بازاره.
برای مهمان‌ها کلیدِ cache بر اساسِ IP هست، نه pk."""
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 CreateOrderView(APIView):
"""
POST /api/payment/orders — ساخت سفارش (فراخوانی‌شده از اپ Subscription)
@@ -299,11 +281,9 @@ class BazaarVerifyPurchaseView(APIView):
permission_classes = [IsAuthenticated]
# این endpoint یک فراخوانیِ سرور-به-سرورِ واقعی به API بازار (verify_purchase)
# تریگر می‌کنه، برای همین throttle_scope مجزا و نرخِ سخت‌گیرانه‌تر (۱۰/hour)
# داره. کاربرهای مهمان با هر deviceId یک pk تازه می‌گیرن، پس
# GuestAwareBazaarThrottle برای اون‌ها بر اساسِ IP کلید می‌سازه تا چرخشِ
# حساب bucket تازه نگیره؛ برای کاربرهای غیرمهمان رفتارِ per-user حفظ می‌شه.
# داره.
throttle_scope = 'bazaar_verify'
throttle_classes = [GuestAwareBazaarThrottle]
throttle_classes = [ScopedRateThrottle]
def get_permissions(self):
if self.request.method == 'GET':