Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
"""
IGNY8 API Module
"""

View File

@@ -0,0 +1,91 @@
"""
Custom authentication classes for DRF
"""
from rest_framework.authentication import SessionAuthentication, BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
import jwt
class CSRFExemptSessionAuthentication(SessionAuthentication):
"""
Session authentication that doesn't enforce CSRF for API endpoints.
This is safe for API usage since we're using session cookies and proper CORS settings.
"""
def enforce_csrf(self, request):
"""
Override to skip CSRF enforcement for API endpoints.
"""
return # Skip CSRF check
class JWTAuthentication(BaseAuthentication):
"""
JWT token authentication for DRF.
Extracts JWT token from Authorization header and validates it.
"""
def authenticate(self, request):
"""
Authenticate the request and return a two-tuple of (user, token).
"""
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if not auth_header.startswith('Bearer '):
return None # No JWT token, let other auth classes handle it
token = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None
if not token:
return None
try:
from igny8_core.auth.utils import decode_token
from igny8_core.auth.models import User, Account
# Decode and validate token
payload = decode_token(token)
# Verify it's an access token
if payload.get('type') != 'access':
# Invalid token type - return None to allow other auth classes to try
return None
# Get user
user_id = payload.get('user_id')
if not user_id:
# Invalid token payload - return None to allow other auth classes to try
return None
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
# User not found - return None to allow other auth classes to try
return None
# Get account from token
account_id = payload.get('account_id')
account = None
if account_id:
try:
account = Account.objects.get(id=account_id)
except Account.DoesNotExist:
pass
if not account:
try:
account = getattr(user, 'account', None)
except (AttributeError, Exception):
# If account access fails, set to None
account = None
# Set account on request
request.account = account
return (user, token)
except jwt.InvalidTokenError:
# Invalid or expired token - return None to allow other auth classes (session) to try
return None
except Exception as e:
# Other errors - return None to allow other auth classes to try
# This allows session authentication to work if JWT fails
return None

View File

@@ -0,0 +1,274 @@
"""
Base ViewSet with account filtering support
"""
from rest_framework import viewsets
from rest_framework.response import Response
from django.core.exceptions import PermissionDenied
class AccountModelViewSet(viewsets.ModelViewSet):
"""
Base ViewSet that automatically filters by account.
All module ViewSets should inherit from this.
"""
def get_queryset(self):
queryset = super().get_queryset()
# Filter by account if model has account field
if hasattr(queryset.model, 'account'):
user = getattr(self.request, 'user', None)
# ADMIN/DEV/SYSTEM ACCOUNT OVERRIDE: Skip account filtering for:
# - Admins and developers (by role)
# - Users in system accounts (aws-admin, default-account)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
try:
# Check if user has admin/developer privileges
is_admin_or_dev = (hasattr(user, 'is_admin_or_developer') and user.is_admin_or_developer()) if user else False
is_system_user = (hasattr(user, 'is_system_account_user') and user.is_system_account_user()) if user else False
if is_admin_or_dev or is_system_user:
# Skip account filtering - allow all accounts
pass
else:
# Get account from request (set by middleware)
account = getattr(self.request, 'account', None)
if account:
queryset = queryset.filter(account=account)
elif hasattr(self.request, 'user') and self.request.user and hasattr(self.request.user, 'is_authenticated') and self.request.user.is_authenticated:
# Fallback to user's account
try:
user_account = getattr(self.request.user, 'account', None)
if user_account:
queryset = queryset.filter(account=user_account)
except (AttributeError, Exception):
# If account access fails (e.g., column mismatch), skip account filtering
pass
except (AttributeError, TypeError) as e:
# If there's an error accessing user attributes, return empty queryset
return queryset.none()
else:
# Require authentication - return empty queryset for unauthenticated users
return queryset.none()
return queryset
def perform_create(self, serializer):
# Set account from request (set by middleware)
account = getattr(self.request, 'account', None)
if not account and hasattr(self.request, 'user') and self.request.user and self.request.user.is_authenticated:
try:
account = getattr(self.request.user, 'account', None)
except (AttributeError, Exception):
# If account access fails (e.g., column mismatch), set to None
account = None
# If model has account field, set it
if account and hasattr(serializer.Meta.model, 'account'):
serializer.save(account=account)
else:
serializer.save()
def get_serializer_context(self):
context = super().get_serializer_context()
# Add account to context for serializers
account = getattr(self.request, 'account', None)
if account:
context['account'] = account
return context
class SiteSectorModelViewSet(AccountModelViewSet):
"""
Base ViewSet for models that belong to Site and Sector (Keywords, Clusters, etc.).
Automatically filters by:
1. Account (via parent class)
2. User's accessible sites (based on role and SiteUserAccess)
3. Optional site/sector query parameters
"""
def get_queryset(self):
queryset = super().get_queryset()
# Check if model has site and sector fields (SiteSectorBaseModel)
if hasattr(queryset.model, 'site') and hasattr(queryset.model, 'sector'):
user = getattr(self.request, 'user', None)
# Check if user is authenticated and is a proper User instance (not AnonymousUser)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated and hasattr(user, 'get_accessible_sites'):
try:
# ADMIN/DEV/SYSTEM ACCOUNT OVERRIDE: Developers, admins, and system account users
# can see all data regardless of site/sector
if (hasattr(user, 'is_admin_or_developer') and user.is_admin_or_developer()) or \
(hasattr(user, 'is_system_account_user') and user.is_system_account_user()):
# Skip site/sector filtering for admins, developers, and system account users
# But still respect optional query params if provided
pass
else:
# Get user's accessible sites
accessible_sites = user.get_accessible_sites()
# If no accessible sites, return empty queryset (unless admin/developer/system account)
if not accessible_sites.exists():
queryset = queryset.none()
else:
# Filter by accessible sites
queryset = queryset.filter(site__in=accessible_sites)
except (AttributeError, TypeError) as e:
# If there's an error accessing user attributes, return empty queryset
queryset = queryset.none()
else:
# Require authentication - return empty queryset for unauthenticated users
queryset = queryset.none()
# Optional: Filter by specific site (from query params)
# Safely access query_params (DRF wraps request with Request class)
try:
query_params = getattr(self.request, 'query_params', None)
if query_params is None:
# Fallback for non-DRF requests
query_params = getattr(self.request, 'GET', {})
site_id = query_params.get('site_id')
else:
site_id = query_params.get('site_id')
except AttributeError:
site_id = None
if site_id:
try:
# Convert site_id to int if it's a string
site_id_int = int(site_id) if site_id else None
if site_id_int:
# ADMIN/DEV/SYSTEM ACCOUNT OVERRIDE: Admins, developers, and system account users
# can filter by any site, others must verify access
if user and hasattr(user, 'is_authenticated') and user.is_authenticated and hasattr(user, 'get_accessible_sites'):
try:
if (hasattr(user, 'is_admin_or_developer') and user.is_admin_or_developer()) or \
(hasattr(user, 'is_system_account_user') and user.is_system_account_user()):
# Admin/Developer/System Account User can filter by any site
queryset = queryset.filter(site_id=site_id_int)
else:
accessible_sites = user.get_accessible_sites()
if accessible_sites.filter(id=site_id_int).exists():
queryset = queryset.filter(site_id=site_id_int)
else:
queryset = queryset.none() # Site not accessible
except (AttributeError, TypeError) as e:
# If there's an error accessing user attributes, return empty queryset
queryset = queryset.none()
else:
# Require authentication for site filtering
queryset = queryset.none()
except (ValueError, TypeError):
# Invalid site_id, return empty queryset
queryset = queryset.none()
# Optional: Filter by specific sector (from query params)
# Safely access query_params (DRF wraps request with Request class)
try:
query_params = getattr(self.request, 'query_params', None)
if query_params is None:
# Fallback for non-DRF requests
query_params = getattr(self.request, 'GET', {})
sector_id = query_params.get('sector_id')
else:
sector_id = query_params.get('sector_id')
except AttributeError:
sector_id = None
if sector_id:
try:
# Convert sector_id to int if it's a string
sector_id_int = int(sector_id) if sector_id else None
if sector_id_int:
queryset = queryset.filter(sector_id=sector_id_int)
# If site_id also provided, ensure sector belongs to that site
if site_id:
try:
site_id_int = int(site_id) if site_id else None
if site_id_int:
queryset = queryset.filter(site_id=site_id_int)
except (ValueError, TypeError):
pass
except (ValueError, TypeError):
# Invalid sector_id, return empty queryset
queryset = queryset.none()
# Always exclude records where site is null (orphaned records)
# This prevents showing keywords/clusters/ideas/tasks that aren't associated with any site
# Only skip this if explicitly requested (e.g., for admin cleanup operations)
queryset = queryset.exclude(site__isnull=True)
return queryset
def perform_create(self, serializer):
# First call parent to set account
super().perform_create(serializer)
# If model has site and sector fields, validate access
if hasattr(serializer.Meta.model, 'site') and hasattr(serializer.Meta.model, 'sector'):
user = getattr(self.request, 'user', None)
site = serializer.validated_data.get('site')
sector = serializer.validated_data.get('sector')
if user and hasattr(user, 'is_authenticated') and user.is_authenticated and site:
try:
# ADMIN/DEV/SYSTEM ACCOUNT OVERRIDE: Admins, developers, and system account users
# can create in any site, others must verify access
if not ((hasattr(user, 'is_admin_or_developer') and user.is_admin_or_developer()) or
(hasattr(user, 'is_system_account_user') and user.is_system_account_user())):
if hasattr(user, 'get_accessible_sites'):
accessible_sites = user.get_accessible_sites()
if not accessible_sites.filter(id=site.id).exists():
raise PermissionDenied("You do not have access to this site")
# Verify sector belongs to site
if sector and hasattr(sector, 'site') and sector.site != site:
raise PermissionDenied("Sector must belong to the selected site")
except (AttributeError, TypeError) as e:
# If there's an error accessing user attributes, raise permission denied
raise PermissionDenied("Unable to verify access permissions")
def get_serializer_context(self):
context = super().get_serializer_context()
user = getattr(self.request, 'user', None)
# Add accessible sites to context for serializer (e.g., for dropdown choices)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated and hasattr(user, 'get_accessible_sites'):
try:
context['accessible_sites'] = user.get_accessible_sites()
# Get accessible sectors from accessible sites
from igny8_core.auth.models import Sector
context['accessible_sectors'] = Sector.objects.filter(
site__in=context['accessible_sites'],
is_active=True
)
except (AttributeError, TypeError):
# If there's an error, set empty querysets
from igny8_core.auth.models import Site, Sector
context['accessible_sites'] = Site.objects.none()
context['accessible_sectors'] = Sector.objects.none()
else:
# Set empty querysets for unauthenticated users
from igny8_core.auth.models import Site, Sector
context['accessible_sites'] = Site.objects.none()
context['accessible_sectors'] = Sector.objects.none()
return context
class StandardResponseMixin:
"""
Mixin for standard API response format.
"""
def get_response(self, data, message=None, status_code=200):
return Response({
'success': True,
'message': message,
'data': data
}, status=status_code)
def get_error_response(self, message, errors=None, status_code=400):
return Response({
'success': False,
'message': message,
'errors': errors
}, status=status_code)

View File

@@ -0,0 +1,18 @@
"""
Custom pagination class for DRF to support dynamic page_size query parameter
"""
from rest_framework.pagination import PageNumberPagination
class CustomPageNumberPagination(PageNumberPagination):
"""
Custom pagination class that allows clients to override the page size
via the page_size query parameter.
Default page size: 10
Max page size: 100
"""
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100