@@ -1049,12 +1049,11 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def login(self, request):
|
||||
"""User login endpoint with remember-me support."""
|
||||
"""User login endpoint."""
|
||||
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)
|
||||
@@ -1088,17 +1087,11 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
from django.contrib.auth import login
|
||||
login(request, user)
|
||||
|
||||
# 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
|
||||
# Generate JWT tokens
|
||||
access_token = generate_access_token(user, account)
|
||||
refresh_token, token_id, refresh_expires_at = generate_refresh_token_pair(
|
||||
user, account, remember_me, device_id, user_agent, ip_address
|
||||
)
|
||||
refresh_token = generate_refresh_token(user, account)
|
||||
access_expires_at = get_token_expiry('access')
|
||||
refresh_expires_at = get_token_expiry('refresh')
|
||||
|
||||
user_serializer = UserSerializer(user)
|
||||
return success_response(
|
||||
@@ -1128,9 +1121,7 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.IsAuthenticated])
|
||||
def change_password(self, request):
|
||||
"""Change password endpoint - revokes all refresh tokens."""
|
||||
from .models_refresh_token import RefreshToken
|
||||
|
||||
"""Change password endpoint."""
|
||||
serializer = ChangePasswordSerializer(data=request.data, context={'request': request})
|
||||
if serializer.is_valid():
|
||||
user = request.user
|
||||
@@ -1144,12 +1135,8 @@ 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. Please login again on all devices.',
|
||||
message='Password changed successfully',
|
||||
request=request
|
||||
)
|
||||
|
||||
@@ -1174,10 +1161,7 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
|
||||
def refresh(self, request):
|
||||
"""Refresh access token using refresh token with atomic rotation."""
|
||||
from .models_refresh_token import RefreshToken
|
||||
from django.db import transaction
|
||||
|
||||
"""Refresh access token using refresh token."""
|
||||
serializer = RefreshTokenSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return error_response(
|
||||
@@ -1190,7 +1174,7 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
refresh_token = serializer.validated_data['refresh']
|
||||
|
||||
try:
|
||||
# Decode and validate refresh token JWT
|
||||
# Decode and validate refresh token
|
||||
payload = decode_token(refresh_token)
|
||||
|
||||
# Verify it's a refresh token
|
||||
@@ -1201,59 +1185,39 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get token_id from payload and validate against database
|
||||
token_id = payload.get('token_id')
|
||||
if not token_id:
|
||||
# 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:
|
||||
return error_response(
|
||||
error='Invalid refresh token - missing token ID',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
error='User not found',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
# 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
|
||||
|
||||
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()
|
||||
if not account:
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
# 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,
|
||||
'refresh': new_refresh_token, # Return new refresh token
|
||||
'access_expires_at': access_expires_at.isoformat(),
|
||||
'refresh_expires_at': refresh_expires_at.isoformat(),
|
||||
'access_expires_at': access_expires_at.isoformat()
|
||||
},
|
||||
request=request
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user