diff --git a/frontend/src/api/domains.js b/frontend/src/api/domains.js index 13948e681..1e5054430 100644 --- a/frontend/src/api/domains.js +++ b/frontend/src/api/domains.js @@ -26,6 +26,9 @@ export default { updateDomain (domainId, data) { return repository.put(`/${domainResource}/${domainId}/`, data) }, + deleteDomain (domainId, data) { + return repository.post(`/${domainResource}/${domainId}/delete/`, data) + }, createDomainAlias (data) { return repository.post(`/${domainAliasResource}/`, data) }, diff --git a/frontend/src/components/domains/DomainAliasForm.vue b/frontend/src/components/domains/DomainAliasForm.vue index fde701cc7..f8615f9cd 100644 --- a/frontend/src/components/domains/DomainAliasForm.vue +++ b/frontend/src/components/domains/DomainAliasForm.vue @@ -48,7 +48,7 @@ Close null + } }, computed: { ...mapGetters({ @@ -96,7 +99,9 @@ export default { methods: { close () { this.$emit('close') - this.form = {} + this.form = { + enabled: true + } }, deleteAlias () { domains.deleteDomainAlias(this.domainAlias.pk).then(resp => { diff --git a/frontend/src/components/domains/DomainCreationForm.vue b/frontend/src/components/domains/DomainCreationForm.vue index 03a7c900a..d9f45769e 100644 --- a/frontend/src/components/domains/DomainCreationForm.vue +++ b/frontend/src/components/domains/DomainCreationForm.vue @@ -31,35 +31,7 @@ New domain / General - - - - - - - - - +
New domain / DNS
- - - - +
Back @@ -113,49 +66,7 @@ New domain / Limitations
- - - - - - - - - - - +
Back @@ -174,45 +85,7 @@ New domain / Options
- - - - - - - - - - +
Back @@ -324,14 +197,20 @@ diff --git a/frontend/src/components/domains/DomainForm.vue b/frontend/src/components/domains/DomainForm.vue index 3327a6c3c..8e2c7653a 100644 --- a/frontend/src/components/domains/DomainForm.vue +++ b/frontend/src/components/domains/DomainForm.vue @@ -3,68 +3,122 @@ -

Add domain

-

Edit domain

+

Edit domain

- - - - - - - - - - - - - - - - - - - + + + + + + General + + + + + + + Name: {{ editedDomain.name }} + + + Type: {{ editedDomain.type }} + + + + + + + + + + + + + + + DNS + + + + + + + DNS checks + mdi-check-circle-outline + mdi-close-circle-outline + + + DKIM signing + mdi-check-circle-outline + mdi-close-circle-outline + + + + + + + + + + + + + + + Limitations + + + + + + + Quota: {{ domain.quota }} + + + Sending limit: {{ domain.message_sending_limit }} + + + No sending limit + + + + + + + + + + + - - Cancel + + Cancel - - Save + + Save
@@ -73,63 +127,51 @@ diff --git a/frontend/src/components/domains/DomainLimitationsForm.vue b/frontend/src/components/domains/DomainLimitationsForm.vue new file mode 100644 index 000000000..42d7c7848 --- /dev/null +++ b/frontend/src/components/domains/DomainLimitationsForm.vue @@ -0,0 +1,51 @@ + + + diff --git a/frontend/src/components/domains/DomainList.vue b/frontend/src/components/domains/DomainList.vue index 7eb117ace..e909c248c 100644 --- a/frontend/src/components/domains/DomainList.vue +++ b/frontend/src/components/domains/DomainList.vue @@ -75,7 +75,7 @@
- + mdi-circle-edit-outline @@ -117,6 +117,12 @@ max-width="800px"> + + + @@ -131,16 +137,19 @@ diff --git a/frontend/src/store/modules/domains.js b/frontend/src/store/modules/domains.js index 94322ab93..5ffd3781a 100644 --- a/frontend/src/store/modules/domains.js +++ b/frontend/src/store/modules/domains.js @@ -5,6 +5,7 @@ import domainApi from '@/api/domains' const SET_DOMAINS = 'SET_DOMAINS' const ADD_DOMAIN = 'ADD_DOMAIN' const UPDATE_DOMAIN = 'UPDATE_DOMAIN' +const DELETE_DOMAIN = 'DELETE_DOMAIN' const SET_DOMAIN_ALIASES = 'SET_DOMAIN_ALIASES' const ADD_DOMAIN_ALIAS = 'ADD_DOMAIN_ALIAS' const UPDATE_DOMAIN_ALIAS = 'UPDATE_DOMAIN_ALIAS' @@ -48,6 +49,11 @@ const actions = { commit(UPDATE_DOMAIN, { domain: response.data }) }) }, + deleteDomain ({ commit }, { id, data }) { + return domainApi.deleteDomain(id, data).then(resp => { + commit(DELETE_DOMAIN, id) + }) + }, getDomainAliases ({ commit }) { return Vue.prototype.$axios.get('/domainaliases/?expand=target').then(response => { commit(SET_DOMAIN_ALIASES, response.data) @@ -86,6 +92,9 @@ const mutations = { } }) }, + [DELETE_DOMAIN] (state, domainId) { + state.domains = state.domains.filter(item => item.pk !== domainId) + }, [SET_DOMAIN_ALIASES] (state, domainAliases) { state.domainAliases = domainAliases }, diff --git a/modoboa/admin/api/__init__.py b/modoboa/admin/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/api/v1/__init__.py b/modoboa/admin/api/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/serializers.py b/modoboa/admin/api/v1/serializers.py similarity index 81% rename from modoboa/admin/serializers.py rename to modoboa/admin/api/v1/serializers.py index a0d3951d0..a9fdc57bc 100644 --- a/modoboa/admin/serializers.py +++ b/modoboa/admin/api/v1/serializers.py @@ -24,7 +24,7 @@ from modoboa.lib import ( ) from modoboa.parameters import tools as param_tools -from . import constants, lib, models +from ... import lib, models class DomainSerializer(serializers.ModelSerializer): @@ -110,85 +110,6 @@ class DomainSerializer(serializers.ModelSerializer): return domain -class CreateDomainAdminSerializer(serializers.Serializer): - """Sub serializer for domain administrator creation.""" - - username = serializers.CharField() - with_random_password = serializers.BooleanField(default=False) - with_mailbox = serializers.BooleanField(default=False) - with_aliases = serializers.BooleanField(default=False) - - -class DomainSerializerV2(DomainSerializer): - """Domain serializer for v2 API.""" - - domain_admin = CreateDomainAdminSerializer(required=False) - - class Meta(DomainSerializer.Meta): - fields = DomainSerializer.Meta.fields + ( - "domain_admin", - ) - - def create(self, validated_data): - """Create administrator and other stuff if needed.""" - domain_admin = validated_data.pop("domain_admin", None) - domain = super().create(validated_data) - if not domain_admin: - return domain - - # 1. Create a domain administrator - username = "%s@%s" % (domain_admin["username"], domain.name) - try: - da = core_models.User.objects.get(username=username) - except core_models.User.DoesNotExist: - pass - else: - raise lib_exceptions.Conflict( - _("User '%s' already exists") % username) - user = self.context["request"].user - core_signals.can_create_object.send( - self.__class__, context=user, klass=models.Mailbox) - da = core_models.User(username=username, email=username, is_active=True) - if domain_admin["with_random_password"]: - password = lib.make_password() - else: - password = param_tools.get_global_parameter( - "default_password", app="core") - da.set_password(password) - da.save() - da.role = "DomainAdmins" - da.post_create(user) - - # 2. Create mailbox if needed - if domain_admin["with_mailbox"]: - dom_admin_username = domain_admin["username"] - mb = models.Mailbox( - address=dom_admin_username, domain=domain, - user=da, use_domain_quota=True - ) - mb.set_quota( - override_rules=user.has_perm("admin.change_domain")) - mb.save(creator=user) - - # 3. Create aliases if needed - condition = ( - domain.type == "domain" and - domain_admin["with_aliases"] and - dom_admin_username != "postmaster" - ) - if condition: - core_signals.can_create_object.send( - self.__class__, context=user, klass=models.Alias) - address = u"postmaster@{}".format(domain.name) - alias = models.Alias.objects.create( - address=address, domain=domain, enabled=True) - alias.set_recipients([mb.full_address]) - alias.post_create(user) - - domain.add_admin(da) - return domain - - class DomainAliasSerializer(FlexFieldsModelSerializer): """Base DomainAlias serializer.""" @@ -571,28 +492,6 @@ class SenderAddressSerializer(serializers.ModelSerializer): return value -class AdminGlobalParametersSerializer(serializers.Serializer): - """A serializer for global parameters.""" - - # Domain settings - enable_mx_checks = serializers.BooleanField(default=True) - valid_mxs = serializers.CharField(allow_blank=True) - domains_must_have_authorized_mx = serializers.BooleanField(default=False) - enable_dnsbl_checks = serializers.BooleanField(default=True) - dkim_keys_storage_dir = serializers.CharField(allow_blank=True) - dkim_default_key_length = serializers.ChoiceField( - default=2048, choices=constants.DKIM_KEY_LENGTHS) - - # Mailboxes settings - handle_mailboxes = serializers.BooleanField(default=False) - mailboxes_owner = serializers.CharField(default="vmail") - default_domain_quota = serializers.IntegerField(default=0) - default_mailbox_quota = serializers.IntegerField(default=0) - auto_account_removal = serializers.BooleanField(default=False) - auto_create_domain_and_mailbox = serializers.BooleanField(default=True) - create_alias_on_mbox_rename = serializers.BooleanField(default=False) - - class ResetPasswordSerializer(serializers.Serializer): """Serializer by the reset password endpoint.""" diff --git a/modoboa/admin/api/v1/urls.py b/modoboa/admin/api/v1/urls.py new file mode 100644 index 000000000..e2e514aaa --- /dev/null +++ b/modoboa/admin/api/v1/urls.py @@ -0,0 +1,16 @@ +"""Admin API urls.""" + +from rest_framework import routers + +from . import viewsets + +router = routers.SimpleRouter() +router.register(r"domains", viewsets.DomainViewSet, basename="domain") +router.register( + r"domainaliases", viewsets.DomainAliasViewSet, basename="domain_alias") +router.register(r"accounts", viewsets.AccountViewSet, basename="account") +router.register(r"aliases", viewsets.AliasViewSet, basename="alias") +router.register( + r"senderaddresses", viewsets.SenderAddressViewSet, basename="sender_address") + +urlpatterns = router.urls diff --git a/modoboa/admin/api.py b/modoboa/admin/api/v1/viewsets.py similarity index 96% rename from modoboa/admin/api.py rename to modoboa/admin/api/v1/viewsets.py index 299c3c3f3..164b05f31 100644 --- a/modoboa/admin/api.py +++ b/modoboa/admin/api/v1/viewsets.py @@ -17,7 +17,8 @@ from modoboa.core import sms_backends from modoboa.lib import renderers as lib_renderers from modoboa.lib import viewsets as lib_viewsets -from . import lib, models, serializers +from ... import lib, models +from . import serializers @extend_schema_view( @@ -38,16 +39,12 @@ class DomainViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet): """Domain viewset.""" permission_classes = [IsAuthenticated, DjangoModelPermissions, ] + serializer_class = serializers.DomainSerializer def get_queryset(self): """Filter queryset based on current user.""" return models.Domain.objects.get_for_admin(self.request.user) - def get_serializer_class(self, *args, **kwargs): - if self.request.version == "v2" and self.action == "create": - return serializers.DomainSerializerV2 - return serializers.DomainSerializer - def perform_destroy(self, instance): """Add custom args to delete call.""" instance.delete(self.request.user) diff --git a/modoboa/admin/api/v2/__init__.py b/modoboa/admin/api/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/api/v2/serializers.py b/modoboa/admin/api/v2/serializers.py new file mode 100644 index 000000000..5f055fca4 --- /dev/null +++ b/modoboa/admin/api/v2/serializers.py @@ -0,0 +1,118 @@ +"""Admin API v2 serializers.""" + +from django.utils.translation import ugettext as _ + +from rest_framework import serializers + +from modoboa.admin.api.v1 import serializers as v1_serializers +from modoboa.core import models as core_models, signals as core_signals +from modoboa.lib import exceptions as lib_exceptions +from modoboa.parameters import tools as param_tools + +from ... import constants, lib, models + + +class CreateDomainAdminSerializer(serializers.Serializer): + """Sub serializer for domain administrator creation.""" + + username = serializers.CharField() + with_random_password = serializers.BooleanField(default=False) + with_mailbox = serializers.BooleanField(default=False) + with_aliases = serializers.BooleanField(default=False) + + +class DomainSerializer(v1_serializers.DomainSerializer): + """Domain serializer for v2 API.""" + + domain_admin = CreateDomainAdminSerializer(required=False) + + class Meta(v1_serializers.DomainSerializer.Meta): + fields = v1_serializers.DomainSerializer.Meta.fields + ( + "domain_admin", + ) + + def create(self, validated_data): + """Create administrator and other stuff if needed.""" + domain_admin = validated_data.pop("domain_admin", None) + domain = super().create(validated_data) + if not domain_admin: + return domain + + # 1. Create a domain administrator + username = "%s@%s" % (domain_admin["username"], domain.name) + try: + da = core_models.User.objects.get(username=username) + except core_models.User.DoesNotExist: + pass + else: + raise lib_exceptions.Conflict( + _("User '%s' already exists") % username) + user = self.context["request"].user + core_signals.can_create_object.send( + self.__class__, context=user, klass=models.Mailbox) + da = core_models.User(username=username, email=username, is_active=True) + if domain_admin["with_random_password"]: + password = lib.make_password() + else: + password = param_tools.get_global_parameter( + "default_password", app="core") + da.set_password(password) + da.save() + da.role = "DomainAdmins" + da.post_create(user) + + # 2. Create mailbox if needed + if domain_admin["with_mailbox"]: + dom_admin_username = domain_admin["username"] + mb = models.Mailbox( + address=dom_admin_username, domain=domain, + user=da, use_domain_quota=True + ) + mb.set_quota( + override_rules=user.has_perm("admin.change_domain")) + mb.save(creator=user) + + # 3. Create aliases if needed + condition = ( + domain.type == "domain" and + domain_admin["with_aliases"] and + dom_admin_username != "postmaster" + ) + if condition: + core_signals.can_create_object.send( + self.__class__, context=user, klass=models.Alias) + address = u"postmaster@{}".format(domain.name) + alias = models.Alias.objects.create( + address=address, domain=domain, enabled=True) + alias.set_recipients([mb.full_address]) + alias.post_create(user) + + domain.add_admin(da) + return domain + + +class DeleteDomainSerializer(serializers.Serializer): + """Serializer used with delete operation.""" + + keep_folder = serializers.BooleanField(default=False) + + +class AdminGlobalParametersSerializer(serializers.Serializer): + """A serializer for global parameters.""" + + # Domain settings + enable_mx_checks = serializers.BooleanField(default=True) + valid_mxs = serializers.CharField(allow_blank=True) + domains_must_have_authorized_mx = serializers.BooleanField(default=False) + enable_dnsbl_checks = serializers.BooleanField(default=True) + dkim_keys_storage_dir = serializers.CharField(allow_blank=True) + dkim_default_key_length = serializers.ChoiceField( + default=2048, choices=constants.DKIM_KEY_LENGTHS) + + # Mailboxes settings + handle_mailboxes = serializers.BooleanField(default=False) + default_domain_quota = serializers.IntegerField(default=0) + default_mailbox_quota = serializers.IntegerField(default=0) + auto_account_removal = serializers.BooleanField(default=False) + auto_create_domain_and_mailbox = serializers.BooleanField(default=True) + create_alias_on_mbox_rename = serializers.BooleanField(default=False) diff --git a/modoboa/admin/api/v2/urls.py b/modoboa/admin/api/v2/urls.py new file mode 100644 index 000000000..3c15e3e18 --- /dev/null +++ b/modoboa/admin/api/v2/urls.py @@ -0,0 +1,19 @@ +"""Admin API v2 urls.""" + +from rest_framework import routers + +from modoboa.admin.api.v1 import viewsets as v1_viewsets + +from . import viewsets + +router = routers.SimpleRouter() +router.register(r"domains", viewsets.DomainViewSet, basename="domain") +router.register( + r"domainaliases", v1_viewsets.DomainAliasViewSet, basename="domain_alias") +router.register(r"accounts", v1_viewsets.AccountViewSet, basename="account") +router.register(r"aliases", v1_viewsets.AliasViewSet, basename="alias") +router.register( + r"senderaddresses", v1_viewsets.SenderAddressViewSet, + basename="sender_address") + +urlpatterns = router.urls diff --git a/modoboa/admin/api/v2/viewsets.py b/modoboa/admin/api/v2/viewsets.py new file mode 100644 index 000000000..6afdece7a --- /dev/null +++ b/modoboa/admin/api/v2/viewsets.py @@ -0,0 +1,70 @@ +"""Admin API v2 viewsets.""" + +from django.utils.translation import ugettext as _ + +from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import mixins, permissions, response, status, viewsets +from rest_framework.decorators import action +from rest_framework.exceptions import PermissionDenied + +from modoboa.admin.api.v1 import serializers as v1_serializers +from modoboa.lib import viewsets as lib_viewsets + +from ... import models +from . import serializers + + +@extend_schema_view( + retrieve=extend_schema( + description="Retrieve a particular domain", + summary="Retrieve a particular domain" + ), + list=extend_schema( + description="Retrieve a list of domains", + summary="Retrieve a list of domains" + ), + create=extend_schema( + description="Create a new domain", + summary="Create a new domain" + ), + delete=extend_schema( + description="Delete a particular domain", + summary="Delete a particular domain" + ) +) +class DomainViewSet(lib_viewsets.RevisionModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet): + """V2 viewset.""" + + permission_classes = ( + permissions.IsAuthenticated, permissions.DjangoModelPermissions, + ) + + def get_queryset(self): + """Filter queryset based on current user.""" + return models.Domain.objects.get_for_admin(self.request.user) + + def get_serializer_class(self, *args, **kwargs): + if self.action == "create": + return serializers.DomainSerializer + if self.action == "delete": + return serializers.DeleteDomainSerializer + return v1_serializers.DomainSerializer + + @action(methods=["post"], detail=True) + def delete(self, request, **kwargs): + """Custom delete method that accepts body arguments.""" + domain = self.get_object() + if not request.user.can_access(domain): + raise PermissionDenied(_("You don't have access to this domain")) + mb = getattr(request.user, "mailbox", None) + if mb and mb.domain == domain: + raise PermissionDenied(_("You can't delete your own domain")) + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + domain.delete(request.user, serializer.validated_data["keep_folder"]) + return response.Response(status=status.HTTP_204_NO_CONTENT) diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index c6809ac91..016c5064d 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -385,7 +385,7 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([ def load_admin_settings(): """Load admin settings.""" from modoboa.parameters import tools as param_tools - from . import serializers + from .api.v2 import serializers param_tools.registry.add( "global", AdminParametersForm, ugettext_lazy("Administration")) diff --git a/modoboa/admin/urls_api.py b/modoboa/admin/urls_api.py deleted file mode 100644 index f0c61b428..000000000 --- a/modoboa/admin/urls_api.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Admin API urls.""" - -from rest_framework import routers - -from . import api - -router = routers.SimpleRouter() -router.register(r"domains", api.DomainViewSet, basename="domain") -router.register( - r"domainaliases", api.DomainAliasViewSet, basename="domain_alias") -router.register(r"accounts", api.AccountViewSet, basename="account") -router.register(r"aliases", api.AliasViewSet, basename="alias") -router.register( - r"senderaddresses", api.SenderAddressViewSet, basename="sender_address") - -urlpatterns = router.urls diff --git a/modoboa/core/api/v2/viewsets.py b/modoboa/core/api/v2/viewsets.py index 6f703bc0a..f96600feb 100644 --- a/modoboa/core/api/v2/viewsets.py +++ b/modoboa/core/api/v2/viewsets.py @@ -3,7 +3,7 @@ from rest_framework import permissions, response, viewsets from rest_framework.decorators import action -from modoboa.admin import serializers as admin_serializers +from modoboa.admin.api.v1 import serializers as admin_v1_serializers from modoboa.core.api.v1 import viewsets as core_v1_viewsets from ... import models @@ -16,7 +16,7 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet): @action(methods=["get"], detail=False) def me(self, request): """Return information about connected user.""" - serializer = admin_serializers.AccountSerializer(request.user) + serializer = admin_v1_serializers.AccountSerializer(request.user) return response.Response(serializer.data) diff --git a/modoboa/urls_api_v1.py b/modoboa/urls_api_v1.py index 62a5de134..0db20c44b 100644 --- a/modoboa/urls_api_v1.py +++ b/modoboa/urls_api_v1.py @@ -8,7 +8,7 @@ app_name = "api" urlpatterns = [ path('', include("modoboa.core.api.v1.urls")), - path('', include("modoboa.admin.urls_api")), + path('', include("modoboa.admin.api.v1.urls")), path('', include("modoboa.limits.urls_api")), path('', include("modoboa.relaydomains.urls_api")) ] diff --git a/modoboa/urls_api_v2.py b/modoboa/urls_api_v2.py index 8886a6bd3..c215bee21 100644 --- a/modoboa/urls_api_v2.py +++ b/modoboa/urls_api_v2.py @@ -16,7 +16,7 @@ urlpatterns = [ path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('', include("modoboa.core.api.v2.urls")), - path('', include("modoboa.admin.urls_api")), + path('', include("modoboa.admin.api.v2.urls")), path('', include("modoboa.parameters.api.v2.urls")), path('', include("modoboa.limits.urls_api")), path('', include("modoboa.relaydomains.urls_api")),