Account detail page

This commit is contained in:
Antoine Nguyen
2022-03-31 15:38:24 +02:00
parent f8164a9bbe
commit 72fc96a3d2
12 changed files with 326 additions and 7 deletions
@@ -0,0 +1,20 @@
<template>
<v-card>
<v-card-title>
<translate class="headline">Aliases</translate>
</v-card-title>
<v-card-text>
<p v-for="alias in account.aliases" :key="alias">
{{ alias }}
</p>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: {
account: Object
}
}
</script>
@@ -98,6 +98,18 @@
<account-mailbox-form ref="mailboxForm" :account="editedAccount" />
</v-expansion-panel-content>
</v-expansion-panel>
<v-expansion-panel v-if="account.role !== 'SimpleUsers'">
<v-expansion-panel-header>
<v-row no-gutters>
<v-col cols="4">
<translate>Limits</translate>
</v-col>
</v-row>
</v-expansion-panel-header>
<v-expansion-panel-content>
<account-resources-form ref="resourcesForm" :account="editedAccount" />
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<div class="mt-4 d-flex justify-end">
<v-btn color="grey lighten-1" @click="$router.go(-1)">
@@ -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)
}
}
}
},
@@ -0,0 +1,39 @@
<template>
<v-card>
<v-card-title>
<translate class="headline">Resources usage</translate>
</v-card-title>
<v-card-text>
<div
v-for="resource in account.resources"
:key="resource.name"
class="mt-4"
>
{{ resource.label }} ({{ Math.ceil(resource.usage) }}%)
<v-progress-linear
:value="resource.usage"
:color="getProgressColor(resource.usage)"
/>
</div>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: {
account: Object
},
methods: {
getProgressColor (value) {
if (value < 50) {
return 'primary'
}
if (value < 80) {
return 'warning'
}
return 'error'
}
}
}
</script>
@@ -0,0 +1,37 @@
<template>
<validation-observer ref="observer">
<validation-provider
v-for="resource in account.resources"
:key="resource.name"
v-slot="{ errors }"
rules="required"
>
<label class="m-label">{{ resource.label }}</label>
<v-text-field
v-model="resource.max_value"
type="number"
outlined
dense
:error-messages="errors"
/>
</validation-provider>
</validation-observer>
</template>
<script>
export default {
props: {
account: Object
},
methods: {
async validateForm () {
return await this.$refs.observer.validate()
},
getPayload () {
return this.account.resources.map(r => {
return { name: r.name, max_value: r.max_value }
})
}
}
}
</script>
@@ -0,0 +1,43 @@
<template>
<v-card>
<v-card-title>
<translate class="headline">General information</translate>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="6"><translate>Creation date</translate></v-col>
<v-col cols="6">{{ account.date_joined|date }}</v-col>
</v-row>
<v-row>
<v-col cols="6"><translate>Last login date</translate></v-col>
<v-col v-if="account.last_login" cols="6">{{ account.last_login|date }}</v-col>
<v-col v-else cols="6"><translate>Never logged-in</translate></v-col>
</v-row>
<v-row>
<v-col cols="6"><translate>Full name</translate></v-col>
<v-col cols="6">{{ account.first_name }} {{ account.last_name }}</v-col>
</v-row>
<v-row>
<v-col cols="6"><translate>Role</translate></v-col>
<v-col cols="6">{{ account.role }}</v-col>
</v-row>
<v-row>
<v-col cols="6"><translate>Secondary email</translate></v-col>
<v-col cols="6">{{ account.secondary_email }}</v-col>
</v-row>
<v-row>
<v-col cols="6"><translate>Phone number</translate></v-col>
<v-col cols="6">{{ account.phone_number }}</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: {
account: Object
}
}
</script>
@@ -0,0 +1,20 @@
<template>
<v-card>
<v-card-title>
<translate class="headline">Administered domains</translate>
</v-card-title>
<v-card-text>
<v-row v-for="domain in account.domains" :key="domain">
<v-col cols="12">{{ domain }}</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: {
account: Object
}
}
</script>
@@ -4,7 +4,7 @@
<v-menu offset-y>
<template v-slot:activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on" small>
Actions <v-icon right>mdi-chevron-down</v-icon>
<translate>Actions</translate> <v-icon right>mdi-chevron-down</v-icon>
</v-btn>
</template>
<v-list dense>
@@ -31,6 +31,16 @@
class="elevation-1"
show-select
>
<template v-slot:item.identity="{ item }">
<template v-if="item.type === 'account'">
<router-link :to="{ name: 'AccountDetail', params: { id: item.pk } }">
{{ item.identity }}
</router-link>
</template>
<template v-else>
{{ item.identity }}
</template>
</template>
<template v-slot:item.tags="{ item }">
<v-chip v-for="(tag, index) in item.tags"
:key="tag.name"
+9
View File
@@ -65,6 +65,15 @@ const routes = [
allowedRoles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
}
},
{
path: '/identities/accounts/:id',
name: 'AccountDetail',
component: () => import('../views/identities/Account.vue'),
meta: {
requiresAuth: true,
allowedRoles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
}
},
{
path: '/identities/accounts/:id/edit',
name: 'AccountEdit',
+60
View File
@@ -0,0 +1,60 @@
<template>
<div>
<v-toolbar flat>
<v-toolbar-title><translate>Account</translate> {{ account.username }}</v-toolbar-title>
<v-btn color="primary" icon :to="{ name: 'AccountEdit', params: { id: account.pk } }">
<v-icon>mdi-circle-edit-outline</v-icon>
</v-btn>
</v-toolbar>
<v-row>
<v-col cols="6">
<account-summary :account="account" />
</v-col>
<v-col cols="6">
<account-resources
v-if="account.resources && account.resources.length"
:account="account"
/>
<account-aliases v-if="account.aliases" class="mt-2" :account="account" />
</v-col>
</v-row>
<v-row v-if="account.role === 'DomainAdmins'">
<v-col cols="6">
<domain-admin-domains :account="account" />
</v-col>
</v-row>
</div>
</template>
<script>
import accounts from '@/api/accounts'
import AccountAliases from '@/components/identities/AccountAliases'
import AccountResources from '@/components/identities/AccountResources'
import AccountSummary from '@/components/identities/AccountSummary'
import DomainAdminDomains from '@/components/identities/DomainAdminDomains'
export default {
components: {
AccountAliases,
AccountResources,
AccountSummary,
DomainAdminDomains
},
data () {
return {
account: { pk: this.$route.params.id }
}
},
mounted () {
accounts.get(this.$route.params.id).then(resp => {
this.account = resp.data
})
}
}
</script>
<style scoped>
.v-toolbar {
background-color: #f7f8fa !important;
}
</style>
+2 -1
View File
@@ -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):
+52 -1
View File
@@ -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
+7 -4
View File
@@ -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):