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 @@
+
+
+
+
+ Administrators of
+ {{ domain.name }}
+
+
+
+
+
+ {{ item.username }}
+
+
+
+ mdi-content-save
+
+
+
+
+
+ {{ item.first_name }} {{ item.last_name }}
+
+
+
+ mdi-delete-outline
+
+
+
+
+
+
+
+ Add
+
+
+ Close
+
+
+
+
+
+
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 @@
+
+
+
+
+ {{ item.icon }}
+
+
+ {{ item.label }}
+
+
+
+
+
+
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)