cleanup: trim bloated multi-line comments across the project
This commit is contained in:
@@ -8,12 +8,9 @@ class Order(models.Model):
|
||||
"""
|
||||
STATUS_CHOICES = [('pending', 'در انتظار'), ('paid', 'پرداختشده'), ('failed', 'ناموفق')]
|
||||
|
||||
# دو مدل پرداخت طبق درخواست سینیور: 'gateway' (زرینپال/زیبال و مشابه) و
|
||||
# 'bazaar' (پرداخت درونبرنامهای اپلیکیشن بازار). این دو منطق پرداخت و
|
||||
# صحتسنجی کاملاً متفاوتی دارن (webhook درگاه در برابر verify سرور-به-
|
||||
# سرور با access_token بازار)؛ برای همین provider جداست، نه فقط یک مقدار
|
||||
# دیگه در Transaction.gateway (که فقط برای گزارشگیریِ درگاه واقعی زرینپال/
|
||||
# زیبال باقی میمونه).
|
||||
# 'gateway' و 'bazaar' منطق و صحتسنجیِ کاملاً متفاوتی دارن (webhook در
|
||||
# برابرِ verifyِ سرور-به-سرور با access_token)؛ برای همین provider جداست
|
||||
# از Transaction.gateway (که فقط برای گزارشگیریِ درگاهِ واقعیه).
|
||||
PROVIDER_CHOICES = [('gateway', 'درگاه پرداخت'), ('bazaar', 'پرداخت درونبرنامهای بازار')]
|
||||
|
||||
user_id = models.IntegerField(db_index=True) # ارجاع سبک به accounts.User
|
||||
@@ -48,18 +45,11 @@ class Transaction(models.Model):
|
||||
class Meta:
|
||||
ordering = ['created_at']
|
||||
constraints = [
|
||||
# 🔒 یکتاییِ سراسریِ (gateway, gateway_ref_id) برای refهای
|
||||
# غیرخالی: این قید سطح DB تضمین میکنه یک gateway_ref_id مشخص
|
||||
# (مثلاً Authority زرینپال یا purchaseToken بازار) در کل جدول
|
||||
# Transaction فقط میتونه متعلق به یک Order باشه — هم جلوی
|
||||
# duplicate/idempotency برای همون Order رو میگیره (چون در کل
|
||||
# جدول حداکثر یک ردیف با این (gateway, ref) میتونه وجود داشته
|
||||
# باشه) و هم جلوی reuse/replay همون ref برای یک Order دیگه رو
|
||||
# (نگاه کنید به apply_order_payment_result). این قید، قیدِ
|
||||
# قدیمیترِ (order, gateway_ref_id, status) رو هم در برمیگیره
|
||||
# (چون سختگیرانهتره)، برای همین دیگه لازم نیست جدا نگه داشته
|
||||
# بشه. gateway_ref_id__gt='' یعنی رفرنسهای خالی (که provider
|
||||
# واقعی هنوز رفرنسی برنگردونده) مشمول این قید نمیشن.
|
||||
# یکتاییِ سراسریِ (gateway, gateway_ref_id) برای refهای غیرخالی:
|
||||
# یک ref مشخص (مثلاً Authorityِ زرینپال) فقط میتونه متعلقِ یک
|
||||
# Order باشه — هم جلوی duplicate رو میگیره هم جلوی reuse/replay
|
||||
# برای Orderِ دیگه (نگاه کنید به apply_order_payment_result).
|
||||
# gateway_ref_id__gt='' یعنی refِ خالی مشمول نیست.
|
||||
models.UniqueConstraint(
|
||||
fields=['gateway', 'gateway_ref_id'],
|
||||
condition=models.Q(gateway_ref_id__gt=''),
|
||||
|
||||
@@ -56,12 +56,9 @@ class SecretDefaultsFailClosedTests(TestCase):
|
||||
self.assertTrue(HasWebhookSecret().has_permission(webhook_request, None))
|
||||
|
||||
def test_debug_mode_local_fallback_still_valid(self):
|
||||
# settings.py این مقادیر رو یکبار موقعِ import، بر اساسِ DEBUG واقعیِ
|
||||
# همون پروسه محاسبه میکنه (نه چیزی که override_settings در تست
|
||||
# میتونه بعداً toggle کنه). برای تستِ صریحِ همین منطق، ماژولِ
|
||||
# config.settings رو با DJANGO_DEBUG=1 و بدونِ ستبودنِ
|
||||
# INTERNAL_SERVICE_KEY/PAYMENT_WEBHOOK_SECRET دوباره reload میکنیم
|
||||
# و مطمئن میشیم دیفالتِ محلیِ قبلیِ dev هنوز برقراره.
|
||||
# settings.py این مقادیر رو یکبار موقعِ import محاسبه میکنه، نه
|
||||
# چیزی که override_settings بعداً toggle کنه؛ پس برای تستِ این
|
||||
# منطق باید خودِ ماژول رو reload کرد.
|
||||
from config import settings as settings_module
|
||||
|
||||
env_overrides = {'DJANGO_DEBUG': '1'}
|
||||
@@ -227,12 +224,8 @@ class BazaarVerifyPurchaseThrottleTests(TestCase):
|
||||
self.assertEqual(blocked_response.status_code, 429)
|
||||
|
||||
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
|
||||
# ⚠️ توجه: از یک کاربرِ *غیرمهمان* استفاده میشه (نه self.user که
|
||||
# مهمان است)، چون طبقِ فیکسِ GuestAwareBazaarThrottle حالا کلیدِ
|
||||
# bucket برای کاربرهای مهمان عمداً بر اساسِ IP است (نگاه کنید به
|
||||
# BazaarVerifyPurchaseGuestThrottleBypassTests پایینتر) — این تست
|
||||
# فقط رفتارِ per-user (نه IP) برای کاربرهایِ ثبتنامشده رو تضمین
|
||||
# میکنه.
|
||||
# کاربرِ غیرمهمان (نه self.user که مهمانه)، چون کلیدِ throttleِ
|
||||
# مهمانها عمداً بر اساسِ IP است؛ این تست فقط رفتارِ per-user رو میسنجه.
|
||||
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000751')
|
||||
registered_token = issue_access_token(registered_user.id)
|
||||
|
||||
|
||||
@@ -23,25 +23,10 @@ from .services import apply_order_payment_result, create_order
|
||||
|
||||
|
||||
class GuestAwareBazaarThrottle(ScopedRateThrottle):
|
||||
"""
|
||||
⚠️ اضافه شده: همون مشکلِ GuestAwareAIThrottle (ai_tutor/views.py)،
|
||||
GuestAwareFamilyThrottle (family/views.py) و GuestAwareDiscountThrottle/
|
||||
GuestAwarePurchaseThrottle (discount/views.py، subscription/views.py)
|
||||
اینجا هم هست. ScopedRateThrottle استاندارد برای درخواستِ احرازهویتشده
|
||||
از request.user.pk بهعنوانِ کلیدِ throttle استفاده میکنه؛ چون
|
||||
GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید)
|
||||
میسازه، یک مهاجم میتونست با ساختنِ پیدرپیِ حسابهای مهمان، برای هر
|
||||
حساب یک bucket تازه از throttle_scope 'bazaar_verify' بگیره — درحالیکه
|
||||
هر درخواستِ پذیرفتهشده یک فراخوانیِ سرور-به-سرور به API بازار رو
|
||||
تریگر میکنه.
|
||||
|
||||
دقیقاً طبقِ همون الگو: فقط برای کاربرهای مهمان (is_guest=True) کلیدِ
|
||||
cache از pk کاربر به IP کلاینت (get_ident) تغییر میکنه، پس همهی
|
||||
حسابهای مهمانِ ساختهشده از یک IP یک bucket مشترک دارن و چرخشِ حساب
|
||||
دیگه bucket تازه نمیده. رفتارِ throttle برای کاربرهای غیرمهمان
|
||||
(ثبتنامشده) دقیقاً مثلِ قبل (کلید بر اساسِ pk) باقی میمونه؛ منطقِ
|
||||
verify/order ownership/transaction/apply-payment دستنخوردهست.
|
||||
"""
|
||||
"""همون الگوی GuestAwareAIThrottle/GuestAwareFamilyThrottle/etc: چرخشِ
|
||||
حساب مهمان (هر deviceId یه pk تازه) نباید bucket تازهی bazaar_verify
|
||||
بده، چون هر درخواستِ پذیرفتهشده یه فراخوانیِ سرور-به-سرور به بازاره.
|
||||
برای مهمانها کلیدِ cache بر اساسِ IP هست، نه pk."""
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
user = request.user
|
||||
@@ -121,14 +106,10 @@ class PaymentWebhookView(APIView):
|
||||
"""
|
||||
|
||||
permission_classes = [HasWebhookSecret]
|
||||
# ⚠️ اضافه شده: این endpoint قبلاً هیچ throttleای نداشت، درحالیکه فقط
|
||||
# با مقایسهی یک هدرِ ثابت (X-Webhook-Secret) محافظت میشه؛ بدونِ
|
||||
# throttle، تعدادِ نامحدودی تلاش برای حدسِ این سکرت (یا fuzz کردنِ
|
||||
# orderId بعد از افشای احتمالیِ سکرت) ممکن بود، درحالیکه یک حدسِ
|
||||
# درست باعثِ paid شدنِ واقعیِ سفارش و فعالسازیِ اشتراک میشه. چون
|
||||
# این caller یک کاربرِ اپ نیست (خودِ درگاهِ پرداخته)، از یک
|
||||
# ScopedRateThrottleِ سادهٔ IP-محور استفاده شده، نه الگویِ
|
||||
# GuestAware*Throttle (که نیازمندِ request.user است).
|
||||
# این endpoint فقط با مقایسهی هدرِ ثابتِ X-Webhook-Secret محافظت میشه،
|
||||
# پس throttle لازمه تا حدسِ سکرت (یا fuzzِ orderId) محدود بمونه. چون
|
||||
# caller خودِ درگاهه نه یک کاربرِ اپ، از ScopedRateThrottleِ IP-محور
|
||||
# استفاده شده، نه الگویِ GuestAware*Throttle که نیازمندِ request.user است.
|
||||
throttle_scope = 'payment_webhook'
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
@@ -182,19 +163,11 @@ class BazaarVerifyPurchaseView(APIView):
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
# ⚠️ افزوده شده: این endpoint یک فراخوانیِ سرور-به-سرور به API بازار
|
||||
# (verify_purchase) رو تریگر میکنه؛ بدون throttle مخصوص، یک کاربر
|
||||
# میتونست با ارسال مکرر purchaseToken نامعتبر، بار زیادی روی سرویس
|
||||
# بازار و بکاند بندازه. throttle_scope مجزا (bazaar_verify) جدا از
|
||||
# نرخهای دیگه (مثل otp_*) تنظیم شده.
|
||||
# ⚠️ اصلاح شده: چون کاربرهای مهمان (GuestSessionView) با هر deviceId
|
||||
# جدید یک pk تازه میگیرن، ScopedRateThrottleِ ساده (کلید بر اساسِ
|
||||
# user.pk) توسطِ چرخشِ حساب مهمان قابلدورزدن بود. حالا از
|
||||
# GuestAwareBazaarThrottle استفاده میشه که برای کاربرهای مهمان کلید
|
||||
# رو بر اساسِ IP میسازه و برای کاربرهای غیرمهمان رفتارِ per-user
|
||||
# قبلی رو دستنخورده نگه میداره. نرخ هم از ۲۰/hour به ۱۰/hour
|
||||
# سختگیرانهتر شد چون هر درخواستِ پذیرفتهشده یک تماسِ سرور-به-سرورِ
|
||||
# واقعی با بازار میزنه.
|
||||
# این endpoint یک فراخوانیِ سرور-به-سرورِ واقعی به API بازار (verify_purchase)
|
||||
# تریگر میکنه، برای همین throttle_scope مجزا و نرخِ سختگیرانهتر (۱۰/hour)
|
||||
# داره. کاربرهای مهمان با هر deviceId یک pk تازه میگیرن، پس
|
||||
# GuestAwareBazaarThrottle برای اونها بر اساسِ IP کلید میسازه تا چرخشِ
|
||||
# حساب bucket تازه نگیره؛ برای کاربرهای غیرمهمان رفتارِ per-user حفظ میشه.
|
||||
throttle_scope = 'bazaar_verify'
|
||||
throttle_classes = [GuestAwareBazaarThrottle]
|
||||
|
||||
@@ -208,13 +181,10 @@ class BazaarVerifyPurchaseView(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
v = serializer.validated_data
|
||||
|
||||
# ⚛️🔒 اصلاح: فراخوانی HTTP به API بازار (تا ~۲۰ ثانیه، دو درخواست
|
||||
# پشتسرهم برای refresh token + verify) قبلاً *داخل* transaction.atomic
|
||||
# و زیر select_for_update روی Order انجام میشد؛ یعنی ردیف Order (و
|
||||
# هر تراکنش دیگهای که بخواد همون Order یا Subscription مرتبط رو لمس
|
||||
# کنه) تا برگشتِ پاسخ بازار قفل میموند. حالا verify_purchase قبل از
|
||||
# باز شدن atomic/lock صدا زده میشه؛ لاک فقط برای اعمال نتیجه (چند
|
||||
# میلیثانیه) گرفته میشه.
|
||||
# verify_purchase (تا ~۲۰ ثانیه، دو درخواستِ پشتسرهم) عمداً *قبل* از
|
||||
# بازشدنِ atomic/lock صدا زده میشه، نه داخلش — وگرنه ردیفِ Order (و هر
|
||||
# تراکنشِ دیگهای روی همون Order/Subscription) تا برگشتِ پاسخِ بازار
|
||||
# قفل میموند. لاک فقط برای اعمالِ نتیجه (چند میلیثانیه) گرفته میشه.
|
||||
order_lookup = get_object_or_404(
|
||||
Order, pk=v['orderId'], user_id=request.user.id, provider='bazaar',
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user