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

@@ -2,7 +2,7 @@ from urllib.parse import urlsplit
from django.conf import settings
from django.db import transaction
from django.http import HttpResponse, HttpResponseRedirect
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.utils.html import escape
from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
@@ -25,7 +25,7 @@ from .serializers import (
OrderDetailResponseSerializer,
ZarinpalCallbackResponseSerializer,
)
from .services import apply_order_payment_result, create_order, reconcile_pending_order
from .services import apply_order_payment_result, create_order, pay_link_is_valid, reconcile_pending_order
def _redirect_to_configured_url(url: str) -> HttpResponseRedirect:
@@ -142,10 +142,10 @@ class PaymentRedirectView(APIView):
(مثلاً دکمه‌ی back مرورگر) همان authority قبلی را دوباره استفاده
می‌کند، نه یک PaymentRequest تازه.
⚠️ AllowAny است و مالکیتِ سفارش را نمی‌سنجد (مرورگر هدرِ Bearer نمی‌فرستد)؛
شناسه‌ی سفارش ترتیبی است. هر کسی با باز کردنِ `/orders/<n>/pay` برای سفارشِ
دیگران authority می‌سازد و از آن به بعد صاحبِ سفارش دیگر نمی‌تواند کدِ
تخفیف بزند (بازبینیِ ۲۰۲۶-۰۹-۲۵، بازتولید شد).
AllowAny است چون مرورگر هدرِ Bearer نمی‌فرستد؛ به‌جایش پارامترِ `t`، امضای
کوتاه‌عمری که فقط در paymentUrlِ صاحبِ سفارش می‌آید (payment/services.py،
pay_link_token)، سنجیده می‌شود. بی‌امضا یا امضای سفارشِ دیگر همان ۴۰۴ِ سفارشِ
ناموجود را می‌گیرد تا وجودِ شناسه هم لو نرود.
"""
permission_classes = [AllowAny]
@@ -164,11 +164,17 @@ class PaymentRedirectView(APIView):
return response
@extend_schema(
parameters=[OpenApiParameter(
't', OpenApiTypes.STR, required=True,
description='امضای همراهِ paymentUrl؛ کلاینت خودش نمی‌سازد، همان آدرسِ کامل را باز می‌کند.',
)],
responses={302: OpenApiTypes.NONE},
description='به درگاهِ زرین‌پال ریدایرکت می‌کند؛ بدنه‌ی JSON ندارد.',
tags=['Payment (payment)'],
)
def get(self, request, order_id):
if not pay_link_is_valid(order_id, request.query_params.get('t', '')):
raise Http404
order = get_object_or_404(Order, pk=order_id, provider='gateway')
if order.status != 'pending':