stop a deploy from silently blocking the frontend or leaving 0000 open
سه حالتِ خراب که هیچکدام در لاگِ سرور ردی نمیگذارند و فقط وقتی کشف میشدند که یک آدم از بیرون اپ را دستی امتحان کند. CORS_ALLOWED_ORIGINS فقط از env خوانده میشد و با DEBUG روشن به localhost محدود میماند. یعنی بکاندِ کاملاً سالم، ولی مرورگر همهی درخواستهای math.tadrisino2.ir را بلاک میکند — خطایی که سمتِ سرور دیده نمیشود و فقط در کنسولِ مرورگر پیداست. origin های شناختهشده حالا پیشفرضِ داخلِ کدند تا یک متغیرِ فراموششده اتصال را نشکند؛ env همچنان بازنویسیشان میکند. /api/health حالا warnings هم میدهد. مهمترینش: با DEBUG روشن روی سرورِ عمومی، DEV_MASTER_OTP یعنی هر کسی با کدِ 0000 میتواند بهجای هر شمارهای وارد شود. این تا امروز فقط با خواندنِ تنظیماتِ سرور معلوم میشد، و ما به سرور دسترسی نداریم. allowedOrigins هم برگردانده میشود تا مشکلِ CORS از بیرون قابلِ تشخیص باشد. deploy.sh قبل از دست زدن به دیتابیس متغیرهای لازم را بررسی میکند و با پیامِ روشن میایستد، بهجای اینکه وسطِ کار بشکند و 502 بدهد. .env.production.example همانها را یکجا مستند میکند. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
44
.env.production.example
Normal file
44
.env.production.example
Normal file
@@ -0,0 +1,44 @@
|
||||
# نمونهی فایلِ .env برای سرور. یکبار پر میشود و بعد دیگر کاری با آن نیست.
|
||||
#
|
||||
# بعد از پر کردن، هر آپدیت فقط این است:
|
||||
# git pull && ./deploy.sh
|
||||
|
||||
# ─── اجباری ──────────────────────────────────────────────────────────────
|
||||
# دامنههایی که این سرویس روی آنها سرو میشود (با ویرگول جدا).
|
||||
DJANGO_ALLOWED_HOSTS=api.math.tadrisino2.ir
|
||||
|
||||
# ─── حالتِ پروداکشن ──────────────────────────────────────────────────────
|
||||
# DJANGO_DEBUG را ست نکنید (یا 0 بگذارید).
|
||||
#
|
||||
# ⚠️ با DJANGO_DEBUG=1 روی یک سرورِ عمومی:
|
||||
# • صفحههای خطا کلِ تنظیمات و traceback را نشان میدهند
|
||||
# • کدِ ورودِ آزمایشیِ 0000 برای هر شمارهای پذیرفته میشود،
|
||||
# یعنی هر کسی میتواند بهجای هر کاربری وارد شود
|
||||
DJANGO_DEBUG=0
|
||||
|
||||
# با DEBUG خاموش این دو اجباریاند:
|
||||
DJANGO_SECRET_KEY=
|
||||
# یک رشتهی تصادفیِ بلند. ساختنش:
|
||||
# python -c "import secrets; print(secrets.token_urlsafe(64))"
|
||||
|
||||
CACHE_URL=redis://127.0.0.1:6379/1
|
||||
# بدونِ کشِ مشترک، محدودیتِ نرخ بینِ workerهای gunicorn مشترک نیست.
|
||||
|
||||
# دامنههایی که فرم/ادمین از آنها POST میکند
|
||||
CSRF_TRUSTED_ORIGINS=https://api.math.tadrisino2.ir
|
||||
|
||||
# ─── اختیاری ─────────────────────────────────────────────────────────────
|
||||
# origin های فرانت. اگر خالی باشد از فهرستِ پیشفرضِ داخلِ کد استفاده میشود
|
||||
# که https://math.tadrisino2.ir را شامل است — معمولاً لازم نیست ست شود.
|
||||
# CORS_ALLOWED_ORIGINS=https://math.tadrisino2.ir
|
||||
|
||||
# کلیدهای سرویسهای بیرونی
|
||||
OPENAI_API_KEY=
|
||||
OPENAI_BASE_URL=https://api.gapgpt.app/v1
|
||||
QDRANT_URL=https://qdrant.tadrisino2.ir:443
|
||||
QDRANT_API_KEY=
|
||||
KAVENEGAR_API_KEY=
|
||||
KAVENEGAR_SENDER=
|
||||
|
||||
# نامِ سرویسِ systemd تا deploy.sh خودش ریاستارت کند
|
||||
# TADRISINO_SERVICE=tadrisino
|
||||
@@ -87,6 +87,30 @@ def _check_integrations() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _warnings() -> list[str]:
|
||||
"""
|
||||
چیزهایی که خرابی نیستند ولی روی یک سرورِ عمومی خطرناکاند.
|
||||
|
||||
اینجا هستند چون تنها راهِ دیدنشان تا امروز این بود که کسی تنظیماتِ سرور را
|
||||
از نزدیک بخواند — و ما به سرور دسترسی نداریم.
|
||||
"""
|
||||
out = []
|
||||
if settings.DEBUG:
|
||||
out.append(
|
||||
'DEBUG روشن است: صفحههای خطا کلِ تنظیمات و traceback را نشان میدهند، '
|
||||
'و کدِ ورودِ آزمایشیِ 0000 برای هر شمارهای پذیرفته میشود. '
|
||||
'روی سرورِ عمومی باید خاموش باشد.'
|
||||
)
|
||||
if getattr(settings, 'DEV_MASTER_OTP', None) is not None:
|
||||
out.append('کدِ یکسانِ ورود (DEV_MASTER_OTP) فعال است — هر کسی میتواند بهجای هر کاربری وارد شود.')
|
||||
backend = settings.CACHES['default']['BACKEND']
|
||||
if not settings.DEBUG and 'LocMem' in backend:
|
||||
out.append('کش محلیِ هر پروسه است؛ محدودیتِ نرخ بینِ workerها مشترک نیست.')
|
||||
if not getattr(settings, 'CORS_ALLOWED_ORIGINS', None):
|
||||
out.append('هیچ origin ای برای CORS مجاز نیست — مرورگر همهی درخواستهای فرانت را بلاک میکند.')
|
||||
return out
|
||||
|
||||
|
||||
class HealthView(APIView):
|
||||
"""
|
||||
GET /api/health — وضعیتِ این نسخهی مستقرشده.
|
||||
@@ -110,10 +134,13 @@ class HealthView(APIView):
|
||||
}
|
||||
critical_ok = all(checks[k]['ok'] for k in ('database', 'migrations', 'cache'))
|
||||
|
||||
warnings = _warnings()
|
||||
payload = {
|
||||
'status': 'ok' if critical_ok else 'error',
|
||||
'debug': bool(settings.DEBUG),
|
||||
'warnings': warnings,
|
||||
'checks': checks,
|
||||
'integrations': _check_integrations(),
|
||||
'allowedOrigins': list(getattr(settings, 'CORS_ALLOWED_ORIGINS', [])),
|
||||
}
|
||||
return Response(payload, status=200 if critical_ok else 503)
|
||||
|
||||
@@ -61,15 +61,18 @@ CSRF_TRUSTED_ORIGINS = [o for o in os.environ.get('CSRF_TRUSTED_ORIGINS', '').sp
|
||||
# هدر است، پس origin ناشناس نباید بتواند از مرورگرِ کاربر به API ما بزند.
|
||||
# CORS_ALLOW_CREDENTIALS هم عمداً خاموش میماند — کوکیای در کار نیست و روشن
|
||||
# کردنش فقط سطحِ حمله اضافه میکند.
|
||||
# origin های شناختهشدهی فرانت. عمداً اینجا و نه فقط در متغیرِ محیطی: اگر
|
||||
# تنها راهش env بود، یک دیپلویِ فراموششده یعنی بکاندِ سالم که مرورگر همهی
|
||||
# درخواستهایش را بلاک میکند — خطایی که در سمتِ سرور هیچ ردی نمیگذارد و فقط
|
||||
# در کنسولِ مرورگر دیده میشود.
|
||||
CORS_DEFAULT_ORIGINS = [
|
||||
'https://math.tadrisino2.ir', # فرانتِ مستقر
|
||||
'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
|
||||
]
|
||||
if DEBUG and not CORS_ALLOWED_ORIGINS:
|
||||
# پورتهای پیشفرضِ Vite/CRA برای توسعهی محلی.
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
'http://localhost:3000', 'http://127.0.0.1:3000',
|
||||
'http://localhost:5173', 'http://127.0.0.1:5173',
|
||||
]
|
||||
] or CORS_DEFAULT_ORIGINS
|
||||
CORS_ALLOW_CREDENTIALS = False
|
||||
# فقط مسیرهای API؛ /admin/ و فایلهای استاتیک لازم نیست از origin دیگر خوانده شوند.
|
||||
CORS_URLS_REGEX = r'^/api/.*$'
|
||||
|
||||
35
deploy.sh
35
deploy.sh
@@ -60,6 +60,41 @@ python -m pip install "${PIP_ARGS[@]}" \
|
||||
|| fail "نصبِ وابستگیها شکست خورد. اگر pypi.org تایماوت میدهد، این را امتحان کنید:
|
||||
PIP_INDEX_URL=https://mirror-pypi.runflare.com/simple ./deploy.sh"
|
||||
|
||||
# ─── ۳.۵ بررسیِ متغیرهای محیطی، قبل از دست زدن به دیتابیس ──────────────────
|
||||
# چرا اینجا: اگر SECRET_KEY یا CACHE_URL نباشد، جنگو موقعِ بالا آمدن میمیرد و
|
||||
# از بیرون فقط 502 دیده میشود. بهتر است همینجا و با پیامِ روشن متوقف شویم.
|
||||
step "بررسیِ متغیرهای محیطی"
|
||||
python - <<'PYCHECK' || fail "متغیرهای محیطیِ لازم ست نیستند؛ هیچ تغییری اعمال نشد."
|
||||
import os, sys
|
||||
|
||||
debug = os.environ.get('DJANGO_DEBUG', '').strip().lower() in {'1', 'true', 'yes', 'on'}
|
||||
missing = []
|
||||
|
||||
if not os.environ.get('DJANGO_ALLOWED_HOSTS', '').strip():
|
||||
missing.append(('DJANGO_ALLOWED_HOSTS', 'دامنههایی که سرو میشوند، مثلاً api.math.tadrisino2.ir'))
|
||||
|
||||
if not debug:
|
||||
if not os.environ.get('DJANGO_SECRET_KEY', '').strip():
|
||||
missing.append(('DJANGO_SECRET_KEY', 'کلیدِ امضا؛ با DEBUG خاموش اجباری است'))
|
||||
if not os.environ.get('CACHE_URL', '').strip():
|
||||
missing.append(('CACHE_URL', 'آدرسِ Redis؛ محدودیتِ نرخ بدونش بینِ workerها مشترک نیست'))
|
||||
|
||||
if missing:
|
||||
print(' این متغیرها ست نیستند:')
|
||||
for name, why in missing:
|
||||
print(f' {name:24} {why}')
|
||||
print()
|
||||
print(' در فایلِ .env سرور اضافهشان کنید (نمونه: .env.production.example).')
|
||||
sys.exit(1)
|
||||
|
||||
if debug:
|
||||
print(' ⚠ DJANGO_DEBUG روشن است.')
|
||||
print(' روی سرورِ عمومی یعنی: صفحههای خطا کلِ تنظیمات را نشان میدهند،')
|
||||
print(' و کدِ ورودِ 0000 برای هر شمارهای پذیرفته میشود.')
|
||||
else:
|
||||
print(' ✓ حالتِ پروداکشن')
|
||||
PYCHECK
|
||||
|
||||
# ─── ۴. دیتابیس ────────────────────────────────────────────────────────────
|
||||
step "اعمالِ مهاجرتهای دیتابیس"
|
||||
python manage.py migrate --noinput || fail "migrate شکست خورد؛ سرویس ریاستارت نشد."
|
||||
|
||||
Reference in New Issue
Block a user