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:
Desktop
2025-11-16 11:35:47 +05:00
parent d492b74d40
commit 64b8280bce
8 changed files with 739 additions and 93 deletions

View File

@@ -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 = [

View File

@@ -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):

View File

@@ -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')

View File

@@ -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]

View File

@@ -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'

View File

@@ -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: