Harden auth, AI tutor, payments, and subscriptions
This commit is contained in:
@@ -45,25 +45,14 @@ class FamilyLinkInviteView(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
v = serializer.validated_data
|
||||
|
||||
# اگه دعوت pending برای همین شماره از قبل هست، بهجای رکورد تکراری
|
||||
# (مثل الگوی idempotency در payment/services.py) همون رو برمیگردونیم.
|
||||
# دعوت pending موجود برای همین شماره، بهجای رکورد تکراری، برگردانده میشود.
|
||||
link = FamilyLink.objects.filter(
|
||||
parent_user_id=request.user.id, child_phone=v['childPhone'], status='pending'
|
||||
).first()
|
||||
|
||||
if not link:
|
||||
# ⚛️🔒 اصلاح race condition: قبلاً یک IntegrityError فقط برای
|
||||
# برخورد روی uniq_pending_family_invite_per_parent_child مدیریت
|
||||
# میشد؛ اگه برخورد واقعاً روی invite_code بود (دو کاربر مختلف،
|
||||
# کد ۶رقمی تصادفی یکسان، همزمان — همون چیزی که چکِ
|
||||
# pre-check بالا هم بهخاطر TOCTOU نمیتونه صد-در-صد جلوش رو
|
||||
# بگیره)، link پیدا نمیشد و exception دوباره raise میشد → ۵۰۰.
|
||||
# حالا در یک retry loop، اگه برخورد مربوط به دعوتِ همین
|
||||
# parent/child نبود (یعنی احتمالاً روی invite_code بوده)، یک کد
|
||||
# جدید تولید و دوباره تلاش میکنیم؛ اگه بود، همون رکورد
|
||||
# pending رو idempotent برمیگردونیم — دقیقاً همون الگویی که
|
||||
# create_order در payment/services.py برای race روی
|
||||
# idempotency_key استفاده میکنه.
|
||||
# اگر برخورد روی invite_code باشد (نه دعوت همین parent/child)، کد جدید
|
||||
# تولید و دوباره تلاش میشود؛ در غیر این صورت رکورد pending موجود برگردانده میشود.
|
||||
for attempt in range(5):
|
||||
code = get_random_string(6, allowed_chars='0123456789')
|
||||
try:
|
||||
@@ -80,7 +69,7 @@ class FamilyLinkInviteView(APIView):
|
||||
).first()
|
||||
if link is not None:
|
||||
break
|
||||
# برخورد روی invite_code بوده؛ کد جدید بساز و دوباره تلاش کن
|
||||
# برخورد روی invite_code بوده؛ کد جدید ساخته و دوباره تلاش میشود.
|
||||
continue
|
||||
else:
|
||||
raise IntegrityError(
|
||||
@@ -106,30 +95,19 @@ class FamilyLinkAcceptView(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
v = serializer.validated_data
|
||||
|
||||
# ⚛️🔒 race condition: قبلاً get_object_or_404 بدون قفل انجام میشد —
|
||||
# دو درخواستِ همزمان برای accept کردنِ همون invite code میتونستن هر
|
||||
# دو link.status='pending' رو ببینن، هر دو accept رو کامل کنن، و
|
||||
# دوبار notify() برای والد بفرستن. حالا خودِ ردیفِ FamilyLink داخل
|
||||
# transaction.atomic قفل میشه (select_for_update)، پس درخواستِ دومِ
|
||||
# همزمان تا commit شدنِ اولی معطل میمونه و بعدش status رو
|
||||
# 'accepted' میبینه (نه 'pending')، پس دیگه match نمیکنه.
|
||||
# ردیف FamilyLink زیر قفل خوانده میشود تا دو accept همزمان روی همان
|
||||
# invite_code هر دو کامل نشوند و دو بار notify() ارسال نشود.
|
||||
with transaction.atomic():
|
||||
link = get_object_or_404(
|
||||
FamilyLink.objects.select_for_update(),
|
||||
invite_code=v['inviteCode'], status='pending',
|
||||
)
|
||||
|
||||
# ⚠️ اصلاح امنیتی: قبلاً هر کاربر لاگینکردهای با دونستنِ inviteCode
|
||||
# میتونست دعوتِ یک والد رو claim کنه، حتی اگه اون دعوت اصلاً برای
|
||||
# شمارهی او نبود (مثلاً کد رو حدس بزنه یا از جای دیگهای ببینه).
|
||||
# حالا اگه دعوت برای یک شمارهی مشخص صادر شده باشه، فقط کاربری که
|
||||
# شمارهاش با child_phone یکی باشه میتونه اون رو accept کنه.
|
||||
# اگر دعوت برای شمارهی مشخصی صادر شده باشد، فقط همان شماره میتواند accept کند.
|
||||
if link.child_phone and link.child_phone != request.user.phone:
|
||||
raise PermissionDenied('این دعوت متعلق به شمارهی دیگری است.')
|
||||
|
||||
# ⚛️ atomic: تغییر وضعیت لینک + ساختِ Notification یک واحدند —
|
||||
# وگرنه ممکنه اتصال بهعنوان accepted ثبت بشه ولی هیچ اعلانی برای
|
||||
# والد ساخته نشه.
|
||||
# اتمیک: تغییر وضعیت لینک + ساخت Notification یک واحد است.
|
||||
link.child_user_id = request.user.id
|
||||
link.status = 'accepted'
|
||||
link.accepted_at = timezone.now()
|
||||
|
||||
Reference in New Issue
Block a user