cleanup: trim bloated multi-line comments across the project

This commit is contained in:
2026-08-26 03:22:20 +03:30
parent 83435c5a5f
commit 683bd40169
27 changed files with 235 additions and 699 deletions

View File

@@ -26,24 +26,11 @@ from .serializers import (
class GuestAwareContentThrottle(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 'question_answer'/'lesson_complete'
بگیره و محدودیتِ واقعی رو دور بزنه — درحالی‌که هر دو endpoint اثرِ
جانبیِ ماندگار دارن (UserAnswer + کسرِ هارت، یا UserLessonCompletion +
xp/coins/progress).
دقیقاً طبقِ همون الگو: فقط برای کاربرهای مهمان (is_guest=True) کلیدِ
cache از pk کاربر به IP کلاینت (get_ident) تغییر می‌کنه، پس همه‌ی
حساب‌های مهمانِ ساخته‌شده از یک IP یک bucket مشترک دارن و چرخشِ حساب
دیگه bucket تازه نمی‌ده. رفتارِ throttle برای کاربرهای غیرمهمان
(ثبت‌نام‌شده) دقیقاً مثلِ قبل (کلید بر اساسِ pk) باقی می‌مونه؛ منطقِ
answer/heart/progress/completion/idempotency دست‌نخورده‌ست.
مشترک با GuestAwareAIThrottle/GuestAwareFamilyThrottle/GuestAwareDiscountThrottle
و مشابه‌هاش: چون GuestSessionView با هر deviceId جدید یک pk تازه می‌سازه،
throttleِ استانداردِ بر اساسِ user.pk با چرخشِ حسابِ مهمان دور زده می‌شه —
درحالی‌که answer/complete اثرِ جانبیِ ماندگار (هارت، xp/coins/progress) دارن.
برای همین فقط کاربرهای مهمان با IP کلید می‌خورن؛ کاربرهای غیرمهمان مثلِ قبل بر اساسِ pk.
"""
def get_cache_key(self, request, view):
@@ -165,18 +152,9 @@ class QuestionAnswerView(APIView):
# userId همیشه از request.user.id گرفته می‌شود، نه ورودی کلاینت.
permission_classes = [IsAuthenticated]
# Idempotency-Key فقط جلوی retry با همون کلید رو می‌گیره، نه حجمِ بالای
# درخواست‌های *متفاوت* (بدون کلید یا با کلیدهای متفاوت) که هرکدوم یک
# UserAnswer جدید می‌سازن و منطقِ هارت/NodeProgress رو اجرا می‌کنن —
# حتی وقتی هارتِ کاربر صفر شده. ScopedRateThrottle وقتی request.user
# احرازهویت‌شده باشه با pk کاربر کلید می‌خوره (نه فقط IP)، پس تغییرِ
# IP این محدودیت رو دور نمی‌زنه.
# ⚠️ اصلاح شده: چون کاربرهای مهمان (GuestSessionView) با هر deviceId
# جدید یک pk تازه می‌گیرن، ScopedRateThrottleِ ساده (کلید بر اساسِ
# user.pk) توسطِ چرخشِ حساب مهمان قابل‌دورزدن بود. حالا از
# GuestAwareContentThrottleِ مشترک استفاده می‌شه که برای کاربرهای
# مهمان کلید رو بر اساسِ IP می‌سازه و برای کاربرهای غیرمهمان رفتارِ
# per-user قبلی رو دست‌نخورده نگه می‌داره.
# Idempotency-Key فقط جلوی retry با همون کلید رو می‌گیره، نه حجمِ درخواست‌های
# *متفاوت* که هرکدوم UserAnswer جدید می‌سازن و هارت/NodeProgress رو تغییر می‌دن؛
# برای همون GuestAwareContentThrottle لازمه (نگاه کنید به تعریفش بالا).
throttle_scope = 'question_answer'
throttle_classes = [GuestAwareContentThrottle]
@@ -274,20 +252,12 @@ class QuestionAnswerView(APIView):
class LessonCompleteView(APIView):
"""POST /api/lessons/{id}/complete — اتمام درس + آمار جدید"""
# ⚠️ اصلاح امنیتی: قبلاً userId از query/body کلاینت خونده می‌شد که به
# کلاینت اجازه می‌داد به‌جای هر کاربر دیگری xp/coins ثبت کنه. حالا auth
# اجباریه و همیشه از request.user.id استفاده می‌شه.
# userId همیشه از request.user.id گرفته می‌شود (نه ورودیِ کلاینت)، وگرنه کلاینت
# می‌تونست به‌جای هر کاربرِ دیگری xp/coins ثبت کنه.
permission_classes = [IsAuthenticated]
# Idempotency-Key فقط جلوی retry با همون کلید رو می‌گیره، نه حجمِ بالای
# درخواست‌های *متفاوت* (بدون کلید یا با کلیدهای متفاوت) که هرکدوم یک
# UserLessonCompletion جدید می‌سازن و سیگنالِ post_save (NodeProgress) +
# به‌روزرسانیِ UserStats رو اجرا می‌کنن. ScopedRateThrottle با pk
# کاربرِ احرازهویت‌شده کلید می‌خوره، نه IP.
# ⚠️ اصلاح شده: همون مشکلِ چرخشِ حسابِ مهمان (نگاه کنید به
# GuestAwareContentThrottle بالا) اینجا هم بود؛ حالا از همون کلاسِ
# مشترک استفاده می‌شه. نرخ هم از ۶۰/min به ۱۰/hour تغییر کرد چون این
# endpoint رکوردِ completion ماندگار می‌سازه و xp/coins/progress رو
# واقعاً تغییر می‌ده (برخلافِ question_answer که فرکانسِ طبیعی‌اش بالاتره).
# Idempotency-Key فقط جلوی retry با همون کلید رو می‌گیره؛ برای همینه که
# GuestAwareContentThrottle لازمه (نگاه کنید به تعریفش بالا). نرخ ۱۰/hour
# (نه ۶۰/min مثلِ question_answer) چون این endpoint رکوردِ ماندگار می‌سازه.
throttle_scope = 'lesson_complete'
throttle_classes = [GuestAwareContentThrottle]
@@ -352,21 +322,12 @@ class LessonCompleteView(APIView):
}
return Response(LessonCompleteResponseSerializer(data).data)
# ⚛️ atomic: UserLessonCompletion.create پایین‌تر یک سیگنال post_save
# رو تریگر می‌کنه (content/receivers.py → NodeProgress + احتمالاً
# StatusChangeLog)؛ به‌علاوه‌ی get_or_create/save روی UserStats.
# بدون atomic، اگه save روی stats fail کنه، تکمیلِ درس ثبت شده ولی
# xp/coins/progress_percent به‌روز نشده — یعنی چون تکمیل هر درس
# یک‌بارمصرفه، کاربر دیگه هیچ‌وقت اون xp رو نمی‌گیره.
#
# ⚛️🔒 race condition: چکِ «is_first_completion» قبلاً قبل از atomic
# و بدون قفل انجام می‌شد — دو درخواستِ همزمان (دابل‌کلیک/رتریِ
# کلاینت) برای تکمیلِ همون درس می‌تونستن هر دو ببینن که هنوز کامل
# نشده و هر دو xp/coins کامل بگیرن (دوبرابر شدنِ جایزه). حالا ردیفِ
# UserStats کاربر قفل می‌شه (select_for_update) و چکِ
# is_first_completion داخلِ همون قفل، بعد از لاک‌شدن، دوباره انجام
# می‌شه — یعنی درخواستِ دومِ همزمان، حالا که اولی commit شده، به‌درستی
# می‌بینه که دیگه first completion نیست.
# atomic لازمه چون create پایین‌تر سیگنالِ post_save رو تریگر می‌کنه
# (NodeProgress) و UserStats هم آپدیت می‌شه؛ بدون اون، fail شدنِ save
# روی stats یعنی تکمیل ثبت شده ولی xp/coins یک‌بارمصرف از دست رفته.
# ردیفِ UserStats قفل می‌شه (select_for_update) و چکِ is_first_completion
# داخلِ همون قفل انجام می‌شه تا دو درخواستِ همزمان (دابل‌کلیک) هردو
# جایزه‌ی کاملِ اول رو نگیرن.
try:
with transaction.atomic():
UserStats.objects.get_or_create(user_id=user_id)