Update project

This commit is contained in:
2026-08-19 17:15:55 +03:30
parent 02d40e1238
commit f945f960c9
25 changed files with 1092 additions and 287 deletions

View File

@@ -2,6 +2,7 @@ import random
import uuid
from django.conf import settings
from django.db import transaction
from django.utils import timezone
from drf_spectacular.utils import extend_schema
from rest_framework import status
@@ -99,44 +100,58 @@ class LoginView(APIView):
phone = serializer.validated_data['phone']
otp = serializer.validated_data['otp']
valid = False
if otp == settings.DEV_MASTER_OTP:
valid = True
else:
otp_qs = OTPCode.objects.filter(
phone=phone, code=otp, is_used=False, expires_at__gte=timezone.now()
).order_by('-id')
otp_obj = otp_qs.first()
if otp_obj:
otp_obj.is_used = True
otp_obj.save(update_fields=['is_used'])
# ⚛️🔒 race condition: قبلاً چکِ «is_used=False» و ستِ «is_used=True»
# دو عملیاتِ جدا و بدون قفل بودن. دو درخواستِ همزمان با دقیقاً همون
# کدِ OTP می‌تونستن هر دو is_used=False رو ببینن و هر دو valid=True
# بگیرن — یعنی یک کدِ یک‌بارمصرف، دوبار مصرف بشه (و در پی‌اش، دوبار
# وارد شاخه‌ی «ثبت‌نامِ واقعی» بشیم). حالا ردیفِ OTP قفل می‌شه
# (select_for_update، دقیقاً همون الگویی که در payment/discount
# استفاده شده) و چک‌وست داخل همون قفل انجام می‌شه، تا کل ورود
# (چک OTP + get_or_create کاربر + سشن) یک واحدِ اتمیکِ قفل‌شده باشه.
with transaction.atomic():
valid = False
if otp == settings.DEV_MASTER_OTP:
valid = True
else:
otp_obj = OTPCode.objects.select_for_update().filter(
phone=phone, code=otp, is_used=False, expires_at__gte=timezone.now()
).order_by('-id').first()
if otp_obj:
otp_obj.is_used = True
otp_obj.save(update_fields=['is_used'])
valid = True
if not valid:
return Response({'detail': 'کد OTP نامعتبر یا منقضی‌شده است.'}, status=status.HTTP_400_BAD_REQUEST)
if not valid:
return Response({'detail': 'کد OTP نامعتبر یا منقضی‌شده است.'}, status=status.HTTP_400_BAD_REQUEST)
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'])
# ⚛️ atomic: get_or_create/save روی User، سیگنال user_registered
# (که آفرهای auto_assign_on_signup رو از طریق discount اعمال می‌کنه
# و خودش UserOffer/Subscription می‌سازه) و ساخت Session، همه یک
# واحدند. بدون این، مثلاً اگه ساخت Session fail کنه، کاربر واقعاً
# «ثبت‌نام‌شده» و صاحب آفر رایگان می‌مونه ولی هیچ سشنی نداره که
# باهاش وارد بشه (لاگین باید کامل موفق یا کامل ناموفق باشه).
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'])
if created:
# ثبت‌نام واقعی (نه guest session): آفرهای auto_assign_on_signup
# (مثل یک هفته رایگان) از طریق discount/receivers.py روی همین
# سیگنال اعمال می‌شن؛ این ویو از وجود آفر/discount چیزی نمی‌دونه.
from .signals import user_registered
user_registered.send(sender=User, user_id=user.id)
if created:
# ثبت‌نام واقعی (نه guest session): آفرهای auto_assign_on_signup
# (مثل یک هفته رایگان) از طریق discount/receivers.py روی همین
# سیگنال اعمال می‌شن؛ این ویو از وجود آفر/discount چیزی نمی‌دونه.
from .signals import user_registered
user_registered.send(sender=User, user_id=user.id)
refresh_token = uuid.uuid4().hex
Session.objects.create(
user=user,
refresh_token=refresh_token,
device_id=request.data.get('deviceId', ''),
expires_at=timezone.now() + timezone.timedelta(days=settings.REFRESH_TOKEN_TTL_DAYS),
)
refresh_token = uuid.uuid4().hex
Session.objects.create(
user=user,
refresh_token=refresh_token,
device_id=request.data.get('deviceId', ''),
expires_at=timezone.now() + timezone.timedelta(days=settings.REFRESH_TOKEN_TTL_DAYS),
)
access_token = issue_access_token(user.id)
data = {