throttling, abuse-protection & logging/monitoring pass

This commit is contained in:
2026-08-23 23:44:00 +03:30
parent cb18c93a3e
commit 80298b47b1
33 changed files with 4202 additions and 23 deletions

View File

@@ -5,6 +5,7 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.throttling import ScopedRateThrottle
from rest_framework.views import APIView
from payment.models import Order
@@ -19,6 +20,41 @@ from .serializers import (
)
class GuestAwarePurchaseThrottle(ScopedRateThrottle):
"""
⚠️ اضافه شده: همون مشکلِ GuestAwareAIThrottle (ai_tutor/views.py)،
GuestAwareFamilyThrottle (family/views.py) و GuestAwareDiscountThrottle
(discount/views.py) اینجا هم هست. ScopedRateThrottle استاندارد برای
درخواستِ احرازهویت‌شده از request.user.pk به‌عنوانِ کلیدِ throttle
استفاده می‌کنه؛ چون GuestSessionView با هر deviceId جدید یک کاربرِ
مهمانِ تازه (pk جدید) می‌سازه، یک مهاجم می‌تونست با ساختنِ پی‌درپیِ
حساب‌های مهمان (تا سقفِ guest_session=20/hour)، برای هر حساب یک
bucket تازه از throttle_scope 'purchase' بگیره — یعنی سقفِ ۱۰/ساعتِ
درخواستِ purchase عملاً با چرخشِ حساب مهمان چند برابر می‌شد. این
endpoint حساسِ مالیه: هر درخواستِ پذیرفته‌شده وارد جریانِ purchase
می‌شه، یک Subscription می‌سازه/آپدیت می‌کنه و create_order() رو صدا
می‌زنه (که در پروداکشن با درگاهِ واقعی جایگزینِ stub فعلی می‌شه).
دقیقاً طبقِ همون الگو: فقط برای کاربرهای مهمان (is_guest=True) کلیدِ
cache از pk کاربر به IP کلاینت (get_ident) تغییر می‌کنه، پس همه‌ی
حساب‌های مهمانِ ساخته‌شده از یک IP یک bucket مشترک دارن و چرخشِ حساب
دیگه bucket تازه نمی‌ده. رفتارِ throttle برای کاربرهای غیرمهمان
(ثبت‌نام‌شده) دقیقاً مثلِ قبل (کلید بر اساسِ pk) باقی می‌مونه؛ منطقِ
purchase/subscription/order/Idempotency-Key/plan-validation دست‌نخورده‌ست.
"""
def get_cache_key(self, request, view):
user = request.user
if user and user.is_authenticated and getattr(user, 'is_guest', False):
ident = f'guest-ip:{self.get_ident(request)}'
elif user and user.is_authenticated:
ident = user.pk
else:
ident = self.get_ident(request)
return self.cache_format % {'scope': self.scope, 'ident': ident}
class PlansListView(APIView):
"""GET /api/subscription/plans — list available plans."""
@@ -49,6 +85,22 @@ class PurchaseView(APIView):
to create an Order."""
permission_classes = [IsAuthenticated]
# scope کیدهای cache رو با pk کاربرِ احرازهویت‌شده می‌سازه (نه فقط IP)؛
# چون Idempotency-Key به‌تنهایی جلوی حجمِ بالای درخواست‌های *متفاوت*
# (با کلیدهای idempotency متفاوت) رو نمی‌گیره، این throttle مستقل و
# مکملِ اون محافظته. ScopedRateThrottle خودش وقتی request.user
# احرازهویت‌شده باشه (is_authenticated=True) به‌جای IP از
# request.user.pk به‌عنوانِ ident استفاده می‌کنه، پس تغییرِ IP از
# همون اکانت این محدودیت رو دور نمی‌زنه.
# ⚠️ اصلاح شده: چون کاربرهای مهمان (GuestSessionView) با هر deviceId
# جدید یک pk تازه می‌گیرن، ScopedRateThrottleِ ساده (کلید بر اساسِ
# user.pk) توسطِ چرخشِ حساب مهمان قابل‌دورزدن بود. حالا از
# GuestAwarePurchaseThrottle استفاده می‌شه که برای کاربرهای مهمان
# کلید رو بر اساسِ IP می‌سازه (دقیقاً همون الگویِ GuestAwareAIThrottle،
# GuestAwareFamilyThrottle و GuestAwareDiscountThrottle) و برای
# کاربرهای غیرمهمان رفتارِ per-user قبلی رو دست‌نخورده نگه می‌داره.
throttle_scope = 'purchase'
throttle_classes = [GuestAwarePurchaseThrottle]
@extend_schema(
request=PurchaseRequestSerializer,