""" 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 # 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: 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 ALLOWED_HOSTS = [] # 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', } } # 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', ], } 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 # در محیط توسعه/تست، این کد OTP همیشه معتبره تا بدون نیاز به SMS واقعی # بشه لاگین رو با Swagger تست کرد. در پروداکشن حتماً حذف بشه. DEV_MASTER_OTP = '0000' # ----------------------------------------------------------------------- # 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‌ه (نه از کلاینت). در عمل 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/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') # ----------------------------------------------------------------------- # 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'