276 lines
9.8 KiB
Python
276 lines
9.8 KiB
Python
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},
|
|
},
|
|
}
|