mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Account detail page
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user