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:
@@ -38,30 +38,6 @@ from .serializers import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuestAwareAccountThrottle(ScopedRateThrottle):
|
||||
"""
|
||||
همان الگویِ GuestAwareContentThrottle (content/views.py)،
|
||||
GuestAwareAIThrottle (ai_tutor/views.py) و بقیه؛ چون ورودِ مهمان (حذفشده؛ حسابهای مهمانِ قبلی هنوز هستند)
|
||||
با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید) میسازه، یک
|
||||
ScopedRateThrottleِ سادهٔ per-user توسطِ چرخشِ حساب مهمان دورزدنی
|
||||
بود. اینجا برای SetUserGradeView (که مهمانها هم میتونن صداش بزنن)
|
||||
از همین کلید بر اساسِ IP برای مهمانها استفاده میشه؛ رفتارِ
|
||||
throttle برای کاربرهای غیرمهمان (per-user، مثلِ قبل) دستنخورده
|
||||
میمونه.
|
||||
"""
|
||||
|
||||
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 RequestOTPView(APIView):
|
||||
"""
|
||||
POST /api/auth/otp/request — درخواستِ کدِ ورود.
|
||||
@@ -300,28 +276,7 @@ class LoginView(APIView):
|
||||
return error
|
||||
|
||||
with transaction.atomic():
|
||||
user = User.objects.filter(phone=phone).first()
|
||||
created = False
|
||||
guest = _upgradable_guest(request, phone) if user is None else None
|
||||
if guest is not None:
|
||||
# مهمانی که ثبتنام میکند همان کاربر میماند: امتیاز، استریک،
|
||||
# جزیرهها و گفتگوها همه با همین user_id ذخیره شدهاند. پیشتر یک
|
||||
# کاربرِ تازه و خالی ساخته میشد و پیشرفتِ مهمان جا میماند.
|
||||
guest.phone = phone
|
||||
# هر نشستی که پیش از این با شناسهی دستگاه گرفته شده بود (گوشیِ مشترک، کسی که
|
||||
# شناسه را دیده) نباید به حسابِ شمارهدار برسد؛ فقط نشستِ همین ورود میماند.
|
||||
guest.tokens_valid_after = timezone.now()
|
||||
guest.save(update_fields=['phone', 'tokens_valid_after'])
|
||||
Session.objects.filter(user=guest, revoked=False).update(revoked=True)
|
||||
user, created = guest, True
|
||||
elif user is None:
|
||||
user, created = User.objects.get_or_create(
|
||||
phone=phone,
|
||||
defaults={'is_guest': False, 'name': ''},
|
||||
)
|
||||
if user.is_guest:
|
||||
user.is_guest = False
|
||||
user.save(update_fields=['is_guest'])
|
||||
user, created = User.objects.get_or_create(phone=phone, defaults={'name': ''})
|
||||
|
||||
# ثبتِ فعالیتِ کاربر برای ورودِ موفق.
|
||||
user.last_activity_at = timezone.now()
|
||||
@@ -336,7 +291,6 @@ class LoginView(APIView):
|
||||
Session.objects.create(
|
||||
user=user,
|
||||
refresh_token=hash_refresh_token(refresh_token),
|
||||
device_id=serializer.validated_data.get('deviceId', ''),
|
||||
expires_at=timezone.now() + timezone.timedelta(days=settings.REFRESH_TOKEN_TTL_DAYS),
|
||||
)
|
||||
access_token = issue_access_token(user.id)
|
||||
@@ -356,27 +310,6 @@ def _count_failed_attempt(phone: str) -> None:
|
||||
active.filter(failed_attempts__gte=settings.OTP_MAX_FAILED_ATTEMPTS).update(is_used=True)
|
||||
|
||||
|
||||
def _upgradable_guest(request, phone: str):
|
||||
"""
|
||||
مهمانی که با توکنِ خودش درخواستِ ورود داده و این شماره هنوز حسابی ندارد.
|
||||
|
||||
شمارههای TEST_LOGIN_PHONES و مهمانهای DEMO_GUEST_DEVICE_IDS عمداً کنار گذاشته میشوند:
|
||||
هر دو حسابِ نمایشیِ مشترکاند و ورود نباید یکی را به دیگری یا به شمارهی یک نفر تبدیل کند.
|
||||
"""
|
||||
if phone in settings.TEST_LOGIN_PHONES:
|
||||
return None
|
||||
user = getattr(request, 'user', None)
|
||||
if user is None or not getattr(user, 'is_authenticated', False) or not user.is_guest:
|
||||
return None
|
||||
# مهمانِ نمایشیِ عمومی (DEMO_GUEST_DEVICE_IDS) هم مالِ یک نفر نیست: بدونِ این، هر کس از
|
||||
# نشستِ آن با شمارهی خودش وارد میشد کلِ حساب و دادهی دمو را به شمارهی خودش میبرد و
|
||||
# نشستِ بقیه را باطل میکرد. آن کاربر حسابِ تازهی خودش را میگیرد.
|
||||
if user.device_id in settings.DEMO_GUEST_DEVICE_IDS:
|
||||
return None
|
||||
return User.objects.filter(pk=user.pk, is_guest=True, phone__isnull=True).first()
|
||||
|
||||
|
||||
DELETE_CONFIRM_WORD = 'حذف'
|
||||
# سوابقِ پرداخت سندِ مالیاند و با حذفِ حساب نمیروند (تراکنشها با سفارش میمانند).
|
||||
# Conversion هم میماند: ردیفِ «کاربرِ معرفیشده» سابقهی پورسانتِ *معرف* است، نه دادهی این
|
||||
# کاربر؛ تا ۲۰۲۶-۱۰-۰۳ با حذفِ حسابِ معرفیشده پورسانتِ معرف هم پاک میشد.
|
||||
@@ -394,9 +327,8 @@ class DeleteAccountView(APIView):
|
||||
- کاربرِ شمارهدار باید کدِ پیامکیِ تازه بدهد (همان verify_phone_otpِ ورود، با همان سقفِ حدس).
|
||||
توکنِ access بهتنهایی کافی نیست: گوشیِ بازمانده دستِ یک بچهی دیگر یا توکنِ دزدیدهشده نباید
|
||||
بتواند حساب را برای همیشه پاک کند.
|
||||
- مهمان شماره ندارد؛ باید کلمهی «حذف» را تایپ کرده باشد. حسابش فقط به همین دستگاه وصل است.
|
||||
- داده در همهی اپها پاک میشود (delete_users)، جز سفارشها و تراکنشهایشان که سندِ مالیاند.
|
||||
- حسابهای دمو (TEST_LOGIN_PHONES و مهمانهای DEMO_GUEST_DEVICE_IDS) حذف نمیشوند؛ مشترکاند و برای ارائه پُرشدهاند.
|
||||
- حسابهای دمو (TEST_LOGIN_PHONES) حذف نمیشوند؛ مشترکاند و برای ارائه پُرشدهاند.
|
||||
- بعد از حذف، توکنهای همین کاربر کار نمیکنند چون کاربری با آن شناسه نمانده.
|
||||
"""
|
||||
|
||||
@@ -417,21 +349,13 @@ class DeleteAccountView(APIView):
|
||||
v = serializer.validated_data
|
||||
user = request.user
|
||||
|
||||
if (user.phone and user.phone in settings.TEST_LOGIN_PHONES) or (
|
||||
user.is_guest and user.device_id in settings.DEMO_GUEST_DEVICE_IDS):
|
||||
if user.phone in settings.TEST_LOGIN_PHONES:
|
||||
return Response({'detail': 'حسابِ نمایشی را نمیشود حذف کرد.'}, status=status.HTTP_403_FORBIDDEN)
|
||||
if user.phone:
|
||||
if not v.get('otp'):
|
||||
return Response({'detail': 'برای حذفِ حساب، کدی را که به شمارهات پیامک شد وارد کن.'},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
error = verify_phone_otp(user.phone, v['otp'])
|
||||
if error is not None:
|
||||
return error
|
||||
elif (v.get('confirm') or '').strip() != DELETE_CONFIRM_WORD:
|
||||
return Response({'detail': f'برای حذفِ حساب کلمهی «{DELETE_CONFIRM_WORD}» را بنویس.'},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
error = verify_phone_otp(user.phone, v['otp'])
|
||||
if error is not None:
|
||||
return error
|
||||
|
||||
logger.info('Account deleted by its owner: user_id=%s guest=%s', user.id, user.is_guest)
|
||||
logger.info('Account deleted by its owner: user_id=%s', user.id)
|
||||
# کدهای پیامکیِ شماره میمانند: سقفِ روزانهی پیامکِ هر شماره از همین ردیفها شمرده میشود.
|
||||
delete_users([user.id], keep=KEPT_ON_ACCOUNT_DELETE, delete_otp_codes=False)
|
||||
return Response(DeleteAccountResponseSerializer({'success': True}).data)
|
||||
@@ -510,7 +434,6 @@ class RefreshTokenView(APIView):
|
||||
new_session = Session.objects.create(
|
||||
user_id=old_session.user_id,
|
||||
refresh_token=hash_refresh_token(new_refresh_token),
|
||||
device_id=old_session.device_id,
|
||||
expires_at=timezone.now() + timezone.timedelta(days=settings.REFRESH_TOKEN_TTL_DAYS),
|
||||
)
|
||||
|
||||
@@ -527,11 +450,10 @@ class SetUserGradeView(APIView):
|
||||
"""POST /api/user/grade — ثبت پایهی کاربر (نیاز به احراز هویت)"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
# بدونِ throttle، یک کاربرِ احرازهویتشده (از جمله مهمان) میتواند
|
||||
# نامحدود درخواستِ نوشتن روی دیتابیس بفرستد. کلیدِ شمارش برای مهمانها
|
||||
# باید IP باشد نه pk — دلیلش در GuestAwareAccountThrottle بالا.
|
||||
# بدونِ throttle، یک کاربرِ احرازهویتشده میتواند
|
||||
# نامحدود درخواستِ نوشتن روی دیتابیس بفرستد.
|
||||
throttle_scope = 'user_profile_write'
|
||||
throttle_classes = [GuestAwareAccountThrottle]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
@extend_schema(
|
||||
request=SetGradeRequestSerializer,
|
||||
@@ -560,7 +482,6 @@ def _profile_payload(user, profile) -> dict:
|
||||
'phone': user.phone,
|
||||
'name': user.name,
|
||||
'avatar': user.avatar,
|
||||
'isGuest': user.is_guest,
|
||||
'gradeId': user.grade_id,
|
||||
'birthYear': profile.birth_year,
|
||||
'birthDate': jalali.to_string(profile.birth_date) if profile.birth_date else None,
|
||||
@@ -581,7 +502,7 @@ class UserProfileView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
# نوشتن است، پس همان throttleِ نوشتنِ پروفایل که SetUserGradeView دارد.
|
||||
throttle_scope = 'user_profile_write'
|
||||
throttle_classes = [GuestAwareAccountThrottle]
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
def get_throttles(self):
|
||||
# GET خواندنی است و نباید سهمیهی نوشتن را مصرف کند.
|
||||
|
||||
Reference in New Issue
Block a user