move the API's own address to mathapi.tadrisino2.ir

The API moved to mathapi.tadrisino2.ir on 2026-09-26 (nginx server_name
changed at 23:22) and api.math.tadrisino2.ir no longer resolves. The code
default for PUBLIC_BASE_URL and the docs' curl examples still named the dead
host, so every paymentUrl, the Zarinpal callback and the Bazaar redirect the
backend builds pointed nowhere. Historical status notes in CLAUDE.md keep the
old name on purpose. config/tests.py now reads the API host from
PUBLIC_BASE_URL instead of a literal. 624 tests green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 01:06:48 +03:30
parent 8768a2406b
commit 1f38465d88
8 changed files with 30 additions and 25 deletions

View File

@@ -45,7 +45,7 @@ CACHE_URL=
# دامنه‌ی فعلی است و لازم نیست ست شوند. برای جابه‌جایی دامنه فقط همین دو را # دامنه‌ی فعلی است و لازم نیست ست شوند. برای جابه‌جایی دامنه فقط همین دو را
# باز کنید (ترتیبِ کار در docs/operations.md، «عوض کردنِ دامنه»): # باز کنید (ترتیبِ کار در docs/operations.md، «عوض کردنِ دامنه»):
# #
# PUBLIC_BASE_URL=https://api.math.tadrisino2.ir # PUBLIC_BASE_URL=https://mathapi.tadrisino2.ir
# FRONTEND_URL=https://math.tadrisino2.ir # FRONTEND_URL=https://math.tadrisino2.ir
# #
# این سه فقط برای دامنه‌ی *اضافه* لازم‌اند (مثلاً دامنه‌ی قبلی در دورانِ جابه‌جایی)؛ # این سه فقط برای دامنه‌ی *اضافه* لازم‌اند (مثلاً دامنه‌ی قبلی در دورانِ جابه‌جایی)؛

View File

@@ -120,7 +120,8 @@ than a coding one, and never assume a step was done just because it was describe
## Frontend ↔ backend (read before wiring the two together) ## Frontend ↔ backend (read before wiring the two together)
The app is two repos and two domains: the React frontend `Tadrisino/MathApp_frontend` The app is two repos and two domains: the React frontend `Tadrisino/MathApp_frontend`
(deployed at `math.tadrisino2.ir`) and this backend (`api.math.tadrisino2.ir`). (deployed at `math.tadrisino2.ir`) and this backend (`mathapi.tadrisino2.ir`; it was
`api.math.tadrisino2.ir` until 2026-09-26, whose DNS record no longer exists).
**Since 2026-09-26 the owner maintains both:** the frontend developer is on leave and **Since 2026-09-26 the owner maintains both:** the frontend developer is on leave and
`AliReza_Zareian` has `write` on the frontend repo (branch protection on its `main` `AliReza_Zareian` has `write` on the frontend repo (branch protection on its `main`
unknown — not visible without admin). Local clone: `~/Desktop/MathApp_frontend`. unknown — not visible without admin). Local clone: `~/Desktop/MathApp_frontend`.
@@ -292,7 +293,7 @@ reliably pull the final answer out of a solution and produced ~40% broken output
## The server: read this before running anything on it ## The server: read this before running anything on it
`api.math.tadrisino2.ir` runs on a **shared company machine** (`administrator@78.38.35.249`, `mathapi.tadrisino2.ir` runs on a **shared company machine** (`administrator@78.38.35.249`,
hostname `haghzad`), not a box that belongs to this project. Our app is one tenant among hostname `haghzad`), not a box that belongs to this project. Our app is one tenant among
many. Also on it, all in production use by other people: many. Also on it, all in production use by other people:

View File

@@ -10,7 +10,7 @@
| | | | | |
|---|---| |---|---|
| **وضعیت** | روی پروداکشن، در حالِ سرویس‌دهی | | **وضعیت** | روی پروداکشن، در حالِ سرویس‌دهی |
| **آدرسِ زنده** | `https://api.math.tadrisino2.ir` · [سلامت](https://api.math.tadrisino2.ir/api/health) | | **آدرسِ زنده** | `https://mathapi.tadrisino2.ir` · [سلامت](https://mathapi.tadrisino2.ir/api/health) |
| **استک** | Python 3.11+ · Django 5.2 · DRF · SQLite · Qdrant (RAG) | | **استک** | Python 3.11+ · Django 5.2 · DRF · SQLite · Qdrant (RAG) |
| **API** | ۶۰ اندپوینت · مستنداتِ زنده در `/api/docs/` | | **API** | ۶۰ اندپوینت · مستنداتِ زنده در `/api/docs/` |
| **تست** | ۵۳۰ تست، همه سبز (`python manage.py test`) | | **تست** | ۵۳۰ تست، همه سبز (`python manage.py test`) |
@@ -104,7 +104,7 @@ seed می‌کند، فایل‌های استاتیک را جمع می‌کند
پس از هر انتشار، از بیرون و بدونِ SSH تأیید کنید: پس از هر انتشار، از بیرون و بدونِ SSH تأیید کنید:
```bash ```bash
curl https://api.math.tadrisino2.ir/api/health curl https://mathapi.tadrisino2.ir/api/health
``` ```
`200` یعنی سالم، `503` یعنی چیزی واقعاً خراب است. هیچ سکرتی برنمی‌گرداند. `200` یعنی سالم، `503` یعنی چیزی واقعاً خراب است. هیچ سکرتی برنمی‌گرداند.

View File

@@ -100,7 +100,7 @@ def _with_defaults(name: str, defaults: list[str]) -> list[str]:
# فرانت را بی‌صدا می‌شکند. `.strip() or` لازم است: کلید ممکن است در .env # فرانت را بی‌صدا می‌شکند. `.strip() or` لازم است: کلید ممکن است در .env
# باشد ولی خالی، و os.environ.get فقط وقتی کلید *نباشد* پیش‌فرض می‌دهد. # باشد ولی خالی، و os.environ.get فقط وقتی کلید *نباشد* پیش‌فرض می‌دهد.
PUBLIC_BASE_URL = ( PUBLIC_BASE_URL = (
os.environ.get('PUBLIC_BASE_URL', '').strip() or 'https://api.math.tadrisino2.ir' os.environ.get('PUBLIC_BASE_URL', '').strip() or 'https://mathapi.tadrisino2.ir'
).rstrip('/') ).rstrip('/')
# آدرسِ خودِ اپ (فرانت)؛ برگشت از درگاه و CORS از آن ساخته می‌شوند. # آدرسِ خودِ اپ (فرانت)؛ برگشت از درگاه و CORS از آن ساخته می‌شوند.
FRONTEND_URL = ( FRONTEND_URL = (

View File

@@ -2,6 +2,7 @@ import importlib
import json import json
import logging import logging
import os import os
from urllib.parse import urlsplit
from unittest.mock import patch from unittest.mock import patch
from django.conf import settings from django.conf import settings
@@ -318,7 +319,8 @@ class ServerEnvDefaultsTests(SimpleTestCase):
اینکه بدونِ هیچ متغیرِ محیطی‌ای مقدارِ کارآمد دارند. اینکه بدونِ هیچ متغیرِ محیطی‌ای مقدارِ کارآمد دارند.
""" """
HOST = 'api.math.tadrisino2.ir' # دامنه‌ی API از PUBLIC_BASE_URL می‌آید، نه از یک رشته‌ی ثابت در تست.
HOST = urlsplit(settings.PUBLIC_BASE_URL).hostname
def test_the_api_domain_is_allowed_without_any_env_var(self): def test_the_api_domain_is_allowed_without_any_env_var(self):
self.assertIn(self.HOST, settings.DEFAULT_ALLOWED_HOSTS) self.assertIn(self.HOST, settings.DEFAULT_ALLOWED_HOSTS)
@@ -341,7 +343,7 @@ class ServerEnvDefaultsTests(SimpleTestCase):
"""کلید در .env هست ولی خالی — os.environ.get پیش‌فرض نمی‌دهد.""" """کلید در .env هست ولی خالی — os.environ.get پیش‌فرض نمی‌دهد."""
with patch.dict(os.environ, {'PUBLIC_BASE_URL': ''}): with patch.dict(os.environ, {'PUBLIC_BASE_URL': ''}):
value = (os.environ.get('PUBLIC_BASE_URL', '').strip() value = (os.environ.get('PUBLIC_BASE_URL', '').strip()
or 'https://api.math.tadrisino2.ir') or 'https://mathapi.tadrisino2.ir')
self.assertTrue(value.startswith('https://')) self.assertTrue(value.startswith('https://'))
def test_admin_login_origin_is_trusted_without_any_env_var(self): def test_admin_login_origin_is_trusted_without_any_env_var(self):

View File

@@ -44,7 +44,7 @@ export const api = createClient({ baseUrl: import.meta.env.VITE_API_URL ?? '/api
برای تست لازم نیست پیامک واقعی برسد. برای تست لازم نیست پیامک واقعی برسد.
**برای تستِ مسیرِ کامل روی سرورِ واقعی، شماره‌ی تست بگیرید.** روی **برای تستِ مسیرِ کامل روی سرورِ واقعی، شماره‌ی تست بگیرید.** روی
`api.math.tadrisino2.ir` سرویسِ پیامک هنوز تنظیم نشده، پس `requestOtp` برای یک `mathapi.tadrisino2.ir` سرویسِ پیامک هنوز تنظیم نشده، پس `requestOtp` برای یک
شماره‌ی معمولی خطای ۵۰۳ می‌دهد. بک‌اند چند شماره‌ی تست با کدِ ثابت دارد شماره‌ی معمولی خطای ۵۰۳ می‌دهد. بک‌اند چند شماره‌ی تست با کدِ ثابت دارد
(`TEST_LOGIN_PHONES` در `.env` سرور): برای آن شماره‌ها پیامکی ارسال نمی‌شود و (`TEST_LOGIN_PHONES` در `.env` سرور): برای آن شماره‌ها پیامکی ارسال نمی‌شود و
همان کد همیشه جواب می‌دهد، دقیقاً مثل مسیرِ واقعی. یک جفت «شماره + کد» برای همان کد همیشه جواب می‌دهد، دقیقاً مثل مسیرِ واقعی. یک جفت «شماره + کد» برای
@@ -57,7 +57,7 @@ export const api = createClient({ baseUrl: import.meta.env.VITE_API_URL ?? '/api
صفحه‌های اعلان و لیدربرد مجبور نباشید منتظرِ تولیدِ داده‌ی واقعی بمانید: صفحه‌های اعلان و لیدربرد مجبور نباشید منتظرِ تولیدِ داده‌ی واقعی بمانید:
```bash ```bash
curl -X POST https://api.math.tadrisino2.ir/api/auth/session \ curl -X POST https://mathapi.tadrisino2.ir/api/auth/session \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{"type":"guest","deviceId":"frontend-test-device"}' -d '{"type":"guest","deviceId":"frontend-test-device"}'
``` ```

View File

@@ -39,7 +39,7 @@ poll کردنِ `/api/health`.
## بررسیِ سلامت ## بررسیِ سلامت
```bash ```bash
curl https://api.math.tadrisino2.ir/api/health curl https://mathapi.tadrisino2.ir/api/health
``` ```
بدونِ احرازِ هویت و بدونِ SSH می‌گوید: دیتابیس سالم است یا نه، مهاجرتِ بدونِ احرازِ هویت و بدونِ SSH می‌گوید: دیتابیس سالم است یا نه، مهاجرتِ
@@ -54,8 +54,8 @@ curl https://api.math.tadrisino2.ir/api/health
تأییدِ سریعِ اینکه API واقعاً جواب می‌دهد: تأییدِ سریعِ اینکه API واقعاً جواب می‌دهد:
```bash ```bash
curl -i https://api.math.tadrisino2.ir/api/schema/ curl -i https://mathapi.tadrisino2.ir/api/schema/
curl -i -X POST https://api.math.tadrisino2.ir/api/auth/session \ curl -i -X POST https://mathapi.tadrisino2.ir/api/auth/session \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{"type":"guest","deviceId":"verify-1"}' -d '{"type":"guest","deviceId":"verify-1"}'
``` ```
@@ -88,7 +88,7 @@ PY
sudo systemctl stop zareian-mathapp sudo systemctl stop zareian-mathapp
cp .deploy-backups/db-<timestamp>.sqlite3 db.sqlite3 cp .deploy-backups/db-<timestamp>.sqlite3 db.sqlite3
sudo systemctl start zareian-mathapp sudo systemctl start zareian-mathapp
curl https://api.math.tadrisino2.ir/api/health curl https://mathapi.tadrisino2.ir/api/health
``` ```
## متغیرهای محیطی ## متغیرهای محیطی
@@ -101,7 +101,7 @@ curl https://api.math.tadrisino2.ir/api/health
|---|---| |---|---|
| `DJANGO_DEBUG` | خاموش (`0`) — که حالتِ درستِ پروداکشن است | | `DJANGO_DEBUG` | خاموش (`0`) — که حالتِ درستِ پروداکشن است |
| `DJANGO_SECRET_KEY` | یک کلیدِ تصادفیِ قوی ساخته و در `.django_secret_key` (مجوز `0600`) ذخیره می‌شود | | `DJANGO_SECRET_KEY` | یک کلیدِ تصادفیِ قوی ساخته و در `.django_secret_key` (مجوز `0600`) ذخیره می‌شود |
| `PUBLIC_BASE_URL` · `FRONTEND_URL` | دامنه‌ی فعلی (`api.math.tadrisino2.ir` / `math.tadrisino2.ir`). بقیه‌ی تنظیم‌های دامنه از همین دو ساخته می‌شوند؛ بخشِ «عوض کردنِ دامنه» | | `PUBLIC_BASE_URL` · `FRONTEND_URL` | دامنه‌ی فعلی (`mathapi.tadrisino2.ir` / `math.tadrisino2.ir`). بقیه‌ی تنظیم‌های دامنه از همین دو ساخته می‌شوند؛ بخشِ «عوض کردنِ دامنه» |
| `DJANGO_ALLOWED_HOSTS` · `CSRF_TRUSTED_ORIGINS` · `CORS_ALLOWED_ORIGINS` | از دو آدرسِ بالا ساخته می‌شوند؛ مقدارِ env فقط دامنه‌ی *اضافه* است | | `DJANGO_ALLOWED_HOSTS` · `CSRF_TRUSTED_ORIGINS` · `CORS_ALLOWED_ORIGINS` | از دو آدرسِ بالا ساخته می‌شوند؛ مقدارِ env فقط دامنه‌ی *اضافه* است |
| `CACHE_URL` | سرویس بالا می‌آید ولی نرخ بینِ workerها مشترک نیست؛ `/api/health` هشدار می‌دهد | | `CACHE_URL` | سرویس بالا می‌آید ولی نرخ بینِ workerها مشترک نیست؛ `/api/health` هشدار می‌دهد |
| `KAVENEGAR_API_KEY` | `POST /api/auth/otp/request` خطای صریحِ `503` می‌دهد، نه موفقیتِ ساختگی | | `KAVENEGAR_API_KEY` | `POST /api/auth/otp/request` خطای صریحِ `503` می‌دهد، نه موفقیتِ ساختگی |
@@ -148,6 +148,9 @@ pendingِ همان کاربر را دوباره با زرین‌پال می‌س
## عوض کردنِ دامنه ## عوض کردنِ دامنه
آخرین جابه‌جایی: ۲۰۲۶-۰۹-۲۶، API از `api.math.tadrisino2.ir` به `mathapi.tadrisino2.ir`
(رکوردِ DNSِ آدرسِ قبلی حذف شده). فرانت همان `math.tadrisino2.ir` ماند.
دامنه در کد فقط یک‌جا آمده: پیش‌فرضِ `PUBLIC_BASE_URL` و `FRONTEND_URL` در دامنه در کد فقط یک‌جا آمده: پیش‌فرضِ `PUBLIC_BASE_URL` و `FRONTEND_URL` در
`config/settings.py`. `ALLOWED_HOSTS`، `CSRF_TRUSTED_ORIGINS`، `CORS_ALLOWED_ORIGINS`، `config/settings.py`. `ALLOWED_HOSTS`، `CSRF_TRUSTED_ORIGINS`، `CORS_ALLOWED_ORIGINS`،
callbackِ زرین‌پال، برگشت از پرداخت، `BAZAAR_REDIRECT_URI` و آدرسِ سلامت در callbackِ زرین‌پال، برگشت از پرداخت، `BAZAAR_REDIRECT_URI` و آدرسِ سلامت در
@@ -159,9 +162,8 @@ callbackِ زرین‌پال، برگشت از پرداخت، `BAZAAR_REDIRECT_U
1. **DNS، آروان و گواهی** برای دامنه‌های تازه، رو به همین سرور، و `server_name`ِ 1. **DNS، آروان و گواهی** برای دامنه‌های تازه، رو به همین سرور، و `server_name`ِ
nginx — کارِ ادمینِ سرور، نه ما. nginx — کارِ ادمینِ سرور، نه ما.
2. **بک‌اند:** در `.env` سرور `PUBLIC_BASE_URL` و `FRONTEND_URL` را عوض کنید. تا DNS 2. **بک‌اند:** در `.env` سرور `PUBLIC_BASE_URL` و `FRONTEND_URL` را عوض کنید. تا DNS
کامل جابه‌جا شود، دامنه‌های قبلی را هم نگه دارید: کامل جابه‌جا شود، دامنه‌های قبلی را هم نگه دارید، مثلاً
`DJANGO_ALLOWED_HOSTS=api.math.tadrisino2.ir,math.tadrisino2.ir` و `DJANGO_ALLOWED_HOSTS=<دامنه‌ی قبلیِ API>` و `CORS_ALLOWED_ORIGINS=https://<دامنه‌ی قبلیِ فرانت>`. بعد ری‌استارت.
`CORS_ALLOWED_ORIGINS=https://math.tadrisino2.ir`. بعد ری‌استارت.
3. **زرین‌پال:** دامنه‌ی تازه‌ی API را در پنلِ ترمینال ثبت کنید. خطای `-14` («دامنه‌ی 3. **زرین‌پال:** دامنه‌ی تازه‌ی API را در پنلِ ترمینال ثبت کنید. خطای `-14` («دامنه‌ی
callback با دامنه‌ی ثبت‌شده نمی‌خواند») دقیقاً نبودنِ همین است. callback با دامنه‌ی ثبت‌شده نمی‌خواند») دقیقاً نبودنِ همین است.
**بازار** (اگر استفاده شود): Redirect URI در پنل باید عیناً **بازار** (اگر استفاده شود): Redirect URI در پنل باید عیناً
@@ -189,9 +191,9 @@ TEST_LOGIN_PHONES=09120000001:834512,09120000002:110947
دیگری مسیرِ عادیِ OTP را می‌رود، بی‌هیچ تغییری. دیگری مسیرِ عادیِ OTP را می‌رود، بی‌هیچ تغییری.
```bash ```bash
curl -X POST https://api.math.tadrisino2.ir/api/auth/otp/request \ curl -X POST https://mathapi.tadrisino2.ir/api/auth/otp/request \
-H 'Content-Type: application/json' -d '{"phone":"09120000001"}' -H 'Content-Type: application/json' -d '{"phone":"09120000001"}'
curl -X POST https://api.math.tadrisino2.ir/api/auth/login \ curl -X POST https://mathapi.tadrisino2.ir/api/auth/login \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{"phone":"09120000001","otp":"834512"}' -d '{"phone":"09120000001","otp":"834512"}'
``` ```
@@ -222,7 +224,7 @@ curl -X POST https://api.math.tadrisino2.ir/api/auth/login \
یک `deviceId` مشخص روی پروداکشن از قبل داده دارد: یک `deviceId` مشخص روی پروداکشن از قبل داده دارد:
```bash ```bash
curl -X POST https://api.math.tadrisino2.ir/api/auth/session \ curl -X POST https://mathapi.tadrisino2.ir/api/auth/session \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{"type":"guest","deviceId":"frontend-test-device"}' -d '{"type":"guest","deviceId":"frontend-test-device"}'
``` ```
@@ -240,7 +242,7 @@ curl -X POST https://api.math.tadrisino2.ir/api/auth/session \
## سرور ## سرور
`api.math.tadrisino2.ir` روی یک **ماشینِ مشترکِ شرکت** اجرا می‌شود `mathapi.tadrisino2.ir` روی یک **ماشینِ مشترکِ شرکت** اجرا می‌شود
(`administrator@78.38.35.249`)، نه سروری که مالِ این پروژه باشد. روی همان ماشین (`administrator@78.38.35.249`)، نه سروری که مالِ این پروژه باشد. روی همان ماشین
Gitea، سرویسِ ایمیل، DNS، چند دیتابیس و اپ‌های تیم‌های دیگر در حالِ کارند. Gitea، سرویسِ ایمیل، DNS، چند دیتابیس و اپ‌های تیم‌های دیگر در حالِ کارند.

View File

@@ -97,7 +97,7 @@
می‌بینند. می‌بینند.
- **رفع:** پاسخِ عمومی فقط `{"status": "ok"|"degraded"}`؛ بقیه پشتِ هدرِ - **رفع:** پاسخِ عمومی فقط `{"status": "ok"|"degraded"}`؛ بقیه پشتِ هدرِ
`X-Internal-Key` (`INTERNAL_SERVICE_KEY`) یا فقط برای staff. `X-Internal-Key` (`INTERNAL_SERVICE_KEY`) یا فقط برای staff.
- **چطور بسنجید:** `curl https://api.math.tadrisino2.ir/api/health` بدونِ هدر نباید - **چطور بسنجید:** `curl https://mathapi.tadrisino2.ir/api/health` بدونِ هدر نباید
`warnings` و `traffic` داشته باشد. `warnings` و `traffic` داشته باشد.
## ۵. مستنداتِ API عمومی ## ۵. مستنداتِ API عمومی
@@ -122,7 +122,7 @@
## ۷. پنلِ ادمین روی دامنه‌ی عمومی ## ۷. پنلِ ادمین روی دامنه‌ی عمومی
- **چیست:** `/admin/` روی `api.math.tadrisino2.ir`، با ورودِ نام‌کاربری/رمزِ جنگو. از - **چیست:** `/admin/` روی `mathapi.tadrisino2.ir`، با ورودِ نام‌کاربری/رمزِ جنگو. از
۲۰۲۶-۰۹-۲۵ سقفِ ۱۰ تلاش در ۱۵ دقیقه برای هر IP دارد (`AdminLoginThrottleMiddleware`)، ۲۰۲۶-۰۹-۲۵ سقفِ ۱۰ تلاش در ۱۵ دقیقه برای هر IP دارد (`AdminLoginThrottleMiddleware`)،
ولی: شمارش per-worker است (مورد ۹)، IP با XFF جعل‌پذیر است (مورد ۸)، و ۲FA ندارد. ولی: شمارش per-worker است (مورد ۹)، IP با XFF جعل‌پذیر است (مورد ۸)، و ۲FA ندارد.
- **خطر:** ادمین همه‌ی محتوا، پلن‌ها، کدهای تخفیف، سفارش‌ها و کاربرها را کنترل می‌کند. - **خطر:** ادمین همه‌ی محتوا، پلن‌ها، کدهای تخفیف، سفارش‌ها و کاربرها را کنترل می‌کند.
@@ -131,7 +131,7 @@
## ۸. دسترسیِ مستقیم به پورتِ ۸۰ (دور زدنِ ArvanCloud) ## ۸. دسترسیِ مستقیم به پورتِ ۸۰ (دور زدنِ ArvanCloud)
- **چیست:** `http://78.38.35.249/api/health` با `Host: api.math.tadrisino2.ir` پاسخ می‌دهد - **چیست:** `http://78.38.35.249/api/health` با `Host: mathapi.tadrisino2.ir` پاسخ می‌دهد
(تأیید: HTTP 200). `NUM_PROXIES=2` فرض می‌کند همیشه دو پروکسی جلوی ماست. (تأیید: HTTP 200). `NUM_PROXIES=2` فرض می‌کند همیشه دو پروکسی جلوی ماست.
- **خطر:** کسی که IP را بداند `X-Forwarded-For` را جعل می‌کند و محدودیت‌های IP-محور - **خطر:** کسی که IP را بداند `X-Forwarded-For` را جعل می‌کند و محدودیت‌های IP-محور
(مهمان، OTP، ادمین) را دور می‌زند. (مهمان، OTP، ادمین) را دور می‌زند.