Better domain update form.

This commit is contained in:
Antoine Nguyen
2021-02-19 17:35:04 +01:00
parent 4fbf0172dc
commit 571fecf098
24 changed files with 657 additions and 404 deletions
+3
View File
@@ -26,6 +26,9 @@ export default {
updateDomain (domainId, data) {
return repository.put(`/${domainResource}/${domainId}/`, data)
},
deleteDomain (domainId, data) {
return repository.post(`/${domainResource}/${domainId}/delete/`, data)
},
createDomainAlias (data) {
return repository.post(`/${domainAliasResource}/`, data)
},
@@ -48,7 +48,7 @@
<translate>Close</translate>
</v-btn>
<v-btn
v-if="domainAlias.pk"
v-if="domainAlias && domainAlias.pk"
color="error"
text
@click="deleteAlias"
@@ -73,7 +73,10 @@ import domains from '@/api/domains'
export default {
props: {
domainAlias: Object
domainAlias: {
type: Object,
default: () => null
}
},
computed: {
...mapGetters({
@@ -96,7 +99,9 @@ export default {
methods: {
close () {
this.$emit('close')
this.form = {}
this.form = {
enabled: true
}
},
deleteAlias () {
domains.deleteDomainAlias(this.domainAlias.pk).then(resp => {
@@ -31,35 +31,7 @@
<translate class="grey--text text--darken-1">New domain</translate> /
<translate>General</translate>
</div>
<validation-observer ref="observer_1">
<validation-provider
v-slot="{ errors }"
rules="required"
>
<v-text-field
:label="'Domain name (ex: domain.tld)' | translate"
v-model="domain.name"
:error-messages="errors"
outlined
/>
</validation-provider>
<validation-provider
v-slot="{ errors }"
rules="required"
>
<choice-field v-model="domain.type"
:label="'Type' | translate"
:choices="domainTypes"
:error-messages="errors"
/>
</validation-provider>
<v-switch
:label="'Enabled' | translate"
v-model="domain.enabled"
:hint="'Control if this domain will be allowed to send and receive messages' | translate"
persistent-hint
/>
</validation-observer>
<domain-general-form ref="form_1" :domain="domain" />
<div class="d-flex justify-end">
<v-btn
color="primary"
@@ -75,26 +47,7 @@
<translate class="grey--text text--darken-1">New domain</translate> /
<translate>DNS</translate>
</div>
<v-switch
:label="'Enable DNS checks' | translate"
v-model="domain.enable_dns_checks"
/>
<v-switch
:label="'Enable DKIM signing' | translate"
v-model="domain.enable_dkim"
/>
<v-text-field
:label="'DKIM key selector' | translate"
v-model="domain.dkim_key_selector"
:disabled="!domain.enable_dkim"
outlined
/>
<choice-field
v-model="domain.dkim_key_length"
:label="'DKIM key length' | translate"
:choices="dkimKeyLengths"
:disabled="!domain.enable_dkim"
/>
<domain-dns-form ref="form_2" :domain="domain" />
<div class="d-flex justify-end mt-8">
<v-btn @click="step = 1" class="mr-10" text>
<translate>Back</translate>
@@ -113,49 +66,7 @@
<translate class="grey--text text--darken-1">New domain</translate> /
<translate>Limitations</translate>
</div>
<validation-observer ref="observer_3">
<validation-provider
v-slot="{ errors }"
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. Leave empty for no limit.' | translate"
persistent-hint
v-model="domain.message_sending_limit"
:error-messages="errors"
outlined
/>
</validation-provider>
</validation-observer>
<domain-limitations-form ref="form_3" :domain="domain" />
<div class="d-flex justify-end mt-8">
<v-btn @click="step = 2" class="mr-10" text>
<translate>Back</translate>
@@ -174,45 +85,7 @@
<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"
/>
<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.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.domain_admin.with_aliases"
:label="'Create aliases' | translate"
:disabled="!createAdmin"
:hint="'Create standard aliases for the domain.' | translate"
persistent-hint
/>
</validation-observer>
<domain-options-form ref="form_4" :domain="domain" @createAdmin="updateCreateAdmin" />
<div class="d-flex justify-end mt-8">
<v-btn @click="step = 3" class="mr-10" text>
<translate>Back</translate>
@@ -324,14 +197,20 @@
<script>
import { bus } from '@/main'
import ChoiceField from '@/components/tools/ChoiceField'
import ConfirmDialog from '@/components/layout/ConfirmDialog'
import DomainDNSForm from './DomainDNSForm'
import DomainGeneralForm from './DomainGeneralForm'
import DomainLimitationsForm from './DomainLimitationsForm'
import DomainOptionsForm from './DomainOptionsForm'
export default {
props: ['value'],
components: {
ChoiceField,
ConfirmDialog
ConfirmDialog,
'domain-dns-form': DomainDNSForm,
DomainGeneralForm,
DomainLimitationsForm,
DomainOptionsForm
},
data () {
return {
@@ -339,32 +218,6 @@ export default {
domain: {
domain_admin: {}
},
domainTypes: [
{
label: 'Domain',
icon: 'mdi-earth',
value: 'domain'
},
{
label: 'Relay domain',
icon: 'mdi-earth',
value: 'relaydomain'
}
],
dkimKeyLengths: [
{
label: '1024',
value: 1024
},
{
label: '2048',
value: 2048
},
{
label: '4096',
value: 4096
}
],
formErrors: {},
step: 1
}
@@ -412,12 +265,15 @@ export default {
this.$refs.observer_4.reset()
},
async goToNextStep (current, next) {
const valid = await this.$refs[`observer_${current}`].validate()
const valid = await this.$refs[`form_${current}`].$refs.observer.validate()
if (!valid) {
return
}
this.step = next
},
updateCreateAdmin (value) {
this.createAdmin = value
},
submit () {
const data = JSON.parse(JSON.stringify(this.domain))
if (!this.createAdmin) {
@@ -0,0 +1,53 @@
<template>
<div>
<v-switch
:label="'Enable DNS checks' | translate"
v-model="domain.enable_dns_checks"
/>
<v-switch
:label="'Enable DKIM signing' | translate"
v-model="domain.enable_dkim"
/>
<v-text-field
:label="'DKIM key selector' | translate"
v-model="domain.dkim_key_selector"
:disabled="!domain.enable_dkim"
outlined
/>
<choice-field
v-model="domain.dkim_key_length"
:label="'DKIM key length' | translate"
:choices="dkimKeyLengths"
:disabled="!domain.enable_dkim"
/>
</div>
</template>
<script>
import ChoiceField from '@/components/tools/ChoiceField'
export default {
components: {
ChoiceField
},
props: ['domain'],
data () {
return {
dkimKeyLengths: [
{
label: '1024',
value: 1024
},
{
label: '2048',
value: 2048
},
{
label: '4096',
value: 4096
}
]
}
}
}
</script>
+146 -104
View File
@@ -3,68 +3,122 @@
<v-flex>
<v-card>
<v-card-title>
<h1 class="title" v-if="!domain.pk">Add domain</h1>
<h1 class="title" v-else>Edit domain</h1>
<h1 class="headline"><translate>Edit domain</translate></h1>
</v-card-title>
<v-card-text>
<v-text-field label="Name"
v-model="domain.name"
:error="formErrors['name'] !== undefined"
:error-messages="formErrors['name']"
>
</v-text-field>
<v-select label="Type" :items="domainTypes" />
<v-text-field label="Quota"
v-model="domain.quota"
suffix="MB"
:error="formErrors['quota'] !== undefined"
:error-messages="formErrors['quota']"
>
</v-text-field>
<v-text-field label="Default mailbox quota"
v-model="domain.default_mailbox_quota"
suffix="MB"
:error="formErrors['default_mailbox_quota']"
:error-messages="formErrors['default_mailbox_quota']"
>
</v-text-field>
<v-text-field label="Message sending limit"
v-model="domain.message_limit"
suffix="per day"
:error="formErrors['message_limit']"
:error-messages="formErrors['message_limit']"
>
</v-text-field>
<v-checkbox label="Enabled"
v-model="domain.enabled"
>
</v-checkbox>
<v-checkbox label="Enable DNS checks"
v-model="domain.enable_dns_checks"
>
</v-checkbox>
<v-checkbox label="Enable DKIM signing"
v-model="domain.enable_dkim"
>
</v-checkbox>
<v-text-field label="DKIM key selector"
v-model="domain.dkim_key_selector"
v-if="domain.enable_dkim"
>
</v-text-field>
<v-text-field label="DKIM key length"
v-model="domain.dkim_key_length"
v-if="domain.enable_dkim"
>
</v-text-field>
<v-expansion-panels v-model="panel">
<v-expansion-panel>
<v-expansion-panel-header v-slot="{ open }">
<v-row no-gutters>
<v-col cols="4">
<translate>General</translate>
</v-col>
<v-col
cols="8"
class="text--secondary"
>
<v-fade-transition leave-absolute>
<span v-if="open"></span>
<v-row
v-else
no-gutters
style="width: 100%"
>
<v-col cols="6">
<translate>Name: </translate> {{ editedDomain.name }}
</v-col>
<v-col cols="6">
<translate>Type: </translate> {{ editedDomain.type }}
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-header>
<v-expansion-panel-content>
<domain-general-form ref="generalForm" :domain="editedDomain" />
</v-expansion-panel-content>
</v-expansion-panel>
<v-expansion-panel>
<v-expansion-panel-header v-slot="{ open }">
<v-row no-gutters>
<v-col cols="4">
<translate>DNS</translate>
</v-col>
<v-col
cols="8"
class="text--secondary"
>
<v-fade-transition leave-absolute>
<span v-if="open"></span>
<v-row
v-else
no-gutters
style="width: 100%"
>
<v-col cols="6">
<translate class="mr-2">DNS checks</translate>
<v-icon color="success" v-if="editedDomain.enable_dns_checks">mdi-check-circle-outline</v-icon>
<v-icon v-else>mdi-close-circle-outline</v-icon>
</v-col>
<v-col cols="6">
<translate class="mr-2">DKIM signing</translate>
<v-icon color="success" v-if="editedDomain.enable_dkim">mdi-check-circle-outline</v-icon>
<v-icon v-else>mdi-close-circle-outline</v-icon>
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-header>
<v-expansion-panel-content>
<domain-dns-form ref="dnsForm" :domain="editedDomain" />
</v-expansion-panel-content>
</v-expansion-panel>
<v-expansion-panel>
<v-expansion-panel-header v-slot="{ open }">
<v-row no-gutters>
<v-col cols="4">
<translate>Limitations</translate>
</v-col>
<v-col
cols="8"
class="text--secondary"
>
<v-fade-transition leave-absolute>
<span v-if="open"></span>
<v-row
v-else
no-gutters
style="width: 100%"
>
<v-col cols="6">
<translate class="mr-2">Quota: </translate> {{ domain.quota }}
</v-col>
<v-col cols="6" v-if="domain.message_sending_limit">
<translate class="mr-2">Sending limit: </translate> {{ domain.message_sending_limit }}
</v-col>
<v-col cols="6" v-else>
<translate class="mr-2">No sending limit</translate>
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-header>
<v-expansion-panel-content>
<domain-limitations-form :domain="editedDomain" />
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="cancel">
Cancel
<v-btn color="grey darken-1" text @click="close">
<translate>Cancel</translate>
</v-btn>
<v-btn color="blue darken-1" text @click="save">
Save
<v-btn color="primary darken-1" text @click="save">
<translate>Save</translate>
</v-btn>
</v-card-actions>
</v-card>
@@ -73,63 +127,51 @@
</template>
<script>
import { mapGetters } from 'vuex'
import { bus } from '@/main'
import DomainDNSForm from './DomainDNSForm'
import DomainGeneralForm from './DomainGeneralForm'
import DomainLimitationsForm from './DomainLimitationsForm'
export default {
components: {
'domain-dns-form': DomainDNSForm,
DomainGeneralForm,
DomainLimitationsForm
},
props: ['domain'],
data () {
return {
formErrors: {},
domain: {},
domainTypes: ['Domain', 'Relay domain']
}
},
computed: {
...mapGetters({
getDomainByPk: 'domains/getDomainByPk'
})
},
mounted () {
var domainPk = this.$route.params.domainPk
if (domainPk) {
if (!this.$store.state.domainsLoaded) {
this.$store.dispatch('domains/getDomains').then(response => {
this.loadDomain(domainPk)
})
} else {
this.loadDomain(domainPk)
}
editedDomain: {},
panel: 0
}
},
methods: {
loadDomain (pk) {
this.domain = JSON.parse(
JSON.stringify(this.getDomainByPk(pk))
)
close () {
this.$emit('close')
this.editedDomain = {}
this.panel = 0
},
onSaveError (response) {
this.formErrors = response.data
},
save () {
var action
var msg
if (this.domain.pk) {
action = 'domains/updateDomain'
msg = 'Domain updated.'
} else {
action = 'domains/createDomain'
msg = 'Domain created.'
async save () {
if (this.$refs.generalForm !== undefined) {
const valid = await this.$refs.generalForm.$refs.observer.validate()
if (!valid) {
return
}
}
this.$store.dispatch(action, this.domain).then(response => {
this.$router.push({ name: 'DomainList' })
this.$toast.open({
message: msg,
type: 'is-success',
position: 'is-bottom'
})
}, this.onSaveError)
},
cancel () {
this.$router.push({ name: 'DomainList' })
this.$store.dispatch('domains/updateDomain', this.editedDomain).then(resp => {
this.close()
bus.$emit('notification', { msg: this.$gettext('Domain updated') })
})
}
},
watch: {
domain: {
handler: function (val) {
if (val) {
this.editedDomain = JSON.parse(JSON.stringify(val))
}
},
immediate: true
}
}
}
@@ -0,0 +1,58 @@
<template>
<validation-observer ref="observer">
<validation-provider
v-slot="{ errors }"
rules="required"
>
<v-text-field
:label="'Domain name (ex: domain.tld)' | translate"
v-model="domain.name"
:error-messages="errors"
outlined
/>
</validation-provider>
<validation-provider
v-slot="{ errors }"
rules="required"
>
<choice-field v-model="domain.type"
:label="'Type' | translate"
:choices="domainTypes"
:error-messages="errors"
/>
</validation-provider>
<v-switch
:label="'Enabled' | translate"
v-model="domain.enabled"
:hint="'Control if this domain will be allowed to send and receive messages' | translate"
persistent-hint
/>
</validation-observer>
</template>
<script>
import ChoiceField from '@/components/tools/ChoiceField'
export default {
components: {
ChoiceField
},
props: ['domain'],
data () {
return {
domainTypes: [
{
label: 'Domain',
icon: 'mdi-earth',
value: 'domain'
},
{
label: 'Relay domain',
icon: 'mdi-earth',
value: 'relaydomain'
}
]
}
}
}
</script>
@@ -0,0 +1,51 @@
<template>
<validation-observer ref="observer">
<validation-provider
v-slot="{ errors }"
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. Leave empty for no limit.' | translate"
persistent-hint
v-model="domain.message_sending_limit"
:error-messages="errors"
outlined
/>
</validation-provider>
</validation-observer>
</template>
<script>
export default {
props: ['domain']
}
</script>
+23 -7
View File
@@ -75,7 +75,7 @@
</v-btn>
</template>
<v-list dense>
<v-list-item :to="{ name: 'DomainEdit', params: { domainPk: item.pk }}">
<v-list-item @click="editDomain(item)">
<v-list-item-icon>
<v-icon>mdi-circle-edit-outline</v-icon>
</v-list-item-icon>
@@ -117,6 +117,12 @@
max-width="800px">
<dns-detail @close="showDNSdetail = false" :domain="selectedDomain" />
</v-dialog>
<v-dialog v-model="showDomainForm"
persistent
max-width="800px"
>
<domain-form :domain="selectedDomain" @close="showDomainForm = false" />
</v-dialog>
<v-dialog v-model="showAliasForm"
persistent
max-width="800px">
@@ -131,16 +137,19 @@
<script>
import { mapGetters } from 'vuex'
import { bus } from '@/main'
import domainApi from '@/api/domains'
import ConfirmDialog from '@/components/layout/ConfirmDialog'
import DNSDetail from '@/components/domains/DNSDetail'
import DomainAliasForm from '@/components/domains/DomainAliasForm'
import DomainForm from '@/components/domains/DomainForm'
export default {
components: {
ConfirmDialog,
'dns-detail': DNSDetail,
DomainAliasForm
DomainAliasForm,
DomainForm
},
computed: mapGetters({
domains: 'domains/domains'
@@ -160,6 +169,7 @@ export default {
showConfirmDialog: false,
showAliasForm: false,
showDNSdetail: false,
showDomainForm: false,
search: '',
selected: [],
expanded: [],
@@ -175,12 +185,8 @@ export default {
this.selectedDomain = null
this.selectedDomainAlias = null
},
confirmDelete (domain) {
this.selectedDomain = domain
this.showConfirmDialog = true
},
async deleteDomain (domain) {
await this.$refs.confirm.open(
const confirm = await this.$refs.confirm.open(
this.$gettext('Warning'),
this.$gettext(`Do you really want to delete the domain ${domain.name}?`),
{
@@ -189,6 +195,12 @@ export default {
agreeLabel: this.$gettext('Yes')
}
)
if (!confirm) {
return
}
this.$store.dispatch('domains/deleteDomain', { id: domain.pk }).then(resp => {
bus.$emit('notification', { msg: this.$gettext('Domain delete') })
})
},
domainAliasDeleted () {
const newList = this.aliases[this.selectedDomain.name].filter(alias => {
@@ -197,6 +209,10 @@ export default {
this.$set(this.aliases, this.selectedDomain.name, newList)
this.closeDomainAliasForm()
},
editDomain (domain) {
this.selectedDomain = domain
this.showDomainForm = true
},
editDomainAlias (domain, alias) {
this.selectedDomain = domain
this.selectedDomainAlias = alias
@@ -0,0 +1,57 @@
<template>
<validation-observer ref="observer">
<v-switch v-model="createAdmin"
:label="'Create a domain administrator' | translate"
@change="update"
/>
<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.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.domain_admin.with_aliases"
:label="'Create aliases' | translate"
:disabled="!createAdmin"
:hint="'Create standard aliases for the domain.' | translate"
persistent-hint
/>
</validation-observer>
</template>
<script>
export default {
props: ['domain'],
data () {
return {
createAdmin: false
}
},
methods: {
update (val) {
this.$emit('createAdmin', val)
}
}
}
</script>
+9
View File
@@ -5,6 +5,7 @@ import domainApi from '@/api/domains'
const SET_DOMAINS = 'SET_DOMAINS'
const ADD_DOMAIN = 'ADD_DOMAIN'
const UPDATE_DOMAIN = 'UPDATE_DOMAIN'
const DELETE_DOMAIN = 'DELETE_DOMAIN'
const SET_DOMAIN_ALIASES = 'SET_DOMAIN_ALIASES'
const ADD_DOMAIN_ALIAS = 'ADD_DOMAIN_ALIAS'
const UPDATE_DOMAIN_ALIAS = 'UPDATE_DOMAIN_ALIAS'
@@ -48,6 +49,11 @@ const actions = {
commit(UPDATE_DOMAIN, { domain: response.data })
})
},
deleteDomain ({ commit }, { id, data }) {
return domainApi.deleteDomain(id, data).then(resp => {
commit(DELETE_DOMAIN, id)
})
},
getDomainAliases ({ commit }) {
return Vue.prototype.$axios.get('/domainaliases/?expand=target').then(response => {
commit(SET_DOMAIN_ALIASES, response.data)
@@ -86,6 +92,9 @@ const mutations = {
}
})
},
[DELETE_DOMAIN] (state, domainId) {
state.domains = state.domains.filter(item => item.pk !== domainId)
},
[SET_DOMAIN_ALIASES] (state, domainAliases) {
state.domainAliases = domainAliases
},
View File
View File
@@ -24,7 +24,7 @@ from modoboa.lib import (
)
from modoboa.parameters import tools as param_tools
from . import constants, lib, models
from ... import lib, models
class DomainSerializer(serializers.ModelSerializer):
@@ -110,85 +110,6 @@ 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."""
@@ -571,28 +492,6 @@ class SenderAddressSerializer(serializers.ModelSerializer):
return value
class AdminGlobalParametersSerializer(serializers.Serializer):
"""A serializer for global parameters."""
# Domain settings
enable_mx_checks = serializers.BooleanField(default=True)
valid_mxs = serializers.CharField(allow_blank=True)
domains_must_have_authorized_mx = serializers.BooleanField(default=False)
enable_dnsbl_checks = serializers.BooleanField(default=True)
dkim_keys_storage_dir = serializers.CharField(allow_blank=True)
dkim_default_key_length = serializers.ChoiceField(
default=2048, choices=constants.DKIM_KEY_LENGTHS)
# Mailboxes settings
handle_mailboxes = serializers.BooleanField(default=False)
mailboxes_owner = serializers.CharField(default="vmail")
default_domain_quota = serializers.IntegerField(default=0)
default_mailbox_quota = serializers.IntegerField(default=0)
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 ResetPasswordSerializer(serializers.Serializer):
"""Serializer by the reset password endpoint."""
+16
View File
@@ -0,0 +1,16 @@
"""Admin API urls."""
from rest_framework import routers
from . import viewsets
router = routers.SimpleRouter()
router.register(r"domains", viewsets.DomainViewSet, basename="domain")
router.register(
r"domainaliases", viewsets.DomainAliasViewSet, basename="domain_alias")
router.register(r"accounts", viewsets.AccountViewSet, basename="account")
router.register(r"aliases", viewsets.AliasViewSet, basename="alias")
router.register(
r"senderaddresses", viewsets.SenderAddressViewSet, basename="sender_address")
urlpatterns = router.urls
@@ -17,7 +17,8 @@ from modoboa.core import sms_backends
from modoboa.lib import renderers as lib_renderers
from modoboa.lib import viewsets as lib_viewsets
from . import lib, models, serializers
from ... import lib, models
from . import serializers
@extend_schema_view(
@@ -38,16 +39,12 @@ class DomainViewSet(lib_viewsets.RevisionModelMixin, 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)
View File
+118
View File
@@ -0,0 +1,118 @@
"""Admin API v2 serializers."""
from django.utils.translation import ugettext as _
from rest_framework import serializers
from modoboa.admin.api.v1 import serializers as v1_serializers
from modoboa.core import models as core_models, signals as core_signals
from modoboa.lib import exceptions as lib_exceptions
from modoboa.parameters import tools as param_tools
from ... import constants, lib, models
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 DomainSerializer(v1_serializers.DomainSerializer):
"""Domain serializer for v2 API."""
domain_admin = CreateDomainAdminSerializer(required=False)
class Meta(v1_serializers.DomainSerializer.Meta):
fields = v1_serializers.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 DeleteDomainSerializer(serializers.Serializer):
"""Serializer used with delete operation."""
keep_folder = serializers.BooleanField(default=False)
class AdminGlobalParametersSerializer(serializers.Serializer):
"""A serializer for global parameters."""
# Domain settings
enable_mx_checks = serializers.BooleanField(default=True)
valid_mxs = serializers.CharField(allow_blank=True)
domains_must_have_authorized_mx = serializers.BooleanField(default=False)
enable_dnsbl_checks = serializers.BooleanField(default=True)
dkim_keys_storage_dir = serializers.CharField(allow_blank=True)
dkim_default_key_length = serializers.ChoiceField(
default=2048, choices=constants.DKIM_KEY_LENGTHS)
# Mailboxes settings
handle_mailboxes = serializers.BooleanField(default=False)
default_domain_quota = serializers.IntegerField(default=0)
default_mailbox_quota = serializers.IntegerField(default=0)
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)
+19
View File
@@ -0,0 +1,19 @@
"""Admin API v2 urls."""
from rest_framework import routers
from modoboa.admin.api.v1 import viewsets as v1_viewsets
from . import viewsets
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"aliases", v1_viewsets.AliasViewSet, basename="alias")
router.register(
r"senderaddresses", v1_viewsets.SenderAddressViewSet,
basename="sender_address")
urlpatterns = router.urls
+70
View File
@@ -0,0 +1,70 @@
"""Admin API v2 viewsets."""
from django.utils.translation import ugettext as _
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import 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.lib import viewsets as lib_viewsets
from ... import models
from . import serializers
@extend_schema_view(
retrieve=extend_schema(
description="Retrieve a particular domain",
summary="Retrieve a particular domain"
),
list=extend_schema(
description="Retrieve a list of domains",
summary="Retrieve a list of domains"
),
create=extend_schema(
description="Create a new domain",
summary="Create a new domain"
),
delete=extend_schema(
description="Delete a particular domain",
summary="Delete a particular domain"
)
)
class DomainViewSet(lib_viewsets.RevisionModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet):
"""V2 viewset."""
permission_classes = (
permissions.IsAuthenticated, permissions.DjangoModelPermissions,
)
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.action == "create":
return serializers.DomainSerializer
if self.action == "delete":
return serializers.DeleteDomainSerializer
return v1_serializers.DomainSerializer
@action(methods=["post"], detail=True)
def delete(self, request, **kwargs):
"""Custom delete method that accepts body arguments."""
domain = self.get_object()
if not request.user.can_access(domain):
raise PermissionDenied(_("You don't have access to this domain"))
mb = getattr(request.user, "mailbox", None)
if mb and mb.domain == domain:
raise PermissionDenied(_("You can't delete your own domain"))
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
domain.delete(request.user, serializer.validated_data["keep_folder"])
return response.Response(status=status.HTTP_204_NO_CONTENT)
+1 -1
View File
@@ -385,7 +385,7 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([
def load_admin_settings():
"""Load admin settings."""
from modoboa.parameters import tools as param_tools
from . import serializers
from .api.v2 import serializers
param_tools.registry.add(
"global", AdminParametersForm, ugettext_lazy("Administration"))
-16
View File
@@ -1,16 +0,0 @@
"""Admin API urls."""
from rest_framework import routers
from . import api
router = routers.SimpleRouter()
router.register(r"domains", api.DomainViewSet, basename="domain")
router.register(
r"domainaliases", api.DomainAliasViewSet, basename="domain_alias")
router.register(r"accounts", api.AccountViewSet, basename="account")
router.register(r"aliases", api.AliasViewSet, basename="alias")
router.register(
r"senderaddresses", api.SenderAddressViewSet, basename="sender_address")
urlpatterns = router.urls
+2 -2
View File
@@ -3,7 +3,7 @@
from rest_framework import permissions, response, viewsets
from rest_framework.decorators import action
from modoboa.admin import serializers as admin_serializers
from modoboa.admin.api.v1 import serializers as admin_v1_serializers
from modoboa.core.api.v1 import viewsets as core_v1_viewsets
from ... import models
@@ -16,7 +16,7 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet):
@action(methods=["get"], detail=False)
def me(self, request):
"""Return information about connected user."""
serializer = admin_serializers.AccountSerializer(request.user)
serializer = admin_v1_serializers.AccountSerializer(request.user)
return response.Response(serializer.data)
+1 -1
View File
@@ -8,7 +8,7 @@ app_name = "api"
urlpatterns = [
path('', include("modoboa.core.api.v1.urls")),
path('', include("modoboa.admin.urls_api")),
path('', include("modoboa.admin.api.v1.urls")),
path('', include("modoboa.limits.urls_api")),
path('', include("modoboa.relaydomains.urls_api"))
]
+1 -1
View File
@@ -16,7 +16,7 @@ urlpatterns = [
path('token/refresh/', TokenRefreshView.as_view(),
name='token_refresh'),
path('', include("modoboa.core.api.v2.urls")),
path('', include("modoboa.admin.urls_api")),
path('', include("modoboa.admin.api.v2.urls")),
path('', include("modoboa.parameters.api.v2.urls")),
path('', include("modoboa.limits.urls_api")),
path('', include("modoboa.relaydomains.urls_api")),