from datetime import timedelta from pathlib import Path import environ BASE_DIR = Path(__file__).resolve().parent.parent.parent env = environ.Env( DJANGO_DEBUG=(bool, False), DJANGO_ALLOWED_HOSTS=(list, []), ) SECRET_KEY = env("DJANGO_SECRET_KEY") DEBUG = env("DJANGO_DEBUG") ALLOWED_HOSTS = env("DJANGO_ALLOWED_HOSTS") DJANGO_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.admin", # listed last so modeltranslation can register before it ] THIRD_PARTY_APPS = [ "rest_framework", "rest_framework_simplejwt.token_blacklist", "drf_spectacular", "django_filters", "corsheaders", ] LOCAL_APPS = [ "apps.core", "apps.accounts", "apps.courses", "apps.enrollments", "apps.announcements", "apps.payments", ] # modeltranslation MUST be installed before django.contrib.admin (and before any # app whose models it patches) so its admin tab integration kicks in. INSTALLED_APPS = ["modeltranslation", *DJANGO_APPS, *THIRD_PARTY_APPS, *LOCAL_APPS] # modeltranslation MODELTRANSLATION_LANGUAGES = ("en", "fa") MODELTRANSLATION_DEFAULT_LANGUAGE = "fa" MODELTRANSLATION_FALLBACK_LANGUAGES = ("fa", "en") MIDDLEWARE = [ # CORS must come before CommonMiddleware so it can short-circuit preflight (OPTIONS) requests. "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] # CORS — frontend (Next.js) calls this from the browser. Origins come from env so prod # can lock it down to the deployed frontend domain. Dev defaults to localhost:3000. CORS_ALLOWED_ORIGINS = env.list( "CORS_ALLOWED_ORIGINS", default=["http://localhost:3000", "http://127.0.0.1:3000"], ) CORS_ALLOW_CREDENTIALS = True ROOT_URLCONF = "ilo.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.template.context_processors.i18n", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] WSGI_APPLICATION = "ilo.wsgi.application" ASGI_APPLICATION = "ilo.asgi.application" DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": env("POSTGRES_DB"), "USER": env("POSTGRES_USER"), "PASSWORD": env("POSTGRES_PASSWORD"), "HOST": env("POSTGRES_HOST", default="db"), "PORT": env("POSTGRES_PORT", default="5432"), } } AUTH_USER_MODEL = "accounts.User" 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"}, ] LANGUAGE_CODE = "en" TIME_ZONE = "Asia/Tehran" USE_I18N = True USE_TZ = True LANGUAGES = [ ("en", "English"), ("fa", "Persian"), ] LOCALE_PATHS = [BASE_DIR / "locale"] STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "staticfiles" STATICFILES_DIRS = [BASE_DIR / "static"] if (BASE_DIR / "static").exists() else [] MEDIA_URL = "media/" MEDIA_ROOT = BASE_DIR / "media" # Pluggable storage. Default is local FS in dev. To switch to S3-compatible # (MinIO / ArvanCloud Object Storage) in production, set # DEFAULT_FILE_STORAGE=storages.backends.s3.S3Storage in .env, add the # `django-storages[s3]` dependency, and set the AWS_* env vars. STORAGES = { "default": { "BACKEND": env( "DEFAULT_FILE_STORAGE", default="django.core.files.storage.FileSystemStorage", ), }, "staticfiles": { "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", }, } DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # --- DRF + JWT ---------------------------------------------------------------- REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "apps.accounts.auth.TrackingJWTAuthentication", ), "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_THROTTLE_CLASSES": [], "DEFAULT_THROTTLE_RATES": { "otp_send_phone": "5/hour", "otp_send_ip": "30/min", }, "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_FILTER_BACKENDS": ( "django_filters.rest_framework.DjangoFilterBackend", "rest_framework.filters.SearchFilter", "rest_framework.filters.OrderingFilter", ), "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 20, } SPECTACULAR_SETTINGS = { "TITLE": "ilo API", "DESCRIPTION": "SaaS platform for instructors. SMS-OTP auth, JWT tokens, role-based access (student / teacher / admin).", "VERSION": "0.1.0", "SERVE_INCLUDE_SCHEMA": False, "COMPONENT_SPLIT_REQUEST": True, "TAGS": [ {"name": "auth", "description": "OTP signup/login, JWT lifecycle, current-user."}, {"name": "courses-public", "description": "Public catalog: browse and inspect published courses."}, {"name": "courses-instructor", "description": "Instructor authoring: create / edit / publish courses, sections, lessons."}, {"name": "categories", "description": "Course categories."}, {"name": "enrollments", "description": "Student enrollment, my-courses dashboard, lesson progress."}, {"name": "announcements", "description": "Course announcements: instructors post, enrolled students read."}, {"name": "payments", "description": "Checkout, gateway callback, order history, instructor sales."}, {"name": "instructors-public", "description": "Public instructor profiles + featured-instructors strip."}, {"name": "analytics", "description": "Instructor KPIs, revenue chart, activity feed."}, {"name": "search", "description": "Global / unified search across courses + instructors (+ lessons for enrolled students)."}, ], "SWAGGER_UI_SETTINGS": { "persistAuthorization": True, }, } SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(days=7), "REFRESH_TOKEN_LIFETIME": timedelta(days=30), "ROTATE_REFRESH_TOKENS": True, "BLACKLIST_AFTER_ROTATION": True, "SIGNING_KEY": SECRET_KEY, "ALGORITHM": "HS256", "AUTH_HEADER_TYPES": ("Bearer",), "USER_ID_FIELD": "id", "USER_ID_CLAIM": "user_id", } # --- OTP ---------------------------------------------------------------------- OTP_CODE_LENGTH = 4 OTP_TTL_SECONDS = 120 OTP_SEND_COOLDOWN_SECONDS = 60 # --- SMS provider ------------------------------------------------------------- # Default: console backend (logs the OTP) — never use in production. # For sms.ir, set SMS_BACKEND=apps.accounts.services.sms.SMSIRBackend and configure SMSIR_* SMS_BACKEND = env("SMS_BACKEND", default="apps.accounts.services.sms.ConsoleSMSBackend") SMSIR_API_KEY = env("SMSIR_API_KEY", default="") SMSIR_LINE_NUMBER = env("SMSIR_LINE_NUMBER", default="") SMSIR_TEMPLATE_ID = env("SMSIR_TEMPLATE_ID", default="") SMSIR_TEMPLATE_PARAM_NAME = env("SMSIR_TEMPLATE_PARAM_NAME", default="Code") SMSIR_BASE_URL = env("SMSIR_BASE_URL", default="https://api.sms.ir") # --- Payments ---------------------------------------------------------------- # Default: console backend (auto-completes orders for dev). Switch to ZarinPal in prod. PAYMENT_BACKEND = env( "PAYMENT_BACKEND", default="apps.payments.services.gateway.ConsolePaymentBackend", ) PAYMENT_CALLBACK_BASE_URL = env( "PAYMENT_CALLBACK_BASE_URL", default="http://localhost:8000" ) # Frontend pages the user lands on after the gateway redirect: PAYMENT_SUCCESS_REDIRECT_URL = env( "PAYMENT_SUCCESS_REDIRECT_URL", default="/payment/success" ) PAYMENT_FAILURE_REDIRECT_URL = env( "PAYMENT_FAILURE_REDIRECT_URL", default="/payment/failed" ) # Platform commission split (whole percent, e.g. 15 = 15%). ILO_COMMISSION_PERCENT = env.int("ILO_COMMISSION_PERCENT", default=15) # Catalog badge thresholds. COURSE_NEW_WINDOW_DAYS = env.int("COURSE_NEW_WINDOW_DAYS", default=30) COURSE_BESTSELLER_THRESHOLD = env.int("COURSE_BESTSELLER_THRESHOLD", default=10) COURSE_BESTSELLER_WINDOW_DAYS = env.int("COURSE_BESTSELLER_WINDOW_DAYS", default=90) # Referral split — percentages of the post-checkout amount. REFERRAL_BUYER_DISCOUNT_PERCENT = env.int("REFERRAL_BUYER_DISCOUNT_PERCENT", default=10) REFERRAL_REFERRER_CREDIT_PERCENT = env.int("REFERRAL_REFERRER_CREDIT_PERCENT", default=10) # ZarinPal (only used when PAYMENT_BACKEND points to ZarinPalBackend) ZARINPAL_MERCHANT_ID = env("ZARINPAL_MERCHANT_ID", default="") ZARINPAL_SANDBOX = env.bool("ZARINPAL_SANDBOX", default=True) # IDPay (only used when PAYMENT_BACKEND points to IDPayBackend) IDPAY_API_KEY = env("IDPAY_API_KEY", default="") IDPAY_SANDBOX = env.bool("IDPAY_SANDBOX", default=True) # --- Logging ------------------------------------------------------------------ LOGGING = { "version": 1, "disable_existing_loggers": False, "handlers": { "console": {"class": "logging.StreamHandler"}, }, "root": {"handlers": ["console"], "level": "INFO"}, "loggers": { "apps.accounts.services.sms": {"handlers": ["console"], "level": "WARNING", "propagate": False}, }, }