stop an existing .env from cancelling the defaults meant to replace it

پیش‌فرض‌های دیروز روی یک سرورِ تازه کار می‌کردند ولی روی سرورِ واقعی نه، و
تستِ با .env واقعی نشانش داد: هر دو حالتِ شایع خنثی‌شان می‌کرد.

مقدارِ موجود ولی ناقص: .env سرور از قبل
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 دارد. چون مقدار خالی نبود، کاملاً
جایگزینِ پیش‌فرض می‌شد و دامنه‌ی api.math.tadrisino2.ir باز هم غایب می‌ماند —
یعنی همان «Disallowed Host» قبلی، و باز هم ویرایشِ دستیِ فایل. حالا
دامنه‌های خودمان به مقدارِ محیطی اضافه می‌شوند نه جایگزینش؛ همین برای
CSRF_TRUSTED_ORIGINS و CORS_ALLOWED_ORIGINS هم اعمال شد.

مقدارِ موجود ولی خالی: PUBLIC_BASE_URL در .env هست و خالی است، و
os.environ.get فقط وقتی کلید *نباشد* پیش‌فرض می‌دهد. نتیجه‌اش paymentUrl
نسبی و callbackِ خالیِ زرین‌پال بود — خرابی‌ای که فقط موقعِ اولین پرداختِ
واقعی معلوم می‌شود. حالا مقدارِ خالی هم به پیش‌فرض می‌افتد.

این سه‌تا همان‌هایی‌اند که تا امروز مسئولِ سرور باید دستی به .env اضافه
می‌کرد و چون در .env.example هم نبودند، هر بار از نو کشف می‌شدند. حالا در
همان فایل مستند شده‌اند و تست نگهبانشان است.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 00:00:35 +03:30
parent 3dacf3a50f
commit a3a47b459d
3 changed files with 109 additions and 21 deletions

View File

@@ -67,22 +67,42 @@ if not SECRET_KEY:
'یا متغیر را ست کنید یا اجازه‌ی نوشتن در پوشه‌ی پروژه بدهید.'
) from exc
# دامنه‌های شناخته‌شده‌ی خودمان پیش‌فرض‌اند تا یک متغیرِ فراموش‌شده باعثِ
# DisallowedHost نشود. متغیرِ محیطی، اگر ست باشد، کاملاً جایگزینشان می‌کند.
def _env_list(name: str) -> list[str]:
return [v.strip() for v in os.environ.get(name, '').split(',') if v.strip()]
def _with_defaults(name: str, defaults: list[str]) -> list[str]:
"""
مقدارِ محیطی + پیش‌فرض‌های خودمان، بدونِ تکرار و با حفظِ ترتیب.
چرا اضافه و نه جایگزین: فایلِ .env روی سرور از قبل وجود دارد و مقدارهای
ناقص دارد (مثلاً DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1). اگر مقدارِ
موجود پیش‌فرض را کنار می‌زد، همان خطای قدیمی برمی‌گشت و باز کسی باید
دستی فایل را ویرایش می‌کرد. دامنه‌های خودمان همیشه باید مجاز باشند.
"""
seen, out = set(), []
for value in _env_list(name) + defaults:
if value not in seen:
seen.add(value)
out.append(value)
return out
# دامنه‌های شناخته‌شده‌ی خودمان همیشه مجازند تا یک .env قدیمی یا ناقص باعثِ
# DisallowedHost نشود؛ هر چه در محیط باشد به این‌ها اضافه می‌شود.
DEFAULT_ALLOWED_HOSTS = [
'api.math.tadrisino2.ir',
'math.tadrisino2.ir',
'localhost', '127.0.0.1',
]
ALLOWED_HOSTS = [
h for h in os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',') if h
] or DEFAULT_ALLOWED_HOSTS
ALLOWED_HOSTS = _with_defaults('DJANGO_ALLOWED_HOSTS', DEFAULT_ALLOWED_HOSTS)
# لازم برای ورود به پنلِ ادمین از پشتِ پراکسیِ HTTPS؛ بدونش فرمِ ورود
# «CSRF verification failed» می‌دهد و کسی نمی‌فهمد چرا.
CSRF_TRUSTED_ORIGINS = [
o for o in os.environ.get('CSRF_TRUSTED_ORIGINS', '').split(',') if o
] or ['https://api.math.tadrisino2.ir', 'https://math.tadrisino2.ir']
CSRF_TRUSTED_ORIGINS = _with_defaults(
'CSRF_TRUSTED_ORIGINS',
['https://api.math.tadrisino2.ir', 'https://math.tadrisino2.ir'],
)
# --- CORS -------------------------------------------------------------------
# فرانت‌اند (Vite/React) از یک origin دیگر صدا می‌زند، پس بدونِ این هدرها مرورگر
@@ -101,9 +121,7 @@ CORS_DEFAULT_ORIGINS = [
'http://localhost:3000', 'http://127.0.0.1:3000',
'http://localhost:5173', 'http://127.0.0.1:5173', # Vite/CRA محلی
]
CORS_ALLOWED_ORIGINS = [
o for o in os.environ.get('CORS_ALLOWED_ORIGINS', '').split(',') if o
] or CORS_DEFAULT_ORIGINS
CORS_ALLOWED_ORIGINS = _with_defaults('CORS_ALLOWED_ORIGINS', CORS_DEFAULT_ORIGINS)
CORS_ALLOW_CREDENTIALS = False
# فقط مسیرهای API؛ /admin/ و فایل‌های استاتیک لازم نیست از origin دیگر خوانده شوند.
CORS_URLS_REGEX = r'^/api/.*$'
@@ -490,8 +508,18 @@ BAZAAR_API_BASE = os.environ.get('BAZAAR_API_BASE', 'https://pardakht.cafebazaar
# -----------------------------------------------------------------------
# آدرسِ عمومیِ خودِ بک‌اند؛ هم برای ساختنِ paymentUrlی که به کلاینت برمی‌گردد
# (PaymentRedirectView) و هم برای callback_url که به زرین‌پال می‌فرستیم لازم
# است. بدونش این دو URL شکسته می‌مانند — سیگنالِ واضحی که هنوز ست نشده.
PUBLIC_BASE_URL = os.environ.get('PUBLIC_BASE_URL', '').rstrip('/')
# است.
#
# چرا پیش‌فرض دارد: خالی بودنش سرویس را نمی‌خواباند، فقط مسیرِ پرداخت را
# بی‌سروصدا می‌شکند — paymentUrl نسبی می‌شود و زرین‌پال callback خالی
# می‌گیرد. این دقیقاً همان دسته خطاست که فقط موقعِ اولین پرداختِ واقعی معلوم
# می‌شود. دامنه‌ی خودمان از قبل معلوم است، پس حدس زدنی نیست.
# .strip() اول لازم است: کلید در .env سرور وجود دارد ولی خالی است، و
# os.environ.get فقط وقتی کلید *نباشد* پیش‌فرض می‌دهد — مقدارِ خالی آن را
# خنثی می‌کرد و مسیرِ پرداخت باز هم بی‌صدا می‌شکست.
PUBLIC_BASE_URL = (
os.environ.get('PUBLIC_BASE_URL', '').strip() or 'https://api.math.tadrisino2.ir'
).rstrip('/')
ZARINPAL_MERCHANT_ID = os.environ.get('ZARINPAL_MERCHANT_ID', '')
ZARINPAL_SANDBOX = os.environ.get('ZARINPAL_SANDBOX') == '1'