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):
|
class HealthView(APIView):
|
||||||
"""
|
"""
|
||||||
GET /api/health — وضعیتِ این نسخهی مستقرشده.
|
GET /api/health — وضعیتِ این نسخهی مستقرشده.
|
||||||
@@ -110,10 +134,13 @@ class HealthView(APIView):
|
|||||||
}
|
}
|
||||||
critical_ok = all(checks[k]['ok'] for k in ('database', 'migrations', 'cache'))
|
critical_ok = all(checks[k]['ok'] for k in ('database', 'migrations', 'cache'))
|
||||||
|
|
||||||
|
warnings = _warnings()
|
||||||
payload = {
|
payload = {
|
||||||
'status': 'ok' if critical_ok else 'error',
|
'status': 'ok' if critical_ok else 'error',
|
||||||
'debug': bool(settings.DEBUG),
|
'debug': bool(settings.DEBUG),
|
||||||
|
'warnings': warnings,
|
||||||
'checks': checks,
|
'checks': checks,
|
||||||
'integrations': _check_integrations(),
|
'integrations': _check_integrations(),
|
||||||
|
'allowedOrigins': list(getattr(settings, 'CORS_ALLOWED_ORIGINS', [])),
|
||||||
}
|
}
|
||||||
return Response(payload, status=200 if critical_ok else 503)
|
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 ما بزند.
|
# هدر است، پس origin ناشناس نباید بتواند از مرورگرِ کاربر به API ما بزند.
|
||||||
# CORS_ALLOW_CREDENTIALS هم عمداً خاموش میماند — کوکیای در کار نیست و روشن
|
# 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 = [
|
CORS_ALLOWED_ORIGINS = [
|
||||||
o for o in os.environ.get('CORS_ALLOWED_ORIGINS', '').split(',') if o
|
o for o in os.environ.get('CORS_ALLOWED_ORIGINS', '').split(',') if o
|
||||||
]
|
] or CORS_DEFAULT_ORIGINS
|
||||||
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',
|
|
||||||
]
|
|
||||||
CORS_ALLOW_CREDENTIALS = False
|
CORS_ALLOW_CREDENTIALS = False
|
||||||
# فقط مسیرهای API؛ /admin/ و فایلهای استاتیک لازم نیست از origin دیگر خوانده شوند.
|
# فقط مسیرهای API؛ /admin/ و فایلهای استاتیک لازم نیست از origin دیگر خوانده شوند.
|
||||||
CORS_URLS_REGEX = r'^/api/.*$'
|
CORS_URLS_REGEX = r'^/api/.*$'
|
||||||
|
|||||||
35
deploy.sh
35
deploy.sh
@@ -60,6 +60,41 @@ python -m pip install "${PIP_ARGS[@]}" \
|
|||||||
|| fail "نصبِ وابستگیها شکست خورد. اگر pypi.org تایماوت میدهد، این را امتحان کنید:
|
|| fail "نصبِ وابستگیها شکست خورد. اگر pypi.org تایماوت میدهد، این را امتحان کنید:
|
||||||
PIP_INDEX_URL=https://mirror-pypi.runflare.com/simple ./deploy.sh"
|
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 "اعمالِ مهاجرتهای دیتابیس"
|
step "اعمالِ مهاجرتهای دیتابیس"
|
||||||
python manage.py migrate --noinput || fail "migrate شکست خورد؛ سرویس ریاستارت نشد."
|
python manage.py migrate --noinput || fail "migrate شکست خورد؛ سرویس ریاستارت نشد."
|
||||||
|
|||||||
Reference in New Issue
Block a user