diff --git a/files/models.py b/files/models.py index 9e9f853..ec8e24c 100644 --- a/files/models.py +++ b/files/models.py @@ -40,6 +40,26 @@ class SharedSpace(models.Model): default=0, help_text="Maximum file size in MB for uploads (0 = unlimited)" ) + + VISIBILITY_CHOICES = [ + ('public', 'Public'), + ('public_read', 'Public Read'), + ('authenticated', 'Authenticated'), + ('members_only', 'Members Only'), + ] + visibility = models.CharField( + max_length=20, + choices=VISIBILITY_CHOICES, + default='public_read', + help_text="Who can access this space" + ) + owner_did = models.CharField( + max_length=255, + blank=True, + default='', + help_text="EncryptID DID of the space owner" + ) + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/files/views.py b/files/views.py index 5ed3ad0..04996f3 100644 --- a/files/views.py +++ b/files/views.py @@ -13,7 +13,8 @@ from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.parsers import MultiPartParser, FormParser, JSONParser -from .models import MediaFile, PublicShare, FileAccessLog +from .models import MediaFile, PublicShare, FileAccessLog, SharedSpace +from config.encryptid_auth import SpacePermission from .serializers import ( MediaFileSerializer, MediaFileUploadSerializer, @@ -30,7 +31,18 @@ class MediaFileViewSet(viewsets.ModelViewSet): queryset = MediaFile.objects.all() serializer_class = MediaFileSerializer parser_classes = [MultiPartParser, FormParser, JSONParser] - permission_classes = [permissions.IsAuthenticatedOrReadOnly] + permission_classes = [permissions.IsAuthenticatedOrReadOnly, SpacePermission] + + def get_space_config(self, request): + """Provide space config for SpacePermission check.""" + space_slug = request.query_params.get('space') + if not space_slug: + return None + try: + space = SharedSpace.objects.get(slug=space_slug, is_active=True) + return {'visibility': space.visibility, 'owner_did': space.owner_did} + except SharedSpace.DoesNotExist: + return None def get_serializer_class(self): if self.action == 'create': diff --git a/portal/views_shared_space.py b/portal/views_shared_space.py index c7bd7d7..deecd16 100644 --- a/portal/views_shared_space.py +++ b/portal/views_shared_space.py @@ -12,6 +12,7 @@ from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator from files.models import SharedSpace, MediaFile, PublicShare +from config.encryptid_auth import check_space_access def get_topic_or_404(request): @@ -54,11 +55,19 @@ class SharedSpaceHomeView(View): @method_decorator(csrf_exempt, name='dispatch') class SharedSpaceUploadAPIView(View): - """Handle file uploads via AJAX for topic.""" + """Handle file uploads via AJAX for topic. Requires auth for non-public spaces.""" def post(self, request): space = get_topic_or_404(request) + # Check space visibility — uploads are always writes + access = check_space_access(request, { + 'visibility': space.visibility, + 'owner_did': space.owner_did, + }) + if not access['allowed']: + return JsonResponse({'error': access['reason']}, status=401) + if not request.FILES.get('file'): return JsonResponse({'error': 'No file provided'}, status=400) @@ -137,6 +146,7 @@ class DirectUploadAPIView(View): """Handle uploads via direct.rfiles.online (bypasses Cloudflare). Space slug is passed as a form field instead of from the subdomain. + Requires auth for non-public spaces. """ def post(self, request): @@ -146,6 +156,14 @@ class DirectUploadAPIView(View): space = get_object_or_404(SharedSpace, slug=space_slug, is_active=True) + # Check space visibility — uploads are always writes + access = check_space_access(request, { + 'visibility': space.visibility, + 'owner_did': space.owner_did, + }) + if not access['allowed']: + return JsonResponse({'error': access['reason']}, status=401) + if not request.FILES.get('file'): return JsonResponse({'error': 'No file provided'}, status=400)