new kw for it services & sectors alignment & viewer access partial fixed

This commit is contained in:
IGNY8 VPS (Salman)
2026-02-20 23:29:51 +00:00
parent 2011e48145
commit 341f7c5bc7
110 changed files with 4768 additions and 36 deletions

View File

@@ -171,6 +171,7 @@ class TeamManagementViewSet(viewsets.ViewSet):
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name,
'role': user.role,
'is_active': user.is_active,
'is_staff': user.is_staff,
'date_joined': user.date_joined.isoformat(),
@@ -182,9 +183,11 @@ class TeamManagementViewSet(viewsets.ViewSet):
})
def create(self, request):
"""Invite new team member"""
"""Invite new team member with role and optional site access."""
account = request.user.account
email = request.data.get('email')
role = request.data.get('role', 'viewer')
site_ids = request.data.get('site_ids', []) # For viewer role: which sites to grant access
if not email:
return Response(
@@ -192,6 +195,13 @@ class TeamManagementViewSet(viewsets.ViewSet):
status=status.HTTP_400_BAD_REQUEST
)
# Validate role - only admin and viewer allowed for invites
if role not in ['admin', 'viewer']:
return Response(
{'error': 'Role must be either "admin" or "viewer"'},
status=status.HTTP_400_BAD_REQUEST
)
# Check if user already exists
if User.objects.filter(email=email).exists():
return Response(
@@ -217,15 +227,39 @@ class TeamManagementViewSet(viewsets.ViewSet):
username = f"{base_username}{counter}"
counter += 1
# Create user and send invitation email
# Create user with assigned role
user = User.objects.create_user(
username=username,
email=email,
first_name=request.data.get('first_name', ''),
last_name=request.data.get('last_name', ''),
account=account
account=account,
role=role,
)
# Grant site access based on role
from igny8_core.auth.models import SiteUserAccess, Site
if role == 'viewer' and site_ids:
# Viewer: grant access only to specified sites
sites = Site.objects.filter(id__in=site_ids, account=account)
for site in sites:
SiteUserAccess.objects.get_or_create(
user=user,
site=site,
defaults={'granted_by': request.user}
)
elif role == 'admin':
# Admin: automatically grant access to ALL current sites
# (admin also sees all sites via get_accessible_sites, but
# creating records ensures consistency)
sites = Site.objects.filter(account=account)
for site in sites:
SiteUserAccess.objects.get_or_create(
user=user,
site=site,
defaults={'granted_by': request.user}
)
# Create password reset token for invite
token = secrets.token_urlsafe(32)
expires_at = timezone.now() + timedelta(hours=24)
@@ -248,6 +282,7 @@ class TeamManagementViewSet(viewsets.ViewSet):
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name,
'role': user.role,
}
}, status=status.HTTP_201_CREATED)

View File

@@ -89,8 +89,8 @@ class IsViewerOrAbove(permissions.BasePermission):
# Check user role
if hasattr(request.user, 'role'):
role = request.user.role
# viewer, editor, admin, owner all have access
allowed = role in ['viewer', 'editor', 'admin', 'owner']
# viewer, editor, admin, owner, developer, system_bot all have access
allowed = role in ['viewer', 'editor', 'admin', 'owner', 'developer', 'system_bot']
if allowed:
logger.info(f"[IsViewerOrAbove] ALLOWED: User {request.user.email} has role {role}")
else:
@@ -114,8 +114,8 @@ class IsEditorOrAbove(permissions.BasePermission):
# Check user role
if hasattr(request.user, 'role'):
role = request.user.role
# editor, admin, owner have access
return role in ['editor', 'admin', 'owner']
# editor, admin, owner, developer have access
return role in ['editor', 'admin', 'owner', 'developer']
# If no role system, allow authenticated users
return True

View File

@@ -13,7 +13,7 @@ from rest_framework.views import APIView
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove, IsViewerOrAbove
from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.auth.models import Site
@@ -74,7 +74,13 @@ class UnifiedSiteSettingsViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'settings'
throttle_classes = [DebugScopedRateThrottle]
def get_permissions(self):
"""Viewers can read settings; writes require editor+."""
if self.action == 'retrieve':
return [IsAuthenticatedAndActive(), IsViewerOrAbove()]
return [IsAuthenticatedAndActive(), IsEditorOrAbove()]
def retrieve(self, request, site_id=None):
"""Get all settings for a site in one response"""
site = get_object_or_404(Site, id=site_id, account=request.user.account)

View File

@@ -25,7 +25,7 @@ from .serializers import (
IndustrySerializer, IndustrySectorSerializer, SeedKeywordSerializer,
RefreshTokenSerializer, RequestPasswordResetSerializer, ResetPasswordSerializer
)
from .permissions import IsOwnerOrAdmin, IsEditorOrAbove
from .permissions import IsOwnerOrAdmin, IsEditorOrAbove, IsViewerOrAbove
from .utils import generate_access_token, generate_refresh_token, get_token_expiry, decode_token
from .models import PasswordResetToken
import jwt
@@ -494,6 +494,17 @@ class PlanViewSet(viewsets.ReadOnlyModelViewSet):
)
class _IsOwnerOnly(permissions.BasePermission):
"""Only owner or developer can perform this action (e.g., create sites)."""
def has_permission(self, request, view):
user = getattr(request, 'user', None)
if not user or not user.is_authenticated:
return False
if getattr(user, 'is_superuser', False):
return True
return getattr(user, 'role', '') in ['owner', 'developer']
@extend_schema_view(
list=extend_schema(tags=['Authentication']),
create=extend_schema(tags=['Authentication']),
@@ -509,14 +520,16 @@ class SiteViewSet(AccountModelViewSet):
authentication_classes = [JWTAuthentication]
def get_permissions(self):
"""Allow normal users (viewer) to create sites, but require editor+ for other operations."""
"""Viewers can list/retrieve sites; creation restricted to owner; writes require editor+."""
# 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':
# For create, only require authentication - not active account status
return [permissions.IsAuthenticated()]
# Only owners and developers can create new sites (admin cannot)
return [permissions.IsAuthenticated(), _IsOwnerOnly()]
if self.action in ['list', 'retrieve']:
return [IsAuthenticatedAndActive(), HasTenantAccess(), IsViewerOrAbove()]
return [IsAuthenticatedAndActive(), HasTenantAccess(), IsEditorOrAbove()]
def get_queryset(self):
@@ -772,7 +785,13 @@ class SectorViewSet(AccountModelViewSet):
serializer_class = SectorSerializer
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove]
authentication_classes = [JWTAuthentication]
def get_permissions(self):
"""Viewers can list/retrieve sectors; writes require editor+."""
if self.action in ['list', 'retrieve']:
return [IsAuthenticatedAndActive(), HasTenantAccess(), IsViewerOrAbove()]
return [IsAuthenticatedAndActive(), HasTenantAccess(), IsEditorOrAbove()]
def get_queryset(self):
"""Return sectors from sites accessible to the current user."""
user = self.request.user

View File

@@ -9,7 +9,7 @@ from django.utils import timezone
from drf_spectacular.utils import extend_schema, extend_schema_view
from igny8_core.api.base import SiteSectorModelViewSet
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove, IsViewerOrAbove
from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.auth.models import Site
@@ -39,7 +39,12 @@ class IntegrationViewSet(SiteSectorModelViewSet):
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'integration'
throttle_classes = [DebugScopedRateThrottle]
def get_permissions(self):
if self.action in ['list', 'retrieve']:
return [IsAuthenticatedAndActive(), IsViewerOrAbove()]
return [IsAuthenticatedAndActive(), IsEditorOrAbove()]
def get_queryset(self):
"""
Override to filter integrations by site.
@@ -998,6 +1003,11 @@ class PublishingSettingsViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'integration'
throttle_classes = [DebugScopedRateThrottle]
def get_permissions(self):
if self.action == 'retrieve':
return [IsAuthenticatedAndActive(), IsViewerOrAbove()]
return [IsAuthenticatedAndActive(), IsEditorOrAbove()]
def _get_site(self, site_id, request):
"""Get site and verify user has access"""

View File

@@ -14,7 +14,7 @@ from django.utils.decorators import method_decorator
from drf_spectacular.utils import extend_schema, extend_schema_view
from igny8_core.api.base import SiteSectorModelViewSet
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove, IsViewerOrAbove
from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.business.publishing.models import PublishingRecord, DeploymentRecord
@@ -37,7 +37,12 @@ class PublishingRecordViewSet(SiteSectorModelViewSet):
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'publisher'
throttle_classes = [DebugScopedRateThrottle]
def get_permissions(self):
if self.action in ['list', 'retrieve']:
return [IsAuthenticatedAndActive(), IsViewerOrAbove()]
return [IsAuthenticatedAndActive(), IsEditorOrAbove()]
def get_serializer_class(self):
# Dynamically create serializer
from rest_framework import serializers
@@ -67,7 +72,12 @@ class DeploymentRecordViewSet(SiteSectorModelViewSet):
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'publisher'
throttle_classes = [DebugScopedRateThrottle]
def get_permissions(self):
if self.action in ['list', 'retrieve']:
return [IsAuthenticatedAndActive(), IsViewerOrAbove()]
return [IsAuthenticatedAndActive(), IsEditorOrAbove()]
def get_serializer_class(self):
# Dynamically create serializer
from rest_framework import serializers
@@ -89,6 +99,11 @@ class PublisherViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'publisher'
throttle_classes = [DebugScopedRateThrottle]
def get_permissions(self):
if self.action == 'get_status':
return [IsAuthenticatedAndActive(), IsViewerOrAbove()]
return [IsAuthenticatedAndActive(), IsEditorOrAbove()]
def __init__(self, **kwargs):
super().__init__(**kwargs)