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

View File

@@ -1,3 +1,5 @@
import hashlib
from django.db import models
@@ -60,11 +62,18 @@ class Profile(models.Model):
return f"profile-{self.user_id}"
def hash_refresh_token(raw: str) -> str:
"""آنچه در Session.refresh_token ذخیره و با آن جست‌وجو می‌شود؛ خودِ توکن فقط دستِ کلاینت است."""
return hashlib.sha256(raw.encode()).hexdigest()
class Session(models.Model):
"""
نگهداری refresh_token هر ورود. توکن دسترسی (access token) در دیتابیس
ذخیره نمی‌شه؛ به‌صورت stateless امضا (signed) می‌شه — دقیقاً طبق فیلدهای
همین مدل که فقط refresh_token داره، نه access token.
یک ورود. توکنِ دسترسی ذخیره نمی‌شود (امضاشده و stateless است، accounts/authentication.py).
`refresh_token` هشِ SHA-256ِ توکنِ تمدید است، نه خودش (hash_refresh_token): هر کس دیتابیس،
یک بکاپ یا پنلِ ادمین را ببیند با آن نمی‌تواند به‌جای کاربر وارد شود. توکن ۶۴ نویسه‌ی تصادفی
است، پس هشِ سریعِ بی‌نمک کافی است.
"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sessions')
refresh_token = models.CharField(max_length=200, unique=True)