cleanup: trim bloated multi-line comments across the project
This commit is contained in:
@@ -227,21 +227,11 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLE_CLASSES': [
|
||||
'rest_framework.throttling.ScopedRateThrottle',
|
||||
],
|
||||
# ⚠️ اضافه شده (امنیتی): بدونِ NUM_PROXIES، DRF (SimpleRateThrottle.get_ident)
|
||||
# هدرِ X-Forwarded-For رو - اگه در درخواست وجود داشته باشه - عیناً و
|
||||
# بدونچک بهعنوانِ IP کلاینت قبول میکنه؛ یعنی هر کلاینتِ ناشناس میتونست
|
||||
# با فرستادنِ یک X-Forwarded-For دلخواه/تصادفی در هر درخواست، برای
|
||||
# هر GuestAware*Throttle (content/ai_tutor/subscription/payment/discount/
|
||||
# support/family/affiliate) یک IP-bucket تازه بگیره - دقیقاً همون
|
||||
# bypassِ چرخشِ حساب مهمان که این throttle-classها قرار بود ببندن، فقط
|
||||
# از طریقِ هدر بهجای deviceId جدید.
|
||||
# با NUM_PROXIES=1 (پیشفرض؛ برای دیپلویِ پشتِ یک لایهی پراکسی/LB که
|
||||
# خودش X-Forwarded-For رو ست/rewrite میکنه، نه پاسدادنِ مقدارِ
|
||||
# کلاینت)، DRF از انتهای زنجیرهی XFF دقیقاً همون تعداد hop رو
|
||||
# میخونه. اگه دیپلویِ پروداکشن تعداد لایهی پراکسیِ دیگهای داره
|
||||
# (مثلاً Cloudflare + Nginx = ۲ لایه)، این مقدار باید با env
|
||||
# NUM_PROXIES مطابقِ همون تعداد ست بشه، وگرنه یا IP واقعی اشتباه
|
||||
# خونده میشه یا (اگه صفر باشه) دوباره همون مشکلِ بالا برمیگرده.
|
||||
# بدونِ NUM_PROXIES، DRF هدرِ X-Forwarded-For رو بدونچک بهعنوانِ IP
|
||||
# کلاینت قبول میکنه؛ یعنی هر کلاینت میتونست با XFF دلخواه، bucket
|
||||
# تازه از GuestAware*Throttleها بگیره (bypassِ چرخشِ حساب مهمان). این
|
||||
# مقدار باید دقیقاً با تعدادِ لایهی پراکسی/LB جلوی اپ یکی باشه (مثلاً
|
||||
# Cloudflare+Nginx=۲)، وگرنه IP واقعی اشتباه خونده میشه.
|
||||
'NUM_PROXIES': int(os.environ.get('NUM_PROXIES', '1')),
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'otp_request': '5/hour',
|
||||
@@ -259,9 +249,6 @@ REST_FRAMEWORK = {
|
||||
'discount_redeem': '10/hour',
|
||||
'family_invite_accept': '10/hour',
|
||||
'family_invite_create': '5/hour',
|
||||
# ⚠️ اضافه شده: چهار endpointی که در ممیزیِ throttleِ امروز بدونِ
|
||||
# throttle_scope پیدا شدن (نگاه کنید به توضیحاتِ throttle_scope
|
||||
# مربوطه در accounts/views.py، affiliate/views.py، payment/views.py).
|
||||
'user_profile_write': '60/hour',
|
||||
'affiliate_track': '10/hour',
|
||||
'payment_webhook': '60/hour',
|
||||
@@ -292,13 +279,9 @@ SPECTACULAR_SETTINGS = {
|
||||
ACCESS_TOKEN_TTL_SECONDS = 3600
|
||||
REFRESH_TOKEN_TTL_DAYS = 30
|
||||
|
||||
# سقفِ تعدادِ گیرنده برای اکشنِ ادمینِ send_sms_to_selected (هم برای
|
||||
# هدفگیریِ 'selected' و هم 'inactive'). این اکشن هنوز کاملاً بلادرنگ و
|
||||
# synchronous توی همون request اجرا میشه (بدون صفِ پسزمینه)، پس این سقف
|
||||
# صرفاً جلویِ یک ارسالِ ناخواستهی خیلی بزرگ (که میتونه request رو تایماوت
|
||||
# کنه یا پروایدرِ پیامک رو overload کنه) رو میگیره؛ اگر تعدادِ گیرندهها
|
||||
# بیشتر از این باشه، اکشن هیچ پیامکی ارسال نمیکنه و از ادمین میخواد
|
||||
# بازه/انتخاب رو محدودتر کنه.
|
||||
# سقفِ گیرندهی اکشنِ ادمینِ send_sms_to_selected؛ این اکشن synchronous توی
|
||||
# همون requestه (بدون صفِ پسزمینه)، پس این سقف جلویِ تایماوت/overloadِ
|
||||
# provider رو میگیره — بیشتر از سقف یعنی هیچ پیامکی ارسال نمیشه.
|
||||
ADMIN_SMS_MAX_RECIPIENTS = int(os.environ.get('ADMIN_SMS_MAX_RECIPIENTS', 200))
|
||||
|
||||
# محدودیت تلاش ورود بر اساس شماره تلفن، مستقل از throttle IP-محورِ otp_login.
|
||||
@@ -311,15 +294,9 @@ OTP_LOGIN_PHONE_WINDOW_SECONDS = 3600
|
||||
OTP_REQUEST_PHONE_MAX_ATTEMPTS = 5
|
||||
OTP_REQUEST_PHONE_WINDOW_SECONDS = 3600
|
||||
|
||||
# حداقل فاصلهی زمانی بین دو درخواستِ صدورِ OTP برای همون شماره. این مستقل
|
||||
# از سقفِ ساعتیِ بالاست: سقفِ ساعتی جلویِ حجمِ کلی رو میگیره اما جلویِ
|
||||
# burst رو نمیگیره (مثلاً هر ۵ تلاشِ مجاز در عرضِ کمتر از یک ثانیه). این
|
||||
# cooldown دقیقاً همون burst رو هدف میگیره؛ نه جایگزینِ سقفِ ساعتیه، نه
|
||||
# باهاش redundant — هر کدوم یک بُعدِ متفاوتِ سوءاستفاده رو میبندن.
|
||||
# ⚠️ تصمیمِ محصول/امنیت: کاربر واقعاً باید هر ۲ دقیقه بتونه دوباره OTP
|
||||
# بگیره؛ enforcement سمتِ سرور هم باید دقیقاً همین مقدار باشه — countdown
|
||||
# سمتِ فرانت صرفاً UX هست و نباید مرجعِ امنیتی باشه، پس این مقدار همیشه
|
||||
# باید با چیزی که فرانت به کاربر نشون میده یکی باشه.
|
||||
# حداقل فاصله بین دو درخواستِ OTP برای همون شماره؛ مستقل از سقفِ ساعتیِ بالا
|
||||
# (که جلویِ burst رو نمیگیره، مثلاً ۵ تلاش در کمتر از یک ثانیه). این مقدار
|
||||
# باید با countdownِ فرانت یکی باشه — countdown صرفاً UX است، نه مرجعِ امنیتی.
|
||||
OTP_REQUEST_PHONE_COOLDOWN_SECONDS = 120
|
||||
|
||||
# در محیط dev، این کد OTP همیشه معتبر است تا بدون SMS واقعی تست شود.
|
||||
@@ -368,14 +345,10 @@ PAYMENT_WEBHOOK_SECRET = os.environ.get(
|
||||
# -----------------------------------------------------------------------
|
||||
# Bazaar In-App Billing (پرداخت درونبرنامهای کافهبازار — REST API v2)
|
||||
# -----------------------------------------------------------------------
|
||||
# طبق داکیومنت رسمی (developers.cafebazaar.ir/fa/guidelines/in-app-billing)،
|
||||
# صحتسنجی پرداخت بازار یک OAuth2 سرور-به-سروره: یک بار با CLIENT_ID/
|
||||
# CLIENT_SECRET و کد بازگشتی از پنل devapi، REFRESH_TOKEN گرفته میشه (یکبار،
|
||||
# دستی)؛ از اون به بعد بکاند با همین REFRESH_TOKEN، ACCESS_TOKEN تازه
|
||||
# میگیره و با اون به endpoint وضعیت خرید/اشتراک درخواست میزنه.
|
||||
# ⚠️ کافهبازار اواخر ۱۴۰۳ روش صحتسنجی رو تغییر داد؛ قبل از اتصال واقعی،
|
||||
# حتماً آخرین مسیر endpointها رو از همون صفحهی رسمی/پنل devapi خودتون
|
||||
# تایید کنید (BAZAAR_API_BASE پایینی رو بر همون اساس ست کنید).
|
||||
# صحتسنجی OAuth2 سرور-به-سروره: REFRESH_TOKEN یکبار دستی از پنل devapi
|
||||
# گرفته میشه، بعد بکاند با همون REFRESH_TOKEN، ACCESS_TOKEN تازه میگیره.
|
||||
# قبل از اتصال واقعی، مسیرِ endpointها رو از پنل devapi تایید کنید — بازار
|
||||
# این روش رو قبلاً تغییر داده.
|
||||
BAZAAR_PACKAGE_NAME = os.environ.get('BAZAAR_PACKAGE_NAME', 'ir.tadrisino.app')
|
||||
BAZAAR_CLIENT_ID = os.environ.get('BAZAAR_CLIENT_ID', '')
|
||||
BAZAAR_CLIENT_SECRET = os.environ.get('BAZAAR_CLIENT_SECRET', '')
|
||||
@@ -388,18 +361,11 @@ BAZAAR_API_BASE = os.environ.get('BAZAAR_API_BASE', 'https://pardakht.cafebazaar
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Logging (آمادهسازیِ لاگهای موجود برای جمعآوریِ متمرکز — Loki/ELK-style)
|
||||
# Logging — بدونِ یک LOGGING صریح، loggerهای موجودِ اپ (config.middleware،
|
||||
# accounts.services، ...) فقط به rootِ پیشفرض میرسیدن که برای INFO عملاً
|
||||
# خروجیای نداره. خروجی: یک JSON در هر خط روی stdout، سازگار با
|
||||
# Promtail/Filebeat برای جمعآوریِ بعدی توسطِ Loki/ELK.
|
||||
# -----------------------------------------------------------------------
|
||||
# ⚠️ افزوده شده: پروژه از قبل چندین logger (config.middleware،
|
||||
# accounts.services، ai_tutor.views، ...) با logging.getLogger(__name__)
|
||||
# داشت، اما بدونِ یک LOGGING صریح در settings.py، اینا فقط به rootِ
|
||||
# پیشفرضِ کتابخانهی logging میرسیدن (که برای سطحِ INFO عملاً هیچ خروجیای
|
||||
# تولید نمیکنه). این تنظیم، بدونِ اضافه/حذفِ هیچ loggerِ اپلیکیشنی یا
|
||||
# eventِ لاگی، همون پیامهای موجود رو روی stdout و بهصورتِ یک JSON در هر
|
||||
# خط (فرمتِ سازگار با agentِ جمعآوریِ بیرونی مثلِ Promtail/Filebeat که
|
||||
# بعداً میتونه به Loki/ELK بفرسته) چاپ میکنه. هیچ appِ جدید، ذخیرهسازیِ
|
||||
# دیتابیسی، API/UIِ لاگ، یا وابستگیِ Elasticsearch/Loki/Grafana به خودِ
|
||||
# کدِ Django اضافه نشده.
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
@@ -420,16 +386,9 @@ LOGGING = {
|
||||
},
|
||||
}
|
||||
|
||||
# ⚠️ افزوده شده (یکپارچهسازیِ Docker + Promtail/Loki/Grafana): وقتی
|
||||
# env var LOG_DIR ست شده باشه (فقط داخلِ docker-compose.yml، روی یک
|
||||
# ولومِ مشترک با کانتینرِ promtail)، همون خطوطِ JSON که از قبل روی
|
||||
# stdout چاپ میشدن، *علاوه* بر stdout در یک فایل هم نوشته میشن؛
|
||||
# Promtail همون فایل رو tail میکنه و به Loki میفرسته.
|
||||
# این فقط یک هندلرِ خروجیِ اضافه با همون formatter (json) هست — نه
|
||||
# پیامهای لاگ، نه محافظتهای حساسیتیِ موجود (بدونِ OTP/token/رمزعبور و
|
||||
# غیره در پیامها)، و نه هیچ loggerِ اپلیکیشنی تغییر نمیکنه.
|
||||
# وقتی LOG_DIR ست نباشه (یعنی اجرای عادیِ خارج از Docker، طبق README)،
|
||||
# رفتار دقیقاً همون چیزیه که از قبل بود: فقط stdout.
|
||||
# وقتی LOG_DIR ست باشه (فقط داخلِ docker-compose.yml، روی ولومِ مشترک با
|
||||
# promtail)، همون خطوطِ JSON علاوه بر stdout در یک فایل هم نوشته میشن تا
|
||||
# Promtail بتونه tail کنه. بدونش (اجرای عادی خارج از Docker)، فقط stdout.
|
||||
LOG_DIR = os.environ.get('LOG_DIR', '')
|
||||
|
||||
if LOG_DIR:
|
||||
|
||||
@@ -52,15 +52,9 @@ class SecretKeyFailClosedTests(SimpleTestCase):
|
||||
)
|
||||
|
||||
def test_production_configured_secret_key_used(self):
|
||||
# ⚠️ اصلاح (باگِ خودِ تست، نه اپلیکیشن): این تست فقط رفتارِ
|
||||
# SECRET_KEY رو میسنجه، اما چون CACHE_URL رو ست نمیکرد، وقتی
|
||||
# فیکسِ جداگانهی CACHE_URL fail-closed (پایینترِ همین فایل، نگاه
|
||||
# کنید به CacheUrlFailClosedTests) اضافه شد، همین import/reload با
|
||||
# DEBUG=False و بدونِ CACHE_URL بهجای رسیدن به assertion، با
|
||||
# ImproperlyConfigured مربوط به CACHE_URL (نه SECRET_KEY) fail
|
||||
# میشد — یک false negative روی چیزی که این تست اصلاً قرار نیست
|
||||
# بسنجه. یک CACHE_URL معتبر هم ست میکنیم تا این تست فقط دقیقاً
|
||||
# همون چیزی که اسمش میگه (SECRET_KEY) رو بسنجه.
|
||||
# CACHE_URL هم باید ست بشه، وگرنه با DEBUG=False همون
|
||||
# ImproperlyConfiguredِ CACHE_URL رخ میده، نه رسیدن به assertionِ
|
||||
# SECRET_KEY که این تست واقعاً میسنجه.
|
||||
settings_module = self._reload_settings_with_env(
|
||||
env_overrides={
|
||||
'DJANGO_DEBUG': '',
|
||||
@@ -196,24 +190,11 @@ class APIRequestLoggingMiddlewareTests(TestCase):
|
||||
self.assertEqual(records, [])
|
||||
|
||||
def test_unhandled_exception_from_view_is_logged_as_error_and_reraised(self):
|
||||
# 🔒 اثبات فیکسِ این دور: اگه self.get_response(request) خودش یک
|
||||
# exceptionِ دستنخورده raise کنه، میانافزار باید همچنان یک خطِ
|
||||
# لاگِ سطحِ ERROR (متد/مسیر/status=500/مدت/user_id، بدونِ جزئیاتِ
|
||||
# حساس) ثبت کنه و بعد دقیقاً همون exceptionِ اصلی رو دوباره raise
|
||||
# کنه — بدونِ اینکه هیچ رفتارِ Django/DRF یا exceptionِ دیگهای رو
|
||||
# تغییر بده. مستقیماً خودِ میانافزار (نه کلِ چرخهی
|
||||
# exception-handlingِ Django) تست میشه، چون همون لایهایست که این
|
||||
# فیکس روش اعمال شده.
|
||||
#
|
||||
# ⚠️ اصلاح (باگِ خودِ تست، نه اپلیکیشن): قبلاً این تست فقط
|
||||
# record.getMessage() رو چک میکرد. getMessage() خودش داخلی
|
||||
# msg %% args رو انجام میده، پس اگه شمارِ آرگومانها با
|
||||
# placeholderهای رشته جور نباشه، همونجا با یک exception (نه یک
|
||||
# پیامِ نادرست) fail میشد و اصلاً به assertion نمیرسید. برای اینکه
|
||||
# این تست واقعاً *صحتِ formatting* رو بسنجه (نه فقط اینکه یکجوری
|
||||
# fail نشه)، حالا با logging.Formatter().format(record) دقیقاً
|
||||
# همون مسیری که یک handlerِ واقعی (کنسول/فایل) طی میکنه رو صدا
|
||||
# میزنیم و روی متنِ نهاییِ فرمتشده assert میکنیم.
|
||||
# میانافزار باید یک خطِ ERROR (متد/مسیر/status=500/مدت/user_id، بدونِ
|
||||
# جزئیاتِ حساس) ثبت کنه و بعد همون exception رو دوباره raise کنه.
|
||||
# با logging.Formatter().format(record) دقیقاً همون مسیری که یک
|
||||
# handlerِ واقعی طی میکنه رو صدا میزنیم (نه فقط getMessage()، که
|
||||
# یه mismatchِ %%-args رو با exceptionِ خودش میپوشونه، نه fail واقعی).
|
||||
request = RequestFactory().get('/api/some/failing/path')
|
||||
|
||||
def failing_get_response(inner_request):
|
||||
|
||||
Reference in New Issue
Block a user