Enhance public access and error handling in site-related views and loaders

- Updated `DebugScopedRateThrottle` to allow public access for blueprint list requests with site filters.
- Modified `SiteViewSet` and `SiteBlueprintViewSet` to permit public read access for list requests.
- Enhanced `loadSiteDefinition` to resolve site slugs to IDs, improving the loading process for site definitions.
- Improved error handling in `SiteDefinitionView` and `loadSiteDefinition` for better user feedback.
- Adjusted CSS styles for better layout and alignment in shared components.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-18 22:40:00 +00:00
parent 8ab15d1d79
commit 6c6133a683
14 changed files with 361 additions and 76 deletions

View File

@@ -28,11 +28,19 @@ class DebugScopedRateThrottle(ScopedRateThrottle):
- IGNY8_DEBUG_THROTTLE environment variable is True
- User belongs to aws-admin or other system accounts
- User is admin/developer role
- Public blueprint list request with site filter (for Sites Renderer)
"""
# Check if throttling should be bypassed
debug_bypass = getattr(settings, 'DEBUG', False)
env_bypass = getattr(settings, 'IGNY8_DEBUG_THROTTLE', False)
# Bypass for public blueprint list requests (Sites Renderer fallback)
public_blueprint_bypass = False
if hasattr(view, 'action') and view.action == 'list':
if hasattr(request, 'query_params') and request.query_params.get('site'):
if not request.user or not hasattr(request.user, 'is_authenticated') or not request.user.is_authenticated:
public_blueprint_bypass = True
# Bypass for system account users (aws-admin, default-account, etc.)
system_account_bypass = False
if hasattr(request, 'user') and request.user and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated:
@@ -47,7 +55,7 @@ class DebugScopedRateThrottle(ScopedRateThrottle):
# If checking fails, continue with normal throttling
pass
if debug_bypass or env_bypass or system_account_bypass:
if debug_bypass or env_bypass or system_account_bypass or public_blueprint_bypass:
# In debug mode or for system accounts, still set throttle headers but don't actually throttle
# This allows testing throttle headers without blocking requests
if hasattr(self, 'get_rate'):

View File

@@ -478,16 +478,26 @@ class SiteViewSet(AccountModelViewSet):
def get_permissions(self):
"""Allow normal users (viewer) to create sites, but require editor+ for other operations."""
# Allow public read access for list requests with slug filter (used by Sites Renderer)
if self.action == 'list' and self.request.query_params.get('slug'):
from rest_framework.permissions import AllowAny
return [AllowAny()]
if self.action == 'create':
return [permissions.IsAuthenticated()]
return [IsEditorOrAbove()]
def get_queryset(self):
"""Return sites accessible to the current user."""
user = self.request.user
if not user or not user.is_authenticated:
# If this is a public request (no auth) with slug filter, return site by slug
if not self.request.user or not self.request.user.is_authenticated:
slug = self.request.query_params.get('slug')
if slug:
# Return queryset directly from model (bypassing base class account filtering)
return Site.objects.filter(slug=slug, is_active=True)
return Site.objects.none()
user = self.request.user
# ADMIN/DEV OVERRIDE: Both admins and developers can see all sites
if user.is_admin_or_developer():
return Site.objects.all().distinct()

View File

@@ -101,6 +101,7 @@ class PageBlueprint(SiteSectorBaseModel):
('draft', 'Draft'),
('generating', 'Generating'),
('ready', 'Ready'),
('published', 'Published'),
]
site_blueprint = models.ForeignKey(

View File

@@ -81,8 +81,10 @@ class CreditBalanceViewSet(viewsets.ViewSet):
'credits_remaining': credits_remaining,
}
serializer = CreditBalanceSerializer(data)
return success_response(data=serializer.data, request=request)
# Validate and serialize data
serializer = CreditBalanceSerializer(data=data)
serializer.is_valid(raise_exception=True)
return success_response(data=serializer.validated_data, request=request)
@extend_schema_view(

View File

@@ -219,61 +219,104 @@ class SiteDefinitionView(APIView):
GET /api/v1/publisher/sites/{site_id}/definition/
"""
sites_data_path = os.getenv('SITES_DATA_PATH', '/data/app/sites-data')
# Try to find latest deployed version
site_dir = Path(sites_data_path) / 'clients' / str(site_id)
if not site_dir.exists():
return error_response(
f'Site {site_id} not found',
status.HTTP_404_NOT_FOUND,
request
)
# Look for latest version (check for 'latest' symlink or highest version number)
latest_path = site_dir / 'latest' / 'site.json'
if latest_path.exists():
try:
sites_data_path = os.getenv('SITES_DATA_PATH', '/data/app/sites-data')
# Try to find latest deployed version
site_dir = Path(sites_data_path) / 'clients' / str(site_id)
if not site_dir.exists():
return error_response(
error=f'Site {site_id} not found at {site_dir}. Site may not be deployed yet.',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
# Look for latest version (check for 'latest' symlink or highest version number)
latest_path = site_dir / 'latest' / 'site.json'
if latest_path.exists():
try:
with open(latest_path, 'r', encoding='utf-8') as f:
definition = json.load(f)
return Response(definition, status=status.HTTP_200_OK)
except json.JSONDecodeError as e:
return error_response(
error=f'Invalid JSON in site definition file: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except Exception as e:
return error_response(
error=f'Failed to load site definition from {latest_path}: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
# Fallback: find highest version number
try:
with open(latest_path, 'r', encoding='utf-8') as f:
definition = json.load(f)
return Response(definition, status=status.HTTP_200_OK)
version_dirs = [d for d in site_dir.iterdir() if d.is_dir() and d.name.startswith('v')]
except PermissionError as e:
return error_response(
error=f'Permission denied accessing site directory: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except Exception as e:
return error_response(
f'Failed to load site definition: {str(e)}',
status.HTTP_500_INTERNAL_SERVER_ERROR,
request
error=f'Error reading site directory: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
if not version_dirs:
return error_response(
error=f'No deployed versions found for site {site_id}. Site may not be deployed yet.',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
# Sort by version number (extract number from 'v1', 'v2', etc.)
try:
version_dirs.sort(key=lambda d: int(d.name[1:]) if d.name[1:].isdigit() else 0, reverse=True)
latest_version_dir = version_dirs[0]
site_json_path = latest_version_dir / 'site.json'
if site_json_path.exists():
with open(site_json_path, 'r', encoding='utf-8') as f:
definition = json.load(f)
return Response(definition, status=status.HTTP_200_OK)
else:
return error_response(
error=f'Site definition file not found at {site_json_path}',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
except json.JSONDecodeError as e:
return error_response(
error=f'Invalid JSON in site definition file: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
except Exception as e:
return error_response(
error=f'Failed to load site definition: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
# Fallback: find highest version number
version_dirs = [d for d in site_dir.iterdir() if d.is_dir() and d.name.startswith('v')]
if not version_dirs:
return error_response(
f'No deployed versions found for site {site_id}',
status.HTTP_404_NOT_FOUND,
request
)
# Sort by version number (extract number from 'v1', 'v2', etc.)
try:
version_dirs.sort(key=lambda d: int(d.name[1:]) if d.name[1:].isdigit() else 0, reverse=True)
latest_version_dir = version_dirs[0]
site_json_path = latest_version_dir / 'site.json'
if site_json_path.exists():
with open(site_json_path, 'r', encoding='utf-8') as f:
definition = json.load(f)
return Response(definition, status=status.HTTP_200_OK)
except Exception as e:
return error_response(
f'Failed to load site definition: {str(e)}',
status.HTTP_500_INTERNAL_SERVER_ERROR,
request
error=f'Site definition not found for site {site_id}',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
except Exception as e:
# Catch any unhandled exceptions
import logging
logger = logging.getLogger(__name__)
logger.error(f'Unhandled exception in SiteDefinitionView: {str(e)}', exc_info=True)
return error_response(
error=f'Internal server error: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
return error_response(
f'Site definition not found for site {site_id}',
status.HTTP_404_NOT_FOUND,
request
)

View File

@@ -39,6 +39,47 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet):
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'site_builder'
throttle_classes = [DebugScopedRateThrottle]
def get_permissions(self):
"""
Allow public read access for list requests with site filter (used by Sites Renderer fallback).
This allows the Sites Renderer to load blueprint data for deployed sites without authentication.
"""
# Allow public access for list requests with site filter (used by Sites Renderer)
if self.action == 'list' and self.request.query_params.get('site'):
from rest_framework.permissions import AllowAny
return [AllowAny()]
# Otherwise use default permissions
return super().get_permissions()
def get_throttles(self):
"""
Bypass throttling for public list requests with site filter (used by Sites Renderer).
"""
# Bypass throttling for public requests (no auth) with site filter
if self.action == 'list' and self.request.query_params.get('site'):
if not self.request.user or not self.request.user.is_authenticated:
return [] # No throttling for public blueprint access
return super().get_throttles()
def get_queryset(self):
"""
Override to allow public access when filtering by site_id.
"""
# If this is a public request (no auth) with site filter, bypass base class filtering
# and return deployed blueprints for that site
if not self.request.user or not self.request.user.is_authenticated:
site_id = self.request.query_params.get('site')
if site_id:
# Return queryset directly from model (bypassing base class account/site filtering)
from igny8_core.business.site_building.models import SiteBlueprint
return SiteBlueprint.objects.filter(
site_id=site_id,
status='deployed'
).prefetch_related('pages').order_by('-version')
# For authenticated users, use base class filtering
return super().get_queryset()
def perform_create(self, serializer):
from igny8_core.auth.models import Site, Sector