Signed payment links, hashed refresh tokens, login-only leaderboard, load test tools

- /api/payment/orders/{id}/pay requires the short-lived signature that paymentUrl carries;
  without it (or with another order's) it is the same 404 as an unknown order
- Session.refresh_token stores the SHA-256 of the token; migration accounts.0007 hashes the
  existing rows so tokens clients already hold keep working
- /api/leaderboard requires a token (guests have one); names of children were scrapable
- RAG mock only for `manage.py test`, not any command with an argument named "test"
- tools/loadtest: user factory, load generator, fake GapGPT+Qdrant, with method notes
- security-before-production.md lists only open items; closed ones become a guards table
  with the tests that hold them. Comments describe current behaviour, not history

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 21:12:28 +03:30
parent b3f624a512
commit 645baf846b
22 changed files with 584 additions and 193 deletions

41
tools/loadtest/README.md Normal file
View File

@@ -0,0 +1,41 @@
# لودتستِ دستیار
سه اسکریپت، برای اینکه «چند دانش‌آموزِ هم‌زمان» هر بار با یک روش سنجیده شود. خروجیِ هر اجرا یک
JSON است با زمانِ هر پرسش و هر درخواستِ صفحه‌ی عادی؛ جمع‌بندی در خودِ خروجیِ ترمینال چاپ می‌شود.
| فایل | کار |
|---|---|
| `mkusers.py` | کاربرانِ آزمایشیِ غیرمهمان (شماره‌ی جعلیِ `0900…`، بی‌پیامک، بی‌سیگنال) با توکن و سه سوالِ متفاوت از بانکِ سوال برای هر کدام |
| `loadtest.py` | N نفر هم‌زمان می‌پرسند (استریم، دقیقاً مثلِ فرانت) و B نفرِ دیگر در خانه/آزمون/خلاصه/سهمیه می‌چرخند |
| `fake_upstream.py` | جایگزینِ GapGPT و Qdrant برای آزمایشگاه: embedding، جوابِ استریم با زمانِ لاگ‌نرمال (پیش‌فرض: میانه‌ی ۵٫۲ و صدکِ ۹۰ِ ۱۳ ثانیه، از جواب‌های واقعیِ سرور) و جست‌وجوی Qdrant |
## آزمایشگاه (بی‌هزینه، روی لپ‌تاپ)
روی یک کپی از پروژه اجرا کنید تا دیتابیسِ اصلیِ لوکال پر نشود. Qdrant هم شبیه‌سازی می‌شود، چون
از لپ‌تاپ پهنای باندِ بالارفتن (هر بردار ~۳۰ کیلوبایت) جست‌وجوها را سریالی نشان می‌دهد؛ از خودِ
سرور ۲۰ جست‌وجوی هم‌زمان ۰٫۲۶ ثانیه است.
```bash
python tools/loadtest/fake_upstream.py & # :8901 (FAKE_SIGMA=0 یعنی همیشه ۵٫۲ ثانیه)
export DJANGO_DEBUG=0 CACHE_URL= NUM_PROXIES=0 AI_DAILY_QUOTA_FREE=1000 HEARTS_ENFORCED=0 RAG_FORCE_MOCK=0 \
DJANGO_ALLOWED_HOSTS=127.0.0.1 OPENAI_BASE_URL=http://127.0.0.1:8901/v1 OPENAI_API_KEY=x \
QDRANT_URL=http://127.0.0.1:8901 QDRANT_API_KEY=x
LT_N=150 python manage.py shell -c "exec(open('tools/loadtest/mkusers.py').read())" | grep ^LTJSON | cut -c7- > users.json
gunicorn --bind 127.0.0.1:8800 config.wsgi:application & # همان gunicorn.conf.py ِ سرور
python tools/loadtest/loadtest.py http://127.0.0.1:8800 users.json 50 10 0 out.json --writes
```
پیش از هر اجرا gunicorn را از نو بالا بیاورید (کشِ جوابِ هر worker خالی شود) و مطمئن شوید پروسه‌ی
قبلی واقعاً رفته است؛ وگرنه سوال‌های تکراری از کش در ۰٫۱ ثانیه جواب می‌گیرند و عدد دروغ می‌گوید.
## سرورِ اصلی (هزینه‌ی واقعی: ~۰٫۱۸ سنت برای هر جواب)
۱. بکاپ از دیتابیس (`docs/operations.md`). ۲. کاربرانِ آزمایشی را روی سرور با `mkusers.py` بسازید
(`LT_PREFIX` یک پیشوندِ جعلی که روی سرور نیست) و خروجی را فقط در یک فایلِ 600 نگه دارید.
۳. `loadtest.py` را با `https://mathapi.tadrisino2.ir` و بدونِ `--writes` اجرا کنید.
۴. زمانِ داخلِ GapGPT برای هر پیام در `ai_tutor.Message.latency_ms` است؛ اختلافِ آن با زمانِ کلاینت
یعنی کارِ سرورِ ما و صفِ انتظار برای نخِ آزاد. ۵. `manage.py delete_users --ids … --yes` و پاک کردنِ
فایلِ توکن‌ها.
سقفِ ساعتیِ دستیار برای مهمان‌ها بر اساسِ IP است؛ با کاربرِ مهمان از یک ماشین بیش از ~۲۰ پرسش در
ساعت ممکن نیست، برای همین `mkusers.py` کاربرِ غیرمهمان می‌سازد.