✨(plugins) add route to create first mailbox of a domain
Add route to create first mailbox of a domain and activate the organization linked.
This commit is contained in:
@@ -8,6 +8,10 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(plugins) add route to create first mailbox of a domain
|
||||
|
||||
### Changed
|
||||
|
||||
- ✨(mailbox) remove secondary email as required field
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""API serializers for the la suite plugin."""
|
||||
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class EmailLocalPartCharField(serializers.RegexField):
|
||||
"""
|
||||
A field that validates an email local part.
|
||||
"""
|
||||
|
||||
default_error_messages = {
|
||||
"invalid": _("Enter a valid email local part ([a-zA-Z0-9.-])."),
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Initialize the EmailLocalPartCharField.
|
||||
"""
|
||||
super().__init__(r"^[a-zA-Z0-9.-]+$", **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
Override the to_internal_value method to convert the data to lowercase.
|
||||
"""
|
||||
data = super().to_internal_value(data)
|
||||
return data.lower()
|
||||
|
||||
|
||||
class OrganizationActivationSerializer(serializers.Serializer): # pylint: disable=abstract-method
|
||||
"""
|
||||
Serializer for organization activation.
|
||||
|
||||
We need enough information to create the organization's first user.
|
||||
"""
|
||||
|
||||
first_name = serializers.CharField()
|
||||
last_name = serializers.CharField()
|
||||
email_local_part = EmailLocalPartCharField()
|
||||
password = serializers.CharField(write_only=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
Override the validate method to validate password.
|
||||
"""
|
||||
validate_password(attrs["password"])
|
||||
return super().validate(attrs)
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Throttles for the la suite plugin."""
|
||||
|
||||
from rest_framework import throttling
|
||||
|
||||
|
||||
class OrganizationTokenAnonRateThrottle(throttling.AnonRateThrottle):
|
||||
"""
|
||||
Throttle for organization token requests.
|
||||
"""
|
||||
|
||||
scope = "organization-token-anon"
|
||||
|
||||
def get_rate(self):
|
||||
"""
|
||||
Get the rate for the throttle.
|
||||
"""
|
||||
return "5/min"
|
||||
@@ -1,16 +1,28 @@
|
||||
"""API viewsets for La Suite plugin"""
|
||||
"""API viewsets for La Suite plugin."""
|
||||
|
||||
from functools import reduce
|
||||
from operator import iconcat
|
||||
|
||||
from rest_framework import viewsets
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.permissions import BasePermission
|
||||
from rest_framework.permissions import BasePermission, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from core.authentication.backends import AccountServiceAuthentication
|
||||
from core.models import Organization
|
||||
|
||||
from mailbox_manager.models import Mailbox
|
||||
from mailbox_manager.utils.dimail import DimailAPIClient
|
||||
from plugins.la_suite.authentication import OrganizationOneTimeTokenAuthentication
|
||||
|
||||
from .serializers import OrganizationActivationSerializer
|
||||
from .throttle import OrganizationTokenAnonRateThrottle
|
||||
|
||||
|
||||
class ScopeAPIPermission(BasePermission):
|
||||
"""Permission to check if the user has the correct scope."""
|
||||
@@ -54,3 +66,67 @@ class ActiveOrganizationsSiret(viewsets.GenericViewSet, ListModelMixin):
|
||||
registration_ids = reduce(iconcat, registration_ids_lists, [])
|
||||
|
||||
return Response(registration_ids)
|
||||
|
||||
|
||||
class OrganizationActivation(viewsets.ViewSet):
|
||||
"""ViewSet to activate an organization.
|
||||
|
||||
Activate organization and create a mailbox for the first user.
|
||||
Endpoint: POST /la-suite/v1.0/activate-organization/
|
||||
"""
|
||||
|
||||
authentication_classes = [OrganizationOneTimeTokenAuthentication]
|
||||
throttle_classes = [OrganizationTokenAnonRateThrottle]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(detail=False, methods=["post"], url_path="activate-organization")
|
||||
def activate_organization(self, request):
|
||||
"""Activate the organization by creating the first user.
|
||||
|
||||
Args:
|
||||
request: The HTTP request object
|
||||
|
||||
Returns:
|
||||
Response: Success message with 201 status code
|
||||
|
||||
Raises:
|
||||
ValidationError: If request data is invalid
|
||||
MailDomain.DoesNotExist: If organization has no mail domain
|
||||
"""
|
||||
organization = request.user
|
||||
|
||||
serializer = OrganizationActivationSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
domain = organization.mail_domains.get() # should fail on purpose
|
||||
|
||||
mailbox_data = {
|
||||
"first_name": serializer.data["first_name"],
|
||||
"last_name": serializer.data["last_name"],
|
||||
"local_part": serializer.data["email_local_part"],
|
||||
"domain": domain,
|
||||
"password": make_password(request.data["password"]),
|
||||
}
|
||||
|
||||
with transaction.atomic():
|
||||
# Create the first mailbox which will allow the user to log in
|
||||
mailbox = Mailbox.objects.create(**mailbox_data)
|
||||
|
||||
# send new mailbox request to dimail
|
||||
client = DimailAPIClient()
|
||||
client.create_mailbox(mailbox)
|
||||
|
||||
# Enable the organization
|
||||
organization.is_active = True
|
||||
organization.save(update_fields=["is_active", "updated_at"])
|
||||
|
||||
# Disable the one-time token
|
||||
token = self.request.auth
|
||||
token.enabled = False
|
||||
token.used_at = timezone.now()
|
||||
token.save(update_fields=["enabled", "used_at"])
|
||||
|
||||
return Response(
|
||||
status=status.HTTP_201_CREATED,
|
||||
data={"message": "Organization activated. First user created."},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Test the organization activation API."""
|
||||
|
||||
import re
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from mailbox_manager import factories as mailbox_factories
|
||||
from mailbox_manager import models as mailbox_models
|
||||
from mailbox_manager.tests.fixtures.dimail import (
|
||||
TOKEN_OK,
|
||||
response_mailbox_created,
|
||||
)
|
||||
from plugins.la_suite import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
API_URL = reverse("la-suite:organizations-activate-organization")
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def test_organization_activation_unauthorized(plugin_urls):
|
||||
"""Test the organization activation API unauthorized"""
|
||||
client = APIClient()
|
||||
response = client.post(API_URL)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@responses.activate
|
||||
def test_organization_activation_authorized(plugin_urls):
|
||||
"""Test the organization activation API authorized"""
|
||||
# create one-time token for mailbox provisioning
|
||||
token = factories.OrganizationOneTimeTokenFactory()
|
||||
organization = token.organization
|
||||
|
||||
# create domain linked to organization
|
||||
domain = mailbox_factories.MailDomainEnabledFactory(
|
||||
organization=organization, name="example.com"
|
||||
)
|
||||
|
||||
# mailbox data to be used in API call to create the firstmailbox
|
||||
mailbox_data = {
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"email_local_part": "john.doe",
|
||||
"password": "password1234",
|
||||
}
|
||||
|
||||
# mock dimail API call for mailbox creation
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
|
||||
body=response_mailbox_created(
|
||||
f"{mailbox_data['email_local_part']}@{domain.name}"
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# call API
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"OrganizationToken {token.key}")
|
||||
response = client.post(API_URL, data=mailbox_data)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# check organization is activated
|
||||
organization.refresh_from_db()
|
||||
assert organization.is_active
|
||||
|
||||
# check user is created
|
||||
mailbox = mailbox_models.Mailbox.objects.get()
|
||||
assert mailbox.first_name == "John"
|
||||
assert mailbox.last_name == "Doe"
|
||||
assert mailbox.local_part == "john.doe"
|
||||
assert mailbox.domain == domain
|
||||
assert mailbox.password
|
||||
|
||||
# check one-time token is disabled
|
||||
token.refresh_from_db()
|
||||
assert token.enabled is False
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .api.viewsets import ActiveOrganizationsSiret
|
||||
from .api.viewsets import ActiveOrganizationsSiret, OrganizationActivation
|
||||
|
||||
app_name = "la-suite"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(
|
||||
@@ -10,5 +12,10 @@ router.register(
|
||||
ActiveOrganizationsSiret,
|
||||
basename="active-organization-sirets",
|
||||
)
|
||||
router.register(
|
||||
"la-suite/v1.0",
|
||||
OrganizationActivation,
|
||||
basename="organizations",
|
||||
)
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
Reference in New Issue
Block a user