Update project
This commit is contained in:
17
accounts/migrations/0002_user_uniq_guest_device_id.py
Normal file
17
accounts/migrations/0002_user_uniq_guest_device_id.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.17 on 2026-08-19 10:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name='user',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('is_guest', True), models.Q(('device_id', None), _negated=True), models.Q(('device_id', ''), _negated=True)), fields=('device_id',), name='uniq_guest_device_id'),
|
||||
),
|
||||
]
|
||||
@@ -20,6 +20,25 @@ class User(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
constraints = [
|
||||
# 🔒 اصلاح race condition: قبلاً device_id هیچ قید یکتاییِ سطح
|
||||
# DB نداشت، پس User.objects.get_or_create(device_id=...) در
|
||||
# GuestSessionView در برابر دو درخواستِ همزمان با همون
|
||||
# device_id، میتونست دو کاربرِ مهمانِ جدا بسازه (هر دو create
|
||||
# میشدن چون هیچکدوم قبل از دیگری commit نشده بود که get بعدی
|
||||
# پیداش کنه). این UniqueConstraint (فقط برای کاربرانِ مهمان و
|
||||
# device_id غیرخالی — کاربرانِ ثبتنامشده با phone میتونن
|
||||
# device_id مشترک/خالی داشته باشن) کافیه؛ چون Django's
|
||||
# get_or_create از قبل خودش IntegrityError روی همین constraint
|
||||
# رو میگیره و رکورد موجود رو دوباره get میکنه (نگاه کنید به
|
||||
# پیادهسازی _create_object_from_params در Django)، پس نیازی به
|
||||
# تغییر GuestSessionView نبود.
|
||||
models.UniqueConstraint(
|
||||
fields=['device_id'],
|
||||
condition=models.Q(is_guest=True) & ~models.Q(device_id=None) & ~models.Q(device_id=''),
|
||||
name='uniq_guest_device_id',
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name or self.phone or f"guest-{self.pk}"
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user