Remove guest login: every user registers with a phone number

POST /api/auth/session is gone (404). Existing is_guest rows keep refresh and
upgrade-by-phone until the demo/guest rows are purged; the code that serves
them is listed in docs/security-before-production.md §10. Tests that used the
endpoint as a fixture now use accounts/testing.guest_session.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 17:38:18 +03:30
parent 03e81abe7e
commit 8835e01595
18 changed files with 101 additions and 198 deletions

View File

@@ -21,8 +21,6 @@ from .services import OTPDeliveryError, send_otp_sms
from .serializers import (
DeleteAccountRequestSerializer,
DeleteAccountResponseSerializer,
GuestSessionRequestSerializer,
GuestSessionResponseSerializer,
LoginRequestSerializer,
LoginResponseSerializer,
LogoutRequestSerializer,
@@ -43,7 +41,7 @@ logger = logging.getLogger(__name__)
class GuestAwareAccountThrottle(ScopedRateThrottle):
"""
همان الگویِ GuestAwareContentThrottle (content/views.py)،
GuestAwareAIThrottle (ai_tutor/views.py) و بقیه؛ چون GuestSessionView
GuestAwareAIThrottle (ai_tutor/views.py) و بقیه؛ چون ورودِ مهمان (حذف‌شده؛ حساب‌های مهمانِ قبلی هنوز هستند)
با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید) می‌سازه، یک
ScopedRateThrottleِ سادهٔ per-user توسطِ چرخشِ حساب مهمان دورزدنی
بود. اینجا برای SetUserGradeView (که مهمان‌ها هم می‌تونن صداش بزنن)
@@ -64,58 +62,6 @@ class GuestAwareAccountThrottle(ScopedRateThrottle):
return self.cache_format % {'scope': self.scope, 'ident': ident}
class GuestSessionView(APIView):
"""POST /api/auth/session — ایجاد سشن مهمان"""
throttle_scope = 'guest_session'
throttle_classes = [ScopedRateThrottle]
@extend_schema(
request=GuestSessionRequestSerializer,
responses=GuestSessionResponseSerializer,
tags=['Identity & Profile (accounts)'],
)
def post(self, request):
serializer = GuestSessionRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
device_id = serializer.validated_data['deviceId']
user, is_new_user = User.objects.get_or_create(
device_id=device_id,
is_guest=True,
defaults={'name': ''},
)
# ثبتِ فعالیتِ کاربر: ایجاد/استفاده‌ی سشنِ مهمان یک سشنِ واقعیِ
# کاربره، پس last_activity_at به‌روزرسانی می‌شه (بدونِ تأثیر روی
# سایرِ رفتارِ endpoint).
user.last_activity_at = timezone.now()
user.save(update_fields=['last_activity_at'])
token = issue_access_token(user.id)
# مهمان هم refresh token می‌گیرد. قبلاً فقط توکنِ یک‌ساعته داشت؛ بعد از
# یک ساعت همه‌ی درخواست‌ها ۴۰۱ می‌شدند درحالی‌که اپ هنوز خودش را
# واردشده می‌دید. امنیتی کم نمی‌شود: خودِ deviceId از قبل برای گرفتنِ
# توکنِ تازه کافی بود.
refresh_token = secrets.token_hex(32)
Session.objects.create(
user=user,
refresh_token=hash_refresh_token(refresh_token),
device_id=device_id,
expires_at=timezone.now() + timezone.timedelta(days=settings.REFRESH_TOKEN_TTL_DAYS),
)
data = {
'userId': str(user.id),
'token': token,
'refreshToken': refresh_token,
'isNewUser': is_new_user,
'isGuest': True,
}
return Response(GuestSessionResponseSerializer(data).data)
class RequestOTPView(APIView):
"""
POST /api/auth/otp/request — درخواستِ کدِ ورود.
@@ -650,7 +596,7 @@ class UserProfileView(APIView):
def get(self, request):
user = request.user
# مدل Profile به‌صورت lazy ساخته می‌شه چون هیچ‌جای دیگه‌ای (نه
# GuestSessionView، نه LoginView) رکورد Profile رو از قبل نمی‌سازه؛
# LoginView) رکورد Profile رو از قبل نمی‌سازه؛
# get_or_create این‌جا امن است چون فقط توسطِ کاربرِ همون رکورد صدا
# زده می‌شه (OneToOne روی user_id جاری، نه ورودیِ خارجی).
profile, _ = Profile.objects.get_or_create(user_id=user.id)