diff --git a/frontend/src/components/identities/AccountAliases.vue b/frontend/src/components/identities/AccountAliases.vue
new file mode 100644
index 000000000..88593cd64
--- /dev/null
+++ b/frontend/src/components/identities/AccountAliases.vue
@@ -0,0 +1,20 @@
+
+
+
+ Aliases
+
+
+
+ {{ alias }}
+
+
+
+
+
+
diff --git a/frontend/src/components/identities/AccountForm.vue b/frontend/src/components/identities/AccountForm.vue
index fbc056284..96954c1dc 100644
--- a/frontend/src/components/identities/AccountForm.vue
+++ b/frontend/src/components/identities/AccountForm.vue
@@ -98,6 +98,18 @@
+
+
+
+
+ Limits
+
+
+
+
+
+
+
@@ -116,12 +128,14 @@ import { bus } from '@/main'
import accounts from '@/api/accounts'
import AccountGeneralForm from './AccountGeneralForm'
import AccountMailboxForm from './AccountMailboxForm'
+import AccountResourcesForm from './AccountResourcesForm'
import AccountRoleForm from './AccountRoleForm'
export default {
components: {
AccountGeneralForm,
AccountMailboxForm,
+ AccountResourcesForm,
AccountRoleForm
},
props: ['account'],
@@ -144,6 +158,12 @@ export default {
return
}
}
+ if (this.$refs.resourcesForm !== undefined) {
+ const valid = await this.$refs.resourcesForm.validateForm()
+ if (!valid) {
+ return
+ }
+ }
try {
const data = JSON.parse(JSON.stringify(this.editedAccount))
if (this.usernameIsEmail) {
@@ -158,6 +178,9 @@ export default {
delete data.password
delete data.password_confirmation
}
+ if (this.$refs.resourcesForm !== undefined) {
+ data.resources = this.$refs.resourcesForm.getPayload()
+ }
await accounts.patch(this.editedAccount.pk, data).then(resp => {
bus.$emit('notification', { msg: this.$gettext('Account updated') })
})
@@ -168,6 +191,9 @@ export default {
if (this.$refs.mailboxForm) {
this.$refs.mailboxForm.$refs.observer.setErrors(error.response.data)
}
+ if (this.$refs.resourcesForm) {
+ this.$refs.resourcesForm.$refs.observer.setErrors(error.response.data)
+ }
}
}
},
diff --git a/frontend/src/components/identities/AccountResources.vue b/frontend/src/components/identities/AccountResources.vue
new file mode 100644
index 000000000..e069e7eee
--- /dev/null
+++ b/frontend/src/components/identities/AccountResources.vue
@@ -0,0 +1,39 @@
+
+
+
+ Resources usage
+
+
+
+ {{ resource.label }} ({{ Math.ceil(resource.usage) }}%)
+
+
+
+
+
+
+
diff --git a/frontend/src/components/identities/AccountResourcesForm.vue b/frontend/src/components/identities/AccountResourcesForm.vue
new file mode 100644
index 000000000..a18785897
--- /dev/null
+++ b/frontend/src/components/identities/AccountResourcesForm.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/identities/AccountSummary.vue b/frontend/src/components/identities/AccountSummary.vue
new file mode 100644
index 000000000..5031f548b
--- /dev/null
+++ b/frontend/src/components/identities/AccountSummary.vue
@@ -0,0 +1,43 @@
+
+
+
+ General information
+
+
+
+ Creation date
+ {{ account.date_joined|date }}
+
+
+ Last login date
+ {{ account.last_login|date }}
+ Never logged-in
+
+
+ Full name
+ {{ account.first_name }} {{ account.last_name }}
+
+
+ Role
+ {{ account.role }}
+
+
+ Secondary email
+ {{ account.secondary_email }}
+
+
+ Phone number
+ {{ account.phone_number }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/identities/DomainAdminDomains.vue b/frontend/src/components/identities/DomainAdminDomains.vue
new file mode 100644
index 000000000..a0fc3d729
--- /dev/null
+++ b/frontend/src/components/identities/DomainAdminDomains.vue
@@ -0,0 +1,20 @@
+
+
+
+ Administered domains
+
+
+
+ {{ domain }}
+
+
+
+
+
+
diff --git a/frontend/src/components/identities/IdentityList.vue b/frontend/src/components/identities/IdentityList.vue
index 49f75e70a..9938f1615 100644
--- a/frontend/src/components/identities/IdentityList.vue
+++ b/frontend/src/components/identities/IdentityList.vue
@@ -4,7 +4,7 @@
- Actions mdi-chevron-down
+ Actions mdi-chevron-down
@@ -31,6 +31,16 @@
class="elevation-1"
show-select
>
+
+
+
+ {{ item.identity }}
+
+
+
+ {{ item.identity }}
+
+
import('../views/identities/Account.vue'),
+ meta: {
+ requiresAuth: true,
+ allowedRoles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
+ }
+ },
{
path: '/identities/accounts/:id/edit',
name: 'AccountEdit',
diff --git a/frontend/src/views/identities/Account.vue b/frontend/src/views/identities/Account.vue
new file mode 100644
index 000000000..db2a2af5c
--- /dev/null
+++ b/frontend/src/views/identities/Account.vue
@@ -0,0 +1,60 @@
+
+
+
+ Account {{ account.username }}
+
+ mdi-circle-edit-outline
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modoboa/admin/api/v1/serializers.py b/modoboa/admin/api/v1/serializers.py
index c69e5d3fe..1027a04ca 100644
--- a/modoboa/admin/api/v1/serializers.py
+++ b/modoboa/admin/api/v1/serializers.py
@@ -195,7 +195,8 @@ class AccountSerializer(serializers.ModelSerializer):
fields = (
"pk", "username", "first_name", "last_name", "is_active",
"master_user", "mailbox", "role", "language", "phone_number",
- "secondary_email", "domains", "tfa_enabled"
+ "secondary_email", "domains", "tfa_enabled", "date_joined",
+ "last_login"
)
def __init__(self, *args, **kwargs):
diff --git a/modoboa/admin/api/v2/serializers.py b/modoboa/admin/api/v2/serializers.py
index 3e15e5151..51206f353 100644
--- a/modoboa/admin/api/v2/serializers.py
+++ b/modoboa/admin/api/v2/serializers.py
@@ -18,6 +18,8 @@ from modoboa.lib import fields as lib_fields
from modoboa.lib import validators, web_utils
from modoboa.lib.sysutils import exec_cmd
from modoboa.parameters import tools as param_tools
+from modoboa.limits import constants as limits_constants
+from modoboa.limits import models as limits_models
from modoboa.transport import models as transport_models
from modoboa.transport.api.v2 import serializers as transport_serializers
@@ -251,6 +253,40 @@ class IdentitySerializer(serializers.Serializer):
tags = TagSerializer(many=True)
+class ResourceSerializer(serializers.ModelSerializer):
+ """Serializer for user resource."""
+
+ class Meta:
+ fields = ("name", "label", "max_value", "current_value", "usage")
+ model = limits_models.UserObjectLimit
+
+
+class AccountSerializer(v1_serializers.AccountSerializer):
+ """Add support for user resources."""
+
+ aliases = serializers.ListField(
+ child=lib_fields.DRFEmailFieldUTF8(),
+ source="mailbox.alias_addresses"
+ )
+ resources = serializers.SerializerMethodField()
+
+ class Meta(v1_serializers.AccountSerializer.Meta):
+ fields = (
+ v1_serializers.AccountSerializer.Meta.fields
+ + ("aliases", "resources", )
+ )
+
+ def get_resources(self, account):
+ resources = []
+ for limit in account.userobjectlimit_set.all():
+ tpl = limits_constants.DEFAULT_USER_LIMITS[limit.name]
+ if "required_role" in tpl:
+ if account.role != tpl["required_role"]:
+ continue
+ resources.append(limit)
+ return ResourceSerializer(resources, many=True).data
+
+
class MailboxSerializer(serializers.ModelSerializer):
"""Base mailbox serializer."""
@@ -277,6 +313,14 @@ class MailboxSerializer(serializers.ModelSerializer):
return data
+class WritableResourceSerializer(serializers.ModelSerializer):
+ """Serializer used to update resource."""
+
+ class Meta:
+ fields = ("name", "max_value")
+ model = limits_models.UserObjectLimit
+
+
class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
"""Add support for aliases and sender addresses."""
@@ -285,13 +329,14 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
required=False
)
mailbox = MailboxSerializer(required=False)
+ resources = WritableResourceSerializer(many=True, required=False)
class Meta(v1_serializers.WritableAccountSerializer.Meta):
fields = tuple(
field
for field in v1_serializers.WritableAccountSerializer.Meta.fields
if field != "random_password"
- ) + ("aliases", )
+ ) + ("aliases", "resources")
def validate_aliases(self, value):
"""Check if required domains are locals and user can access them."""
@@ -405,6 +450,12 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
instance.email = validated_data["username"]
self._create_mailbox(creator, instance, mailbox_data)
instance.save()
+ resources = validated_data.get("resources")
+ if resources:
+ for resource in resources:
+ instance.userobjectlimit_set.filter(
+ name=resource["name"]
+ ).update(max_value=resource["max_value"])
self.set_permissions(instance, domains)
return instance
diff --git a/modoboa/admin/api/v2/viewsets.py b/modoboa/admin/api/v2/viewsets.py
index 7527d7d27..b5b032c31 100644
--- a/modoboa/admin/api/v2/viewsets.py
+++ b/modoboa/admin/api/v2/viewsets.py
@@ -12,9 +12,7 @@ from rest_framework import (
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
-from modoboa.admin.api.v1 import (
- serializers as v1_serializers, viewsets as v1_viewsets
-)
+from modoboa.admin.api.v1 import viewsets as v1_viewsets
from modoboa.core import models as core_models
from modoboa.lib import viewsets as lib_viewsets
@@ -139,6 +137,8 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
return serializers.WritableAccountSerializer
if self.action == "delete":
return serializers.DeleteAccountSerializer
+ if self.action in ["list", "retrieve"]:
+ return serializers.AccountSerializer
return super().get_serializer_class()
def get_queryset(self):
@@ -149,7 +149,10 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
.filter(content_type=ContentType.objects.get_for_model(user))
.values_list("object_id", flat=True)
)
- return core_models.User.objects.filter(pk__in=ids)
+ return (
+ core_models.User.objects.filter(pk__in=ids)
+ .prefetch_related("userobjectlimit_set")
+ )
@action(methods=["post"], detail=False)
def validate(self, request, **kwargs):