throttling, abuse-protection & logging/monitoring pass
This commit is contained in:
@@ -90,6 +90,11 @@ MIDDLEWARE = [
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
# ⚠️ افزوده شده: لاگِ سبکِ هر درخواستِ API (متد/مسیر/وضعیت/مدت/کاربر)؛
|
||||
# عمداً آخرِ لیست قرار گرفته تا نزدیکترین میانافزار به خودِ view باشه
|
||||
# (مدتزمانِ دقیقتر، و request.user که توسطِ DRF authentication در
|
||||
# حینِ پردازشِ view ست میشه، در دسترس باشه).
|
||||
'config.middleware.APIRequestLoggingMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'config.urls'
|
||||
@@ -208,10 +213,44 @@ 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': int(os.environ.get('NUM_PROXIES', '1')),
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'otp_request': '5/hour',
|
||||
'otp_login': '10/hour',
|
||||
'guest_session': '20/hour',
|
||||
'bazaar_verify': '10/hour',
|
||||
'ai_ask': '20/hour',
|
||||
'purchase': '10/hour',
|
||||
'token_refresh': '30/hour',
|
||||
'question_answer': '60/min',
|
||||
'lesson_complete': '10/hour',
|
||||
'support_create': '30/hour',
|
||||
'offer_redeem': '10/hour',
|
||||
'discount_validate': '20/hour',
|
||||
'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',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -243,6 +282,23 @@ REFRESH_TOKEN_TTL_DAYS = 30
|
||||
OTP_LOGIN_PHONE_MAX_ATTEMPTS = 10
|
||||
OTP_LOGIN_PHONE_WINDOW_SECONDS = 3600
|
||||
|
||||
# محدودیتِ صدورِ/ارسالِ دوبارهِ OTP بر اساسِ شمارهتلفن، مستقل از throttle
|
||||
# IP-محورِ otp_request؛ جلوی flood کردنِ پیامک به یک شماره از طریق چند IP
|
||||
# مختلف رو میگیره (فقط تعدادِ درخواست رو محدود میکنه، نه شمارهی IP رو).
|
||||
OTP_REQUEST_PHONE_MAX_ATTEMPTS = 5
|
||||
OTP_REQUEST_PHONE_WINDOW_SECONDS = 3600
|
||||
|
||||
# حداقل فاصلهی زمانی بین دو درخواستِ صدورِ OTP برای همون شماره. این مستقل
|
||||
# از سقفِ ساعتیِ بالاست: سقفِ ساعتی جلویِ حجمِ کلی رو میگیره اما جلویِ
|
||||
# burst رو نمیگیره (مثلاً هر ۵ تلاشِ مجاز در عرضِ کمتر از یک ثانیه). این
|
||||
# cooldown دقیقاً همون burst رو هدف میگیره؛ نه جایگزینِ سقفِ ساعتیه، نه
|
||||
# باهاش redundant — هر کدوم یک بُعدِ متفاوتِ سوءاستفاده رو میبندن.
|
||||
# ⚠️ تصمیمِ محصول/امنیت: کاربر واقعاً باید هر ۲ دقیقه بتونه دوباره OTP
|
||||
# بگیره؛ enforcement سمتِ سرور هم باید دقیقاً همین مقدار باشه — countdown
|
||||
# سمتِ فرانت صرفاً UX هست و نباید مرجعِ امنیتی باشه، پس این مقدار همیشه
|
||||
# باید با چیزی که فرانت به کاربر نشون میده یکی باشه.
|
||||
OTP_REQUEST_PHONE_COOLDOWN_SECONDS = 120
|
||||
|
||||
# در محیط dev، این کد OTP همیشه معتبر است تا بدون SMS واقعی تست شود.
|
||||
# فقط وقتی DEBUG=True مقدار دارد؛ در پروداکشن None است.
|
||||
DEV_MASTER_OTP = '0000' if DEBUG else None
|
||||
@@ -307,3 +363,59 @@ BAZAAR_API_BASE = os.environ.get('BAZAAR_API_BASE', 'https://pardakht.cafebazaar
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Logging (آمادهسازیِ لاگهای موجود برای جمعآوریِ متمرکز — Loki/ELK-style)
|
||||
# -----------------------------------------------------------------------
|
||||
# ⚠️ افزوده شده: پروژه از قبل چندین 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,
|
||||
'formatters': {
|
||||
'json': {
|
||||
'()': 'config.log_formatters.JSONLogFormatter',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'json',
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
}
|
||||
|
||||
# ⚠️ افزوده شده (یکپارچهسازیِ 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 = os.environ.get('LOG_DIR', '')
|
||||
|
||||
if LOG_DIR:
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
LOGGING['handlers']['file'] = {
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': os.path.join(LOG_DIR, 'app.log'),
|
||||
'maxBytes': 10 * 1024 * 1024, # 10MB در هر فایل، قبل از rotate
|
||||
'backupCount': 5,
|
||||
'formatter': 'json',
|
||||
}
|
||||
LOGGING['root']['handlers'].append('file')
|
||||
|
||||
Reference in New Issue
Block a user