From 332f626bdc1e113375946686d818a538295c6a32 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 20 Feb 2021 19:05:54 +0100 Subject: [PATCH] Added new form to manipulate domain admins. --- frontend/src/api/accounts.js | 9 ++ frontend/src/api/domains.js | 9 ++ .../components/domains/AdministratorList.vue | 131 ++++++++++++++++++ .../src/components/domains/DomainList.vue | 44 +++--- frontend/src/components/tools/MenuItems.vue | 21 +++ frontend/src/router/index.js | 17 --- modoboa/admin/api/v1/viewsets.py | 2 +- modoboa/admin/api/v2/serializers.py | 16 +++ modoboa/admin/api/v2/urls.py | 2 +- modoboa/admin/api/v2/viewsets.py | 82 ++++++++++- 10 files changed, 292 insertions(+), 41 deletions(-) create mode 100644 frontend/src/api/accounts.js create mode 100644 frontend/src/components/domains/AdministratorList.vue create mode 100644 frontend/src/components/tools/MenuItems.vue diff --git a/frontend/src/api/accounts.js b/frontend/src/api/accounts.js new file mode 100644 index 000000000..ba04cf807 --- /dev/null +++ b/frontend/src/api/accounts.js @@ -0,0 +1,9 @@ +import repository from './repository' + +const resource = 'accounts' + +export default { + getAll ({ domain, role }) { + return repository.get(`/${resource}/`, { params: { domain, role } }) + } +} diff --git a/frontend/src/api/domains.js b/frontend/src/api/domains.js index 1e5054430..f24bdade8 100644 --- a/frontend/src/api/domains.js +++ b/frontend/src/api/domains.js @@ -20,6 +20,15 @@ export default { } return repository.get(url) }, + getDomainAdministrators (domainId) { + return repository.get(`/${domainResource}/${domainId}/administrators/`) + }, + addDomainAdministrator (domainId, accountId) { + return repository.post(`/${domainResource}/${domainId}/administrators/add/`, { account: accountId }) + }, + removeDomainAdministrator (domainId, accountId) { + return repository.post(`/${domainResource}/${domainId}/administrators/remove/`, { account: accountId }) + }, createDomain (data) { return repository.post(`/${domainResource}/`, data) }, diff --git a/frontend/src/components/domains/AdministratorList.vue b/frontend/src/components/domains/AdministratorList.vue new file mode 100644 index 000000000..05083b901 --- /dev/null +++ b/frontend/src/components/domains/AdministratorList.vue @@ -0,0 +1,131 @@ + + + diff --git a/frontend/src/components/domains/DomainList.vue b/frontend/src/components/domains/DomainList.vue index e909c248c..c553cbe8f 100644 --- a/frontend/src/components/domains/DomainList.vue +++ b/frontend/src/components/domains/DomainList.vue @@ -74,24 +74,7 @@ mdi-dots-horizontal - - - - mdi-circle-edit-outline - - - Edit - - - - - mdi-delete-outline - - - Delete - - - + @@ -132,6 +115,15 @@ @alias-deleted="domainAliasDeleted" /> + + + @@ -139,23 +131,32 @@ import { mapGetters } from 'vuex' import { bus } from '@/main' import domainApi from '@/api/domains' +import AdministratorList from './AdministratorList' import ConfirmDialog from '@/components/layout/ConfirmDialog' import DNSDetail from '@/components/domains/DNSDetail' import DomainAliasForm from '@/components/domains/DomainAliasForm' import DomainForm from '@/components/domains/DomainForm' +import MenuItems from '@/components/tools/MenuItems' export default { components: { + AdministratorList, ConfirmDialog, 'dns-detail': DNSDetail, DomainAliasForm, - DomainForm + DomainForm, + MenuItems }, computed: mapGetters({ domains: 'domains/domains' }), data () { return { + domainMenuItems: [ + { label: this.$gettext('Administrators'), icon: 'mdi-account-supervisor', onClick: this.openAdminList }, + { label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editDomain }, + { label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteDomain, color: 'red' } + ], headers: [ { text: 'Name', value: 'name' }, { text: 'Aliases', value: 'domainalias_count' }, @@ -166,6 +167,7 @@ export default { ], selectedDomain: null, selectedDomainAlias: null, + showAdminList: false, showConfirmDialog: false, showAliasForm: false, showDNSdetail: false, @@ -260,6 +262,10 @@ export default { this.selectedDomain = domain this.showDNSdetail = true }, + openAdminList (domain) { + this.selectedDomain = domain + this.showAdminList = true + }, showAliases (item) { this.expanded = [{ item }] } diff --git a/frontend/src/components/tools/MenuItems.vue b/frontend/src/components/tools/MenuItems.vue new file mode 100644 index 000000000..aee966ecd --- /dev/null +++ b/frontend/src/components/tools/MenuItems.vue @@ -0,0 +1,21 @@ + + + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 74ef3a0f4..35376f61f 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -3,7 +3,6 @@ import VueRouter from 'vue-router' import store from '@/store' -import DomainForm from '@/components/domains/DomainForm' import ParametersForm from '@/components/parameters/ParametersForm' import LogList from '@/components/logs/LogList' @@ -26,22 +25,6 @@ const routes = [ requiresAuth: true } }, - { - path: '/domains/add/', - name: 'DomainAdd', - component: DomainForm, - meta: { - requiresAuth: true - } - }, - { - path: '/domains/:domainPk([0-9]+)/', - name: 'DomainEdit', - component: DomainForm, - meta: { - requiresAuth: true - } - }, { path: '/parameters/:app', name: 'ParametersEdit', diff --git a/modoboa/admin/api/v1/viewsets.py b/modoboa/admin/api/v1/viewsets.py index 164b05f31..b61240aff 100644 --- a/modoboa/admin/api/v1/viewsets.py +++ b/modoboa/admin/api/v1/viewsets.py @@ -84,7 +84,7 @@ class DomainAliasViewSet(lib_viewsets.RevisionModelMixin, class AccountViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet): """ViewSet for User/Mailbox.""" - filter_backends = [filters.SearchFilter] + filter_backends = (filters.SearchFilter, ) permission_classes = [IsAuthenticated, DjangoModelPermissions, ] search_fields = ("^first_name", "^last_name", "^email") diff --git a/modoboa/admin/api/v2/serializers.py b/modoboa/admin/api/v2/serializers.py index 5f055fca4..0bc1cbb55 100644 --- a/modoboa/admin/api/v2/serializers.py +++ b/modoboa/admin/api/v2/serializers.py @@ -116,3 +116,19 @@ class AdminGlobalParametersSerializer(serializers.Serializer): 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 DomainAdminSerializer(serializers.ModelSerializer): + """Serializer used for administrator related routes.""" + + class Meta: + model = core_models.User + fields = ("id", "username", "first_name", "last_name") + + +class SimpleDomainAdminSerializer(serializers.Serializer): + """Serializer used for add/remove operations.""" + + account = serializers.PrimaryKeyRelatedField( + queryset=core_models.User.objects.all() + ) diff --git a/modoboa/admin/api/v2/urls.py b/modoboa/admin/api/v2/urls.py index 3c15e3e18..afcf104a4 100644 --- a/modoboa/admin/api/v2/urls.py +++ b/modoboa/admin/api/v2/urls.py @@ -10,7 +10,7 @@ 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"accounts", viewsets.AccountViewSet, basename="account") router.register(r"aliases", v1_viewsets.AliasViewSet, basename="alias") router.register( r"senderaddresses", v1_viewsets.SenderAddressViewSet, diff --git a/modoboa/admin/api/v2/viewsets.py b/modoboa/admin/api/v2/viewsets.py index 6afdece7a..905884bbb 100644 --- a/modoboa/admin/api/v2/viewsets.py +++ b/modoboa/admin/api/v2/viewsets.py @@ -2,12 +2,20 @@ from django.utils.translation import ugettext as _ +from django.contrib.contenttypes.models import ContentType + +from django_filters import rest_framework as dj_filters from drf_spectacular.utils import extend_schema, extend_schema_view -from rest_framework import mixins, permissions, response, status, viewsets +from rest_framework import ( + filters, 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.admin.api.v1 import ( + serializers as v1_serializers, viewsets as v1_viewsets +) +from modoboa.core import models as core_models from modoboa.lib import viewsets as lib_viewsets from ... import models @@ -30,7 +38,7 @@ from . import serializers delete=extend_schema( description="Delete a particular domain", summary="Delete a particular domain" - ) + ), ) class DomainViewSet(lib_viewsets.RevisionModelMixin, mixins.ListModelMixin, @@ -53,6 +61,10 @@ class DomainViewSet(lib_viewsets.RevisionModelMixin, return serializers.DomainSerializer if self.action == "delete": return serializers.DeleteDomainSerializer + if self.action == "administrators": + return serializers.DomainAdminSerializer + if self.action in ["add_administrator", "remove_administrator"]: + return serializers.SimpleDomainAdminSerializer return v1_serializers.DomainSerializer @action(methods=["post"], detail=True) @@ -68,3 +80,67 @@ class DomainViewSet(lib_viewsets.RevisionModelMixin, serializer.is_valid(raise_exception=True) domain.delete(request.user, serializer.validated_data["keep_folder"]) return response.Response(status=status.HTTP_204_NO_CONTENT) + + @action(methods=["get"], detail=True) + def administrators(self, request, **kwargs): + """Retrieve all administrators of a domain.""" + domain = self.get_object() + serializer = self.get_serializer(domain.admins, many=True) + return response.Response(serializer.data) + + @action(methods=["post"], detail=True, url_path="administrators/add") + def add_administrator(self, request, **kwargs): + """Add an administrator to a domain.""" + domain = self.get_object() + context = self.get_serializer_context() + context["domain"] = domain + serializer = self.get_serializer(data=request.data, context=context) + serializer.is_valid(raise_exception=True) + domain.add_admin(serializer.validated_data["account"]) + return response.Response() + + @action(methods=["post"], detail=True, url_path="administrators/remove") + def remove_administrator(self, request, **kwargs): + """Remove an administrator from a domain.""" + domain = self.get_object() + context = self.get_serializer_context() + context["domain"] = domain + serializer = self.get_serializer(data=request.data, context=context) + serializer.is_valid(raise_exception=True) + domain.remove_admin(serializer.validated_data["account"]) + return response.Response() + + +class AccountFilterSet(dj_filters.FilterSet): + """Custom FilterSet for Account.""" + + domain = dj_filters.ModelChoiceFilter( + queryset=lambda request: models.Domain.objects.get_for_admin( + request.user), + field_name="mailbox__domain" + ) + role = dj_filters.CharFilter(method="filter_role") + + class Meta: + model = core_models.User + fields = ["domain", "role"] + + def filter_role(self, queryset, name, value): + return queryset.filter(groups__name=value) + + +class AccountViewSet(v1_viewsets.AccountViewSet): + """ViewSet for User/Mailbox.""" + + filter_backends = (filters.SearchFilter, dj_filters.DjangoFilterBackend) + filterset_class = AccountFilterSet + + def get_queryset(self): + """Filter queryset based on current user.""" + user = self.request.user + ids = ( + user.objectaccess_set + .filter(content_type=ContentType.objects.get_for_model(user)) + .values_list("object_id", flat=True) + ) + return core_models.User.objects.filter(pk__in=ids)