accounts.0008 drops the columns and refuses to run while guest rows exist. Docs describe the current state; tests that only proved guest behaviour are gone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
327 lines
34 KiB
Markdown
327 lines
34 KiB
Markdown
# پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز ماندهاند
|
||
|
||
**آخرین بازبینی با کد: ۲۰۲۶-۱۰-۰۵ (۱۳ مهر ۱۴۰۵)؛ با سرورِ زنده: ۲۰۲۶-۱۰-۰۴.**
|
||
|
||
این پروژه تا این تاریخ در حالتِ **دمو و ارائهی کارآموزی** است، نه محصولِ نهایی. چند چیز
|
||
عمداً باز گذاشته شده تا توسعهدهندهها، فرانتکار و ارائه کار کنند. هر کدام برای یک سرورِ
|
||
عمومی با کاربرِ واقعی (کودک، پرداختِ واقعی) **خطر است** و باید پیش از آمدنِ اولین کاربرِ
|
||
واقعی بسته شود.
|
||
|
||
**برای agentها و توسعهدهندههای بعدی:** این فهرست ادعاست، نه ضمانت؛ ممکن است از این
|
||
تاریخ به بعد چیزی عوض شده باشد. پیش از بستنِ هر مورد، وضعیتِ فعلیاش را با خودِ سرور یا
|
||
کد بسنجید (ستونِ «چطور بسنجید»). مورد که بسته شد، از فهرست برداشته میشود و یک خط در
|
||
«حفاظهایی که هست» (پایینِ همین فایل) میگیرد، با نامِ تستی که آن را نگه میدارد. تاریخ و
|
||
دلیلِ تغییر در پیامِ کامیت است، نه اینجا.
|
||
|
||
هر آزمونِ «از بیرون» با `curl` بدونِ توکن انجام شده و در تاریخِ بالا تأیید شده است.
|
||
|
||
---
|
||
|
||
## خلاصه، به ترتیبِ خطر
|
||
|
||
| # | مورد | خطر | کارِ کیست |
|
||
|---|---|---|---|
|
||
| ۱ | کلیدِ امضای توکن روی سرور همان رشتهی داخلِ ریپو است | بحرانی — جعلِ ورود بهجای هر کاربر | کد آماده؛ اعمال روی سرور |
|
||
| ۲ | ۷ حسابِ دمو با کدِ ورودِ ثابت روی پروداکشن | بالا | `.env` سرور |
|
||
| ۳ | دادهی دمو روی دیتابیسِ پروداکشن | بالا | دیتابیسِ سرور |
|
||
| ۴ | `/api/health` جزئیاتِ داخلی را به همه نشان میدهد | متوسط | کد |
|
||
| ۵ | Swagger، ReDoc و schemaی کاملِ API عمومیاند | متوسط | کد |
|
||
| ۷ | پنلِ `/admin/` روی دامنهی عمومی | متوسط | nginx + کد |
|
||
| ۸ | سرور بدونِ ArvanCloud روی پورتِ ۸۰ هم جواب میدهد | متوسط | ادمینِ سرور |
|
||
| ۹ | کشِ مشترک (Redis) نیست؛ محدودیتها per-workerاند | متوسط | `.env` سرور |
|
||
| ۱۱ | کدِ OTP بیهش در دیتابیس و در فهرستِ پنل | کم | کد |
|
||
| ۱۲ | هزینهی دستیارِ AI با ساختِ شمارهی تازه (هر شماره ۳ سوالِ رایگان) قابلِ بالا بردن است | کم (مالی) | کد |
|
||
| ۱۴ | SQLite روی دیسکِ مشترکِ پُر، بدونِ بکاپِ خودکارِ بیرون از ماشین | کم (داده) | زیرساخت |
|
||
| ۱۵ | دسترسیِ سرور: حسابِ مشترک، رمز، sudoِ کامل | بالا | ادمینِ سرور |
|
||
| ۱۶ | فرانت توکن را در `localStorage` نگه میدارد | کم | فرانت |
|
||
| ۲۰ | یونیتِ systemd `DJANGO_DEBUG=1` و کلیدِ ناامن دارد؛ فقط یک خطِ `.env` جلوی DEBUG را گرفته | بالا (نهفته) | ادمینِ سرور |
|
||
| ۲۲ | سایتِ فرانت هیچ هدرِ امنیتی ندارد (CSP، frame-ancestors، HSTS) | کم | nginx/فرانت |
|
||
| ۲۴ | سقفِ روزانهی ۳۰۰ پیامکِ کلِ سامانه را یک ناشناس پر میکند و ورود با پیامک برای همه بسته میشود | بالا (دسترسپذیری، هزینه) | تصمیمِ طراحی |
|
||
| ۲۵ | با دانستنِ شمارهی کسی، ورودِ او تا ۲۴ ساعت بسته میشود | متوسط | تصمیمِ طراحی |
|
||
| ۲۶ | پایانِ درس و جزیره به گفتهی کلاینت است (جزیرهی قفل، قلبِ صفر، گزارشِ والدین) | متوسط | کد، بعد از ارائه |
|
||
| ۲۷ | کلیدِ جوابِ هر سوال با یک حسابِ دوم درمیآید؛ امتیازِ هر جوابِ درست دو بار داده میشود | کم | تصمیمِ محصولی |
|
||
| ۲۸ | استفادهی دوباره از توکنِ تمدیدِ دزدیدهشده تشخیص داده نمیشود؛ «خروج از همهی دستگاهها» نیست | کم | کد، بعد از ارائه |
|
||
| ۲۹ | پیشنهادِ ثبتنام با حذفِ حساب و ورودِ دوباره با همان شماره دوباره داده میشود | کم | کد |
|
||
| ۳۰ | چند مسیرِ خواندنی (از جمله بانکِ سوال، بیورود) throttle ندارند | کم | کد، بعد از ارائه |
|
||
| ۳۱ | لپتاپِ توسعه با `mobile-preview` روی شبکهی محلی باز است و در DEBUG کدِ `0000` را میپذیرد | کم | لپتاپِ توسعه |
|
||
|
||
---
|
||
|
||
## ۱. کلیدِ امضای توکن (`DJANGO_SECRET_KEY`) روی سرور
|
||
|
||
- **چیست:** توکنِ ورود فقط `user_id`ِ امضاشده با `django.core.signing` است
|
||
(`accounts/authentication.py`). امضا با `DJANGO_SECRET_KEY` انجام میشود. روی سرور این
|
||
مقدار هنوز همان fallbackِ `django-insecure-…` است که در `config/settings.py` کامیت شده
|
||
(آخرین تأیید: ۲۰۲۶-۰۹-۲۴، بهصورتِ بولین).
|
||
- **خطر:** هر کس ریپو را ببیند (Gitea، zipِ تحویلی، هر کپی) میتواند برای **هر `user_id`**
|
||
توکنِ معتبر بسازد و بهجای آن کاربر وارد شود؛ از جمله خریدِ اشتراک، دیدنِ گفتگوها و
|
||
دادهی خانواده.
|
||
- **چرا باز مانده:** عوض کردنش همهی کاربرهای واردشده (توسعهدهندهها، فرانتکار، حسابهای
|
||
دمو در حالِ ارائه) را بیرون میاندازد. تصمیمِ صاحبِ پروژه: بعد از ارائه.
|
||
- **رفع:** `DJANGO_SECRET_KEY` را از `.env` **و** از یونیتِ systemd حذف کنید تا
|
||
`config/settings.py` یک کلیدِ تصادفی بسازد و در `.django_secret_key` (مجوز `0600`) نگه
|
||
دارد. سرویس را ریاستارت کنید. همه یک بار دوباره وارد میشوند.
|
||
- **چطور بسنجید:** `manage.py check --deploy` نباید `security.W009` بدهد؛ روی سرور
|
||
`venv/bin/python -c "from django.conf import settings; print(settings.SECRET_KEY.startswith('django-insecure'))"`
|
||
باید `False` چاپ کند (خودِ کلید را چاپ نکنید).
|
||
|
||
## ۲. حسابهای دمو با کدِ ورودِ ثابت (`TEST_LOGIN_PHONES`)
|
||
|
||
- **چیست:** ۷ شمارهی `09120000101` تا `09120000107` با کدِ ثابت در `.env` سرور. شمارهها
|
||
در ریپو (`docs/operations.md`، `CLAUDE.md`) آمدهاند؛ کدها فقط در راهنمای ارائهی صاحبِ
|
||
پروژهاند.
|
||
- **خطر:** کسی که کد را از راهنما، اسکرینشات یا ارائه ببیند وارد آن حسابها میشود. این
|
||
کدها با `OTP_MAX_FAILED_ATTEMPTS` باطل نمیشوند (ردیفِ `OTPCode` ندارند)؛ فقط سقفِ
|
||
per-workerِ تلاشِ ورود جلویشان است.
|
||
- **رفع:** `TEST_LOGIN_PHONES` را از `.env` سرور پاک کنید و ریاستارت کنید.
|
||
- **چطور بسنجید:** `/api/health` دیگر هشدارِ «شمارههای تستِ ورود با کدِ ثابت فعالاند» ندارد.
|
||
|
||
## ۳. دادهی دمو روی دیتابیسِ پروداکشن
|
||
|
||
- **چیست:** کاربرهای ۱۰ تا ۱۵ و ۲۵ (حسابهای دمو با کدِ ثابت، بند ۲) با درس، گفتگو، تیکت و
|
||
سفارش روی دیتابیسِ پروداکشن ساخته شدهاند. حسابِ ۱۰ («علی رضایی»، با ۷ سفارش) حسابِ نمایشیِ اصلی است.
|
||
- **خطر:** دادهی ساختگی در آمار و جدولِ رتبهبندیِ واقعی.
|
||
- **رفع:** پیش از کاربرِ واقعی با `delete_users --ids … --yes` (پیش از آن بکاپ) پاک شوند.
|
||
`purge_demo_data` اینها را **نمیگیرد** (از مسیرِ واقعیِ viewها ساخته شدهاند).
|
||
- **چطور بسنجید:** `/api/health` → `traffic.users` فقط کاربرانِ واقعی را بشمارد.
|
||
|
||
## ۴. `/api/health` عمومی و پرجزئیات
|
||
|
||
- **چیست:** `config/health.py`، `AllowAny`. بدونِ توکن برمیگرداند: `debug`، `warnings`
|
||
(مثلاً «شمارههای تست فعالاند»، «محدودیتِ نرخ بینِ workerها مشترک نیست»)، `integrations`
|
||
(کدام سرویسها پیکربندیاند، وضعیتِ اعتبارِ AI، نامِ مدل)، `traffic` (تعدادِ کاربر، فعالِ
|
||
۲۴ ساعتِ اخیر، زمانِ آخرین فعالیت)، `allowedOrigins`، `clientIp`، شمارِ محتوا و مهاجرتها.
|
||
- **خطر:** نقشهی حمله: مهاجم میفهمد محدودیتها سهبرابر دورزدنیاند، حسابِ کدِ ثابت
|
||
هست، اعتبارِ AI کی تمام شده، و ترافیکِ واقعی چقدر است.
|
||
- **چرا باز مانده:** صاحبِ پروژه و agentها بدونِ دسترسیِ SSH از همین صفحه سلامتِ سرور را
|
||
میبینند.
|
||
- **رفع:** پاسخِ عمومی فقط `{"status": "ok"|"degraded"}`؛ بقیه پشتِ هدرِ
|
||
`X-Internal-Key` (`INTERNAL_SERVICE_KEY`) یا فقط برای staff.
|
||
- **چطور بسنجید:** `curl https://mathapi.tadrisino2.ir/api/health` بدونِ هدر نباید
|
||
`warnings` و `traffic` داشته باشد.
|
||
|
||
## ۵. مستنداتِ API عمومی
|
||
|
||
- **چیست:** `/api/docs/` (Swagger)، `/api/redoc/` و `/api/schema/` بدونِ ورود بازاند
|
||
(تأیید: HTTP 200). schema همهی endpointها، پارامترها، کدهای خطا و هدرهای داخلی
|
||
(`X-Internal-Key`) را فهرست میکند.
|
||
- **خطر:** شناساییِ کاملِ سطحِ حمله بدونِ زحمت.
|
||
- **چرا باز مانده:** قراردادِ مشترک با فرانت همینجاست؛ فرانتکار و ارائه به آن نیاز دارند.
|
||
- **رفع:** مسیر را در nginx به IPهای تیم ببندید، یا در `SPECTACULAR_SETTINGS` هر دوِ
|
||
`SERVE_PERMISSIONS = ['rest_framework.permissions.IsAdminUser']` و
|
||
`SERVE_AUTHENTICATION = ['rest_framework.authentication.SessionAuthentication']` را بگذارید.
|
||
`IsAdminUser` بهتنهایی کار نمیکند: احرازِ پیشفرضِ DRF فقط توکنِ Bearer است، پس ادمینِ واردشده
|
||
به پنل هم ۴۰۱ میگیرد (آزموده شد). اسکریپتِ Swagger/ReDoc نسخهی ثابت دارد (نه `@latest`)، چون
|
||
روی همان دامنهی `/admin/` اجرا میشود.
|
||
- **چطور بسنجید:** `curl -o /dev/null -w "%{http_code}" …/api/schema/` باید ۴۰۱/۴۰۳ بدهد.
|
||
|
||
## ۷. پنلِ ادمین روی دامنهی عمومی
|
||
|
||
- **چیست:** `/admin/` روی `mathapi.tadrisino2.ir`، با ورودِ نامکاربری/رمزِ جنگو. سقفِ ۱۰ تلاش در ۱۵ دقیقه برای هر IP دارد (`AdminLoginThrottleMiddleware`)،
|
||
ولی: شمارش per-worker است (مورد ۹)، IP با XFF جعلپذیر است (مورد ۸)، و ۲FA ندارد.
|
||
- **خطر:** ادمین همهی محتوا، پلنها، کدهای تخفیف، سفارشها و کاربرها را کنترل میکند.
|
||
- **رفع:** محدود کردنِ `/admin/` در nginx به IPهای تیم یا VPN؛ تغییرِ مسیرِ پیشفرض؛ ۲FA؛
|
||
رمزهای قوی و حسابِ staffِ جدا برای هر نفر.
|
||
|
||
## ۸. دسترسیِ مستقیم به پورتِ ۸۰ (دور زدنِ ArvanCloud)
|
||
|
||
- **چیست:** `http://78.38.35.249/api/health` با `Host: mathapi.tadrisino2.ir` پاسخ میدهد
|
||
(تأیید: HTTP 200). `NUM_PROXIES=2` فرض میکند همیشه دو پروکسی جلوی ماست.
|
||
- **خطر:** کسی که IP را بداند `X-Forwarded-For` را جعل میکند و محدودیتهای IP-محور
|
||
(OTP، ادمین) را دور میزند.
|
||
- **رفع (ادمینِ سرور):** پورتِ ۸۰/۴۴۳ فقط برای رنجهای ArvanCloud، یا `set_real_ip_from` +
|
||
`real_ip_header` در nginx.
|
||
|
||
- **بازتولید شد ۲۰۲۶-۱۰-۰۴:** `curl -H 'Host: mathapi.tadrisino2.ir' -H 'X-Forwarded-For: 1.2.3.4'
|
||
http://78.38.35.249/api/health` جواب داد `clientIp: 1.2.3.4`. یعنی هر سقفِ IP-محور (ورودِ ادمین
|
||
۱۰ در ۱۵ دقیقه، درخواست و ورودِ OTP) با عوض کردنِ این هدر در هر
|
||
درخواست دور زده میشود، و حفاظتِ DDoS/WAFِ آروان هم برای این مسیر وجود ندارد. سقفهای
|
||
per-phone و per-code که در دیتابیس شمرده میشوند متأثر نیستند. پورتهای ۸۰ و ۴۴۳ هر دو باز بودند.
|
||
|
||
## ۹. نبودِ کشِ مشترک (`CACHE_URL`)
|
||
|
||
- **چیست:** بدونِ `CACHE_URL` هر worker کشِ `LocMemCache` خودش را دارد (۳ worker).
|
||
- **خطر:** همهی محدودیتهای مبتنی بر کش تا سه برابر شلترند: throttleهای DRF، cooldown و
|
||
سقفِ درخواستِ OTP، سقفِ تلاشِ ورودِ OTP بهازای شماره، سقفِ ورودِ ادمین؛ و باطلشدنِ کش
|
||
بعد از تغییرِ ادمین فقط در یک worker. (سقفِ حدسِ هر کدِ OTP در دیتابیس است و متأثر نیست.)
|
||
- **رفع:** `CACHE_URL=redis://127.0.0.1:6379/1` (db0 مالِ اپِ دیگری است) و ریاستارت.
|
||
- **چطور بسنجید:** `/api/health` → `checks.cache.backend` نباید `LocMemCache` باشد.
|
||
|
||
## ۱۱. کدِ OTP بیهش در دیتابیس
|
||
|
||
- **چیست:** `OTPCode.code` متنِ ساده است و در فهرستِ پنلِ ادمین (`OTPCodeAdmin`) دیده میشود.
|
||
توکنِ تمدید (`Session.refresh_token`) هششده است و اینجا شامل نیست.
|
||
- **چرا باز مانده:** تیم برای آزمایش بیپیامک یک ردیفِ `OTPCode` در پنل میسازد و کدش را میبیند.
|
||
- **خطر:** کسی که پنل یا دیتابیس را ببیند، در چند دقیقهی عمرِ هر کد میتواند با آن وارد شود.
|
||
- **رفع:** ذخیرهی هش و حذفِ ستون از فهرستِ پنل، همراه با راهِ دیگری برای ورودِ آزمایشی.
|
||
|
||
## ۱۲. هزینهی دستیارِ AI
|
||
|
||
- **چیست:** هر کاربر روزی ۳ سؤالِ رایگان دارد (`AI_DAILY_QUOTA_FREE`)
|
||
و بعد فقط با اعتبارِ خریدهشده؛ ۲۰ در ساعت. هر شمارهی تازه ۳ سؤالِ رایگان میگیرد و ساختنِ شماره با سقفِ پیامک
|
||
(بند ۲۴) محدود است. سقفِ
|
||
روزانهی کلِ سامانه (`AI_GLOBAL_DAILY_MAX`، ۳۰۰۰) خرجِ کل را محدود میکند. طولِ سؤال حداکثر ۲۰۰۰ نویسه است.
|
||
- **خطر:** با چند IP، اعتبارِ GapGPT در چند ساعت تمام میشود (اعتبار یک بار در ۲۰۲۶-۰۹-۲۵
|
||
به صفر رسید، البته به دلیلِ دیگر).
|
||
- **رفع:** سقفِ سراسریِ روزانه برای کلِ اپ؛ دستیار فقط برای کاربرِ واردشده با شماره یا
|
||
با سقفِ بسیار کمتر برای مهمان؛ هشدار وقتی `aiProvider.state` در `/api/health` برابرِ
|
||
`quota_exhausted` شد.
|
||
|
||
## ۱۴. دیتابیس و بکاپ
|
||
|
||
- **چیست:** SQLite در `~/mathapp-v2/db.sqlite3`، روی دیسکی که بینِ چند سرویس مشترک است و
|
||
~۸۳٪ پر است. `transaction_mode: IMMEDIATE` همزمانی را درست ولی سریالی میکند. بکاپ دستی
|
||
است (`deploy.sh` پیش از `migrate` بکاپ نمیگیرد) و در همان پوشه میماند.
|
||
- **خطر:** از دست رفتنِ کلِ داده با یک خطای دیسک یا یک دستورِ اشتباه؛ پُر شدنِ دیسک
|
||
سرویسهای دیگر (Gitea، ایمیل) را هم میخواباند.
|
||
- **رفع:** مهاجرت به Postgres (روی همان ماشین نصب است)، بکاپِ خودکار پیش از هر `migrate`، و
|
||
نگهداریِ بکاپ بیرون از ماشین.
|
||
|
||
## ۱۵. دسترسیِ سرور
|
||
|
||
- **چیست:** ورود با رمز به حسابِ مشترکِ `administrator` با `sudo (ALL : ALL) ALL` و عضوِ
|
||
`docker` و `lxd`، روی ماشینی که Gitea و ایمیلِ شرکت هم رویش است. رمز یک بار در گفتگو با
|
||
agent گذاشته شده و روی دسکتاپِ صاحبِ پروژه در یک فایلِ متنی نگه داشته میشود.
|
||
- **خطر:** هر نشتِ رمز یعنی دسترسیِ root به کلِ ماشینِ شرکت، نه فقط این پروژه.
|
||
۲۰۲۶-۱۰-۰۴: `PasswordAuthentication yes` در `sshd_config` و پورتِ ۲۲ از اینترنت باز است؛ پس رمز
|
||
از هر جای اینترنت حدسزدنی است (fail2ban سنجیده نشد).
|
||
- **رفع (ادمینِ سرور):** کلیدِ SSH بهجای رمز، تعویضِ رمز، حسابِ جدا برای این پروژه با
|
||
دسترسیِ فقط به `~/mathapp-v2` و یک خطِ `NOPASSWD` فقط برای
|
||
`systemctl restart zareian-mathapp`؛ پاک کردنِ فایلِ رمز از دسکتاپ (Keychain جایگزین است).
|
||
|
||
## ۱۶. نگهداریِ توکن در فرانت
|
||
|
||
- **چیست:** کلاینتِ فرانت (`src/api/index.ts` در مخزنِ فرانت) توکن و refresh را در
|
||
`localStorage` نگه میدارد.
|
||
- **خطر:** هر XSS در سایت هر دو را میخواند.
|
||
- **رفع:** CSPِ سختگیرانه روی `math.tadrisino2.ir`؛ در بلندمدت refresh در کوکیِ `HttpOnly`.
|
||
|
||
## ۲۰. یونیتِ systemd: DEBUG و کلیدِ ناامن
|
||
|
||
- **چیست:** `systemctl cat gunicorn-mathapp` هنوز `Environment="DJANGO_DEBUG=1"` و
|
||
`Environment="DJANGO_SECRET_KEY=django-insecure-…"` دارد. `EnvironmentFile=.env` بعد از آن میآید و
|
||
`.env` خطِ `DJANGO_DEBUG=0` دارد (شمرده شد، مقدار چاپ نشد)؛ برای همین الان DEBUG خاموش است.
|
||
- **خطر:** اگر روزی آن یک خط از `.env` پاک شود (مثلاً با این فکر که «پیشفرضِ DEBUG خاموش است»)،
|
||
بعد از ریاستارت DEBUG روشن میشود و کدِ `0000` برای هر شمارهای پذیرفته میشود. `deploy.sh` این را
|
||
نمیبیند، چون تنظیماتِ مؤثر را با محیطِ شِل میخواند نه محیطِ یونیت.
|
||
- **رفع (ادمینِ سرور، با بکاپِ یونیت):** هر دو خطِ `Environment=` را از یونیت بردارید، `daemon-reload`،
|
||
ریاستارت. حذفِ خطِ کلید بهتنهایی کسی را بیرون نمیاندازد اگر `.env` همان کلید را داشته باشد؛
|
||
عوض کردنِ خودِ کلید (مورد ۱) جداست.
|
||
|
||
## ۲۲. هدرهای امنیتیِ فرانت
|
||
|
||
- **چیست:** `https://math.tadrisino2.ir/` فقط `x-xss-protection` دارد؛ نه CSP، نه `X-Frame-Options`/
|
||
`frame-ancestors`، نه HSTS، نه `X-Content-Type-Options`. API همهی اینها را جز HSTS و CSP دارد.
|
||
- **خطر:** صفحهی اپ را میشود در iframeِ سایتِ دیگری گذاشت (clickjacking)؛ و چون توکن در
|
||
`localStorage` است (مورد ۱۶)، نبودِ CSP اثرِ هر XSSِ احتمالی را کامل میکند. در خودِ فرانت سینکِ XSS
|
||
پیدا نشد (تنها `dangerouslySetInnerHTML` روی SVGِ ثابتِ شکلکهاست).
|
||
- **رفع:** در nginxِ `mathapp-front` یا در آروان: `Content-Security-Policy` با `frame-ancestors 'none'`
|
||
و `connect-src` به دامنهی API، و `X-Content-Type-Options: nosniff`.
|
||
|
||
## ۲۴. پر کردنِ سقفِ سراسریِ پیامک
|
||
|
||
- **چیست:** `OTP_SMS_GLOBAL_DAILY_MAX` (۳۰۰ در ۲۴ ساعت) جلوی خرجِ بیسقفِ پیامک را میگیرد. هر
|
||
ناشناسی با درخواستِ کد برای شمارههای تصادفی آن را پر میکند؛ از یک IP بیجعل ساعتی ۲۰ درخواست،
|
||
یعنی روزی ۴۸۰، پس یک نفر سقف را همیشه پر نگه میدارد.
|
||
- **بازتولید شد:** ۳۰۰ درخواست پذیرفته و ۳۰۰ پیامک فرستاده شد؛ بعد درخواستِ کاربرِ واقعی ۵۰۳.
|
||
- **خطر:** هیچ ثبتنامِ تازه، ورودِ دوباره یا حذفِ حسابی ممکن نیست؛ هزینهی ۳۰۰ پیامک در روز.
|
||
- **رفع (تصمیمِ طراحی):** بعد از گذشتن از یک حد، CAPTCHA یا مانعِ مشابه پیش از ارسال؛ شمارشِ سقف
|
||
بر اساسِ IP/شبکه؛ هشدار در نیمهی سقف.
|
||
|
||
## ۲۵. بستنِ ورودِ یک شمارهی مشخص
|
||
|
||
- **چیست:** ۱۰ درخواستِ کد در ~۲ ساعت سقفِ روزانهی آن شماره را پر میکند (صاحبش تا ۲۴ ساعت کد
|
||
نمیگیرد)، و ۵ حدسِ غلطِ هر کسی کدِ تازهی او را باطل میکند. هر دو بازتولید شد.
|
||
- **رفع (تصمیمِ طراحی):** کد به درخواستکننده گره بخورد (درخواستِ کد یک شناسهی موقت برگرداند که
|
||
ورود لازمش دارد) و حدسِ غلط برای هر درخواستکننده جدا شمرده شود.
|
||
|
||
## ۲۶. پایانِ درس و جزیره به گفتهی کلاینت
|
||
|
||
- **چیست:** `POST /api/nodes/{id}/quiz/complete` و `POST /api/lessons/{id}/complete` تعدادِ سوال و
|
||
جوابِ درست را از کلاینت میگیرند. بازتولید شد: با قلبِ صفر و `totalQuestions=1` جزیره تمام
|
||
میشود؛ جزیرهی قفل با `correctCount=0` تمام میشود؛ درس و جزیره بیهیچ جوابی تمام میشوند، پس
|
||
`GET /api/user/weekly-report` پیشرفتِ ۱۰۰٪ نشان میدهد بیآنکه سوالی جواب داده شده باشد.
|
||
- **چرا باز مانده:** رفتارِ آزمون در فرانت به همین شکل است؛ عوض کردنش پیش از ارائه جریانِ نقشه را
|
||
به خطر میاندازد.
|
||
- **رفع:** تعدادِ سوالِ آزمون را سرور تعیین کند (همان `/quiz` با seed)، همان تعداد جوابِ ثبتشده
|
||
لازم باشد، و جزیرهی قبلی تمام شده باشد.
|
||
|
||
## ۲۷. کلیدِ جواب با حسابِ دوم، امتیازِ دوباره
|
||
|
||
- **چیست:** جوابِ غلط `correctOptionId` را برمیگرداند و برای همه یکی است؛ پایانِ درس و پایانِ
|
||
جزیره هر کدام جدا امتیاز میدهند. یک مهمانِ دورریختنی کلید را درمیآورد و حسابِ اصلی همه را درست
|
||
میزند (۴ سوال، ۸۰ امتیاز). رتبهبندی و آمارِ درصدِ غلط آلوده میشوند.
|
||
- **رفع (تصمیمِ محصولی):** امتیازِ هر سوال یک بار؛ گزینهی درست فقط بعد از پایانِ آزمون.
|
||
|
||
## ۲۸. توکنِ تمدیدِ دزدیدهشده
|
||
|
||
- **چیست:** استفادهی دوباره از یک توکنِ تمدیدِ باطلشده فقط ۴۰۱ میگیرد؛ اگر دزد اول تمدید کند،
|
||
صاحبِ حساب بیرون میافتد و دزد میماند. «خروج از همهی دستگاهها» هم نیست.
|
||
- **چرا باز مانده:** باطل کردنِ کلِ زنجیره با استفادهی دوباره، حسابهای دمو را که در ارائه چند
|
||
نفر همزمان با آنها کار میکنند بیرون میاندازد.
|
||
- **رفع:** شناسهی خانواده روی `Session`؛ با استفادهی دوباره کلِ خانواده باطل شود؛ گزینهی خروج از همهجا.
|
||
|
||
## ۲۹. پیشنهادِ ثبتنام بعد از حذفِ حساب
|
||
|
||
- **چیست:** پیشنهادِ خودکارِ ثبتنام به `user_id` گره خورده است؛ حذفِ حساب و ورودِ دوباره با همان
|
||
شماره یک کاربرِ تازه میسازد و پیشنهاد دوباره داده میشود. فقط وقتی اثر دارد که یک Offerِ
|
||
ثبتنام فعال باشد.
|
||
- **رفع:** گره زدنِ پیشنهادِ ثبتنام به هشِ شماره.
|
||
|
||
## ۳۰. مسیرهای خواندنیِ بیthrottle
|
||
|
||
- **چیست:** `lessons/{id}/questions` و `lessons/{id}/intro` (بیورود)، `progress/nodes`،
|
||
`user/hearts`، `notifications/{id}/read`، `support/tickets/{id}` و چند مسیرِ دیگر `throttle_scope`
|
||
ندارند؛ کلِ بانکِ سوال بیورود قابلِ کپی است. (`/api/schema/` سقفِ `read` دارد.)
|
||
- **رفع:** برای هر کدام `throttle_scope` و ردیفِ نرخ در `config/settings.py` اضافه شود.
|
||
|
||
## ۳۱. لپتاپِ توسعه روی شبکهی محلی
|
||
|
||
- **چیست:** `mobile-preview/serve.py` عمداً روی کلِ شبکه گوش میدهد (برای گوشیِ روی همان وایفای) و
|
||
`/api/` را به runserverِ محلی میفرستد؛ در DEBUG کدِ `0000` هر شمارهای را وارد میکند و کلیدِ
|
||
GapGPTِ لپتاپ همان حسابِ سرور است. (`docker compose` فقط روی `127.0.0.1` باز است و کلیدِ سرویسِ
|
||
داخلی دیگر پیشفرض ندارد.)
|
||
- **رفع:** روی شبکهی عمومی (کافه، مدرسه) اجرا نشود؛ برای توسعه کلیدهای جدای GapGPT و Qdrant.
|
||
|
||
---
|
||
|
||
## حفاظهایی که هست (تا دوباره باز نشوند)
|
||
|
||
هر تغییری در این مسیرها باید تستهای نامبرده را سبز نگه دارد. جزئیاتِ پرداخت و تخفیف در
|
||
`docs/architecture.md`.
|
||
|
||
| حفاظ | تست |
|
||
|---|---|
|
||
| ورودِ مهمان وجود ندارد؛ مدلِ `User` ستونِ `is_guest`/`device_id` ندارد و migrationِ حذفشان تا مهمانی مانده باشد میشکند | `accounts/migrations/0008_remove_guest_login.py` |
|
||
| پلنِ هر خرید روی سفارش؛ خرید اشتراکِ فعال را دست نمیزند؛ یک کدِ تخفیف پیش از درگاه؛ امتیاز فقط از جوابِ ثبتشده | `payment/tests_audit_fixes.py`، `accounts/tests_audit_fixes.py` |
|
||
| لینکِ پرداخت (`/api/payment/orders/{id}/pay`) فقط با امضای کوتاهعمرِ همراهِ `paymentUrl` باز میشود | `payment/tests.py` (`PaymentRedirectViewTests`) |
|
||
| توکنِ تمدید فقط بهصورتِ هشِ SHA-256 ذخیره میشود | `accounts/tests.py` (`RefreshTokenStoredHashedTests`) |
|
||
| جدولِ رتبهبندی فقط برای کاربرِ واردشده | `progress/tests.py` (`LeaderboardPeriodValidationTests`) |
|
||
| شمارهی موبایل نرمال میشود؛ cooldown، سقفِ روزانهی هر شماره و سقفِ کلِ پیامک در دیتابیس شمرده میشوند | `accounts/tests_otp_limits.py` |
|
||
| OTPِ ۶ رقمی با ۵ حدس برای هر کد؛ خروج از حساب توکنهای قبلی را باطل میکند | `accounts/tests_audit_fixes.py` |
|
||
| قلبِ صفر جوابِ سوال را میبندد؛ چکش داخلِ تراکنش است (پایانِ جزیره را فقط کامل نمیبندد، مورد ۲۶) | `content/tests_hearts_gate.py` |
|
||
| `select_for_update` مؤثر با `transaction_mode: IMMEDIATE`؛ مقایسهی زمانثابتِ `X-Internal-Key` | `payment/tests_audit_fixes.py` |
|
||
| دیتابیس و بکاپها فقط برای صاحبِ سرویس خواندنیاند (مرحلهی «مجوزِ فایلهای داده» در `deploy.sh`) | با `sudo -u nobody head -c 15 ~/mathapp-v2/db.sqlite3` روی سرور باید خطای دسترسی بدهد |
|
||
| کتابخانهها بدونِ آسیبپذیریِ شناختهشده (`urllib3>=2.8.0`، `python-dotenv 1.2.2`) | پایگاهِ OSV روی `pip freeze` |
|
||
| actionهای لغو/بازپرداختِ سفارش، علامتِ «بررسیشده» و لینکِ بازار اجازهی تغییر میخواهند؛ کارمندِ فقطخواندنی (Supervisor) اجرایشان نمیکند | `payment/tests_security_review.py`، `content/tests_security_review.py` |
|
||
| ورودِ فایلِ سوال/برنامهی درسی اجازهی افزودن (و برای جایگزینی، حذف) میخواهد؛ مرورگرِ محتوا اجازهی دیدنِ سوال | `content/tests_security_review.py` |
|
||
| حذفِ حساب سابقهی کدِ تخفیف، پیشنهادها و کدهای پیامکیِ شماره را نگه میدارد؛ حسابِ دمو حذف نمیشود | `accounts/tests_security_review.py` |
|
||
| صافیِ اطلاعاتِ تماس در نامِ نمایشی (شماره با هر جداکننده، آیدیِ شبکهی اجتماعی) | `accounts/tests_security_review.py` |
|
||
| کلیدِ کاوهنگار و توکنِ خریدِ بازار در لاگِ خطای شبکه نمیآیند | `accounts/tests_security_review.py` |
|
||
| برگشت از زرینپال بی`Authority` ۴۰۰؛ خریدِ دومِ بازار روی سفارشِ پرداختشده ۴۰۹؛ آنچه داده یا پس گرفته میشود از خودِ سفارش است، نه پلنِ فعلی | `payment/tests_security_review.py` |
|
||
| شکلِ خرابِ مدل جواب را رایگان نمیکند؛ علامتِ «خارج از ریاضی» در هر جای استریم جواب را رد میکند و در کش نمیرود؛ بستنِ صفحه جوابِ ناقص را ثبت میکند؛ collectionِ بیرون از `KNOWN_COLLECTIONS` جستجو نمیشود | `ai_tutor/tests_security_review.py` |
|
||
| ورودیِ عددیِ غیرِ ASCII (مثلِ `²`) ۴۰۰ است نه ۵۰۰؛ متنِ تیکت/گزارشِ باگ حداکثر ۵۰۰۰ نویسه | `content/tests_security_review.py` |
|
||
| `INTERNAL_SERVICE_KEY` در هیچ حالتی پیشفرض ندارد | `payment/tests.py` (`test_debug_mode_has_no_default_internal_key`) |
|
||
| کلیدِ امضای خودساخته (`.django_secret_key`) یک بار و اتمی ساخته میشود؛ سه workerِ همزمان یک کلید دارند | دستی: سه پروسهی همزمان که `config.settings` را import میکنند، بیفایلِ کلید؛ ۲۰ از ۲۰ یک کلید |
|
||
| فرانت: شمارهی سفارشِ برگشتی از درگاه فقط عدد است، مسیرها encode میشوند و «پرداخت موفق» فقط از پاسخِ سرور میآید؛ دو تبِ همزمان با تمدیدِ توکن خارج نمیشوند | مخزنِ فرانت: `src/app/launchParams.ts`، `src/api/http.ts` |
|
||
| سفارش و تراکنش در پنل کاملاً فقطخواندنیاند (کارمندِ لغو/بازپرداخت مبلغ یا اعتبارِ سفارش را عوض نمیکند) و از پنل ساخته نمیشوند | `payment/tests_security_review.py` (`OrderAdminMoneyFieldsReadOnlyTests`) |
|
||
| ردِ بازار توکنِ خرید را نمیسوزاند؛ سفارشِ بازپرداختشده توکنِ تازه نمیپذیرد | `payment/tests_security_review.py` (`BazaarRejectedVerifyDoesNotBurnTokenTests`) |
|
||
| کدِ تخفیفِ وسطِ رفتن به درگاه، مبلغِ authority را با سفارش ناهمخوان نمیکند | `payment/tests_security_review.py` (`DiscountDuringPaymentRequestTests`) |
|
||
| پرداختِ بستهای که پلنش پاک شده از عددهای ثبتشدهی سفارش اعتبار میدهد (و اگر نداشت ERROR لاگ میشود)؛ پلن فقط با IRT ذخیره میشود | `subscription/tests_security_review.py` (`PaidPackWithDeletedPlanTests`، `PlanCurrencyIsTomanOnlyTests`) |
|
||
| توکنِ بازار (`refresh_token`، `pending_state`) با فیلترِ querystringِ پنل حرفبهحرف درنمیآید | `payment/tests_security_review.py` (`BazaarTokenNotGuessableByAdminFilterTests`) |
|
||
| شمارهی کاربر را فقط ابرکاربر عوض میکند؛ حسابهای کارمند و گروهها فقط با ابرکاربر ساخته/تغییر میکنند | `accounts/tests_security_review.py` (`StaffCannotSwapLoginPhoneTests`، `StaffAccountsAdminSuperuserOnlyTests`) |
|
||
| لاگِ تحویلِ کمپین اجازهی view میخواهد (هر کارمندی نمیبیند) | `campaigns/tests_security_review.py` |
|
||
| هر رقمِ یونیکد (تمامعرض، ریاضی…) در شماره به لاتین برمیگردد؛ کدِ غیرِ ASCII، پایهی ناموجود یا عظیم و تاریخِ تولدِ با «²» ۴۰۰ است نه ۵۰۰؛ صافیِ نامِ نمایشی ارقامِ یونیکد و جداکنندهی دلخواه را میگیرد | `accounts/tests_security_review.py` (`UnicodeInputsAreNormalisedOrRejectedTests`) |
|
||
| `/api/home?gradeId=` ناموجود کش نمیشود (کشِ پُر شمارندهی سقفِ ورودِ ادمین را پاک میکرد)؛ `/api/progress/nodes` یک کوئری است و `unit` عظیم ۴۰۰ | `content/tests_security_review.py` (`JunkGradeIdsDoNotFlushCacheTests`، `ProgressNodesInputAndQueriesTests`) |
|
||
|