Remove the guest system entirely: no is_guest/device_id, no guest throttles, phone login is the only way in

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>
This commit is contained in:
2026-10-05 18:20:10 +03:30
parent 8835e01595
commit 663252a260
61 changed files with 357 additions and 1518 deletions

View File

@@ -23,21 +23,19 @@
|---|---|---|---|
| ۱ | کلیدِ امضای توکن روی سرور همان رشته‌ی داخلِ ریپو است | بحرانی — جعلِ ورود به‌جای هر کاربر | کد آماده؛ اعمال روی سرور |
| ۲ | ۷ حسابِ دمو با کدِ ورودِ ثابت روی پروداکشن | بالا | `.env` سرور |
| ۳ | داده‌ی دمو و حسابِ مهمانِ نمایشیِ `frontend-test-device` | بالا | دیتابیسِ سرور |
| ۳ | داده‌ی دمو روی دیتابیسِ پروداکشن | بالا | دیتابیسِ سرور |
| ۴ | `/api/health` جزئیاتِ داخلی را به همه نشان می‌دهد | متوسط | کد |
| ۵ | Swagger، ReDoc و schemaی کاملِ API عمومی‌اند | متوسط | کد |
| ۷ | پنلِ `/admin/` روی دامنه‌ی عمومی | متوسط | nginx + کد |
| ۸ | سرور بدونِ ArvanCloud روی پورتِ ۸۰ هم جواب می‌دهد | متوسط | ادمینِ سرور |
| ۹ | کشِ مشترک (Redis) نیست؛ محدودیت‌ها per-worker‌اند | متوسط | `.env` سرور |
| ۱۰ | حساب‌های مهمانِ قدیمی (دمو و کاربرانِ پیش از حذفِ ورودِ مهمان) هنوز در دیتابیس‌اند | کم | دیتابیسِ سرور |
| ۱۱ | کدِ OTP بی‌هش در دیتابیس و در فهرستِ پنل | کم | کد |
| ۱۲ | هزینه‌ی دستیارِ AI با ساختِ حسابِ تازه (هر شماره ۳ سوالِ رایگان) قابلِ بالا بردن است | کم (مالی) | کد |
| ۱۲ | هزینه‌ی دستیارِ AI با ساختِ شماره‌ی تازه (هر شماره ۳ سوالِ رایگان) قابلِ بالا بردن است | کم (مالی) | کد |
| ۱۴ | SQLite روی دیسکِ مشترکِ پُر، بدونِ بکاپِ خودکارِ بیرون از ماشین | کم (داده) | زیرساخت |
| ۱۵ | دسترسیِ سرور: حسابِ مشترک، رمز، sudoِ کامل | بالا | ادمینِ سرور |
| ۱۶ | فرانت توکن را در `localStorage` نگه می‌دارد | کم | فرانت |
| ۲۰ | یونیتِ systemd `DJANGO_DEBUG=1` و کلیدِ ناامن دارد؛ فقط یک خطِ `.env` جلوی DEBUG را گرفته | بالا (نهفته) | ادمینِ سرور |
| ۲۲ | سایتِ فرانت هیچ هدرِ امنیتی ندارد (CSP، frame-ancestors، HSTS) | کم | nginx/فرانت |
| ۲۳ | سقفِ دستیار برای مهمان‌ها بر اساسِ IP مشترک است؛ کلاسِ پشتِ یک IP بعد از ۲۰ سوال در ساعت ۴۲۹ می‌گیرد | متوسط (دسترس‌پذیری) | تصمیمِ محصولی |
| ۲۴ | سقفِ روزانه‌ی ۳۰۰ پیامکِ کلِ سامانه را یک ناشناس پر می‌کند و ورود با پیامک برای همه بسته می‌شود | بالا (دسترس‌پذیری، هزینه) | تصمیمِ طراحی |
| ۲۵ | با دانستنِ شماره‌ی کسی، ورودِ او تا ۲۴ ساعت بسته می‌شود | متوسط | تصمیمِ طراحی |
| ۲۶ | پایانِ درس و جزیره به گفته‌ی کلاینت است (جزیره‌ی قفل، قلبِ صفر، گزارشِ والدین) | متوسط | کد، بعد از ارائه |
@@ -78,22 +76,14 @@
- **رفع:** `TEST_LOGIN_PHONES` را از `.env` سرور پاک کنید و ری‌استارت کنید.
- **چطور بسنجید:** `/api/health` دیگر هشدارِ «شماره‌های تستِ ورود با کدِ ثابت فعال‌اند» ندارد.
## ۳. داده‌ی دمو و مهمانِ عمومی
## ۳. داده‌ی دمو روی دیتابیسِ پروداکشن
- **افزوده ۲۰۲۶-۰۹-۲۵:** کاربرِ ۱۷ (`09000000001`، «کاربر تست»، `deviceId =
deploy-check-20260925`) از تستِ دیپلویِ پیامک، و کاربرِ ۱۸ (مهمان، `deploy-check-ai-20260925`)
از تستِ دستیار؛ همراهِ بقیه پاک شوند.
- **چیست:** کاربرهای ۶ (`deviceId = frontend-test-device`) و ۱۰ تا ۱۵، با درس، گفتگو،
تیکت، خانواده و زیرمجموعه، روی دیتابیسِ پروداکشن ساخته شده‌اند. `frontend-test-device`
در ریپو نوشته شده. `POST /api/auth/session` حذف شده، پس با آن دیگر توکن نمی‌دهد؛ ولی خودِ
ردیف‌ها (با `is_guest=True`) تا پاک نشوند در دیتابیس‌اند.
- **خطر:** داده‌ی ساختگی در آمار و جدولِ رتبه‌بندیِ واقعی؛ یک حسابِ عمومی که هر کس
می‌تواند با آن کار کند (ثبتِ تیکت، گفتگو با AI به هزینه‌ی ما).
- **رفع:** پیش از کاربرِ واقعی پاک شوند. `purge_demo_data` این‌ها را **نمی‌گیرد** (از مسیرِ
واقعیِ viewها ساخته شده‌اند)؛ باید با شناسه پاک شوند، پیش از آن بکاپ. همراهِ آن پیش‌فرضِ
`DEMO_GUEST_DEVICE_IDS` در `config/settings.py` (که حذفِ این مهمان را با `POST /api/user/delete`
می‌بندد) هم برداشته شود.
- **چطور بسنجید:** در پنل یا shell، `User.objects.filter(is_guest=True).count()` پس از پاک‌سازی باید صفر باشد.
- **چیست:** کاربرهای ۱۰ تا ۱۵ و ۲۵ (حساب‌های دمو با کدِ ثابت، بند ۲) با درس، گفتگو، تیکت و
سفارش روی دیتابیسِ پروداکشن ساخته شده‌اند. حسابِ ۱۰ («علی رضایی»، با ۷ سفارش) حسابِ نمایشیِ اصلی است.
- **خطر:** داده‌ی ساختگی در آمار و جدولِ رتبه‌بندیِ واقعی.
- **رفع:** پیش از کاربرِ واقعی با `delete_users --ids … --yes` (پیش از آن بکاپ) پاک شوند.
`purge_demo_data` این‌ها را **نمی‌گیرد** (از مسیرِ واقعیِ viewها ساخته شده‌اند).
- **چطور بسنجید:** `/api/health` → `traffic.users` فقط کاربرانِ واقعی را بشمارد.
## ۴. `/api/health` عمومی و پرجزئیات
@@ -138,13 +128,13 @@
- **چیست:** `http://78.38.35.249/api/health` با `Host: mathapi.tadrisino2.ir` پاسخ می‌دهد
(تأیید: HTTP 200). `NUM_PROXIES=2` فرض می‌کند همیشه دو پروکسی جلوی ماست.
- **خطر:** کسی که IP را بداند `X-Forwarded-For` را جعل می‌کند و محدودیت‌های IP-محور
(مهمان، OTP، ادمین) را دور می‌زند.
(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، ساختِ مهمان، دستیارِ مهمان) با عوض کردنِ این هدر در هر
۱۰ در ۱۵ دقیقه، درخواست و ورودِ OTP) با عوض کردنِ این هدر در هر
درخواست دور زده می‌شود، و حفاظتِ DDoS/WAFِ آروان هم برای این مسیر وجود ندارد. سقف‌های
per-phone و per-code که در دیتابیس شمرده می‌شوند متأثر نیستند. پورت‌های ۸۰ و ۴۴۳ هر دو باز بودند.
@@ -157,16 +147,6 @@
- **رفع:** `CACHE_URL=redis://127.0.0.1:6379/1` (db0 مالِ اپِ دیگری است) و ری‌استارت.
- **چطور بسنجید:** `/api/health` → `checks.cache.backend` نباید `LocMemCache` باشد.
## ۱۰. حساب‌های مهمانِ قدیمی
- **چیست:** ورودِ مهمان (`POST /api/auth/session`) حذف شده و هر کاربرِ تازه با شماره ثبت‌نام می‌کند.
ردیف‌های مهمانِ پیشین (`User.is_guest=True`، شامل دموها) در دیتابیس مانده‌اند و کدِ ارتقای مهمان به
حسابِ شماره‌دار (`LoginView`) و throttleهای مهمان‌محور هنوز برایشان کار می‌کنند.
- **خطر:** مهمانِ قدیمی هنوز با `refreshToken` ذخیره‌شده در مرورگرش وارد می‌ماند؛ شناسه‌ی دستگاه دیگر
راهِ ورود نیست.
- **رفع:** پس از پاک‌سازیِ دموها (بند ۳) و دیدنِ این‌که مهمانِ واقعی نمانده، کدِ مهمان
(`is_guest`، `GuestAware*Throttle`، ارتقا) حذف شود. تا آن روز عمداً نگه داشته شده.
## ۱۱. کدِ OTP بی‌هش در دیتابیس
- **چیست:** `OTPCode.code` متنِ ساده است و در فهرستِ پنلِ ادمین (`OTPCodeAdmin`) دیده می‌شود.
@@ -238,17 +218,6 @@
- **رفع:** در nginxِ `mathapp-front` یا در آروان: `Content-Security-Policy` با `frame-ancestors 'none'`
و `connect-src` به دامنه‌ی API، و `X-Content-Type-Options: nosniff`.
## ۲۳. سقفِ دستیار برای مهمان‌ها روی IPِ مشترک
- **چیست:** `GuestAwareAIThrottle` برای مهمان‌ها سطلِ `ai_ask` (۲۰ در ساعت) را بر اساسِ IP می‌سازد تا
ساختنِ مهمانِ تازه سقف را دور نزند.
- **بازتولید شد:** ۲۲ مهمانِ متفاوت از یک IP، هر کدام اولین سوالش: ۲۰ تا ۲۰۰، ۲۱اُم و ۲۲اُم ۴۲۹.
دانش‌آموزانِ مهمانِ یک کلاس پشتِ وای‌فایِ مدرسه یا CGNAT با هم ساعتی ۲۰ سوال دارند (روی سرور با ۳
workerِ بی‌Redis تا حدودِ ۶۰).
- **چرا تصمیمِ محصولی است:** سقفِ IP جلوی «مهمان بساز، سه سوالِ رایگان بگیر» را می‌گیرد.
سقفِ سراسریِ `AI_GLOBAL_DAILY_MAX` (۳۰۰۰ در روز، حدودِ ۶ دلار) هزینه را جدا محدود
می‌کند، پس می‌شود سطل را per-user کرد و فقط ساختِ مهمان را محدود نگه داشت. تا تصمیم، دست نخورد.
## ۲۴. پر کردنِ سقفِ سراسریِ پیامک
- **چیست:** `OTP_SMS_GLOBAL_DAILY_MAX` (۳۰۰ در ۲۴ ساعت) جلوی خرجِ بی‌سقفِ پیامک را می‌گیرد. هر
@@ -304,8 +273,7 @@
- **چیست:** `lessons/{id}/questions` و `lessons/{id}/intro` (بی‌ورود)، `progress/nodes`،
`user/hearts`، `notifications/{id}/read`، `support/tickets/{id}` و چند مسیرِ دیگر `throttle_scope`
ندارند؛ کلِ بانکِ سوال بی‌ورود قابلِ کپی است. (`/api/schema/` سقفِ `read` دارد.)
- **چرا باز مانده:** سقفِ مهمان‌ها بر اساسِ IP است (مورد ۲۳)؛ افزودنِ سقف به این مسیرها برای کلاسی
که پشتِ یک وای‌فای با مهمان کار می‌کند ۴۲۹ می‌سازد. همراهِ تصمیمِ مورد ۲۳ بسته شود.
- **رفع:** برای هر کدام `throttle_scope` و ردیفِ نرخ در `config/settings.py` اضافه شود.
## ۳۱. لپ‌تاپِ توسعه روی شبکه‌ی محلی
@@ -324,7 +292,7 @@
| حفاظ | تست |
|---|---|
| ورودِ مهمان وجود ندارد (`POST /api/auth/session` ۴۰۴ می‌دهد و کاربر نمی‌سازد)؛ حسابِ مهمانِ قدیمی همچنان refresh و ارتقا دارد | `accounts/tests.py` (`test_guest_login_endpoint_is_gone`)، `accounts/tests_guest_refresh.py` |
| ورودِ مهمان وجود ندارد؛ مدلِ `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`) |
@@ -337,8 +305,7 @@
| کتابخانه‌ها بدونِ آسیب‌پذیریِ شناخته‌شده (`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` |
| صافیِ اطلاعاتِ تماس در نامِ نمایشی (شماره با هر جداکننده، آیدیِ شبکه‌ی اجتماعی) | `accounts/tests_security_review.py` |
| کلیدِ کاوه‌نگار و توکنِ خریدِ بازار در لاگِ خطای شبکه نمی‌آیند | `accounts/tests_security_review.py` |
| برگشت از زرین‌پال بی‌`Authority` ۴۰۰؛ خریدِ دومِ بازار روی سفارشِ پرداخت‌شده ۴۰۹؛ آنچه داده یا پس گرفته می‌شود از خودِ سفارش است، نه پلنِ فعلی | `payment/tests_security_review.py` |
@@ -352,9 +319,8 @@
| کدِ تخفیفِ وسطِ رفتن به درگاه، مبلغِ 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`) |
| `device_id` با فیلترِ querystringِ پنل برای غیرِ ابرکاربر درنمی‌آید؛ شماره و `is_guest` را فقط ابرکاربر عوض می‌کند؛ حساب‌های کارمند و گروه‌ها فقط با ابرکاربر ساخته/تغییر می‌کنند | `accounts/tests_security_review.py` (`DeviceIdNotGuessableByAdminFilterTests`، `StaffCannotSwapLoginPhoneTests`، `StaffAccountsAdminSuperuserOnlyTests`) |
| شماره‌ی کاربر را فقط ابرکاربر عوض می‌کند؛ حساب‌های کارمند و گروه‌ها فقط با ابرکاربر ساخته/تغییر می‌کنند | `accounts/tests_security_review.py` (`StaffCannotSwapLoginPhoneTests`، `StaffAccountsAdminSuperuserOnlyTests`) |
| لاگِ تحویلِ کمپین اجازه‌ی view می‌خواهد (هر کارمندی نمی‌بیند) | `campaigns/tests_security_review.py` |
| هر رقمِ یونیکد (تمام‌عرض، ریاضی…) در شماره به لاتین برمی‌گردد؛ کدِ غیرِ ASCII، `deviceId` null، پایه‌ی ناموجود یا عظیم و تاریخِ تولدِ با «²» ۴۰۰ است نه ۵۰۰؛ صافیِ نامِ نمایشی ارقامِ یونیکد و جداکننده‌ی دلخواه را می‌گیرد | `accounts/tests_security_review.py` (`UnicodeInputsAreNormalisedOrRejectedTests`) |
| مهمانِ نمایشیِ مشترک با ورودِ یک شماره به آن شماره تبدیل نمی‌شود | `accounts/tests_security_review.py` (`DemoGuestNotUpgradableTests`) |
| هر رقمِ یونیکد (تمام‌عرض، ریاضی…) در شماره به لاتین برمی‌گردد؛ کدِ غیرِ ASCII، پایه‌ی ناموجود یا عظیم و تاریخِ تولدِ با «²» ۴۰۰ است نه ۵۰۰؛ صافیِ نامِ نمایشی ارقامِ یونیکد و جداکننده‌ی دلخواه را می‌گیرد | `accounts/tests_security_review.py` (`UnicodeInputsAreNormalisedOrRejectedTests`) |
| `/api/home?gradeId=` ناموجود کش نمی‌شود (کشِ پُر شمارنده‌ی سقفِ ورودِ ادمین را پاک می‌کرد)؛ `/api/progress/nodes` یک کوئری است و `unit` عظیم ۴۰۰ | `content/tests_security_review.py` (`JunkGradeIdsDoNotFlushCacheTests`، `ProgressNodesInputAndQueriesTests`) |