This commit is contained in:
2026-08-24 15:54:08 +03:30
commit 236002a87c
133 changed files with 5946 additions and 0 deletions
View File
+7
View File
@@ -0,0 +1,7 @@
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_asgi_application()
+228
View File
@@ -0,0 +1,228 @@
# config/settings.py
"""
تنظیمات جنگو برای بک‌اند «پیامک‌بان».
همه‌ی مقادیر حساس/محیطی از .env خوانده می‌شوند (با python-decouple) تا برای
بالا آوردن روی هر سروری فقط کافی باشد یک فایل .env ساخته شود؛ نیازی به تغییر
کد نیست.
"""
from datetime import timedelta
from pathlib import Path
from decouple import Config, Csv, RepositoryEnv
BASE_DIR = Path(__file__).resolve().parent.parent
# مسیر .env را عمداً صریح می‌دهیم (نه decouple.config پیش‌فرض که بر اساس
# فریم صدازننده حدس می‌زند) — آن حدسِ مبتنی‌بر عمقِ استک، بسته به این‌که
# جنگو از کجا اجرا شود (manage.py check در برابر gunicorn در برابر
# manage.py shell)، می‌تواند مسیر اشتباه را پیدا کند و بی‌صدا از مقادیر
# default() استفاده کند — یعنی .env واقعی هیچ‌وقت خوانده نمی‌شود.
_env_path = BASE_DIR / ".env"
if not _env_path.exists():
raise RuntimeError(
f"فایل .env پیدا نشد ({_env_path}) — یک‌بار «cp .env.example .env» بزن و مقادیر واقعی را پر کن."
)
config = Config(RepositoryEnv(str(_env_path)))
# -------------------- امنیت و محیط اجرا --------------------
SECRET_KEY = config("SECRET_KEY", default="django-insecure-change-me-in-production")
DEBUG = config("DEBUG", default=False, cast=bool)
ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost,127.0.0.1", cast=Csv())
# -------------------- اپلیکیشن‌ها --------------------
DJANGO_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
THIRD_PARTY_APPS = [
"corsheaders",
"rest_framework",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
]
LOCAL_APPS = [
"apps.account",
"apps.banker",
"apps.messaging",
"apps.bizapps",
"apps.billing",
"apps.notifications",
"apps.landing",
"apps.orders",
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"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"
WSGI_APPLICATION = "config.wsgi.application"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
# -------------------- دیتابیس --------------------
# پیش‌فرض SQLite (بدون نیاز به .env برای اجرای محلی)؛ برای پروداکشن/داکر با
# گذاشتن DB_ENGINE=django.db.backends.postgresql در .env به PostgreSQL سوییچ کن.
if config("DB_ENGINE", default="django.db.backends.sqlite3") == "django.db.backends.sqlite3":
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
else:
DATABASES = {
"default": {
"ENGINE": config("DB_ENGINE"),
"NAME": config("DB_NAME"),
"USER": config("DB_USER"),
"PASSWORD": config("DB_PASSWORD"),
"HOST": config("DB_HOST", default="localhost"),
"PORT": config("DB_PORT", default="5432"),
}
}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
]
AUTH_USER_MODEL = "account.User"
# -------------------- بین‌الملل --------------------
LANGUAGE_CODE = "fa-ir"
TIME_ZONE = "Asia/Tehran"
USE_I18N = True
USE_TZ = True
# -------------------- فایل‌های استاتیک --------------------
# استاتیک (CSS/JS پنل ادمین و غیره) را WhiteNoise middleware سرو می‌کند —
# چه DEBUG=True چه False، بدون نیاز به تغییری در urls.py.
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# -------------------- لندینگ‌پیج --------------------
# پوشه‌ی landing/ (HTML/CSS ساده، بدون build step) دقیقا از ریشه‌ی دامنه
# سرو می‌شود — یعنی landing/index.html روی «/»، landing/articles/index.html
# روی «/articles/» و غیره؛ همان WhiteNoise که استاتیک را سرو می‌کند، این
# را هم سرو می‌کند، بدون نیاز به هیچ view یا route دستی در urls.py.
WHITENOISE_ROOT = BASE_DIR / "landing"
WHITENOISE_INDEX_FILE = True
# -------------------- فایل‌های رسانه (آپلودی) --------------------
# فعلا هیچ مدلی ImageField/FileField ندارد، ولی مسیرش را از الان آماده
# می‌کنیم. برخلاف استاتیک، WhiteNoise رسانه را سرو نمی‌کند — سروش در
# urls.py و فقط برای DEBUG=True است (dev)؛ در production باید Nginx/سرور
# وب مستقیم از MEDIA_ROOT سرو کند، نه خودِ جنگو.
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# -------------------- CORS --------------------
CORS_ALLOWED_ORIGINS = config("CORS_ALLOWED_ORIGINS", default="", cast=Csv())
CORS_ALLOW_ALL_ORIGINS = config("CORS_ALLOW_ALL_ORIGINS", default=False, cast=bool)
# دامنه‌های مجاز برای پنل ادمین (فرم‌های session-cookie/CSRF جنگو، برخلاف
# JWT API که این محدودیت را ندارد) — بدون این، پشت HTTPS/دامنه‌ی واقعی، لاگین
# ادمین با خطای «CSRF verification failed» رد می‌شود.
CSRF_TRUSTED_ORIGINS = config("CSRF_TRUSTED_ORIGINS", default="", cast=Csv())
# -------------------- Django REST Framework --------------------
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.ScopedRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"otp_send": "5/hour",
"otp_verify": "10/hour",
"backup": "30/hour",
"messaging_send": "30/hour",
"billing_write": "30/hour",
},
}
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=config("JWT_ACCESS_LIFETIME_DAYS", default=1, cast=int)),
"REFRESH_TOKEN_LIFETIME": timedelta(days=config("JWT_REFRESH_LIFETIME_DAYS", default=30, cast=int)),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
}
# -------------------- پیامک (OTP) --------------------
# فقط دو مقدار لازم است: نام قالب/پترن پیام و آدرس سرویس ارسال (توکن).
# اگر بخواهی خودت payload را با سرویس‌دهنده‌ات هماهنگ کنی، apps/account/send_sms.py را ببین.
SMS_PATTERN_NAME = config("SMS_PATTERN_NAME", default="")
SMS_TOKEN_URL = config("SMS_TOKEN_URL", default="")
# -------------------- ارسال گروهی چندکاناله (apps.messaging) --------------------
# هرکدام خالی بماند، همان کانال به‌جای ارسال خودکار needs_manual برمی‌گرداند
# (صف deep-link سمت اپ) — نه خطا، دقیقا مثل رفتار SMS_TOKEN_URL بالا.
# پیامک بالک با متن آزاد — جدا از SMS_PATTERN_NAME/SMS_TOKEN_URL که فقط OTP است.
KAVENEGAR_API_KEY = config("KAVENEGAR_API_KEY", default="")
KAVENEGAR_SENDER_LINE = config("KAVENEGAR_SENDER_LINE", default="")
# WhatsApp Business Cloud API رسمی (Meta) — نیاز به Template تاییدشده دارد.
WHATSAPP_ACCESS_TOKEN = config("WHATSAPP_ACCESS_TOKEN", default="")
WHATSAPP_PHONE_NUMBER_ID = config("WHATSAPP_PHONE_NUMBER_ID", default="")
WHATSAPP_TEMPLATE_NAME = config("WHATSAPP_TEMPLATE_NAME", default="")
# Bot API رسمی هرکدام — فقط برای مخاطبینی که قبلا با بات چت را شروع کرده‌اند.
TELEGRAM_BOT_TOKEN = config("TELEGRAM_BOT_TOKEN", default="")
BALE_BOT_TOKEN = config("BALE_BOT_TOKEN", default="")
EITAA_BOT_TOKEN = config("EITAA_BOT_TOKEN", default="")
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {"class": "logging.StreamHandler"},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
}
+26
View File
@@ -0,0 +1,26 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("api/account/", include("apps.account.urls")),
path("api/banker/", include("apps.banker.urls")),
path("api/messaging/", include("apps.messaging.urls")),
path("api/bizapps/", include("apps.bizapps.urls")),
path("api/billing/", include("apps.billing.urls")),
path("api/notifications/", include("apps.notifications.urls")),
path("api/orders/", include("apps.orders.urls")),
# فایل‌های استاتیک قبلی landing/articles/*.html حذف شده‌اند — چون
# WhiteNoise میدل‌ور همیشه قبل از urls.py بررسی می‌شود و اگر آن فایل‌ها
# هنوز وجود داشتند، هیچ‌وقت به این view دینامیک نمی‌رسیدیم.
path("articles/", include("apps.landing.urls")),
]
# سرو مدیا فقط در DEBUG=True (توسعه‌ی لوکال) — در production باید
# Nginx/سرور وب مستقیم از MEDIA_ROOT سرو کند، نه خودِ جنگو. استاتیک نیازی
# به این‌جا ندارد، چون WhiteNoise middleware همیشه (چه DEBUG چه نه) سروش
# می‌کند.
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+7
View File
@@ -0,0 +1,7 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()