✨(api) add endpoint to manage ThreadAccess

Allows creating, updating, and deleting access to threads based on
user role.
This commit is contained in:
Sabrina Demagny
2025-05-16 12:18:14 +02:00
parent bbb8f8cb4a
commit eca469e666
7 changed files with 653 additions and 11 deletions
+67
View File
@@ -276,3 +276,70 @@ class IsAllowedToCreateMessage(IsAuthenticated):
# return True
#
# return False
class IsAllowedToManageThreadAccess(IsAuthenticated):
"""Permission class for access to create, update, delete and list thread accesses."""
def has_permission(self, request, view):
# Get thread_id from URL kwargs instead of query params/data
thread_id = view.kwargs.get("thread_id")
if not thread_id:
return False
# if create action, check if user has admin/editor access to the mailbox and the thread access role is editor
if view.action == "create":
# authenticated user wants to create a thread access for a specific thread
# check if user has admin/editor access to the mailbox and the thread access role is editor already exists for them
return (
models.ThreadAccess.objects.select_related("mailbox")
.filter(
thread_id=thread_id,
mailbox__accesses__user=request.user,
mailbox__accesses__role__in=[
enums.MailboxRoleChoices.ADMIN,
enums.MailboxRoleChoices.EDITOR,
],
role=enums.ThreadAccessRoleChoices.EDITOR,
)
.exists()
)
elif view.action == "list":
# list is only allowed for a user with access to the thread
return (
models.ThreadAccess.objects.select_related("mailbox")
.filter(
thread_id=thread_id,
mailbox__accesses__user=request.user,
mailbox__accesses__role__in=[
enums.MailboxRoleChoices.ADMIN,
enums.MailboxRoleChoices.EDITOR,
],
role=enums.ThreadAccessRoleChoices.EDITOR,
)
.exists()
)
else:
return True # to proceed to object-level checks
def has_object_permission(self, request, view, obj):
"""Check if user has permission to access the specific object (ThreadAccess).
Manage retrieve, update, destroy actions here.
"""
# Verify the thread access belongs to the thread in the URL
if str(obj.thread.id) != view.kwargs.get("thread_id"):
return False
return (
models.ThreadAccess.objects.select_related("mailbox")
.filter(
thread=obj.thread,
mailbox__accesses__user=request.user,
mailbox__accesses__role__in=[
enums.MailboxRoleChoices.ADMIN,
enums.MailboxRoleChoices.EDITOR,
],
role=enums.ThreadAccessRoleChoices.EDITOR,
)
.exists()
)
+9
View File
@@ -279,3 +279,12 @@ class MessageSerializer(serializers.ModelSerializer):
"is_trashed",
]
read_only_fields = fields # Mark all as read-only
class ThreadAccessSerializer(serializers.ModelSerializer):
"""Serialize thread access information."""
class Meta:
model = models.ThreadAccess
fields = ["id", "thread", "mailbox", "role", "created_at", "updated_at"]
read_only_fields = ["id", "created_at", "updated_at"]
@@ -0,0 +1,68 @@
"""API ViewSet for ThreadAccess model."""
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
extend_schema,
)
from rest_framework import mixins, viewsets
from core import models
from .. import permissions, serializers
@extend_schema(
tags=["thread-access"],
parameters=[
OpenApiParameter(
name="mailbox_id",
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Filter thread accesses by mailbox ID.",
required=False,
),
],
)
class ThreadAccessViewSet(
viewsets.GenericViewSet,
mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
):
"""ViewSet for ThreadAccess model."""
serializer_class = serializers.ThreadAccessSerializer
permission_classes = [
permissions.IsAuthenticated,
permissions.IsAllowedToManageThreadAccess,
]
lookup_field = "id"
lookup_url_kwarg = "id"
queryset = models.ThreadAccess.objects.all()
def get_queryset(self):
"""Restrict results to thread accesses for the specified thread."""
# Get thread_id from URL kwargs (provided by nested router)
thread_id = self.kwargs.get("thread_id")
if not thread_id:
return models.ThreadAccess.objects.none()
# Filter by thread_id from URL
queryset = self.queryset.filter(thread_id=thread_id)
# Optional mailbox filter
mailbox_id = self.request.GET.get("mailbox_id")
if mailbox_id:
queryset = queryset.filter(mailbox_id=mailbox_id)
return queryset.distinct()
def create(self, request, *args, **kwargs):
"""Create a new thread access."""
request.data["thread"] = self.kwargs.get("thread_id")
return super().create(request, *args, **kwargs)
def list(self, request, *args, **kwargs):
"""List thread accesses for the specified thread."""
return super().list(request, *args, **kwargs)