Add refresh token functionality and improve login response handling
- Introduced RefreshTokenView to allow users to refresh their access tokens using a valid refresh token. - Enhanced LoginView to ensure correct user/account loading and improved error handling during user serialization. - Updated API response structure to include access and refresh token expiration times. - Adjusted frontend API handling to support both new and legacy token response formats.
This commit is contained in:
@@ -14,8 +14,10 @@ from .views import (
|
||||
SiteUserAccessViewSet, PlanViewSet, SiteViewSet, SectorViewSet,
|
||||
IndustryViewSet, SeedKeywordViewSet
|
||||
)
|
||||
from .serializers import RegisterSerializer, LoginSerializer, ChangePasswordSerializer, UserSerializer
|
||||
from .serializers import RegisterSerializer, LoginSerializer, ChangePasswordSerializer, UserSerializer, RefreshTokenSerializer
|
||||
from .models import User
|
||||
from .utils import generate_access_token, get_token_expiry, decode_token
|
||||
import jwt
|
||||
|
||||
router = DefaultRouter()
|
||||
# Main structure: Groups, Users, Accounts, Subscriptions, Site User Access
|
||||
@@ -78,7 +80,7 @@ class LoginView(APIView):
|
||||
password = serializer.validated_data['password']
|
||||
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
user = User.objects.select_related('account', 'account__plan').get(email=email)
|
||||
except User.DoesNotExist:
|
||||
return error_response(
|
||||
error='Invalid credentials',
|
||||
@@ -107,9 +109,17 @@ class LoginView(APIView):
|
||||
user_data = user_serializer.data
|
||||
except Exception as e:
|
||||
# Fallback if serializer fails (e.g., missing account_id column)
|
||||
# Log the error for debugging but don't fail the login
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"UserSerializer failed for user {user.id}: {e}", exc_info=True)
|
||||
|
||||
# Ensure username is properly set (use email prefix if username is empty/default)
|
||||
username = user.username if user.username and user.username != 'user' else user.email.split('@')[0]
|
||||
|
||||
user_data = {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'username': username,
|
||||
'email': user.email,
|
||||
'role': user.role,
|
||||
'account': None,
|
||||
@@ -119,12 +129,10 @@ class LoginView(APIView):
|
||||
return success_response(
|
||||
data={
|
||||
'user': user_data,
|
||||
'tokens': {
|
||||
'access': access_token,
|
||||
'refresh': refresh_token,
|
||||
'access_expires_at': access_expires_at.isoformat(),
|
||||
'refresh_expires_at': refresh_expires_at.isoformat(),
|
||||
}
|
||||
'access': access_token,
|
||||
'refresh': refresh_token,
|
||||
'access_expires_at': access_expires_at.isoformat(),
|
||||
'refresh_expires_at': refresh_expires_at.isoformat(),
|
||||
},
|
||||
message='Login successful',
|
||||
request=request
|
||||
@@ -180,6 +188,84 @@ class ChangePasswordView(APIView):
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
tags=['Authentication'],
|
||||
summary='Refresh Token',
|
||||
description='Refresh access token using refresh token'
|
||||
)
|
||||
class RefreshTokenView(APIView):
|
||||
"""Refresh access token endpoint."""
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
serializer = RefreshTokenSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return error_response(
|
||||
error='Validation failed',
|
||||
errors=serializer.errors,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
refresh_token = serializer.validated_data['refresh']
|
||||
|
||||
try:
|
||||
# Decode and validate refresh token
|
||||
payload = decode_token(refresh_token)
|
||||
|
||||
# Verify it's a refresh token
|
||||
if payload.get('type') != 'refresh':
|
||||
return error_response(
|
||||
error='Invalid token type',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get user
|
||||
user_id = payload.get('user_id')
|
||||
account_id = payload.get('account_id')
|
||||
|
||||
try:
|
||||
user = User.objects.select_related('account', 'account__plan').get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return error_response(
|
||||
error='User not found',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get account
|
||||
account = None
|
||||
if account_id:
|
||||
try:
|
||||
from .models import Account
|
||||
account = Account.objects.get(id=account_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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,
|
||||
'access_expires_at': access_expires_at.isoformat()
|
||||
},
|
||||
request=request
|
||||
)
|
||||
|
||||
except jwt.InvalidTokenError:
|
||||
return error_response(
|
||||
error='Invalid or expired refresh token',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(exclude=True) # Exclude from public API documentation - internal authenticated endpoint
|
||||
class MeView(APIView):
|
||||
"""Get current user information."""
|
||||
@@ -201,6 +287,7 @@ urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
path('register/', csrf_exempt(RegisterView.as_view()), name='auth-register'),
|
||||
path('login/', csrf_exempt(LoginView.as_view()), name='auth-login'),
|
||||
path('refresh/', csrf_exempt(RefreshTokenView.as_view()), name='auth-refresh'),
|
||||
path('change-password/', ChangePasswordView.as_view(), name='auth-change-password'),
|
||||
path('me/', MeView.as_view(), name='auth-me'),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user