""" Django settings for config project. Generated by 'django-admin startproject' using Django 5.2.17. For more information on this file, see https://docs.djangoproject.com/en/5.2/topics/settings/ For the full list of settings and their values, see 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 # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # SECURITY WARNING: don't run with debug turned on in production! # Defaults to False; must be explicitly enabled via env for local dev. DEBUG = os.environ.get('DJANGO_DEBUG', '') == '1' # 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 INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 3rd party 'rest_framework', 'drf_spectacular', # Tadrisino apps (فاز اول - ۴ از ۱۱ اپ) 'accounts', 'content', 'progress', 'ai_tutor', # Tadrisino apps (فاز دوم - بخش ۱ از ۳: subscription, payment, discount) 'subscription', 'payment', 'discount', # Tadrisino apps (فاز دوم - بخش ۲: notifications) 'notifications', # Tadrisino apps (فاز دوم - بخش ۳: affiliate) 'affiliate', # Tadrisino apps (فاز دوم - بخش ۴: support) 'support', # Tadrisino apps (فاز دوم - بخش ۵: family) 'family', # Tadrisino apps (سیستم پرداخت بازار + آفر/تخفیف + لاگ عمومی تغییر Status) 'statuslog', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'config.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'config.wsgi.application' # Database # https://docs.djangoproject.com/en/5.2/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } # 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 AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ LANGUAGE_CODE = 'fa-ir' TIME_ZONE = 'Asia/Tehran' USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.2/howto/static-files/ STATIC_URL = 'static/' # ----------------------------------------------------------------------- # Tadrisino — تنظیمات پروژه # ----------------------------------------------------------------------- # این پروژه از مدل User جنگو (auth) استفاده نمی‌کنه؛ طبق طراحی سینیورت # accounts.User یک مدل مستقل (business model) هست، نه AUTH_USER_MODEL. # احراز هویت با یک TokenAuthentication سفارشی (accounts/authentication.py) # روی همین مدل accounts.User انجام می‌شه؛ نه سشن/کوکی جنگو. REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'accounts.authentication.TadrisinoTokenAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.AllowAny', ], 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_RENDERER_CLASSES': [ '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 = { 'TITLE': 'Tadrisino API', 'DESCRIPTION': ( 'بک‌اند اپلیکیشن آموزشی Tadrisino — احراز هویت با OTP، محتوای درسی ' 'و پیشرفت کاربر، دستیار هوش مصنوعی، اشتراک و پرداخت، تخفیف، ' 'اعلان‌ها، همکاری در فروش، پشتیبانی و حساب خانواده.' ), 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, 'COMPONENT_SPLIT_REQUEST': True, 'SWAGGER_UI_SETTINGS': { 'deepLinking': True, 'displayRequestDuration': True, 'defaultModelsExpandDepth': -1, 'docExpansion': 'list', 'filter': True, 'persistAuthorization': True, }, } # اعتبار توکن دسترسی (ثانیه) و رفرش‌توکن (روز) ACCESS_TOKEN_TTL_SECONDS = 3600 REFRESH_TOKEN_TTL_DAYS = 30 # محدودیت تلاش ورود بر اساس شماره تلفن، مستقل از 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) # ----------------------------------------------------------------------- # بعد از چند روز بدون فعالیت روی یک Node که status اون in_progress هست، # management command مربوطه (content/management/commands/ # mark_abandoned_node_progress.py) اونو abandoned علامت می‌زنه. ۱۴ روز # (نه ۳-۷ روزِ رایج در اپ‌های مشابه) عمداً انتخاب شده چون قطعی/محدودیت # اینترنت و دیتای موبایل در ایران باعث وقفه‌های چندروزه‌ی غیرارادی # می‌شه که نباید فوراً به‌عنوان «رهاکردن درس» حساب بشه. NODE_PROGRESS_ABANDON_DAYS = 14 # ----------------------------------------------------------------------- # Payment security # ----------------------------------------------------------------------- # 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 باید از درگاه پرداخت بیاید، نه یک کلاینت دلخواه. # فعلاً یک 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) # ----------------------------------------------------------------------- # طبق داکیومنت رسمی (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 پایینی رو بر همون اساس ست کنید). 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', '') BAZAAR_REFRESH_TOKEN = os.environ.get('BAZAAR_REFRESH_TOKEN', '') BAZAAR_API_BASE = os.environ.get('BAZAAR_API_BASE', 'https://pardakht.cafebazaar.ir/devapi/v2') # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'