From eca469e6663345ca71663abd2b0ad68302276b24 Mon Sep 17 00:00:00 2001 From: Sabrina Demagny Date: Thu, 15 May 2025 13:53:25 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(api)=20add=20endpoint=20to=20manage?= =?UTF-8?q?=20ThreadAccess?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allows creating, updating, and deleting access to threads based on user role. --- src/backend/core/api/permissions.py | 67 +++ src/backend/core/api/serializers.py | 9 + .../core/api/viewsets/thread_access.py | 68 +++ src/backend/core/enums.py | 14 +- .../core/tests/api/test_messages_create.py | 1 - .../core/tests/api/test_thread_access.py | 479 ++++++++++++++++++ src/backend/core/urls.py | 26 +- 7 files changed, 653 insertions(+), 11 deletions(-) create mode 100644 src/backend/core/api/viewsets/thread_access.py create mode 100644 src/backend/core/tests/api/test_thread_access.py diff --git a/src/backend/core/api/permissions.py b/src/backend/core/api/permissions.py index 3530f623..49712bc3 100644 --- a/src/backend/core/api/permissions.py +++ b/src/backend/core/api/permissions.py @@ -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() + ) diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 878c17ca..9b72d11a 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -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"] diff --git a/src/backend/core/api/viewsets/thread_access.py b/src/backend/core/api/viewsets/thread_access.py new file mode 100644 index 00000000..9e2f9487 --- /dev/null +++ b/src/backend/core/api/viewsets/thread_access.py @@ -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) diff --git a/src/backend/core/enums.py b/src/backend/core/enums.py index ea5d7fe6..6bbe4594 100644 --- a/src/backend/core/enums.py +++ b/src/backend/core/enums.py @@ -21,6 +21,13 @@ class MailboxRoleChoices(models.TextChoices): ADMIN = "admin", _("Admin") +class ThreadAccessRoleChoices(models.TextChoices): + """Defines the possible roles a mailbox can have to access to a thread.""" + + VIEWER = "viewer", _("Viewer") + EDITOR = "editor", _("Editor") + + class MessageRecipientTypeChoices(models.TextChoices): """Defines the possible types of message recipients.""" @@ -29,13 +36,6 @@ class MessageRecipientTypeChoices(models.TextChoices): BCC = "bcc", _("Bcc") -class ThreadAccessRoleChoices(models.TextChoices): - """Defines the possible roles a mailbox can have to access to a thread.""" - - VIEWER = "viewer", _("Viewer") - EDITOR = "editor", _("Editor") - - THREAD_STATS_FIELDS_MAP = { "unread": "count_unread", "trashed": "count_trashed", diff --git a/src/backend/core/tests/api/test_messages_create.py b/src/backend/core/tests/api/test_messages_create.py index 341258d2..a56653e1 100644 --- a/src/backend/core/tests/api/test_messages_create.py +++ b/src/backend/core/tests/api/test_messages_create.py @@ -187,7 +187,6 @@ class TestApiDraftAndSendMessage: assert sent_message.thread.sender_names == [sent_message.sender.name] assert sent_message.thread.messaged_at is not None - @patch("core.mda.outbound.send_outbound_message") def test_draft_and_send_message_success_delegated_access( self, mock_send_outbound_message, mailbox, authenticated_user, send_url diff --git a/src/backend/core/tests/api/test_thread_access.py b/src/backend/core/tests/api/test_thread_access.py new file mode 100644 index 00000000..08546e41 --- /dev/null +++ b/src/backend/core/tests/api/test_thread_access.py @@ -0,0 +1,479 @@ +"""Tests for the ThreadAccess API endpoints.""" + +import uuid + +from django.urls import reverse + +import pytest +from rest_framework import status + +from core import enums, factories, models + +pytestmark = pytest.mark.django_db + + +def get_thread_access_url(thread_id, access_id=None): + """Helper function to get the thread access URL.""" + if access_id: + return reverse("thread-access-detail", args=[thread_id, access_id]) + return reverse("thread-access-list", args=[thread_id]) + + +@pytest.fixture +def mailbox_with_access(): + """Create a mailbox with access for a user.""" + user = factories.UserFactory() + mailbox = factories.MailboxFactory() + factories.MailboxAccessFactory( + mailbox=mailbox, + user=user, + role=enums.MailboxRoleChoices.ADMIN, + ) + return user, mailbox + + +@pytest.fixture +def thread_with_editor_access(mailbox_with_access): + """Create a thread with access for a mailbox.""" + user, mailbox = mailbox_with_access + thread = factories.ThreadFactory() + thread_access = factories.ThreadAccessFactory( + mailbox=mailbox, + thread=thread, + role=enums.ThreadAccessRoleChoices.EDITOR, + ) + return user, mailbox, thread, thread_access + + +class TestThreadAccessList: + """Test the GET /threads/{thread_id}/accesses/ endpoint.""" + + @pytest.mark.parametrize( + "thread_access_role, mailbox_access_role", + [ + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.ADMIN), + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.EDITOR), + ], + ) + def test_list_thread_access_success( + self, + api_client, + thread_access_role, + mailbox_access_role, + django_assert_num_queries, + ): + """Test listing thread accesses of a thread.""" + user = factories.UserFactory() + mailbox = factories.MailboxFactory() + factories.MailboxAccessFactory( + mailbox=mailbox, + user=user, + role=mailbox_access_role, + ) + + thread = factories.ThreadFactory() + factories.ThreadAccessFactory( + mailbox=mailbox, + thread=thread, + role=thread_access_role, + ) + api_client.force_authenticate(user=user) + # Create other accesses for thread + factories.ThreadAccessFactory.create_batch(10, thread=thread) + # Create others thread accesses for different threads + other_thread = factories.ThreadFactory() + factories.ThreadAccessFactory( + mailbox=mailbox, + thread=other_thread, + ) + factories.ThreadAccessFactory.create_batch(5, thread=other_thread) + + with django_assert_num_queries(3): + response = api_client.get(get_thread_access_url(thread.id)) + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 11 + assert response.data["results"][0]["thread"] == thread.id + + def test_list_thread_access_filter_by_mailbox( + self, api_client, thread_with_editor_access, django_assert_num_queries + ): + """Test listing thread accesses filtered by mailbox.""" + user, mailbox, thread, thread_access = thread_with_editor_access + api_client.force_authenticate(user=user) + + # Create another thread access for a different mailbox + other_mailbox = factories.MailboxFactory() + factories.MailboxAccessFactory( + mailbox=other_mailbox, + user=user, + role=enums.MailboxRoleChoices.ADMIN, + ) + factories.ThreadAccessFactory( + mailbox=other_mailbox, + thread=thread, + role=enums.ThreadAccessRoleChoices.EDITOR, + ) + with django_assert_num_queries(3): + response = api_client.get( + f"{get_thread_access_url(thread.id)}?mailbox_id={mailbox.id}" + ) + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert response.data["results"][0]["mailbox"] == mailbox.id + + @pytest.mark.parametrize( + "thread_access_role, mailbox_access_role", + [ + (enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.ADMIN), + (enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.EDITOR), + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.VIEWER), + ], + ) + def test_list_thread_access_forbidden( + self, api_client, thread_access_role, mailbox_access_role + ): + """Test listing thread accesses without permission.""" + user = factories.UserFactory() + api_client.force_authenticate(user=user) + + # Create a mailbox and thread access that the user doesn't have access to manage + mailbox = factories.MailboxFactory() + thread = factories.ThreadFactory() + factories.ThreadAccessFactory( + mailbox=mailbox, + thread=thread, + role=thread_access_role, + ) + factories.MailboxAccessFactory( + mailbox=mailbox, + user=user, + role=mailbox_access_role, + ) + + # Test that user cannot access thread accesses for a thread they don't have proper access to + response = api_client.get(get_thread_access_url(thread.id)) + assert response.status_code == status.HTTP_403_FORBIDDEN + + # Test that user cannot access thread accesses for a non-existent thread + response = api_client.get(get_thread_access_url(uuid.uuid4())) + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_list_thread_access_unauthorized(self, api_client): + """Test listing thread accesses without authentication.""" + thread = factories.ThreadFactory() + response = api_client.get(get_thread_access_url(thread.id)) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +class TestThreadAccessCreate: + """Test the POST /threads/{thread_id}/accesses/ endpoint.""" + + @pytest.mark.parametrize( + "thread_access_role, mailbox_access_role", + [ + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.ADMIN), + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.EDITOR), + ], + ) + def test_create_thread_access_success( + self, api_client, thread_access_role, mailbox_access_role + ): + """Test creating a thread access successfully.""" + user = factories.UserFactory() + mailbox = factories.MailboxFactory() + factories.MailboxAccessFactory( + mailbox=mailbox, + user=user, + role=mailbox_access_role, + ) + + thread = factories.ThreadFactory() + factories.ThreadAccessFactory( + mailbox=mailbox, + thread=thread, + role=thread_access_role, + ) + api_client.force_authenticate(user=user) + + delegated_mailbox = factories.MailboxFactory() + data = { + "mailbox": str(delegated_mailbox.id), + "role": enums.ThreadAccessRoleChoices.VIEWER, + } + + response = api_client.post(get_thread_access_url(thread.id), data) + assert response.status_code == status.HTTP_201_CREATED + assert response.data["thread"] == thread.id + assert response.data["mailbox"] == delegated_mailbox.id + assert response.data["role"] == enums.ThreadAccessRoleChoices.VIEWER + + def test_create_thread_access_duplicate( + self, api_client, thread_with_editor_access + ): + """Test creating a duplicate thread access.""" + user, mailbox, thread, _ = thread_with_editor_access + api_client.force_authenticate(user=user) + + data = { + "mailbox": str(mailbox.id), + "role": enums.ThreadAccessRoleChoices.EDITOR, + } + + response = api_client.post(get_thread_access_url(thread.id), data) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.parametrize( + "thread_access_role, mailbox_access_role", + [ + (enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.ADMIN), + (enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.EDITOR), + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.VIEWER), + ], + ) + def test_create_thread_access_forbidden( + self, api_client, thread_access_role, mailbox_access_role + ): + """Test creating a thread access without permission.""" + user = factories.UserFactory() + mailbox = factories.MailboxFactory() + factories.MailboxAccessFactory( + mailbox=mailbox, + user=user, + role=mailbox_access_role, + ) + + thread = factories.ThreadFactory() + factories.ThreadAccessFactory( + mailbox=mailbox, + thread=thread, + role=thread_access_role, + ) + api_client.force_authenticate(user=user) + + delegated_mailbox = factories.MailboxFactory() + data = { + "mailbox": str(delegated_mailbox.id), + "role": enums.ThreadAccessRoleChoices.VIEWER, + } + + response = api_client.post(get_thread_access_url(thread.id), data) + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_create_thread_access_invalid_data( + self, api_client, thread_with_editor_access + ): + """Test creating a thread access with invalid data.""" + user, mailbox, thread, _ = thread_with_editor_access + api_client.force_authenticate(user=user) + + data = { + "mailbox": str(mailbox.id), + "role": "invalid_role", # Invalid role + } + + response = api_client.post(get_thread_access_url(thread.id), data) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_create_thread_access_unauthorized(self, api_client): + """Test creating a thread access without authentication.""" + thread = factories.ThreadFactory() + response = api_client.post(get_thread_access_url(thread.id), {}) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +class TestThreadAccessUpdate: + """Test the PUT/PATCH /threads/{thread_id}/accesses/{id}/ endpoint.""" + + @pytest.mark.parametrize( + "thread_access_role, mailbox_access_role", + [ + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.ADMIN), + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.EDITOR), + ], + ) + def test_update_thread_access_success( + self, api_client, thread_access_role, mailbox_access_role + ): + """Test updating a thread access successfully.""" + user = factories.UserFactory() + mailbox = factories.MailboxFactory() + factories.MailboxAccessFactory( + mailbox=mailbox, + user=user, + role=mailbox_access_role, + ) + + thread = factories.ThreadFactory() + factories.ThreadAccessFactory( + mailbox=mailbox, + thread=thread, + role=thread_access_role, + ) + api_client.force_authenticate(user=user) + + thread_access = factories.ThreadAccessFactory( + thread=thread, role=enums.ThreadAccessRoleChoices.VIEWER + ) + + url = get_thread_access_url(thread.id, thread_access.id) + data = {"role": enums.ThreadAccessRoleChoices.EDITOR} + + response = api_client.patch(url, data) + assert response.status_code == status.HTTP_200_OK + assert response.data["role"] == enums.ThreadAccessRoleChoices.EDITOR + + @pytest.mark.parametrize( + "thread_access_role, mailbox_access_role", + [ + (enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.ADMIN), + (enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.EDITOR), + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.VIEWER), + ], + ) + def test_update_thread_access_forbidden( + self, api_client, thread_access_role, mailbox_access_role + ): + """Test updating a thread access without permission.""" + user = factories.UserFactory() + api_client.force_authenticate(user=user) + + # Create a thread access that the user doesn't have right role to modify + mailbox = factories.MailboxFactory() + factories.MailboxAccessFactory( + mailbox=mailbox, + user=user, + role=mailbox_access_role, + ) + thread = factories.ThreadFactory() + thread_access = factories.ThreadAccessFactory( + mailbox=mailbox, + thread=thread, + role=thread_access_role, + ) + url = get_thread_access_url(thread.id, thread_access.id) + data = {"role": enums.ThreadAccessRoleChoices.EDITOR} + + response = api_client.patch(url, data) + assert response.status_code == status.HTTP_403_FORBIDDEN + + # Create a thread access that the user doesn't have any role to modify + thread = factories.ThreadFactory() + thread_access = factories.ThreadAccessFactory() + + url = get_thread_access_url(thread.id, thread_access.id) + data = {"role": enums.ThreadAccessRoleChoices.EDITOR} + + response = api_client.patch(url, data) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_update_thread_access_not_found(self, api_client, mailbox_with_access): + """Test updating a non-existent thread access.""" + user, _ = mailbox_with_access + api_client.force_authenticate(user=user) + thread = factories.ThreadFactory() + + url = get_thread_access_url(thread.id, uuid.uuid4()) + data = {"role": enums.ThreadAccessRoleChoices.EDITOR} + + response = api_client.patch(url, data) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_update_thread_access_unauthorized(self, api_client): + """Test updating a thread access without authentication.""" + thread = factories.ThreadFactory() + url = get_thread_access_url(thread.id, uuid.uuid4()) + response = api_client.patch(url, {}) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +class TestThreadAccessDelete: + """Test the DELETE /threads/{thread_id}/accesses/{id}/ endpoint.""" + + @pytest.mark.parametrize( + "thread_access_role, mailbox_access_role", + [ + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.ADMIN), + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.EDITOR), + ], + ) + def test_delete_thread_access_success( + self, api_client, thread_access_role, mailbox_access_role + ): + """Test deleting a thread access successfully.""" + user = factories.UserFactory() + mailbox = factories.MailboxFactory() + factories.MailboxAccessFactory( + mailbox=mailbox, + user=user, + role=mailbox_access_role, + ) + thread = factories.ThreadFactory() + thread_access = factories.ThreadAccessFactory( + mailbox=mailbox, + thread=thread, + role=thread_access_role, + ) + api_client.force_authenticate(user=user) + + url = get_thread_access_url(thread.id, thread_access.id) + response = api_client.delete(url) + assert response.status_code == status.HTTP_204_NO_CONTENT + + # Verify the thread access was deleted + assert not models.ThreadAccess.objects.filter(id=thread_access.id).exists() + + @pytest.mark.parametrize( + "thread_access_role, mailbox_access_role", + [ + (enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.ADMIN), + (enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.EDITOR), + (enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.VIEWER), + ], + ) + def test_delete_thread_access_forbidden( + self, api_client, thread_access_role, mailbox_access_role + ): + """Test deleting a thread access without permission.""" + user = factories.UserFactory() + api_client.force_authenticate(user=user) + + # Create a thread access that the user doesn't have any role to delete + mailbox = factories.MailboxFactory() + factories.MailboxAccessFactory( + mailbox=mailbox, + user=user, + role=mailbox_access_role, + ) + thread = factories.ThreadFactory() + thread_access = factories.ThreadAccessFactory( + mailbox=mailbox, + thread=thread, + role=thread_access_role, + ) + + url = get_thread_access_url(thread.id, thread_access.id) + response = api_client.delete(url) + assert response.status_code == status.HTTP_403_FORBIDDEN + + # Create a thread access that the user doesn't have any role to delete + thread_access = factories.ThreadAccessFactory() + url = get_thread_access_url(thread.id, thread_access.id) + response = api_client.delete(url) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_delete_thread_access_not_found(self, api_client, mailbox_with_access): + """Test deleting a non-existent thread access.""" + user, _ = mailbox_with_access + api_client.force_authenticate(user=user) + thread = factories.ThreadFactory() + + url = get_thread_access_url(thread.id, uuid.uuid4()) + response = api_client.delete(url) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_delete_thread_access_unauthorized(self, api_client): + """Test deleting a thread access without authentication.""" + thread = factories.ThreadFactory() + url = get_thread_access_url(thread.id, uuid.uuid4()) + response = api_client.delete(url) + assert response.status_code == status.HTTP_401_UNAUTHORIZED diff --git a/src/backend/core/urls.py b/src/backend/core/urls.py index 2b3be3a7..8c9eb0e6 100644 --- a/src/backend/core/urls.py +++ b/src/backend/core/urls.py @@ -1,7 +1,7 @@ """URL configuration for the core app.""" from django.conf import settings -from django.urls import include, path +from django.urls import include, path, re_path from rest_framework.routers import DefaultRouter @@ -15,6 +15,7 @@ from core.api.viewsets.mta import MTAViewSet from core.api.viewsets.send import SendMessageView from core.api.viewsets.task import TaskDetailView from core.api.viewsets.thread import ThreadViewSet +from core.api.viewsets.thread_access import ThreadAccessViewSet from core.api.viewsets.user import UserViewSet from core.authentication.urls import urlpatterns as oidc_urls @@ -23,14 +24,33 @@ router = DefaultRouter() router.register("mta", MTAViewSet, basename="mta") router.register("users", UserViewSet, basename="users") router.register("mailboxes", MailboxViewSet, basename="mailboxes") -router.register("threads", ThreadViewSet, basename="threads") router.register("messages", MessageViewSet, basename="messages") router.register("blob", BlobViewSet, basename="blob") +router.register("thread-access", ThreadAccessViewSet, basename="thread-access") + +# Add nested router for thread accesses +thread_router = DefaultRouter() +thread_router.register("threads", ThreadViewSet, basename="threads") + +thread_related_router = DefaultRouter() +thread_related_router.register( + "accesses", ThreadAccessViewSet, basename="thread-access" +) urlpatterns = [ path( f"api/{settings.API_VERSION}/", - include([*router.urls, *oidc_urls]), + include( + [ + *router.urls, + *thread_router.urls, + re_path( + r"^threads/(?P[\w-]+)/", + include(thread_related_router.urls), + ), + *oidc_urls, + ] + ), ), path(f"api/{settings.API_VERSION}/config/", ConfigView.as_view()), path(