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:
@@ -16,7 +16,7 @@ from rest_framework.throttling import ScopedRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .authentication import invalidate_user_cache, issue_access_token
|
||||
from .models import OTPCode, Profile, Session, User
|
||||
from .models import OTPCode, Profile, Session, User, hash_refresh_token
|
||||
from .services import OTPDeliveryError, send_otp_sms
|
||||
from .serializers import (
|
||||
DeleteAccountRequestSerializer,
|
||||
@@ -101,7 +101,7 @@ class GuestSessionView(APIView):
|
||||
refresh_token = secrets.token_hex(32)
|
||||
Session.objects.create(
|
||||
user=user,
|
||||
refresh_token=refresh_token,
|
||||
refresh_token=hash_refresh_token(refresh_token),
|
||||
device_id=device_id,
|
||||
expires_at=timezone.now() + timezone.timedelta(days=settings.REFRESH_TOKEN_TTL_DAYS),
|
||||
)
|
||||
@@ -385,7 +385,7 @@ class LoginView(APIView):
|
||||
refresh_token = uuid.uuid4().hex
|
||||
Session.objects.create(
|
||||
user=user,
|
||||
refresh_token=refresh_token,
|
||||
refresh_token=hash_refresh_token(refresh_token),
|
||||
device_id=request.data.get('deviceId', ''),
|
||||
expires_at=timezone.now() + timezone.timedelta(days=settings.REFRESH_TOKEN_TTL_DAYS),
|
||||
)
|
||||
@@ -504,7 +504,7 @@ class LogoutView(APIView):
|
||||
with transaction.atomic():
|
||||
if refresh_token:
|
||||
Session.objects.filter(
|
||||
user_id=request.user.id, refresh_token=refresh_token,
|
||||
user_id=request.user.id, refresh_token=hash_refresh_token(refresh_token),
|
||||
).update(revoked=True)
|
||||
User.objects.filter(pk=request.user.id).update(tokens_valid_after=timezone.now())
|
||||
invalidate_user_cache(request.user.id)
|
||||
@@ -536,13 +536,13 @@ class RefreshTokenView(APIView):
|
||||
# UPDATE اتمیک جلوی استفادهی همزمان از یک refresh token را میگیرد (replay protection).
|
||||
with transaction.atomic():
|
||||
updated_count = Session.objects.filter(
|
||||
refresh_token=refresh_token, revoked=False, expires_at__gte=timezone.now()
|
||||
refresh_token=hash_refresh_token(refresh_token), revoked=False, expires_at__gte=timezone.now()
|
||||
).update(revoked=True)
|
||||
|
||||
if not updated_count:
|
||||
return Response({'detail': 'رفرشتوکن نامعتبر یا منقضیشده است.'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
old_session = Session.objects.get(refresh_token=refresh_token)
|
||||
old_session = Session.objects.get(refresh_token=hash_refresh_token(refresh_token))
|
||||
|
||||
# ثبتِ فعالیتِ کاربر برای رفرشِ موفقِ توکن.
|
||||
User.objects.filter(pk=old_session.user_id).update(last_activity_at=timezone.now())
|
||||
@@ -550,7 +550,7 @@ class RefreshTokenView(APIView):
|
||||
new_refresh_token = secrets.token_hex(32)
|
||||
new_session = Session.objects.create(
|
||||
user_id=old_session.user_id,
|
||||
refresh_token=new_refresh_token,
|
||||
refresh_token=hash_refresh_token(new_refresh_token),
|
||||
device_id=old_session.device_id,
|
||||
expires_at=timezone.now() + timezone.timedelta(days=settings.REFRESH_TOKEN_TTL_DAYS),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user