Enhance API response handling and implement unified API standard across multiple modules. Added feature flags for unified exception handling and debug throttling in settings. Updated pagination and response formats in various viewsets to align with the new standard. Improved error handling and response validation in frontend components for better user feedback.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
Authentication Views - Structured as: Groups, Users, Accounts, Subscriptions, Site User Access
|
||||
Unified API Standard v1.0 compliant
|
||||
"""
|
||||
from rest_framework import viewsets, status, permissions, filters
|
||||
from rest_framework.decorators import action
|
||||
@@ -11,6 +12,9 @@ from django.db import transaction
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from igny8_core.api.base import AccountModelViewSet
|
||||
from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAuthentication
|
||||
from igny8_core.api.response import success_response, error_response
|
||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||
from igny8_core.api.pagination import CustomPageNumberPagination
|
||||
from .models import User, Account, Plan, Subscription, Site, Sector, SiteUserAccess, Industry, IndustrySector, SeedKeyword
|
||||
from .serializers import (
|
||||
UserSerializer, AccountSerializer, PlanSerializer, SubscriptionSerializer,
|
||||
@@ -33,8 +37,11 @@ class GroupsViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
ViewSet for managing user roles and permissions (Groups).
|
||||
Groups are defined by the User.ROLE_CHOICES.
|
||||
Unified API Standard v1.0 compliant
|
||||
"""
|
||||
permission_classes = [IsOwnerOrAdmin]
|
||||
throttle_scope = 'auth'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
|
||||
def list(self, request):
|
||||
"""List all available roles/groups."""
|
||||
@@ -76,17 +83,18 @@ class GroupsViewSet(viewsets.ViewSet):
|
||||
'permissions': ['automation_only']
|
||||
}
|
||||
]
|
||||
return Response({
|
||||
'success': True,
|
||||
'groups': roles
|
||||
})
|
||||
return success_response(data={'groups': roles}, request=request)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='permissions')
|
||||
def permissions(self, request):
|
||||
"""Get permissions for a specific role."""
|
||||
role = request.query_params.get('role')
|
||||
if not role:
|
||||
return Response({'error': 'role parameter is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='role parameter is required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
role_permissions = {
|
||||
'developer': ['full_access', 'bypass_filters', 'all_modules', 'all_accounts'],
|
||||
@@ -98,11 +106,13 @@ class GroupsViewSet(viewsets.ViewSet):
|
||||
}
|
||||
|
||||
permissions_list = role_permissions.get(role, [])
|
||||
return Response({
|
||||
'success': True,
|
||||
'role': role,
|
||||
'permissions': permissions_list
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'role': role,
|
||||
'permissions': permissions_list
|
||||
},
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -113,10 +123,14 @@ class UsersViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet for managing global user records and credentials.
|
||||
Users are global, but belong to accounts.
|
||||
Unified API Standard v1.0 compliant
|
||||
"""
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsOwnerOrAdmin]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'auth'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return users based on access level."""
|
||||
@@ -147,17 +161,21 @@ class UsersViewSet(viewsets.ModelViewSet):
|
||||
account_id = request.data.get('account_id')
|
||||
|
||||
if not email or not username or not password:
|
||||
return Response({
|
||||
'error': 'email, username, and password are required'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='email, username, and password are required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Validate password
|
||||
try:
|
||||
validate_password(password)
|
||||
except Exception as e:
|
||||
return Response({
|
||||
'error': str(e)
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error=str(e),
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get account
|
||||
account = None
|
||||
@@ -165,9 +183,11 @@ class UsersViewSet(viewsets.ModelViewSet):
|
||||
try:
|
||||
account = Account.objects.get(id=account_id)
|
||||
except Account.DoesNotExist:
|
||||
return Response({
|
||||
'error': f'Account with id {account_id} does not exist'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error=f'Account with id {account_id} does not exist',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
else:
|
||||
# Use current user's account
|
||||
if request.user.account:
|
||||
@@ -183,14 +203,17 @@ class UsersViewSet(viewsets.ModelViewSet):
|
||||
account=account
|
||||
)
|
||||
serializer = UserSerializer(user)
|
||||
return Response({
|
||||
'success': True,
|
||||
'user': serializer.data
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
return success_response(
|
||||
data={'user': serializer.data},
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
return Response({
|
||||
'error': str(e)
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error=str(e),
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def update_role(self, request, pk=None):
|
||||
@@ -199,23 +222,24 @@ class UsersViewSet(viewsets.ModelViewSet):
|
||||
new_role = request.data.get('role')
|
||||
|
||||
if not new_role:
|
||||
return Response({
|
||||
'error': 'role is required'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='role is required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
if new_role not in [choice[0] for choice in User.ROLE_CHOICES]:
|
||||
return Response({
|
||||
'error': f'Invalid role. Must be one of: {[c[0] for c in User.ROLE_CHOICES]}'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error=f'Invalid role. Must be one of: {[c[0] for c in User.ROLE_CHOICES]}',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
user.role = new_role
|
||||
user.save()
|
||||
|
||||
serializer = UserSerializer(user)
|
||||
return Response({
|
||||
'success': True,
|
||||
'user': serializer.data
|
||||
})
|
||||
return success_response(data={'user': serializer.data}, request=request)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -308,14 +332,16 @@ class SubscriptionsViewSet(viewsets.ModelViewSet):
|
||||
try:
|
||||
subscription = Subscription.objects.get(account_id=account_id)
|
||||
serializer = self.get_serializer(subscription)
|
||||
return Response({
|
||||
'success': True,
|
||||
'subscription': serializer.data
|
||||
})
|
||||
return success_response(
|
||||
data={'subscription': serializer.data},
|
||||
request=request
|
||||
)
|
||||
except Subscription.DoesNotExist:
|
||||
return Response({
|
||||
'error': 'Subscription not found for this account'
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
return error_response(
|
||||
error='Subscription not found for this account',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -424,7 +450,10 @@ class SiteViewSet(AccountModelViewSet):
|
||||
site = self.get_object()
|
||||
sectors = site.sectors.filter(is_active=True)
|
||||
serializer = SectorSerializer(sectors, many=True)
|
||||
return Response(serializer.data)
|
||||
return success_response(
|
||||
data=serializer.data,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='set_active')
|
||||
def set_active(self, request, pk=None):
|
||||
@@ -437,11 +466,11 @@ class SiteViewSet(AccountModelViewSet):
|
||||
site.save()
|
||||
|
||||
serializer = self.get_serializer(site)
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': f'Site "{site.name}" is now active',
|
||||
'site': serializer.data
|
||||
})
|
||||
return success_response(
|
||||
data={'site': serializer.data},
|
||||
message=f'Site "{site.name}" is now active',
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='select_sectors')
|
||||
def select_sectors(self, request, pk=None):
|
||||
@@ -453,43 +482,53 @@ class SiteViewSet(AccountModelViewSet):
|
||||
site = self.get_object()
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting site object: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'error': f'Site not found: {str(e)}'
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
return error_response(
|
||||
error=f'Site not found: {str(e)}',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
|
||||
sector_slugs = request.data.get('sector_slugs', [])
|
||||
industry_slug = request.data.get('industry_slug')
|
||||
|
||||
if not industry_slug:
|
||||
return Response({
|
||||
'error': 'Industry slug is required'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Industry slug is required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
try:
|
||||
industry = Industry.objects.get(slug=industry_slug, is_active=True)
|
||||
except Industry.DoesNotExist:
|
||||
return Response({
|
||||
'error': f'Industry with slug "{industry_slug}" not found'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error=f'Industry with slug "{industry_slug}" not found',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
site.industry = industry
|
||||
site.save()
|
||||
|
||||
if not sector_slugs:
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': f'Industry "{industry.name}" set for site. No sectors selected.',
|
||||
'site': SiteSerializer(site).data,
|
||||
'sectors': []
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'site': SiteSerializer(site).data,
|
||||
'sectors': []
|
||||
},
|
||||
message=f'Industry "{industry.name}" set for site. No sectors selected.',
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get plan's max_industries limit (if set), otherwise default to 5
|
||||
max_sectors = site.get_max_sectors_limit()
|
||||
|
||||
if len(sector_slugs) > max_sectors:
|
||||
return Response({
|
||||
'error': f'Maximum {max_sectors} sectors allowed per site for this plan'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error=f'Maximum {max_sectors} sectors allowed per site for this plan',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
created_sectors = []
|
||||
updated_sectors = []
|
||||
@@ -506,9 +545,11 @@ class SiteViewSet(AccountModelViewSet):
|
||||
).first()
|
||||
|
||||
if not industry_sector:
|
||||
return Response({
|
||||
'error': f'Sector "{sector_slug}" not found in industry "{industry.name}"'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error=f'Sector "{sector_slug}" not found in industry "{industry.name}"',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
industry_sectors_map[sector_slug] = industry_sector
|
||||
|
||||
@@ -517,9 +558,11 @@ class SiteViewSet(AccountModelViewSet):
|
||||
# Check if site has account before proceeding
|
||||
if not site.account:
|
||||
logger.error(f"Site {site.id} has no account assigned")
|
||||
return Response({
|
||||
'error': f'Site "{site.name}" has no account assigned. Please contact support.'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
error=f'Site "{site.name}" has no account assigned. Please contact support.',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Create or get sector - account will be set automatically in save() method
|
||||
# But we need to pass it in defaults for get_or_create to work
|
||||
@@ -552,27 +595,33 @@ class SiteViewSet(AccountModelViewSet):
|
||||
created_sectors.append(sector)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating/updating sector {sector_slug}: {str(e)}", exc_info=True)
|
||||
return Response({
|
||||
'error': f'Failed to create/update sector "{sector_slug}": {str(e)}'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return error_response(
|
||||
error=f'Failed to create/update sector "{sector_slug}": {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get plan's max_industries limit (if set), otherwise default to 5
|
||||
max_sectors = site.get_max_sectors_limit()
|
||||
|
||||
if site.get_active_sectors_count() > max_sectors:
|
||||
return Response({
|
||||
'error': f'Maximum {max_sectors} sectors allowed per site for this plan'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error=f'Maximum {max_sectors} sectors allowed per site for this plan',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
serializer = SectorSerializer(site.sectors.filter(is_active=True), many=True)
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': f'Selected {len(sector_slugs)} sectors from industry "{industry.name}".',
|
||||
'created_count': len(created_sectors),
|
||||
'updated_count': len(updated_sectors),
|
||||
'sectors': serializer.data,
|
||||
'site': SiteSerializer(site).data
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'created_count': len(created_sectors),
|
||||
'updated_count': len(updated_sectors),
|
||||
'sectors': serializer.data,
|
||||
'site': SiteSerializer(site).data
|
||||
},
|
||||
message=f'Selected {len(sector_slugs)} sectors from industry "{industry.name}".',
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
class SectorViewSet(AccountModelViewSet):
|
||||
@@ -606,7 +655,10 @@ class SectorViewSet(AccountModelViewSet):
|
||||
"""Override list to apply site filter."""
|
||||
queryset = self.get_queryset_with_site_filter()
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
return success_response(
|
||||
data=serializer.data,
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
class IndustryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@@ -619,10 +671,10 @@ class IndustryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""Get all industries with their sectors."""
|
||||
industries = self.get_queryset()
|
||||
serializer = self.get_serializer(industries, many=True)
|
||||
return Response({
|
||||
'success': True,
|
||||
'industries': serializer.data
|
||||
})
|
||||
return success_response(
|
||||
data={'industries': serializer.data},
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
class SeedKeywordViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@@ -656,8 +708,12 @@ class SeedKeywordViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
# ============================================================================
|
||||
|
||||
class AuthViewSet(viewsets.GenericViewSet):
|
||||
"""Authentication endpoints."""
|
||||
"""Authentication endpoints.
|
||||
Unified API Standard v1.0 compliant
|
||||
"""
|
||||
permission_classes = [permissions.AllowAny]
|
||||
throttle_scope = 'auth_strict'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def register(self, request):
|
||||
@@ -680,21 +736,26 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
refresh_expires_at = get_token_expiry('refresh')
|
||||
|
||||
user_serializer = UserSerializer(user)
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Registration successful',
|
||||
'user': user_serializer.data,
|
||||
'tokens': {
|
||||
'access': access_token,
|
||||
'refresh': refresh_token,
|
||||
'access_expires_at': access_expires_at.isoformat(),
|
||||
'refresh_expires_at': refresh_expires_at.isoformat(),
|
||||
}
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
return Response({
|
||||
'success': False,
|
||||
'errors': serializer.errors
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return success_response(
|
||||
data={
|
||||
'user': user_serializer.data,
|
||||
'tokens': {
|
||||
'access': access_token,
|
||||
'refresh': refresh_token,
|
||||
'access_expires_at': access_expires_at.isoformat(),
|
||||
'refresh_expires_at': refresh_expires_at.isoformat(),
|
||||
}
|
||||
},
|
||||
message='Registration successful',
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
request=request
|
||||
)
|
||||
return error_response(
|
||||
error='Validation failed',
|
||||
errors=serializer.errors,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def login(self, request):
|
||||
@@ -707,10 +768,11 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
try:
|
||||
user = User.objects.select_related('account', 'account__plan').get(email=email)
|
||||
except User.DoesNotExist:
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': 'Invalid credentials'
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
return error_response(
|
||||
error='Invalid credentials',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
if user.check_password(password):
|
||||
# Log the user in (create session for session authentication)
|
||||
@@ -727,27 +789,32 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
refresh_expires_at = get_token_expiry('refresh')
|
||||
|
||||
user_serializer = UserSerializer(user)
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Login successful',
|
||||
'user': user_serializer.data,
|
||||
'tokens': {
|
||||
'access': access_token,
|
||||
'refresh': refresh_token,
|
||||
'access_expires_at': access_expires_at.isoformat(),
|
||||
'refresh_expires_at': refresh_expires_at.isoformat(),
|
||||
}
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'user': user_serializer.data,
|
||||
'tokens': {
|
||||
'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
|
||||
)
|
||||
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': 'Invalid credentials'
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
return error_response(
|
||||
error='Invalid credentials',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
return Response({
|
||||
'success': False,
|
||||
'errors': serializer.errors
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Validation failed',
|
||||
errors=serializer.errors,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.IsAuthenticated])
|
||||
def change_password(self, request):
|
||||
@@ -756,23 +823,26 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
if serializer.is_valid():
|
||||
user = request.user
|
||||
if not user.check_password(serializer.validated_data['old_password']):
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': 'Current password is incorrect'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Current password is incorrect',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
user.set_password(serializer.validated_data['new_password'])
|
||||
user.save()
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Password changed successfully'
|
||||
})
|
||||
return success_response(
|
||||
message='Password changed successfully',
|
||||
request=request
|
||||
)
|
||||
|
||||
return Response({
|
||||
'success': False,
|
||||
'errors': serializer.errors
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Validation failed',
|
||||
errors=serializer.errors,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['get'], permission_classes=[permissions.IsAuthenticated])
|
||||
def me(self, request):
|
||||
@@ -781,20 +851,22 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
# This ensures account/plan changes are reflected immediately
|
||||
user = User.objects.select_related('account', 'account__plan').get(id=request.user.id)
|
||||
serializer = UserSerializer(user)
|
||||
return Response({
|
||||
'success': True,
|
||||
'user': serializer.data
|
||||
})
|
||||
return success_response(
|
||||
data={'user': serializer.data},
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
|
||||
def refresh(self, request):
|
||||
"""Refresh access token using refresh token."""
|
||||
serializer = RefreshTokenSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response({
|
||||
'success': False,
|
||||
'errors': serializer.errors
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Validation failed',
|
||||
errors=serializer.errors,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
refresh_token = serializer.validated_data['refresh']
|
||||
|
||||
@@ -804,10 +876,11 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
|
||||
# Verify it's a refresh token
|
||||
if payload.get('type') != 'refresh':
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': 'Invalid token type'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Invalid token type',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get user
|
||||
user_id = payload.get('user_id')
|
||||
@@ -816,10 +889,11 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': 'User not found'
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
return error_response(
|
||||
error='User not found',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get account
|
||||
account_id = payload.get('account_id')
|
||||
@@ -837,27 +911,32 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
access_token = generate_access_token(user, account)
|
||||
access_expires_at = get_token_expiry('access')
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'access': access_token,
|
||||
'access_expires_at': access_expires_at.isoformat()
|
||||
})
|
||||
return success_response(
|
||||
data={
|
||||
'access': access_token,
|
||||
'access_expires_at': access_expires_at.isoformat()
|
||||
},
|
||||
request=request
|
||||
)
|
||||
|
||||
except jwt.InvalidTokenError as e:
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': 'Invalid or expired refresh token'
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
return error_response(
|
||||
error='Invalid or expired refresh token',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
|
||||
def request_reset(self, request):
|
||||
"""Request password reset - sends email with reset token."""
|
||||
serializer = RequestPasswordResetSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response({
|
||||
'success': False,
|
||||
'errors': serializer.errors
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Validation failed',
|
||||
errors=serializer.errors,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
email = serializer.validated_data['email']
|
||||
|
||||
@@ -865,10 +944,10 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
user = User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
# Don't reveal if email exists - return success anyway
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'If an account with that email exists, a password reset link has been sent.'
|
||||
})
|
||||
return success_response(
|
||||
message='If an account with that email exists, a password reset link has been sent.',
|
||||
request=request
|
||||
)
|
||||
|
||||
# Generate secure token
|
||||
import secrets
|
||||
@@ -904,20 +983,22 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'If an account with that email exists, a password reset link has been sent.'
|
||||
})
|
||||
return success_response(
|
||||
message='If an account with that email exists, a password reset link has been sent.',
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
|
||||
def reset_password(self, request):
|
||||
"""Reset password using reset token."""
|
||||
serializer = ResetPasswordSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response({
|
||||
'success': False,
|
||||
'errors': serializer.errors
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Validation failed',
|
||||
errors=serializer.errors,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
token = serializer.validated_data['token']
|
||||
new_password = serializer.validated_data['new_password']
|
||||
@@ -925,17 +1006,19 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
try:
|
||||
reset_token = PasswordResetToken.objects.get(token=token)
|
||||
except PasswordResetToken.DoesNotExist:
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': 'Invalid reset token'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Invalid reset token',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Check if token is valid
|
||||
if not reset_token.is_valid():
|
||||
return Response({
|
||||
'success': False,
|
||||
'message': 'Reset token has expired or has already been used'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return error_response(
|
||||
error='Reset token has expired or has already been used',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Update password
|
||||
user = reset_token.user
|
||||
@@ -946,7 +1029,7 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
reset_token.used = True
|
||||
reset_token.save()
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Password has been reset successfully'
|
||||
})
|
||||
return success_response(
|
||||
message='Password has been reset successfully',
|
||||
request=request
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user