Harden auth, AI tutor, payments, and subscriptions
This commit is contained in:
@@ -13,6 +13,8 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
@@ -20,13 +22,23 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-jn!s*25lz^+bjm101tzmlz8jlj70i0qka++*h-(!0s^^vk-0ss'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
# Defaults to False; must be explicitly enabled via env for local dev.
|
||||
DEBUG = os.environ.get('DJANGO_DEBUG', '') == '1'
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
# No hardcoded fallback when DEBUG=False — fails closed at startup instead.
|
||||
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
|
||||
if not SECRET_KEY:
|
||||
if DEBUG:
|
||||
SECRET_KEY = 'django-insecure-jn!s*25lz^+bjm101tzmlz8jlj70i0qka++*h-(!0s^^vk-0ss'
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
'DJANGO_SECRET_KEY environment variable is not set. '
|
||||
'A predictable fallback secret key cannot be used when DEBUG=False.'
|
||||
)
|
||||
|
||||
ALLOWED_HOSTS = [h for h in os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',') if h]
|
||||
|
||||
|
||||
# Application definition
|
||||
@@ -110,6 +122,31 @@ DATABASES = {
|
||||
}
|
||||
}
|
||||
|
||||
# In production a shared cache is required for rate limiting to work across
|
||||
# workers/instances; falls back to LocMemCache only when DEBUG=True.
|
||||
CACHE_URL = os.environ.get('CACHE_URL', '')
|
||||
|
||||
if CACHE_URL:
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||
'LOCATION': CACHE_URL,
|
||||
}
|
||||
}
|
||||
elif DEBUG:
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
}
|
||||
}
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
'CACHE_URL environment variable is not set. '
|
||||
'A shared cache is required when DEBUG=False, since rate limits '
|
||||
'(OTP throttles and the phone-based login limit) rely on a cache '
|
||||
'shared across all workers/instances; LocMemCache cannot be used in production.'
|
||||
)
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
@@ -168,6 +205,14 @@ REST_FRAMEWORK = {
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
],
|
||||
'DEFAULT_THROTTLE_CLASSES': [
|
||||
'rest_framework.throttling.ScopedRateThrottle',
|
||||
],
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'otp_request': '5/hour',
|
||||
'otp_login': '10/hour',
|
||||
'guest_session': '20/hour',
|
||||
},
|
||||
}
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
@@ -194,9 +239,22 @@ SPECTACULAR_SETTINGS = {
|
||||
ACCESS_TOKEN_TTL_SECONDS = 3600
|
||||
REFRESH_TOKEN_TTL_DAYS = 30
|
||||
|
||||
# در محیط توسعه/تست، این کد OTP همیشه معتبره تا بدون نیاز به SMS واقعی
|
||||
# بشه لاگین رو با Swagger تست کرد. در پروداکشن حتماً حذف بشه.
|
||||
DEV_MASTER_OTP = '0000'
|
||||
# محدودیت تلاش ورود بر اساس شماره تلفن، مستقل از throttle IP-محورِ otp_login.
|
||||
OTP_LOGIN_PHONE_MAX_ATTEMPTS = 10
|
||||
OTP_LOGIN_PHONE_WINDOW_SECONDS = 3600
|
||||
|
||||
# در محیط dev، این کد OTP همیشه معتبر است تا بدون SMS واقعی تست شود.
|
||||
# فقط وقتی DEBUG=True مقدار دارد؛ در پروداکشن None است.
|
||||
DEV_MASTER_OTP = '0000' if DEBUG else None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# OTP SMS delivery (accounts/services.py: send_otp_sms)
|
||||
# -----------------------------------------------------------------------
|
||||
# در پروداکشن send_otp_sms از طریق کاوهنگار پیامک واقعی میفرستد.
|
||||
# بدون تنظیم این env، ارسال باید صراحتاً خطا بدهد، نه silently موفق باشد.
|
||||
KAVENEGAR_API_KEY = os.environ.get('KAVENEGAR_API_KEY', '')
|
||||
KAVENEGAR_SENDER = os.environ.get('KAVENEGAR_SENDER', '')
|
||||
OTP_SMS_TEMPLATE = os.environ.get('OTP_SMS_TEMPLATE', 'کد ورود شما به تدریسینو: {code}')
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Content progress (NodeProgress status)
|
||||
@@ -212,23 +270,21 @@ NODE_PROGRESS_ABANDON_DAYS = 14
|
||||
# -----------------------------------------------------------------------
|
||||
# Payment security
|
||||
# -----------------------------------------------------------------------
|
||||
# POST /api/payment/orders طبق سند یک فراخوانی سرور-به-سرور از اپ
|
||||
# Subscriptionه (نه از کلاینت). در عمل subscription/views.py مستقیماً
|
||||
# تابع payment.services.create_order رو import و صدا میزنه، پس این
|
||||
# endpoint اصلاً لازم نیست از بیرون صدا زده بشه؛ اما چون در urls.py
|
||||
# رجیستر و AllowAnyه، هر کلاینتی میتونه با یک userId دلخواه سفارش بسازه.
|
||||
# برای بستن این حفره، درخواست باید هدر X-Internal-Key رو برابر همین مقدار
|
||||
# بفرسته؛ در پروداکشن حتماً از env خونده بشه و یک مقدار تصادفی/طولانی باشه.
|
||||
INTERNAL_SERVICE_KEY = os.environ.get('INTERNAL_SERVICE_KEY', 'dev-internal-service-key-change-me')
|
||||
# POST /api/payment/orders باید فقط سرور-به-سرور (از subscription) صدا زده شود؛
|
||||
# کلاینت باید هدر X-Internal-Key برابر همین مقدار بفرستد. بدون env در
|
||||
# پروداکشن مقدار خالی میماند و HasInternalServiceKey صراحتاً رد میکند.
|
||||
INTERNAL_SERVICE_KEY = os.environ.get(
|
||||
'INTERNAL_SERVICE_KEY',
|
||||
'dev-internal-service-key-change-me' if DEBUG else '',
|
||||
)
|
||||
|
||||
# POST /api/payment/webhook قراره از درگاه پرداخت بیاد، نه از یک کلاینت
|
||||
# دلخواه؛ بدون امضا هر کسی میتونه با یک orderId معتبر status=paid بفرسته
|
||||
# و بدون پرداخت واقعی، اشتراک رو فعال کنه. درگاههای واقعی (زرینپال و غیره)
|
||||
# امضای HMAC واقعی خودشون رو دارن؛ اینجا چون درگاه واقعی وصل نیست، بهجای
|
||||
# HMAC یک shared-secret ساده به اسم X-Webhook-Secret استفاده شده — همون
|
||||
# سطح حفاظتی که در سند API برای این endpoint خواسته شده. برای اتصال واقعی
|
||||
# این باید با بررسی امضای HMAC درگاه جایگزین بشه.
|
||||
PAYMENT_WEBHOOK_SECRET = os.environ.get('PAYMENT_WEBHOOK_SECRET', 'dev-webhook-secret-change-me')
|
||||
# POST /api/payment/webhook باید از درگاه پرداخت بیاید، نه یک کلاینت دلخواه.
|
||||
# فعلاً یک shared-secret ساده (X-Webhook-Secret) استفاده میشود؛ در اتصال
|
||||
# واقعی باید با بررسی امضای HMAC درگاه جایگزین شود.
|
||||
PAYMENT_WEBHOOK_SECRET = os.environ.get(
|
||||
'PAYMENT_WEBHOOK_SECRET',
|
||||
'dev-webhook-secret-change-me' if DEBUG else '',
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Bazaar In-App Billing (پرداخت درونبرنامهای کافهبازار — REST API v2)
|
||||
|
||||
Reference in New Issue
Block a user