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

@@ -9,6 +9,7 @@ from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework.exceptions import APIException, Throttled
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 accounts.models import User as AccountUser
@@ -26,6 +27,39 @@ logger = logging.getLogger(__name__)
DAILY_AI_QUOTA = 15
class GuestAwareAIThrottle(ScopedRateThrottle):
"""
⚠️ اضافه شده: ScopedRateThrottle استاندارد برای کاربرِ احرازهویت‌شده
(مهمان یا نه) از request.user.pk به‌عنوانِ کلیدِ throttle استفاده
می‌کنه. چون GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ
تازه (pk جدید) می‌سازه، یک مهاجم می‌تونست با ساختنِ پی‌درپیِ حساب‌های
مهمان، برای هر حساب یک bucket مستقلِ throttle (و همین‌طور کوتای
روزانه‌ی AskAIView که هم بر اساسِ user_id شمارش می‌شه) بگیره — یعنی
نرخِ واقعیِ استفاده از این endpoint گرون‌قیمت به‌جایِ محدود به نرخِ
ai_ask در ساعت، عملاً فقط به نرخِ ساختِ حساب مهمان (guest_session)
محدود می‌شد.
این کلاس فقط برای کاربرهای مهمان (is_guest=True) کلیدِ cache رو از
pk کاربر به IP کلاینت (همون get_ident که guest_session هم استفاده
می‌کنه) تغییر می‌ده؛ یعنی همه‌ی حساب‌های مهمانِ ساخته‌شده از یک IP
مشترکِ throttle bucket ai_ask رو دارن و دیگه با عوض‌کردنِ حساب
نمی‌شه bucket تازه گرفت. رفتارِ throttle برای کاربرهای غیرمهمان
(ثبت‌نام‌شده با شماره) دقیقاً مثلِ قبل (کلید بر اساسِ pk) باقی
می‌مونه؛ منطقِ DAILY_AI_QUOTA و بقیه‌ی view دست‌نخورده‌ست.
"""
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}
def _is_db_ref_id(value) -> bool:
"""
⚠️ افزوده شده: lessonId/chapterId/questionId در قراردادِ API به‌صورتِ
@@ -105,6 +139,13 @@ class AskAIView(APIView):
"""POST /api/ai/ask — سوال از AI (استریم اختیاری؛ در این فاز استریم پیاده نشده)"""
permission_classes = [IsAuthenticated]
# ⚠️ افزوده شده: قبلاً این endpoint هیچ throttle مخصوصی نداشت؛ DAILY_AI_QUOTA
# فقط سقفِ روزانه است و جلوی burst کوتاه‌مدت (مثلاً چند ده درخواست در چند
# ثانیه به generate_tutor_answer) را نمی‌گیرد. throttle_scope مجزا
# (ai_ask) این burst را محدود می‌کند، بدون اینکه به کوتای روزانه/idempotency
# یا منطق AI دست بزند.
throttle_scope = 'ai_ask'
throttle_classes = [GuestAwareAIThrottle]
@extend_schema(
request=AskRequestSerializer,