Better domain creation form.

This commit is contained in:
Antoine Nguyen
2021-02-03 17:26:36 +01:00
parent 818979715f
commit 3e94ac66ab
9 changed files with 342 additions and 102 deletions
+36 -5
View File
@@ -13,11 +13,31 @@
<router-view/>
</v-container>
</v-main>
<v-snackbar
v-model="snackbar"
:color="notificationColor"
:timeout="notificationTimeout"
top
>
{{ notification }}
<template v-slot:action="{ attrs }">
<v-btn
color="white"
text
v-bind="attrs"
@click="snackbar = false"
>
<translate>Close</translate>
</v-btn>
</template>
</v-snackbar>
</v-app>
</template>
<script>
import { mapGetters } from 'vuex'
import { bus } from '@/main'
import Navbar from '@/components/layout/Navbar'
export default {
@@ -30,12 +50,23 @@ export default {
components: {
Navbar
},
// created () {
// this.$store.dispatch('auth/initialize')
// },
created () {
// this.$store.dispatch('auth/initialize')
bus.$on('notification', this.showNotification)
},
data: () => ({
//
})
snackbar: false,
notification: '',
notificationColor: 'success',
notificationTimeout: 2000
}),
methods: {
showNotification (options) {
this.notification = options.msg
this.notificationColor = (options.type) ? options.type : 'success'
this.snackbar = true
}
}
}
</script>
@@ -4,20 +4,39 @@
<span class="headline">{{ title }}</span>
</v-card-title>
<v-card-text>
<v-text-field v-model="form.name" label="Name" />
<v-select
v-model="form.target"
:items="domains"
item-text="name"
item-value="pk"
label="Choose a domain"
single-line
></v-select>
<v-switch
v-model="form.enabled"
label="Enabled"
color="primary"
/>
<validation-observer ref="observer">
<validation-provider
v-slot="{ errors }"
name="name"
rules="required"
>
<v-text-field
v-model="form.name"
:label="'Name' | translate"
:error-messages="errors"
/>
</validation-provider>
<validation-provider
v-slot="{ errors }"
name="target"
rules="required"
>
<v-select
v-model="form.target"
:items="domains"
item-text="name"
item-value="pk"
:label="'Choose a domain' | translate"
:error-messages="errors"
single-line
/>
</validation-provider>
<v-switch
v-model="form.enabled"
label="Enabled"
color="primary"
/>
</validation-observer>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
@@ -41,6 +60,7 @@
<script>
import { mapGetters } from 'vuex'
import { bus } from '@/main'
export default {
props: {
@@ -69,17 +89,27 @@ export default {
this.$emit('close')
this.form = {}
},
submit () {
if (!this.form.pk) {
this.$store.dispatch('domains/addDomainAlias', this.form).then(resp => {
this.close()
})
} else {
const data = JSON.parse(JSON.stringify(this.form))
data.target = data.target.pk
this.$store.dispatch('domains/updateDomainAlias', { id: this.form.pk, data: data }).then(resp => {
this.close()
})
async submit () {
const valid = await this.$refs.observer.validate()
if (!valid) {
return
}
try {
if (!this.form.pk) {
await this.$store.dispatch('domains/addDomainAlias', this.form).then(resp => {
bus.$emit('notification', { msg: this.$gettext('Domain alias created') })
this.close()
})
} else {
const data = JSON.parse(JSON.stringify(this.form))
data.target = data.target.pk
await this.$store.dispatch('domains/updateDomainAlias', { id: this.form.pk, data: data }).then(resp => {
bus.$emit('notification', { msg: this.$gettext('Domain alias updated') })
this.close()
})
}
} catch (error) {
this.$refs.observer.setErrors(error.response.data)
}
}
},
@@ -1,7 +1,7 @@
<template>
<div class="d-flex justify-center inner">
<v-stepper v-model="step">
<v-stepper-header class=" align-center">
<v-stepper-header class="align-center px-10">
<v-img
src="../../assets/Modoboa_RVB-ORANGE-SANS.png"
max-width="190"
@@ -15,7 +15,7 @@
<v-stepper-step :complete="step > 3" step="3">
<translate>Limitations</translate>
</v-stepper-step>
<v-stepper-step step="4">
<v-stepper-step :complete="step > 4" step="4">
<translate>Options</translate>
</v-stepper-step>
<v-stepper-step step="5">
@@ -25,8 +25,12 @@
<v-icon color="primary" x-large>mdi-close</v-icon>
</v-btn>
</v-stepper-header>
<v-stepper-items class="mx-16 mt-4">
<v-stepper-content step="1">
<v-stepper-items class="mt-4 d-flex justify-center">
<v-stepper-content step="1" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New domain</translate> /
<translate>General</translate>
</div>
<validation-observer ref="observer_1">
<validation-provider
v-slot="{ errors }"
@@ -61,17 +65,21 @@
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="2">
<v-stepper-content step="2" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New domain</translate> /
<translate>DNS</translate>
</div>
<v-switch
label="Enable DNS checks"
:label="'Enable DNS checks' | translate"
v-model="domain.enable_dns_checks"
/>
<v-switch
label="Enable DKIM signing"
:label="'Enable DKIM signing' | translate"
v-model="domain.enable_dkim"
/>
<v-text-field
label="DKIM key selector"
:label="'DKIM key selector' | translate"
v-model="domain.dkim_key_selector"
:disabled="!domain.enable_dkim"
outlined
@@ -80,6 +88,7 @@
v-model="domain.dkim_key_length"
:label="'DKIM key length' | translate"
:choices="dkimKeyLengths"
:disabled="!domain.enable_dkim"
/>
<div class="d-flex justify-center mt-8">
<v-btn @click="step = 1" class="mr-10" text>
@@ -94,31 +103,47 @@
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="3">
<v-stepper-content step="3" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New domain</translate> /
<translate>Limitations</translate>
</div>
<validation-observer ref="observer_3">
<v-text-field
:label="'Quota' | translate"
:hint="'Quota shared between mailboxes. Can be expressed in KB, MB (default) or GB. A value of 0 means no quota.' | translate"
persistent-hint
v-model="domain.quota"
:error-messages="errors"
outlined
/>
<v-text-field
:label="'Default mailbox quota' | translate"
:hint="'Default quota applied to mailboxes. Can be expressed in KB, MB (default) or GB. A value of 0 means no quota.' | translate"
persistent-hint
v-model="domain.default_mailbox_quota"
:error-messages="errors"
outlined
/>
<validation-provider
v-slot="{ errors }"
rules="number"
rules="required"
>
<v-text-field
:label="'Quota' | translate"
:hint="'Quota shared between mailboxes. Can be expressed in KB, MB (default) or GB. A value of 0 means no quota.' | translate"
persistent-hint
v-model="domain.quota"
:error-messages="errors"
class="mb-4"
outlined
/>
</validation-provider>
<validation-provider
v-slot="{ errors }"
rules="required"
>
<v-text-field
:label="'Default mailbox quota' | translate"
:hint="'Default quota applied to mailboxes. Can be expressed in KB, MB (default) or GB. A value of 0 means no quota.' | translate"
persistent-hint
v-model="domain.default_mailbox_quota"
:error-messages="errors"
class="mb-4"
outlined
/>
</validation-provider>
<validation-provider
v-slot="{ errors }"
rules="numeric"
>
<v-text-field
:label="'Message sending limit' | translate"
:hint="'Number of messages this domain can send per day.' | translate"
:hint="'Number of messages this domain can send per day. Leave empty for no limit.' | translate"
persistent-hint
v-model="domain.message_sending_limit"
:error-messages="errors"
@@ -139,33 +164,43 @@
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="4">
<v-stepper-content step="4" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New domain</translate> /
<translate>Options</translate>
</div>
<validation-observer ref="observer_4">
<v-switch v-model="createAdmin"
:label="'Create a domain administrator' | translate"
/>
<v-text-field
:label="'Name' | translate"
:hint="'Name of the administrator' | translate"
persistent-hint
v-model="domain.dom_admin_username"
:error-messages="errors"
outlined
:disabled="!createAdmin"
/>
<v-switch v-model="domain.dom_admin_random_password"
<validation-provider
v-slot="{ errors }"
:rules="(createAdmin) ? 'required' : ''"
>
<v-text-field
:label="'Name' | translate"
:hint="'Name of the administrator' | translate"
persistent-hint
v-model="domain.domain_admin.username"
:error-messages="errors"
outlined
:disabled="!createAdmin"
:suffix="`@${domain.name}`"
/>
</validation-provider>
<v-switch v-model="domain.domain_admin.with_random_password"
:label="'Random password' | translate"
:disabled="!createAdmin"
:hint="'Generate a random password for the administrator.' | translate"
persistent-hint
/>
<v-switch v-model="domain.dom_admin_with_mailbox"
<v-switch v-model="domain.domain_admin.with_mailbox"
:label="'With a mailbox' | translate"
:disabled="!createAdmin"
:hint="'Create a mailbox for the administrator.' | translate"
persistent-hint
/>
<v-switch v-model="domain.dom_admin_create_aliases"
<v-switch v-model="domain.domain_admin.with_aliases"
:label="'Create aliases' | translate"
:disabled="!createAdmin"
:hint="'Create standard aliases for the domain.' | translate"
@@ -186,10 +221,11 @@
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="5">
<v-stepper-content step="5" class="flex-grow-0">
<div class="text-center text-h3"><translate>Summary</translate></div>
<div class="subtitle text-subtitle-1 mt-4">
<translate>General</translate>
<div class="subtitle mt-4 d-flex">
<translate class="text-h6">General</translate>
<a href="#" class="edit-link ml-auto" @click="step = 1"><translate>Modify</translate></a>
</div>
<v-row dense>
<v-col><translate class="grey--text">Name</translate></v-col>
@@ -201,18 +237,19 @@
</v-row>
<v-row dense>
<v-col><translate class="grey--text">Enabled</translate></v-col>
<v-col class="text-right">{{ domain.enabled }}</v-col>
<v-col class="text-right">{{ domain.enabled|yesno }}</v-col>
</v-row>
<div class="subtitle text-subtitle-1 mt-4">
<translate>DNS</translate>
<div class="subtitle mt-4 d-flex">
<translate class="text-h6">DNS</translate>
<a href="#" class="edit-link ml-auto" @click="step = 2"><translate>Modify</translate></a>
</div>
<v-row dense>
<v-col><translate class="grey--text">Enable DNS checks</translate></v-col>
<v-col class="text-right">{{ domain.enable_dns_checks }}</v-col>
<v-col class="text-right">{{ domain.enable_dns_checks|yesno }}</v-col>
</v-row>
<v-row dense>
<v-col><translate class="grey--text">Enable DKIM signing</translate></v-col>
<v-col class="text-right">{{ domain.enable_dkim }}</v-col>
<v-col class="text-right">{{ domain.enable_dkim|yesno }}</v-col>
</v-row>
<v-row dense v-if="domain.enable_dkim">
<v-col><translate class="grey--text">DKIM key selector</translate></v-col>
@@ -222,8 +259,9 @@
<v-col><translate class="grey--text">DKIM key length</translate></v-col>
<v-col class="text-right">{{ domain.dkim_key_length }}</v-col>
</v-row>
<div class="subtitle text-subtitle-1 mt-4">
<translate>Limitations</translate>
<div class="subtitle mt-4 d-flex">
<translate class="text-h6">Limitations</translate>
<a href="#" class="edit-link ml-auto" @click="step = 3"><translate>Modify</translate></a>
</div>
<v-row dense>
<v-col><translate class="grey--text">Quota</translate></v-col>
@@ -237,29 +275,30 @@
<v-col><translate class="grey--text">Message sending limit</translate></v-col>
<v-col class="text-right">{{ domain.message_sending_limit }}</v-col>
</v-row>
<div class="subtitle text-subtitle-1 mt-4">
<translate>Options</translate>
<div class="subtitle mt-4 d-flex">
<translate class="text-h6">Options</translate>
<a href="#" class="edit-link ml-auto" @click="step = 4"><translate>Modify</translate></a>
</div>
<v-row dense>
<v-col><translate class="grey--text">Create a domain administrator</translate></v-col>
<v-col class="text-right">{{ createAdmin }}</v-col>
<v-col class="text-right">{{ createAdmin|yesno }}</v-col>
</v-row>
<div v-if="createAdmin">
<v-row dense>
<v-col><translate class="grey--text">Administrator name</translate></v-col>
<v-col class="text-right">{{ domain.dom_admin_username }}</v-col>
<v-col class="text-right">{{ domain.domain_admin.username }}</v-col>
</v-row>
<v-row dense>
<v-col><translate class="grey--text">Random password</translate></v-col>
<v-col class="text-right">{{ domain.dom_admin_random_password }}</v-col>
<v-col class="text-right">{{ domain.domain_admin.with_random_password|yesno }}</v-col>
</v-row>
<v-row dense>
<v-col><translate class="grey--text">With mailbox</translate></v-col>
<v-col class="text-right">{{ domain.dom_admin_with_mailbox }}</v-col>
<v-col class="text-right">{{ domain.domain_admin.with_mailbox|yesno }}</v-col>
</v-row>
<v-row dense>
<v-col><translate class="grey--text">Create aliases</translate></v-col>
<v-col class="text-right">{{ domain.dom_admin_create_aliases }}</v-col>
<v-col class="text-right">{{ domain.domain_admin.with_aliases|yesno }}</v-col>
</v-row>
</div>
<div class="d-flex justify-center mt-8">
@@ -278,7 +317,9 @@
</template>
<script>
import { bus } from '@/main'
import ChoiceField from '@/components/tools/ChoiceField'
export default {
props: ['value'],
components: {
@@ -287,7 +328,9 @@ export default {
data () {
return {
createAdmin: false,
domain: {},
domain: {
domain_admin: {}
},
domainTypes: [
{
label: 'Domain',
@@ -324,6 +367,7 @@ export default {
methods: {
initDomain () {
this.domain = {
name: '',
type: 'domain',
enabled: true,
enable_dns_checks: true,
@@ -331,10 +375,12 @@ export default {
dkim_key_selector: 'modoboa',
quota: 0,
default_mailbox_quota: 0,
dom_admin_username: 'admin',
dom_admin_random_password: false,
dom_admin_with_mailbox: false,
dom_admin_create_aliases: false
domain_admin: {
username: 'admin',
with_random_password: false,
with_mailbox: false,
with_aliases: false
}
}
},
close () {
@@ -350,7 +396,12 @@ export default {
this.step = next
},
submit () {
this.$store.dispatch('domains/createDomain', this.domain).then(resp => {
const data = JSON.parse(JSON.stringify(this.domain))
if (!this.createAdmin) {
delete data.domain_admin
}
this.$store.dispatch('domains/createDomain', data).then(resp => {
bus.$emit('notification', { msg: this.$gettext('Domain created') })
this.close()
})
}
@@ -358,17 +409,28 @@ export default {
}
</script>
<style lang="scss" scoped>
<style lang="scss">
.inner {
height: 100%;
background-color: #fff;
}
.v-stepper {
height: 100%;
width: 70%;
width: 100%;
overflow: auto;
&__content {
width: 60%;
}
&__items {
overflow-y: auto;
}
}
.subtitle {
color: #000;
border-bottom: 1px solid #DBDDDF;
}
.edit-link {
text-decoration: none;
}
</style>
+27 -4
View File
@@ -5,7 +5,7 @@
<div v-for="(choice, index) in choices"
:key="index"
class="choice rounded pa-10 mr-4 text-center"
:class="{ 'choice--selected': currentChoice === choice.value }"
:class="{ 'choice--disabled': disabled, 'choice--selected': !disabled && currentChoice === choice.value }"
@click="selectChoice(choice.value)">
<v-icon v-if="choice.icon" class="d-block mb-2" :color="iconColor(choice.value)" x-large>{{ choice.icon }}</v-icon>
<translate class="grey--text text--darken-1">{{ choice.label }}</translate>
@@ -16,20 +16,39 @@
<script>
export default {
props: ['value', 'label', 'choices'],
props: {
value: [Number, String],
label: String,
choices: Array,
disabled: {
type: Boolean,
default: false
}
},
data () {
return {
currentChoice: this.value
currentChoice: null
}
},
methods: {
iconColor (value) {
return (value === this.currentChoice) ? 'primary' : ''
return (!this.disabled && value === this.currentChoice) ? 'primary' : ''
},
selectChoice (value) {
if (this.disabled) {
return
}
this.currentChoice = value
this.$emit('input', value)
}
},
watch: {
value: {
handler (val) {
this.currentChoice = val
},
immediate: true
}
}
}
</script>
@@ -44,5 +63,9 @@ export default {
&--selected {
border-color: #046BF8 !important;
}
&--disabled {
cursor: unset;
}
}
</style>
+7
View File
@@ -0,0 +1,7 @@
import Vue from 'vue'
import { translate } from 'vue-gettext'
const { gettext: $gettext } = translate
Vue.filter('yesno', function (value) {
return (value) ? $gettext('yes') : $gettext('no')
})
+3
View File
@@ -3,6 +3,7 @@ import './plugins/axios'
import './plugins/vee-validate'
import './plugins/vue-cookie'
import './plugins/vue-gettext'
import './filters'
import App from './App.vue'
import router from './router'
import store from './store'
@@ -10,6 +11,8 @@ import vuetify from './plugins/vuetify'
Vue.config.productionTip = false
export const bus = new Vue()
new Vue({
router,
store,
+1
View File
@@ -30,6 +30,7 @@
<v-dialog
v-model="showDomainWizard"
fullscreen
scrollable
transition="dialog-bottom-transition"
>
<domain-creation-form @close="showDomainWizard = false" />
+5 -1
View File
@@ -38,12 +38,16 @@ class DomainViewSet(viewsets.ModelViewSet):
"""Domain viewset."""
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
serializer_class = serializers.DomainSerializer
def get_queryset(self):
"""Filter queryset based on current user."""
return models.Domain.objects.get_for_admin(self.request.user)
def get_serializer_class(self, *args, **kwargs):
if self.request.version == "v2" and self.action == "create":
return serializers.DomainSerializerV2
return serializers.DomainSerializer
def perform_destroy(self, instance):
"""Add custom args to delete call."""
instance.delete(self.request.user)
+79
View File
@@ -110,6 +110,85 @@ class DomainSerializer(serializers.ModelSerializer):
return domain
class CreateDomainAdminSerializer(serializers.Serializer):
"""Sub serializer for domain administrator creation."""
username = serializers.CharField()
with_random_password = serializers.BooleanField(default=False)
with_mailbox = serializers.BooleanField(default=False)
with_aliases = serializers.BooleanField(default=False)
class DomainSerializerV2(DomainSerializer):
"""Domain serializer for v2 API."""
domain_admin = CreateDomainAdminSerializer(required=False)
class Meta(DomainSerializer.Meta):
fields = DomainSerializer.Meta.fields + (
"domain_admin",
)
def create(self, validated_data):
"""Create administrator and other stuff if needed."""
domain_admin = validated_data.pop("domain_admin", None)
domain = super().create(validated_data)
if not domain_admin:
return domain
# 1. Create a domain administrator
username = "%s@%s" % (domain_admin["username"], domain.name)
try:
da = core_models.User.objects.get(username=username)
except core_models.User.DoesNotExist:
pass
else:
raise lib_exceptions.Conflict(
_("User '%s' already exists") % username)
user = self.context["request"].user
core_signals.can_create_object.send(
self.__class__, context=user, klass=models.Mailbox)
da = core_models.User(username=username, email=username, is_active=True)
if domain_admin["with_random_password"]:
password = lib.make_password()
else:
password = param_tools.get_global_parameter(
"default_password", app="core")
da.set_password(password)
da.save()
da.role = "DomainAdmins"
da.post_create(user)
# 2. Create mailbox if needed
if domain_admin["with_mailbox"]:
dom_admin_username = domain_admin["username"]
mb = models.Mailbox(
address=dom_admin_username, domain=domain,
user=da, use_domain_quota=True
)
mb.set_quota(
override_rules=user.has_perm("admin.change_domain"))
mb.save(creator=user)
# 3. Create aliases if needed
condition = (
domain.type == "domain" and
domain_admin["with_aliases"] and
dom_admin_username != "postmaster"
)
if condition:
core_signals.can_create_object.send(
self.__class__, context=user, klass=models.Alias)
address = u"postmaster@{}".format(domain.name)
alias = models.Alias.objects.create(
address=address, domain=domain, enabled=True)
alias.set_recipients([mb.full_address])
alias.post_create(user)
domain.add_admin(da)
return domain
class DomainAliasSerializer(FlexFieldsModelSerializer):
"""Base DomainAlias serializer."""