Implement security enhancements and unified response formats across API endpoints. Update permission classes for various ViewSets to ensure proper tenant isolation and compliance with API standards. Refactor authentication endpoints to utilize success and error response helpers, improving error tracking and response consistency. Complete documentation updates reflecting these changes and achieving full compliance with API Standard v1.0.
This commit is contained in:
@@ -8,6 +8,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status, permissions
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from igny8_core.api.response import success_response, error_response
|
||||
from .views import (
|
||||
GroupsViewSet, UsersViewSet, AccountsViewSet, SubscriptionsViewSet,
|
||||
SiteUserAccessViewSet, PlanViewSet, SiteViewSet, SectorViewSet,
|
||||
@@ -47,15 +48,18 @@ class RegisterView(APIView):
|
||||
if serializer.is_valid():
|
||||
user = serializer.save()
|
||||
user_serializer = UserSerializer(user)
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Registration successful',
|
||||
'user': user_serializer.data
|
||||
}, 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},
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
@@ -76,10 +80,11 @@ class LoginView(APIView):
|
||||
try:
|
||||
user = User.objects.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)
|
||||
@@ -111,27 +116,32 @@ class LoginView(APIView):
|
||||
'accessible_sites': [],
|
||||
}
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Login successful',
|
||||
'user': user_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_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
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
@@ -148,23 +158,26 @@ class ChangePasswordView(APIView):
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
@@ -182,10 +195,10 @@ class MeView(APIView):
|
||||
from .models import User as UserModel
|
||||
user = UserModel.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
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -16,6 +16,7 @@ from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAu
|
||||
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 igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess
|
||||
from .models import User, Account, Plan, Subscription, Site, Sector, SiteUserAccess, Industry, IndustrySector, SeedKeyword
|
||||
from .serializers import (
|
||||
UserSerializer, AccountSerializer, PlanSerializer, SubscriptionSerializer,
|
||||
@@ -140,7 +141,7 @@ class UsersViewSet(AccountModelViewSet):
|
||||
"""
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsOwnerOrAdmin]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsOwnerOrAdmin]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'auth'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
@@ -274,7 +275,7 @@ class AccountsViewSet(AccountModelViewSet):
|
||||
"""
|
||||
queryset = Account.objects.all()
|
||||
serializer_class = AccountSerializer
|
||||
permission_classes = [IsOwnerOrAdmin]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsOwnerOrAdmin]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'auth'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
@@ -338,7 +339,7 @@ class SubscriptionsViewSet(AccountModelViewSet):
|
||||
Unified API Standard v1.0 compliant
|
||||
"""
|
||||
queryset = Subscription.objects.all()
|
||||
permission_classes = [IsOwnerOrAdmin]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsOwnerOrAdmin]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'auth'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
@@ -400,7 +401,7 @@ class SiteUserAccessViewSet(AccountModelViewSet):
|
||||
Unified API Standard v1.0 compliant
|
||||
"""
|
||||
serializer_class = SiteUserAccessSerializer
|
||||
permission_classes = [IsOwnerOrAdmin]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsOwnerOrAdmin]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'auth'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
@@ -472,7 +473,7 @@ class PlanViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class SiteViewSet(AccountModelViewSet):
|
||||
"""ViewSet for managing Sites."""
|
||||
serializer_class = SiteSerializer
|
||||
permission_classes = [IsEditorOrAbove]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove]
|
||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||
|
||||
def get_permissions(self):
|
||||
@@ -715,7 +716,7 @@ class SiteViewSet(AccountModelViewSet):
|
||||
class SectorViewSet(AccountModelViewSet):
|
||||
"""ViewSet for managing Sectors."""
|
||||
serializer_class = SectorSerializer
|
||||
permission_classes = [IsEditorOrAbove]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove]
|
||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -15,6 +15,7 @@ from igny8_core.api.pagination import CustomPageNumberPagination
|
||||
from igny8_core.api.response import success_response, error_response
|
||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||
from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAuthentication
|
||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
|
||||
from .models import CreditTransaction, CreditUsageLog
|
||||
from .serializers import (
|
||||
CreditTransactionSerializer, CreditUsageLogSerializer,
|
||||
@@ -32,7 +33,7 @@ class CreditBalanceViewSet(viewsets.ViewSet):
|
||||
ViewSet for credit balance operations
|
||||
Unified API Standard v1.0 compliant
|
||||
"""
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||
throttle_scope = 'billing'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
@@ -81,14 +82,14 @@ class CreditBalanceViewSet(viewsets.ViewSet):
|
||||
list=extend_schema(tags=['Billing']),
|
||||
retrieve=extend_schema(tags=['Billing']),
|
||||
)
|
||||
class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class CreditUsageViewSet(AccountModelViewSet):
|
||||
"""
|
||||
ViewSet for credit usage logs
|
||||
Unified API Standard v1.0 compliant
|
||||
"""
|
||||
queryset = CreditUsageLog.objects.all()
|
||||
serializer_class = CreditUsageLogSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'billing'
|
||||
@@ -97,17 +98,8 @@ class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filter_backends = []
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get usage logs for current account"""
|
||||
account = getattr(self.request, 'account', None)
|
||||
if not account:
|
||||
user = getattr(self.request, 'user', None)
|
||||
if user:
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
if not account:
|
||||
return CreditUsageLog.objects.none()
|
||||
|
||||
queryset = CreditUsageLog.objects.filter(account=account)
|
||||
"""Get usage logs for current account - base class handles account filtering"""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Filter by operation type
|
||||
operation_type = self.request.query_params.get('operation_type')
|
||||
@@ -456,31 +448,22 @@ class CreditUsageViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
list=extend_schema(tags=['Billing']),
|
||||
retrieve=extend_schema(tags=['Billing']),
|
||||
)
|
||||
class CreditTransactionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class CreditTransactionViewSet(AccountModelViewSet):
|
||||
"""
|
||||
ViewSet for credit transaction history
|
||||
Unified API Standard v1.0 compliant
|
||||
"""
|
||||
queryset = CreditTransaction.objects.all()
|
||||
serializer_class = CreditTransactionSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
|
||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'billing'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get transactions for current account"""
|
||||
account = getattr(self.request, 'account', None)
|
||||
if not account:
|
||||
user = getattr(self.request, 'user', None)
|
||||
if user:
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
if not account:
|
||||
return CreditTransaction.objects.none()
|
||||
|
||||
queryset = CreditTransaction.objects.filter(account=account)
|
||||
"""Get transactions for current account - base class handles account filtering"""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Filter by transaction type
|
||||
transaction_type = self.request.query_params.get('transaction_type')
|
||||
|
||||
@@ -11,7 +11,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from igny8_core.api.base import AccountModelViewSet
|
||||
from igny8_core.api.response import success_response, error_response
|
||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsAdminOrOwner
|
||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -31,7 +31,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
Following reference plugin pattern: WordPress uses update_option() for igny8_api_settings
|
||||
We store in IntegrationSettings model with account isolation
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedAndActive, IsAdminOrOwner]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
|
||||
|
||||
throttle_scope = 'system_admin'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
|
||||
@@ -12,6 +12,7 @@ from igny8_core.api.response import success_response, error_response
|
||||
from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAuthentication
|
||||
from igny8_core.api.pagination import CustomPageNumberPagination
|
||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
|
||||
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
|
||||
from .settings_serializers import (
|
||||
SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer,
|
||||
@@ -34,7 +35,7 @@ class SystemSettingsViewSet(AccountModelViewSet):
|
||||
"""
|
||||
queryset = SystemSettings.objects.all()
|
||||
serializer_class = SystemSettingsSerializer
|
||||
permission_classes = [permissions.IsAuthenticated] # Require authentication
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'system'
|
||||
@@ -43,8 +44,8 @@ class SystemSettingsViewSet(AccountModelViewSet):
|
||||
def get_permissions(self):
|
||||
"""Admin only for write operations, read for authenticated users"""
|
||||
if self.action in ['create', 'update', 'partial_update', 'destroy']:
|
||||
return [permissions.IsAdminUser()]
|
||||
return [permissions.IsAuthenticated()]
|
||||
return [IsAdminOrOwner()]
|
||||
return [IsAuthenticatedAndActive(), HasTenantAccess()]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get all system settings"""
|
||||
@@ -85,7 +86,7 @@ class AccountSettingsViewSet(AccountModelViewSet):
|
||||
"""
|
||||
queryset = AccountSettings.objects.all()
|
||||
serializer_class = AccountSettingsSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'system'
|
||||
@@ -145,7 +146,7 @@ class UserSettingsViewSet(AccountModelViewSet):
|
||||
"""
|
||||
queryset = UserSettings.objects.all()
|
||||
serializer_class = UserSettingsSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'system'
|
||||
@@ -211,7 +212,7 @@ class ModuleSettingsViewSet(AccountModelViewSet):
|
||||
"""
|
||||
queryset = ModuleSettings.objects.all()
|
||||
serializer_class = ModuleSettingsSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'system'
|
||||
@@ -290,7 +291,7 @@ class AISettingsViewSet(AccountModelViewSet):
|
||||
"""
|
||||
queryset = AISettings.objects.all()
|
||||
serializer_class = AISettingsSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'system'
|
||||
|
||||
@@ -15,7 +15,7 @@ from django_filters.rest_framework import DjangoFilterBackend
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from igny8_core.api.base import AccountModelViewSet
|
||||
from igny8_core.api.response import success_response, error_response
|
||||
from igny8_core.api.permissions import IsEditorOrAbove, IsAuthenticatedAndActive, IsViewerOrAbove
|
||||
from igny8_core.api.permissions import IsEditorOrAbove, IsAuthenticatedAndActive, IsViewerOrAbove, HasTenantAccess
|
||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||
from igny8_core.api.pagination import CustomPageNumberPagination
|
||||
from .models import AIPrompt, AuthorProfile, Strategy
|
||||
@@ -39,7 +39,7 @@ class AIPromptViewSet(AccountModelViewSet):
|
||||
"""
|
||||
queryset = AIPrompt.objects.all()
|
||||
serializer_class = AIPromptSerializer
|
||||
permission_classes = [] # Allow any for now (backward compatibility)
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
throttle_scope = 'system'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
pagination_class = CustomPageNumberPagination # Explicitly use custom pagination
|
||||
@@ -72,6 +72,14 @@ class AIPromptViewSet(AccountModelViewSet):
|
||||
@action(detail=False, methods=['post'], url_path='save', url_name='save')
|
||||
def save_prompt(self, request):
|
||||
"""Save or update a prompt - requires editor or above"""
|
||||
# Check if user has editor or above permissions
|
||||
if not IsEditorOrAbove().has_permission(request, self):
|
||||
return error_response(
|
||||
error='Permission denied. Editor or above role required.',
|
||||
status_code=http_status.HTTP_403_FORBIDDEN,
|
||||
request=request
|
||||
)
|
||||
|
||||
prompt_type = request.data.get('prompt_type')
|
||||
prompt_value = request.data.get('prompt_value')
|
||||
|
||||
@@ -140,7 +148,15 @@ class AIPromptViewSet(AccountModelViewSet):
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='reset', url_name='reset')
|
||||
def reset_prompt(self, request):
|
||||
"""Reset prompt to default"""
|
||||
"""Reset prompt to default - requires editor or above"""
|
||||
# Check if user has editor or above permissions
|
||||
if not IsEditorOrAbove().has_permission(request, self):
|
||||
return error_response(
|
||||
error='Permission denied. Editor or above role required.',
|
||||
status_code=http_status.HTTP_403_FORBIDDEN,
|
||||
request=request
|
||||
)
|
||||
|
||||
prompt_type = request.data.get('prompt_type')
|
||||
|
||||
if not prompt_type:
|
||||
|
||||
Reference in New Issue
Block a user