🐛(domains) fix attemps to send invitations to existing users

Users can only search other users inside their own organization.
This leads to a bug where we try to invite an email already linked to an user
This commit is contained in:
Marie PUPO JEAMMET
2025-07-03 15:44:40 +02:00
committed by Marie
parent 0df8f53037
commit 63b8984eb2
6 changed files with 76 additions and 18 deletions

View File

@@ -14,7 +14,11 @@ and this project adheres to
- ✨(front) add show invitations mails domains access #1040 - ✨(front) add show invitations mails domains access #1040
- ✨(invitations) can delete domain invitations - ✨(invitations) can delete domain invitations
## Changed ### Fixed
- 🐛(domains) fix attemps to send invitations to existing users #953
### Changed
- 🏗️(core) migrate from pip to uv - 🏗️(core) migrate from pip to uv
- ✨(front) add show invitations mails domains access #1040 - ✨(front) add show invitations mails domains access #1040

View File

@@ -36,6 +36,8 @@ from core.plugins.registry import registry as plugin_hooks_registry
from core.utils.webhooks import webhooks_synchronizer from core.utils.webhooks import webhooks_synchronizer
from core.validators import get_field_validators_from_setting from core.validators import get_field_validators_from_setting
from mailbox_manager.exceptions import EmailAlreadyKnownException
logger = getLogger(__name__) logger = getLogger(__name__)
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -997,9 +999,7 @@ class BaseInvitation(BaseModel):
# Check if a user already exists for the provided email # Check if a user already exists for the provided email
if User.objects.filter(email=self.email).exists(): if User.objects.filter(email=self.email).exists():
raise exceptions.ValidationError( raise EmailAlreadyKnownException
{"email": _("This email is already associated to a registered user.")}
)
@property @property
def is_expired(self): def is_expired(self):

View File

@@ -171,10 +171,11 @@ def test_api_team_invitations__create__cannot_invite_existing_users():
format="json", format="json",
) )
assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.status_code == status.HTTP_201_CREATED
assert response.json()["email"] == [ assert (
"This email is already associated to a registered user." response.json()["detail"]
] == "Email already known. Invitation not sent but access created instead."
)
def test_api_team_invitations__list__anonymous_user(): def test_api_team_invitations__list__anonymous_user():

View File

@@ -13,6 +13,7 @@ from core.api.client.serializers import UserSerializer
from mailbox_manager import enums, models from mailbox_manager import enums, models
from mailbox_manager.api import permissions from mailbox_manager.api import permissions
from mailbox_manager.api.client import serializers from mailbox_manager.api.client import serializers
from mailbox_manager.exceptions import EmailAlreadyKnownException
from mailbox_manager.utils.dimail import DimailAPIClient from mailbox_manager.utils.dimail import DimailAPIClient
@@ -363,6 +364,7 @@ class MailDomainInvitationViewset(
- issuer : User, automatically added from user making query, if allowed - issuer : User, automatically added from user making query, if allowed
- domain : Domain, automatically added from requested URI - domain : Domain, automatically added from requested URI
Return a newly created invitation Return a newly created invitation
or an access if email is already linked to an existing user
PUT / PATCH : Not permitted. Instead of updating your invitation, PUT / PATCH : Not permitted. Instead of updating your invitation,
delete and create a new one. delete and create a new one.
@@ -410,6 +412,34 @@ class MailDomainInvitationViewset(
return queryset return queryset
def perform_create(self, serializer):
"""Lookup for existing user before inviting."""
if email := serializer.validated_data["email"]:
existing_user = models.User.objects.filter(email=email)
if existing_user.exists():
return models.MailDomainAccess.objects.create(
user=existing_user[0],
domain=serializer.validated_data["domain"],
role=serializer.validated_data["role"],
)
return super().perform_create(serializer)
def create(self, request, *args, **kwargs):
"""Attempt to create invitation. If user is already registered,
they don't need an invitation but an access, which we create here."""
try:
return super().create(request, *args, **kwargs)
except EmailAlreadyKnownException as exc:
user = models.User.objects.get(email=email)
models.MailDomainAccess.objects.create(
user=user,
domain=models.MailDomain.objects.get(slug=kwargs["domain_slug"]),
role=request.data["role"],
)
raise exc
class AliasViewSet( class AliasViewSet(
mixins.CreateModelMixin, mixins.CreateModelMixin,

View File

@@ -0,0 +1,18 @@
"""
Custom exceptions for mailbox manager app
"""
from django.utils.translation import gettext_lazy as _
from rest_framework import status
from rest_framework.exceptions import APIException
class EmailAlreadyKnownException(APIException):
"""Exception raised when trying to create a user with an already existing email address."""
status_code = status.HTTP_201_CREATED
default_detail = _(
"Email already known. Invitation not sent but access created instead."
)
default_code = "email already known"

View File

@@ -11,7 +11,7 @@ from rest_framework.test import APIClient
from core import factories as core_factories from core import factories as core_factories
from mailbox_manager import enums, factories from mailbox_manager import enums, factories, models
from mailbox_manager.api.client import serializers from mailbox_manager.api.client import serializers
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@@ -141,9 +141,10 @@ def test_api_domain_invitations__should_not_create_duplicate_invitations():
assert response.json()["__all__"] == [ assert response.json()["__all__"] == [
"Mail domain invitation with this Email address and Domain already exists." "Mail domain invitation with this Email address and Domain already exists."
] ]
assert models.MailDomainInvitation.objects.count() == 1 # and specifically, not 2
def test_api_domain_invitations__should_not_invite_when_user_already_exists(): def test_api_domain_invitations__should_create_access_when_user_already_exists():
"""Already existing users should not be invited but given access directly.""" """Already existing users should not be invited but given access directly."""
existing_user = core_factories.UserFactory() existing_user = core_factories.UserFactory()
@@ -152,15 +153,19 @@ def test_api_domain_invitations__should_not_invite_when_user_already_exists():
client = APIClient() client = APIClient()
client.force_login(access.user) client.force_login(access.user)
invitation_values = serializers.MailDomainInvitationSerializer(
factories.MailDomainInvitationFactory.build(email=existing_user.email)
).data
response = client.post( response = client.post(
f"/api/v1.0/mail-domains/{access.domain.slug}/invitations/", f"/api/v1.0/mail-domains/{access.domain.slug}/invitations/",
invitation_values, {
"email": existing_user.email,
"role": "owner",
},
format="json", format="json",
) )
assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.status_code == status.HTTP_201_CREATED
assert response.json()["email"] == [ assert (
"This email is already associated to a registered user." response.json()["detail"]
] == "Email already known. Invitation not sent but access created instead."
)
assert not models.MailDomainInvitation.objects.exists()
assert models.MailDomainAccess.objects.filter(user=existing_user).exists()