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

@@ -20,40 +20,6 @@ from .serializers import (
from .services import redeem_offer_code
class GuestAwareDiscountThrottle(ScopedRateThrottle):
"""
همان مشکلِ GuestAwareAIThrottle (ai_tutor/views.py) اینجا هم هست.
ScopedRateThrottle استاندارد برای درخواستِ احرازهویت‌شده از
request.user.pk به‌عنوانِ کلیدِ throttle استفاده می‌کنه؛ چون
GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید)
می‌سازه، یک مهاجم می‌تونست با ساختنِ پی‌درپیِ حساب‌های مهمان (تا سقفِ
guest_session=20/hour)، برای هر حساب یک bucket تازه از throttle_scope
'discount_redeem' بگیره. ریسکِ این endpoint از بقیه هم بیشتره، چون
DiscountCode یک رکوردِ سراسری با max_uses مشترکِ بینِ همه‌ی کاربرهاست؛
یک حدسِ درست صرفاً اطلاعات لو نمی‌ده، بلکه یک Redemption واقعی
می‌سازه و مبلغِ سفارش رو کم می‌کنه — یعنی throttle دورزده‌شده به
مهاجم guessهای واقعاً بیشتری روی یک کدِ سراسری می‌ده.
دقیقاً طبقِ همون الگو: فقط برای کاربرهای مهمان (is_guest=True) کلیدِ
cache از pk کاربر به IP کلاینت (get_ident) تغییر می‌کنه، پس همه‌ی
حساب‌های مهمانِ ساخته‌شده از یک IP یک bucket مشترک دارن و چرخشِ حساب
دیگه bucket تازه نمی‌ده. رفتارِ throttle برای کاربرهای غیرمهمان
(ثبت‌نام‌شده) دقیقاً مثلِ قبل (کلید بر اساسِ pk) باقی می‌مونه؛ منطقِ
lookup/redemption/order دست‌نخورده‌ست.
"""
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}
def _is_usable(discount: DiscountCode) -> bool:
# ⚠️ هر Redemption شمرده می‌شود، حتی روی سفارشی که هرگز پرداخت نشد؛ پس
# سبدِ رهاشده سهمِ max_uses را برای همیشه می‌سوزاند و خودِ همان کاربر هم
@@ -73,11 +39,9 @@ class ValidateDiscountView(APIView):
# این endpoint type/value کد رو برمی‌گردونه، پس بدونِ throttle یک
# کاربر می‌تونست بی‌نهایت کد رو probe کنه؛ نرخ کمی بالاتر از
# offer_redeem چون این یک چکِ read-only پیش از خریده. GuestAwareDiscountThrottle
# مشترک (هم اینجا هم RedeemDiscountView/RedeemOfferCodeView) برای
# مهمان‌ها کلید رو بر اساسِ IP می‌سازه تا چرخشِ حساب bucket تازه نده.
# offer_redeem چون این یک چکِ read-only پیش از خریده.
throttle_scope = 'discount_validate'
throttle_classes = [GuestAwareDiscountThrottle]
throttle_classes = [ScopedRateThrottle]
@extend_schema(
parameters=[OpenApiParameter('code', OpenApiTypes.STR, required=True)],
@@ -112,10 +76,9 @@ class RedeemDiscountView(APIView):
# همون oracleِ probe مثلِ ValidateDiscountView، ولی ریسکش بیشتره: حدسِ
# درست یه کدِ واقعی رو مصرف می‌کنه (سهمِ max_uses مشترک) و مبلغِ سفارش
# رو واقعاً کم می‌کنه، ضمن اینکه هر تلاش دو select_for_update و یه
# تراکنشه. GuestAwareDiscountThrottle برای مهمان‌ها کلید رو بر اساسِ IP
# می‌سازه تا چرخشِ حساب bucket تازه نده.
# تراکنشه.
throttle_scope = 'discount_redeem'
throttle_classes = [GuestAwareDiscountThrottle]
throttle_classes = [ScopedRateThrottle]
@extend_schema(request=RedeemRequestSerializer, responses=RedeemResponseSerializer,
tags=['Discount (discount)'])
@@ -209,11 +172,9 @@ class RedeemOfferCodeView(APIView):
# بدونِ throttle، کدهای آفر قابلِ probe بودن؛ اینجا حساس‌تره چون
# Offer.code سراسری/یکتاست و redeem_offer_code() فقط جلوی تکرارِ همون
# کاربر رو می‌گیره — یک حسابِ مهمانِ چرخشی می‌تونست همون آفرِ سراسری رو
# دوباره بگیره. GuestAwareDiscountThrottle برای مهمان‌ها کلید رو بر
# اساسِ IP می‌سازه تا چرخشِ حساب bucket تازه نده.
# کاربر رو می‌گیره.
throttle_scope = 'offer_redeem'
throttle_classes = [GuestAwareDiscountThrottle]
throttle_classes = [ScopedRateThrottle]
@extend_schema(
request=RedeemOfferCodeRequestSerializer, responses=UserOfferResponseSerializer,