messy logout fixing

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-15 12:01:41 +00:00
parent 06e5f252a4
commit 4fb3a144d7
27 changed files with 4396 additions and 95 deletions

View File

@@ -1049,11 +1049,12 @@ class AuthViewSet(viewsets.GenericViewSet):
@action(detail=False, methods=['post'])
def login(self, request):
"""User login endpoint."""
"""User login endpoint with remember-me support."""
serializer = LoginSerializer(data=request.data)
if serializer.is_valid():
email = serializer.validated_data['email']
password = serializer.validated_data['password']
remember_me = serializer.validated_data.get('remember_me', False)
try:
user = User.objects.select_related('account', 'account__plan').get(email=email)
@@ -1087,11 +1088,17 @@ class AuthViewSet(viewsets.GenericViewSet):
from django.contrib.auth import login
login(request, user)
# Generate JWT tokens
# Extract device information from request
device_id = request.data.get('device_id', '')
user_agent = request.META.get('HTTP_USER_AGENT', '')
ip_address = request.META.get('REMOTE_ADDR')
# Generate JWT tokens with remember-me support
access_token = generate_access_token(user, account)
refresh_token = generate_refresh_token(user, account)
refresh_token, token_id, refresh_expires_at = generate_refresh_token_pair(
user, account, remember_me, device_id, user_agent, ip_address
)
access_expires_at = get_token_expiry('access')
refresh_expires_at = get_token_expiry('refresh')
user_serializer = UserSerializer(user)
return success_response(
@@ -1121,7 +1128,9 @@ class AuthViewSet(viewsets.GenericViewSet):
@action(detail=False, methods=['post'], permission_classes=[permissions.IsAuthenticated])
def change_password(self, request):
"""Change password endpoint."""
"""Change password endpoint - revokes all refresh tokens."""
from .models_refresh_token import RefreshToken
serializer = ChangePasswordSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
user = request.user
@@ -1135,8 +1144,12 @@ class AuthViewSet(viewsets.GenericViewSet):
user.set_password(serializer.validated_data['new_password'])
user.save()
# CRITICAL: Revoke all refresh tokens when password changes
# This forces re-login on all devices for security
RefreshToken.revoke_all_for_user(user)
return success_response(
message='Password changed successfully',
message='Password changed successfully. Please login again on all devices.',
request=request
)
@@ -1161,7 +1174,10 @@ class AuthViewSet(viewsets.GenericViewSet):
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
def refresh(self, request):
"""Refresh access token using refresh token."""
"""Refresh access token using refresh token with atomic rotation."""
from .models_refresh_token import RefreshToken
from django.db import transaction
serializer = RefreshTokenSerializer(data=request.data)
if not serializer.is_valid():
return error_response(
@@ -1174,7 +1190,7 @@ class AuthViewSet(viewsets.GenericViewSet):
refresh_token = serializer.validated_data['refresh']
try:
# Decode and validate refresh token
# Decode and validate refresh token JWT
payload = decode_token(refresh_token)
# Verify it's a refresh token
@@ -1185,39 +1201,59 @@ class AuthViewSet(viewsets.GenericViewSet):
request=request
)
# Get user
user_id = payload.get('user_id')
account_id = payload.get('account_id')
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
# Get token_id from payload and validate against database
token_id = payload.get('token_id')
if not token_id:
return error_response(
error='User not found',
status_code=status.HTTP_404_NOT_FOUND,
error='Invalid refresh token - missing token ID',
status_code=status.HTTP_401_UNAUTHORIZED,
request=request
)
# Get account
account_id = payload.get('account_id')
account = None
if account_id:
try:
account = Account.objects.get(id=account_id)
except Account.DoesNotExist:
pass
# Validate token exists, not revoked, and not expired in database
token_record = RefreshToken.get_valid_token(token_id)
if not token_record:
return error_response(
error='Refresh token is invalid, revoked, or expired',
status_code=status.HTTP_401_UNAUTHORIZED,
request=request
)
if not account:
account = getattr(user, 'account', None)
user = token_record.user
account = getattr(user, 'account', None)
# Extract device information from request
device_id = request.data.get('device_id', token_record.device_id)
user_agent = request.META.get('HTTP_USER_AGENT', '')
ip_address = request.META.get('REMOTE_ADDR')
# ATOMIC ROTATION: Create new token, then revoke old token
# This ensures no gap where user could be logged out
with transaction.atomic():
# Generate new access and refresh tokens
access_token = generate_access_token(user, account)
new_refresh_token, new_token_id, refresh_expires_at = generate_refresh_token_pair(
user=user,
account=account,
remember_me=token_record.remember_me, # Preserve remember_me setting
device_id=device_id,
user_agent=user_agent,
ip_address=ip_address,
parent_token_id=token_id # Track rotation chain
)
# Only revoke old token AFTER new one is created
token_record.revoke()
token_record.mark_used()
# Generate new access token
access_token = generate_access_token(user, account)
access_expires_at = get_token_expiry('access')
return success_response(
data={
'access': access_token,
'access_expires_at': access_expires_at.isoformat()
'refresh': new_refresh_token, # Return new refresh token
'access_expires_at': access_expires_at.isoformat(),
'refresh_expires_at': refresh_expires_at.isoformat(),
},
request=request
)