mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Added more actions on identities:
* create/edit/delete alias * edit/delete account
This commit is contained in:
@@ -20,5 +20,8 @@ export default {
|
||||
},
|
||||
patch (accountId, data) {
|
||||
return repository.patch(`${resource}/${accountId}/`, data)
|
||||
},
|
||||
delete (accountId, data) {
|
||||
return repository.post(`${resource}/${accountId}/delete/`, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import repository from './repository'
|
||||
|
||||
const resource = 'aliases'
|
||||
|
||||
export default {
|
||||
get (aliasId) {
|
||||
return repository.get(`${resource}/${aliasId}/`)
|
||||
},
|
||||
getRandomAddress () {
|
||||
return repository.get(`${resource}/random_address/`)
|
||||
},
|
||||
validate (data) {
|
||||
return repository.post(`${resource}/validate/`, data)
|
||||
},
|
||||
create (data) {
|
||||
return repository.post(`${resource}/`, data)
|
||||
},
|
||||
patch (aliasId, data) {
|
||||
return repository.patch(`${resource}/${aliasId}/`, data)
|
||||
},
|
||||
delete (aliasId) {
|
||||
return repository.delete(`${resource}/${aliasId}/`)
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,13 @@ export default {
|
||||
getApplications () {
|
||||
return repository.get(`/${resource}/applications/`)
|
||||
},
|
||||
getAppliction (app) {
|
||||
getApplication (app) {
|
||||
return repository.get(`/${resource}/${app}/`)
|
||||
},
|
||||
getApplicationStructure (app) {
|
||||
return repository.get(`/${resource}/structure/?app=${app}`)
|
||||
},
|
||||
saveApplication (app, data) {
|
||||
return repository.post(`/${resource}/${app}/`, data)
|
||||
return repository.put(`/${resource}/${app}/`, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
name="name"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label">{{ $gettext('Name') }}</label>
|
||||
<v-text-field
|
||||
v-model="form.name"
|
||||
:label="'Name' | translate"
|
||||
dense
|
||||
outlined
|
||||
:error-messages="errors"
|
||||
/>
|
||||
</validation-provider>
|
||||
@@ -21,14 +23,16 @@
|
||||
name="target"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label">{{ $gettext('Choose a domain') }}</label>
|
||||
<v-select
|
||||
v-model="form.target"
|
||||
:items="domains"
|
||||
item-text="name"
|
||||
item-value="pk"
|
||||
:label="'Choose a domain' | translate"
|
||||
:error-messages="errors"
|
||||
single-line
|
||||
dense
|
||||
outlined
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-switch
|
||||
|
||||
@@ -1,130 +1,31 @@
|
||||
<template>
|
||||
<div class="d-flex justify-center inner">
|
||||
<v-stepper v-model="step">
|
||||
<v-stepper-header class="align-center px-10">
|
||||
<v-img
|
||||
src="../../assets/Modoboa_RVB-BLEU-SANS.png"
|
||||
max-width="190"
|
||||
/>
|
||||
<v-stepper-step :complete="step > 1" step="1">
|
||||
<translate>General</translate>
|
||||
</v-stepper-step>
|
||||
<v-stepper-step :complete="step > 2" step="2">
|
||||
<translate>DNS</translate>
|
||||
</v-stepper-step>
|
||||
<v-stepper-step :complete="step > 3" step="3">
|
||||
<translate>Limitations</translate>
|
||||
</v-stepper-step>
|
||||
<v-stepper-step :complete="step > 4" step="4">
|
||||
<translate>Options</translate>
|
||||
</v-stepper-step>
|
||||
<v-stepper-step step="5">
|
||||
<translate>Summary</translate>
|
||||
</v-stepper-step>
|
||||
<v-btn icon @click="close">
|
||||
<v-icon color="primary" x-large>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-stepper-header>
|
||||
<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>
|
||||
<domain-general-form ref="form_1" :domain="domain" />
|
||||
<div class="d-flex justify-end">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="goToNextStep(1, 2)"
|
||||
large
|
||||
>
|
||||
<translate>Next</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
<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>
|
||||
<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>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="step = 3"
|
||||
large
|
||||
>
|
||||
<translate>Next</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
<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>
|
||||
<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>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="goToNextStep(3, 4)"
|
||||
large
|
||||
>
|
||||
<translate>Next</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
<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>
|
||||
<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>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="goToNextStep(4, 5)"
|
||||
large
|
||||
>
|
||||
<translate>Next</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
<v-stepper-content step="5" class="flex-grow-0">
|
||||
<div class="text-center text-h3"><translate>Summary</translate></div>
|
||||
<creation-summary
|
||||
:sections="summarySections"
|
||||
@modify-step="val => step = val"
|
||||
/>
|
||||
<div class="d-flex justify-center mt-8">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="submit"
|
||||
large
|
||||
>
|
||||
<translate>Confirm and create</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
</v-stepper-items>
|
||||
</v-stepper>
|
||||
<confirm-dialog ref="confirm" />
|
||||
</div>
|
||||
<creation-form
|
||||
:title="'New domain'|translate"
|
||||
:steps="steps"
|
||||
:form-observer-getter="getObserver"
|
||||
:validate-object="validateDomain"
|
||||
:summary-sections="summarySections"
|
||||
@close="close"
|
||||
@create="submit"
|
||||
>
|
||||
<template v-slot:form.general="{ step }">
|
||||
<domain-general-form :ref="`form_${step}`" :domain="domain" />
|
||||
</template>
|
||||
<template v-slot:form.dns="{ step }">
|
||||
<domain-dns-form :ref="`form_${step}`" :domain="domain" />
|
||||
</template>
|
||||
<template v-slot:form.limitations="{ step }">
|
||||
<domain-limitations-form :ref="`form_${step}`" :domain="domain" />
|
||||
</template>
|
||||
<template v-slot:form.options="{ step }">
|
||||
<domain-options-form :ref="`form_${step}`" :domain="domain" @createAdmin="updateCreateAdmin" />
|
||||
</template>
|
||||
</creation-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog'
|
||||
import CreationSummary from '@/components/tools/CreationSummary'
|
||||
import CreationForm from '@/components/tools/CreationForm'
|
||||
import DomainDNSForm from './DomainDNSForm'
|
||||
import DomainGeneralForm from './DomainGeneralForm'
|
||||
import DomainLimitationsForm from './DomainLimitationsForm'
|
||||
@@ -133,8 +34,7 @@ import DomainOptionsForm from './DomainOptionsForm'
|
||||
export default {
|
||||
props: ['value'],
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
CreationSummary,
|
||||
CreationForm,
|
||||
'domain-dns-form': DomainDNSForm,
|
||||
DomainGeneralForm,
|
||||
DomainLimitationsForm,
|
||||
@@ -205,7 +105,12 @@ export default {
|
||||
domain_admin: {}
|
||||
},
|
||||
formErrors: {},
|
||||
step: 1
|
||||
steps: [
|
||||
{ name: 'general', title: this.$gettext('General') },
|
||||
{ name: 'dns', title: this.$gettext('DNS') },
|
||||
{ name: 'limitations', title: this.$gettext('Limitations') },
|
||||
{ name: 'options', title: this.$gettext('Options') }
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
@@ -230,32 +135,15 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
async close (withConfirm) {
|
||||
if (withConfirm) {
|
||||
const confirm = await this.$refs.confirm.open(
|
||||
this.$gettext('Warning'),
|
||||
this.$gettext('If you close this form now, your modifications won\'t be saved. Do you confirm?'),
|
||||
{
|
||||
color: 'error'
|
||||
}
|
||||
)
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.$emit('close')
|
||||
close () {
|
||||
this.initDomain()
|
||||
this.step = 1
|
||||
this.$refs.observer_1.reset()
|
||||
this.$refs.observer_3.reset()
|
||||
this.$refs.observer_4.reset()
|
||||
this.$emit('close')
|
||||
},
|
||||
async goToNextStep (current, next) {
|
||||
const valid = await this.$refs[`form_${current}`].$refs.observer.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
this.step = next
|
||||
getObserver (step) {
|
||||
return this.$refs[`form_${step}`].$refs.observer
|
||||
},
|
||||
validateDomain () {
|
||||
|
||||
},
|
||||
updateCreateAdmin (value) {
|
||||
this.createAdmin = value
|
||||
@@ -273,33 +161,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.inner {
|
||||
background-color: #fff;
|
||||
}
|
||||
.v-stepper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&__content {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
&__items {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
.subtitle {
|
||||
color: #000;
|
||||
border-bottom: 1px solid #DBDDDF;
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,143 +1,44 @@
|
||||
<template>
|
||||
<div class="d-flex justify-center inner">
|
||||
<v-stepper v-model="step">
|
||||
<v-stepper-header class="align-center px-10">
|
||||
<v-img
|
||||
src="../../assets/Modoboa_RVB-BLEU-SANS.png"
|
||||
max-width="190"
|
||||
/>
|
||||
<v-stepper-step :complete="step > 1" step="1">
|
||||
<translate>Role</translate>
|
||||
</v-stepper-step>
|
||||
<v-stepper-step :complete="step > 2" step="2">
|
||||
<translate>Identification</translate>
|
||||
</v-stepper-step>
|
||||
<v-stepper-step :complete="step > 3" step="3">
|
||||
<translate>Mailbox</translate>
|
||||
</v-stepper-step>
|
||||
<v-stepper-step :complete="step > 4" step="4">
|
||||
<translate>Aliases</translate>
|
||||
</v-stepper-step>
|
||||
<v-stepper-step step="5">
|
||||
<translate>Summary</translate>
|
||||
</v-stepper-step>
|
||||
<v-btn icon @click="close">
|
||||
<v-icon color="primary" x-large>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-stepper-header>
|
||||
<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 account</translate> /
|
||||
<translate>Role</translate>
|
||||
</div>
|
||||
<account-role-form ref="form_1" :account="account" />
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="goToNextStep(1, 2)"
|
||||
large
|
||||
>
|
||||
<translate>Next</translate>
|
||||
<creation-form
|
||||
:title="'New account'|translate"
|
||||
:steps="steps"
|
||||
:form-observer-getter="getObserver"
|
||||
:validate-object="validateAccount"
|
||||
:summary-sections="summarySections"
|
||||
@close="close"
|
||||
@create="submit"
|
||||
>
|
||||
<template v-slot:form.role="{ step }">
|
||||
<account-role-form :ref="`form_${step}`" :account="account" />
|
||||
</template>
|
||||
<template v-slot:form.identification="{ step }">
|
||||
<account-general-form v-if="step >= 2" :ref="`form_${step}`" :account="account" />
|
||||
</template>
|
||||
<template v-slot:form.mailbox="{ step }">
|
||||
<account-mailbox-form :ref="`form_${step}`" :account="account" />
|
||||
</template>
|
||||
<template v-slot:form.aliases="{ step }">
|
||||
<account-alias-form v-if="step >= 4" :ref="`form_${step}`" :account="account" />
|
||||
</template>
|
||||
<template v-slot:item.random_password="{ item }">
|
||||
<v-col cols="12" class="highligth white--text">
|
||||
<v-row>
|
||||
<v-col><span>{{ item.key }}</span></v-col>
|
||||
<v-col class="text-right" v-if="item.type === 'yesno'">
|
||||
{{ item.value|yesno }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="text-right py-1">
|
||||
{{ account.password }}
|
||||
<v-btn icon color="white" :title="'Copy to clipboard'|translate" @click="copyPassword">
|
||||
<v-icon>mdi-clipboard-multiple-outline</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
<v-stepper-content step="2" class="flex-grow-0">
|
||||
<div class="mb-6 text-h5">
|
||||
<translate class="grey--text text--darken-1">New account</translate> /
|
||||
<translate>Identification</translate>
|
||||
</div>
|
||||
<account-general-form v-if="step >= 2" ref="form_2" :account="account" />
|
||||
<div class="d-flex justify-end">
|
||||
<v-btn @click="step = 1" class="mr-10" text>
|
||||
<translate>Back</translate>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="goToNextStep(2, 3)"
|
||||
large
|
||||
>
|
||||
<translate>Next</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
<v-stepper-content step="3" class="flex-grow-0">
|
||||
<div class="mb-6 text-h5">
|
||||
<translate class="grey--text text--darken-1">New account</translate> /
|
||||
<translate>Mailbox</translate>
|
||||
</div>
|
||||
<account-mailbox-form ref="form_3" :account="account" />
|
||||
<div class="d-flex justify-end">
|
||||
<v-btn @click="step = 2" class="mr-10" text>
|
||||
<translate>Back</translate>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="goToNextStep(3, 4)"
|
||||
large
|
||||
>
|
||||
<translate>Next</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
<v-stepper-content step="4" class="flex-grow-0">
|
||||
<div class="mb-6 text-h5">
|
||||
<translate class="grey--text text--darken-1">New account</translate> /
|
||||
<translate>Aliases</translate>
|
||||
</div>
|
||||
<account-alias-form v-if="step >= 4" ref="form_4" :account="account" />
|
||||
<div class="d-flex justify-end">
|
||||
<v-btn @click="step = 3" class="mr-10" text>
|
||||
<translate>Back</translate>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="goToNextStep(4, 5)"
|
||||
large
|
||||
>
|
||||
<translate>Next</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
<v-stepper-content step="5" class="flex-grow-0">
|
||||
<div class="text-center text-h3"><translate>Summary</translate></div>
|
||||
<creation-summary
|
||||
:sections="summarySections"
|
||||
@modify-step="val => step = val"
|
||||
>
|
||||
<template v-slot:item.random_password="{ item }">
|
||||
<v-col cols="12" class="highligth white--text">
|
||||
<v-row>
|
||||
<v-col><span>{{ item.key }}</span></v-col>
|
||||
<v-col class="text-right" v-if="item.type === 'yesno'">
|
||||
{{ item.value|yesno }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="text-right py-1">
|
||||
{{ account.password }}
|
||||
<v-btn icon color="white" :title="'Copy to clipboard'|translate" @click="copyPassword">
|
||||
<v-icon>mdi-clipboard-multiple-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</template>
|
||||
</creation-summary>
|
||||
<div class="d-flex justify-center mt-8">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="submit"
|
||||
large
|
||||
>
|
||||
<translate>Confirm and create</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
</v-stepper-items>
|
||||
</v-stepper>
|
||||
<confirm-dialog ref="confirm" />
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</template>
|
||||
</creation-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -147,8 +48,7 @@ import AccountAliasForm from './AccountAliasForm'
|
||||
import AccountGeneralForm from './AccountGeneralForm'
|
||||
import AccountMailboxForm from './AccountMailboxForm'
|
||||
import AccountRoleForm from './AccountRoleForm'
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog'
|
||||
import CreationSummary from '@/components/tools/CreationSummary'
|
||||
import CreationForm from '@/components/tools/CreationForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -156,8 +56,7 @@ export default {
|
||||
AccountGeneralForm,
|
||||
AccountMailboxForm,
|
||||
AccountRoleForm,
|
||||
ConfirmDialog,
|
||||
CreationSummary
|
||||
CreationForm
|
||||
},
|
||||
computed: {
|
||||
summarySections () {
|
||||
@@ -220,7 +119,24 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
account: this.getInitialForm(),
|
||||
step: 1
|
||||
steps: [
|
||||
{
|
||||
name: 'role',
|
||||
title: this.$gettext('Role')
|
||||
},
|
||||
{
|
||||
name: 'identification',
|
||||
title: this.$gettext('Identification')
|
||||
},
|
||||
{
|
||||
name: 'mailbox',
|
||||
title: this.$gettext('Mailbox')
|
||||
},
|
||||
{
|
||||
name: 'aliases',
|
||||
title: this.$gettext('Aliases')
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -240,40 +156,15 @@ export default {
|
||||
random_password: true
|
||||
}
|
||||
},
|
||||
async close (withConfirm) {
|
||||
if (withConfirm) {
|
||||
const confirm = await this.$refs.confirm.open(
|
||||
this.$gettext('Warning'),
|
||||
this.$gettext('If you close this form now, your modifications won\'t be saved. Do you confirm?'),
|
||||
{
|
||||
color: 'error'
|
||||
}
|
||||
)
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
}
|
||||
getObserver (step) {
|
||||
return this.$refs[`form_${step}`].$refs.observer
|
||||
},
|
||||
validateAccount () {
|
||||
return accounts.validate(this.account)
|
||||
},
|
||||
close () {
|
||||
this.account = this.getInitialForm()
|
||||
this.$emit('close')
|
||||
this.step = 1
|
||||
},
|
||||
async goToNextStep (current, next) {
|
||||
const valid = await this.$refs[`form_${current}`].$refs.observer.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await accounts.validate(this.account)
|
||||
} catch (error) {
|
||||
if (error.response.data.mailbox && error.response.data.mailbox.full_address) {
|
||||
this.$refs[`form_${current}`].$refs.observer.setErrors({
|
||||
username: error.response.data.mailbox.full_address
|
||||
})
|
||||
}
|
||||
this.$refs[`form_${current}`].$refs.observer.setErrors(error.response.data)
|
||||
return
|
||||
}
|
||||
this.step = next
|
||||
},
|
||||
submit () {
|
||||
accounts.create(this.account).then(resp => {
|
||||
@@ -290,33 +181,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.inner {
|
||||
background-color: #fff;
|
||||
}
|
||||
.v-stepper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&__content {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
&__items {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
.subtitle {
|
||||
color: #000;
|
||||
border-bottom: 1px solid #DBDDDF;
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
.highligth {
|
||||
background-color: #515D78;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<creation-form
|
||||
:title="'New alias'|translate"
|
||||
:steps="steps"
|
||||
:form-observer-getter="getObserver"
|
||||
:validate-object="validateAlias"
|
||||
:summary-sections="summarySections"
|
||||
@close="close"
|
||||
@create="submit"
|
||||
>
|
||||
<template v-slot:form.general="{ step }">
|
||||
<alias-general-form :ref="`form_${step}`" :alias="alias" />
|
||||
</template>
|
||||
<template v-slot:form.recipients="{ step }">
|
||||
<alias-recipient-form :ref="`form_${step}`" :alias="alias" />
|
||||
</template>
|
||||
</creation-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import aliases from '@/api/aliases'
|
||||
import AliasGeneralForm from '@/components/identities/AliasGeneralForm'
|
||||
import AliasRecipientForm from './AliasRecipientForm'
|
||||
import CreationForm from '@/components/tools/CreationForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AliasGeneralForm,
|
||||
AliasRecipientForm,
|
||||
CreationForm
|
||||
},
|
||||
computed: {
|
||||
summarySections () {
|
||||
const result = [
|
||||
{
|
||||
title: this.$gettext('General'),
|
||||
items: [
|
||||
{ key: this.$gettext('Random address'), value: this.alias.random, type: 'yesno' },
|
||||
{ key: this.$gettext('Address'), value: this.alias.address }
|
||||
]
|
||||
}
|
||||
]
|
||||
if (this.alias.expire_at !== undefined) {
|
||||
result[0].items.push({
|
||||
key: this.$gettext('Expire at'), value: this.alias.expire_at
|
||||
})
|
||||
}
|
||||
if (this.alias.description !== undefined) {
|
||||
result[0].items.push({
|
||||
key: this.$gettext('Description'), value: this.alias.description
|
||||
})
|
||||
}
|
||||
result.push({
|
||||
title: this.$gettext('Recipients'),
|
||||
items: [
|
||||
{ key: this.$gettext('Recipients'), value: this.alias.recipients.join(', ') }
|
||||
]
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
alias: this.getInitialForm(),
|
||||
steps: [
|
||||
{
|
||||
name: 'general',
|
||||
title: this.$gettext('General')
|
||||
},
|
||||
{
|
||||
name: 'recipients',
|
||||
title: this.$gettext('Recipients')
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.alias = this.getInitialForm()
|
||||
this.$emit('close')
|
||||
},
|
||||
getInitialForm () {
|
||||
return {
|
||||
random: false,
|
||||
enabled: true,
|
||||
recipients: []
|
||||
}
|
||||
},
|
||||
getObserver (step) {
|
||||
return this.$refs[`form_${step}`].$refs.observer
|
||||
},
|
||||
validateAlias () {
|
||||
const data = { ...this.alias }
|
||||
if (data.recipients.length === 0) {
|
||||
delete data.recipients
|
||||
}
|
||||
return aliases.validate(data)
|
||||
},
|
||||
submit () {
|
||||
aliases.create(this.alias).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Alias created') })
|
||||
this.$emit('created')
|
||||
this.close()
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('identities/fetchDomains')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div>
|
||||
<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>Address: </translate> {{ editedAlias.address }}
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<translate class="mr-2">Enabled</translate>
|
||||
<v-icon color="success" v-if="editedAlias.enabled">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>
|
||||
<alias-general-form ref="generalForm" :alias="editedAlias" />
|
||||
</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>Recipients</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-chip
|
||||
v-for="(rcpt, index) in editedAlias.recipients"
|
||||
:key="index"
|
||||
small
|
||||
>
|
||||
{{ rcpt }}
|
||||
</v-chip>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<alias-recipient-form ref="recipientForm" :alias="editedAlias" />
|
||||
</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)">
|
||||
<translate>Cancel</translate>
|
||||
</v-btn>
|
||||
<v-btn class="ml-4" color="primary darken-1" @click="save">
|
||||
<translate>Save</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import aliases from '@/api/aliases'
|
||||
import AliasGeneralForm from './AliasGeneralForm'
|
||||
import AliasRecipientForm from './AliasRecipientForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AliasGeneralForm,
|
||||
AliasRecipientForm
|
||||
},
|
||||
props: ['alias'],
|
||||
data () {
|
||||
return {
|
||||
editedAlias: {},
|
||||
panel: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save () {
|
||||
if (this.$refs.generalForm !== undefined) {
|
||||
const valid = await this.$refs.generalForm.$refs.observer.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
await aliases.patch(this.editedAlias.pk, this.editedAlias).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Alias updated') })
|
||||
})
|
||||
} catch (error) {
|
||||
if (this.$refs.generalForm) {
|
||||
this.$refs.generalForm.$refs.observer.setErrors(error.response.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
alias: {
|
||||
handler: function (val) {
|
||||
if (val) {
|
||||
this.editedAlias = { ...val }
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<v-switch
|
||||
v-if="!alias.pk"
|
||||
v-model="alias.random"
|
||||
:label="'Random address'|translate"
|
||||
/>
|
||||
<validation-provider
|
||||
v-if="!alias.random"
|
||||
v-slot="{ errors }"
|
||||
name="address"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label">{{ $gettext('Email address') }}</label>
|
||||
<email-field
|
||||
ref="address"
|
||||
v-model="alias.address"
|
||||
:error-messages="errors"
|
||||
type="email"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-if="alias.random"
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label">{{ $gettext('Domain') }}</label>
|
||||
<v-select
|
||||
v-model="alias.domain"
|
||||
:items="domains"
|
||||
:error-messages="errors"
|
||||
item-text="name"
|
||||
return-object
|
||||
outlined
|
||||
dense
|
||||
@change="updateAddress"
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-switch
|
||||
v-model="alias.enabled"
|
||||
:label="'Enabled'|translate"
|
||||
/>
|
||||
<label class="m-label">{{ $gettext('Expire at') }}</label>
|
||||
<v-text-field
|
||||
v-model="alias.expire"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
<label class="m-label">{{ $gettext('Description') }}</label>
|
||||
<v-textarea
|
||||
v-model="alias.description"
|
||||
rows="2"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import aliases from '@/api/aliases'
|
||||
import EmailField from '@/components/tools/EmailField'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmailField
|
||||
},
|
||||
props: ['alias'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
domains: 'identities/domains'
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
updateAddress (value) {
|
||||
aliases.getRandomAddress().then(resp => {
|
||||
this.$set(this.alias, 'address', `${resp.data.address}@${value.name}`)
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
alias (val) {
|
||||
console.log(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
vid="recipients"
|
||||
v-slot="{ errors }"
|
||||
>
|
||||
<email-field
|
||||
ref="recipientField"
|
||||
v-model="recipient"
|
||||
:placeholder="'Start typing a name here...'|translate"
|
||||
@domain-selected="addRecipient"
|
||||
:hint="'Alias(es) of this mailbox. To create a catchall alias, just enter the domain name (@domain.tld).'|translate"
|
||||
persistent-hint
|
||||
allow-add
|
||||
:error-messages="errors"
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-chip
|
||||
v-for="(recipient, index) in alias.recipients"
|
||||
:key="index"
|
||||
class="mr-2 mt-2"
|
||||
close
|
||||
@click:close="alias.recipients.splice(index, 1)"
|
||||
>
|
||||
{{ recipient }}
|
||||
</v-chip>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmailField from '@/components/tools/EmailField'
|
||||
|
||||
export default {
|
||||
props: ['alias'],
|
||||
components: {
|
||||
EmailField
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
recipient: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async addRecipient () {
|
||||
this.alias.recipients.push(this.recipient)
|
||||
this.$refs.recipientField.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -54,16 +54,27 @@
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<confirm-dialog ref="confirmAlias" />
|
||||
<confirm-dialog ref="confirmAccount">
|
||||
<v-checkbox v-model="keepAccountFolder"
|
||||
:label="'Do not delete account folder'|translate"
|
||||
hide-details
|
||||
/>
|
||||
</confirm-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import identities from '@/api/identities'
|
||||
import accounts from '@/api/accounts'
|
||||
import aliases from '@/api/aliases'
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog'
|
||||
import MenuItems from '@/components/tools/MenuItems'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
MenuItems
|
||||
},
|
||||
data () {
|
||||
@@ -75,23 +86,73 @@ export default {
|
||||
{ text: this.$gettext('Actions'), value: 'actions', sortable: false, align: 'right' }
|
||||
],
|
||||
identities: [],
|
||||
keepAccountFolder: false,
|
||||
search: '',
|
||||
selected: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async deleteAlias (alias) {
|
||||
const confirm = await this.$refs.confirmAlias.open(
|
||||
this.$gettext('Warning'),
|
||||
this.$gettext(`Do you really want to delete the alias ${alias.identity}?`),
|
||||
{
|
||||
color: 'error',
|
||||
cancelLabel: this.$gettext('No'),
|
||||
agreeLabel: this.$gettext('Yes')
|
||||
}
|
||||
)
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
aliases.delete(alias.pk).then(() => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Alias deleted') })
|
||||
this.fetchIdentities()
|
||||
})
|
||||
},
|
||||
async deleteAccount (account) {
|
||||
const confirm = await this.$refs.confirmAccount.open(
|
||||
this.$gettext('Warning'),
|
||||
this.$gettext(`Do you really want to delete the account ${account.identity}?`),
|
||||
{
|
||||
color: 'error',
|
||||
cancelLabel: this.$gettext('No'),
|
||||
agreeLabel: this.$gettext('Yes')
|
||||
}
|
||||
)
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
const data = { keepdir: this.keepAccountFolder }
|
||||
accounts.delete(account.pk, data).then(() => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Account deleted') })
|
||||
this.keepAccountFolder = false
|
||||
this.fetchIdentities()
|
||||
}).catch(error => {
|
||||
bus.$emit('notification', { msg: error.response.data, type: 'error' })
|
||||
})
|
||||
},
|
||||
editAccount (account) {
|
||||
this.$router.push({ name: 'AccountEdit', params: { id: account.pk } })
|
||||
},
|
||||
editAlias (alias) {
|
||||
this.$router.push({ name: 'AliasEdit', params: { id: alias.pk } })
|
||||
},
|
||||
fetchIdentities () {
|
||||
identities.getAll().then(resp => {
|
||||
this.identities = resp.data
|
||||
})
|
||||
},
|
||||
getMenuItems (item) {
|
||||
return [
|
||||
{ label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editAccount }
|
||||
]
|
||||
const result = []
|
||||
if (item.type === 'account') {
|
||||
result.push({ label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editAccount })
|
||||
result.push({ label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteAccount, color: 'red' })
|
||||
} else if (item.type === 'alias') {
|
||||
result.push({ label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editAlias })
|
||||
result.push({ label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteAlias, color: 'red' })
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
|
||||
@@ -15,34 +15,39 @@
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<template v-for="(param, index) in displayableParams(element.parameters)">
|
||||
<v-switch :key="index"
|
||||
:label="param.label"
|
||||
v-model="parameters[param.name]"
|
||||
:hint="param.help_text"
|
||||
persistent-hint
|
||||
class="my-2"
|
||||
v-if="param.widget === 'BooleanField'"
|
||||
/>
|
||||
<v-select :key="index"
|
||||
:label="param.label"
|
||||
v-model="parameters[param.name]"
|
||||
:items="param.choices"
|
||||
:hint="param.help_text"
|
||||
persistent-hint
|
||||
class="my-2"
|
||||
v-else-if="param.widget === 'ChoiceField'"
|
||||
/>
|
||||
<v-text-field
|
||||
:key="index"
|
||||
:label="param.label"
|
||||
:hint="param.help_text"
|
||||
persistent-hint
|
||||
v-model="parameters[param.name]"
|
||||
:error="formErrors[param.name] !== undefined"
|
||||
:error-messages="formErrors[param.name]"
|
||||
class="my-2"
|
||||
v-else
|
||||
/>
|
||||
<div class="my-4" :key="index">
|
||||
<template v-if="param.widget === 'BooleanField'">
|
||||
<v-switch
|
||||
v-model="parameters[param.name]"
|
||||
:label="param.label"
|
||||
:hint="param.help_text"
|
||||
persistent-hint
|
||||
v-if="param.widget === 'BooleanField'"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<label class="m-label">{{ param.label }}</label>
|
||||
<v-select
|
||||
v-model="parameters[param.name]"
|
||||
:items="param.choices"
|
||||
:hint="param.help_text"
|
||||
persistent-hint
|
||||
v-if="param.widget === 'ChoiceField'"
|
||||
dense
|
||||
outlined
|
||||
/>
|
||||
<v-text-field
|
||||
:hint="param.help_text"
|
||||
persistent-hint
|
||||
v-model="parameters[param.name]"
|
||||
:error="formErrors[param.name] !== undefined"
|
||||
:error-messages="formErrors[param.name]"
|
||||
v-else
|
||||
dense
|
||||
outlined
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -64,6 +69,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import parameters from '@/api/parameters'
|
||||
|
||||
export default {
|
||||
@@ -112,9 +118,10 @@ export default {
|
||||
return params.filter(param => this.display(param))
|
||||
},
|
||||
save () {
|
||||
parameters.saveApplication(this.$route.params.app, this.parameters).then(response => {
|
||||
}, response => {
|
||||
this.formErrors = response.data
|
||||
parameters.saveApplication(this.$route.params.app, this.parameters).then(() => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Parameters updated') })
|
||||
}).catch(error => {
|
||||
this.formErrors = error.response.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="d-flex justify-center inner">
|
||||
<v-stepper v-model="currentStep">
|
||||
<v-stepper-header class="align-center px-10">
|
||||
<v-img
|
||||
src="../../assets/Modoboa_RVB-BLEU-SANS.png"
|
||||
max-width="190"
|
||||
/>
|
||||
<v-stepper-step
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
:complete="currentStep > index + 1" :step="index + 1">
|
||||
{{ step.title }}
|
||||
</v-stepper-step>
|
||||
<v-stepper-step :step="steps.length + 1">
|
||||
<translate>Summary</translate>
|
||||
</v-stepper-step>
|
||||
<v-btn icon @click="close">
|
||||
<v-icon color="primary" x-large>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-stepper-header>
|
||||
<v-stepper-items class="mt-4 d-flex justify-center">
|
||||
<v-stepper-content
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
:step="index + 1"
|
||||
class="flex-grow-0"
|
||||
>
|
||||
<div class="mb-6 text-h5">
|
||||
<translate class="grey--text text--darken-1">{{ title }}</translate> /
|
||||
<translate>{{ step.title }}</translate>
|
||||
</div>
|
||||
<slot :name="`form.${step.name}`" v-bind:step="index + 1" />
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn v-if="index + 1 > 1" @click="currentStep = index" class="mr-10" text>
|
||||
<translate>Back</translate>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="goToNextStep(index + 1, index + 2)"
|
||||
large
|
||||
>
|
||||
<translate>Next</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
<v-stepper-content :step="steps.length + 1" class="flex-grow-0">
|
||||
<div class="text-center text-h3"><translate>Summary</translate></div>
|
||||
<creation-summary
|
||||
:sections="summarySections"
|
||||
@modify-step="val => currentStep = val"
|
||||
>
|
||||
<template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
|
||||
<slot :name="slot" v-bind="scope" />
|
||||
</template>
|
||||
</creation-summary>
|
||||
<div class="d-flex justify-center mt-8">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="$emit('create')"
|
||||
large
|
||||
>
|
||||
<translate>Confirm and create</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
</v-stepper-items>
|
||||
</v-stepper>
|
||||
<confirm-dialog ref="confirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog'
|
||||
import CreationSummary from '@/components/tools/CreationSummary'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
CreationSummary
|
||||
},
|
||||
props: {
|
||||
title: String,
|
||||
steps: Array,
|
||||
summarySections: Array,
|
||||
formObserverGetter: Function,
|
||||
validateObject: Function
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentStep: 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async close (withConfirm) {
|
||||
if (withConfirm) {
|
||||
const confirm = await this.$refs.confirm.open(
|
||||
this.$gettext('Warning'),
|
||||
this.$gettext('If you close this form now, your modifications won\'t be saved. Do you confirm?'),
|
||||
{
|
||||
color: 'error'
|
||||
}
|
||||
)
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.$emit('close')
|
||||
this.currentStep = 1
|
||||
this.steps.forEach((item, index) => {
|
||||
const observer = this.formObserverGetter(index + 1)
|
||||
if (observer !== undefined) {
|
||||
observer.reset()
|
||||
}
|
||||
})
|
||||
},
|
||||
async goToNextStep (current, next) {
|
||||
const observer = this.formObserverGetter(current)
|
||||
if (observer !== undefined) {
|
||||
const valid = await observer.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.validateObject()
|
||||
} catch (error) {
|
||||
observer.setErrors(error.response.data)
|
||||
return
|
||||
}
|
||||
}
|
||||
this.currentStep = next
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.inner {
|
||||
background-color: #fff;
|
||||
}
|
||||
.v-stepper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&__content {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
&__items {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@
|
||||
ref="input"
|
||||
v-model="input"
|
||||
outlined
|
||||
dense
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
@input="update"
|
||||
@@ -31,7 +32,13 @@
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
props: {
|
||||
value: String,
|
||||
allowAdd: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
domains: 'identities/domains'
|
||||
@@ -75,6 +82,8 @@ export default {
|
||||
// on enter or tab
|
||||
if (this.filteredDomains.length > 0) {
|
||||
this.selectDomain(this.filteredDomains[this.selectionIndex])
|
||||
} else if (this.allowAdd) {
|
||||
this.selectDomain()
|
||||
}
|
||||
e.preventDefault()
|
||||
} else if (keyCode === 27) {
|
||||
@@ -100,7 +109,9 @@ export default {
|
||||
}
|
||||
},
|
||||
selectDomain (domain) {
|
||||
this.input = this.input.split('@')[0] + '@' + domain.name
|
||||
if (domain !== undefined) {
|
||||
this.input = this.input.split('@')[0] + '@' + domain.name
|
||||
}
|
||||
this.showMenu = false
|
||||
this.$emit('input', this.input)
|
||||
this.$emit('domain-selected')
|
||||
@@ -108,7 +119,11 @@ export default {
|
||||
update (value) {
|
||||
if (this.input.indexOf('@') !== -1) {
|
||||
this.domainSearch = this.input.split('@')[1]
|
||||
this.showMenu = true
|
||||
if (this.allowAdd) {
|
||||
this.showMenu = this.filteredDomains.length > 0
|
||||
} else {
|
||||
this.showMenu = true
|
||||
}
|
||||
} else {
|
||||
this.showMenu = false
|
||||
}
|
||||
@@ -132,6 +147,11 @@ export default {
|
||||
this.$nextTick(() => {
|
||||
this.updateMinWidthProperty()
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
value (newValue) {
|
||||
this.input = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -57,7 +57,14 @@ const routes = [
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: '/identities/aliases/:id/edit',
|
||||
name: 'AliasEdit',
|
||||
component: () => import('../views/identities/AliasEdit.vue'),
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/parameters/:app',
|
||||
name: 'ParametersEdit',
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="text-h4"><translate>Edit alias</translate></span>
|
||||
<div class="mt-4" />
|
||||
<alias-form :alias="alias" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import aliases from '@/api/aliases'
|
||||
import AliasForm from '@/components/identities/AliasForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AliasForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
alias: {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
aliases.get(this.$route.params.id).then(resp => {
|
||||
this.alias = resp.data
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -20,7 +20,7 @@
|
||||
<v-list-item @click="showCreationWizard = true">
|
||||
<v-list-item-title><translate>Account</translate></v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showAliasForm = true">
|
||||
<v-list-item @click="showAliasCreationWizard = true">
|
||||
<v-list-item-title><translate>Alias</translate></v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -37,24 +37,39 @@
|
||||
>
|
||||
<account-creation-form @close="closeCreationWizard" @created="updateIdentities" />
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-model="showAliasCreationWizard"
|
||||
fullscreen
|
||||
scrollable
|
||||
transition="dialog-bottom-transition"
|
||||
persistent
|
||||
>
|
||||
<alias-creation-form @close="closeAliasWizard" @created="updateIdentities" />
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AccountCreationForm from '@/components/identities/AccountCreationForm'
|
||||
import AliasCreationForm from '@/components/identities/AliasCreationForm'
|
||||
import IdentityList from '@/components/identities/IdentityList'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountCreationForm,
|
||||
AliasCreationForm,
|
||||
IdentityList
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showAliasCreationWizard: false,
|
||||
showCreationWizard: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeAliasWizard () {
|
||||
this.showAliasCreationWizard = false
|
||||
},
|
||||
closeCreationWizard () {
|
||||
this.showCreationWizard = false
|
||||
},
|
||||
|
||||
@@ -454,7 +454,7 @@ class AliasSerializer(serializers.ModelSerializer):
|
||||
def validate_address(self, value):
|
||||
"""Check domain."""
|
||||
local_part, self.domain = admin_models.validate_alias_address(
|
||||
value, self.context["request"].user)
|
||||
value, self.context["request"].user, instance=self.instance)
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
|
||||
@@ -184,7 +184,6 @@ class AliasViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
|
||||
serializer_class = serializers.AliasSerializer
|
||||
http_method_names = ["get", "post", "put", "delete"]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter queryset based on current user."""
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
"""Admin API v2 serializers."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from django.contrib.auth import password_validation
|
||||
|
||||
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.lib import fields as lib_fields
|
||||
from modoboa.lib import validators, web_utils
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from ... import constants, lib, models
|
||||
@@ -105,15 +110,24 @@ class AdminGlobalParametersSerializer(serializers.Serializer):
|
||||
enable_mx_checks = serializers.BooleanField(default=True)
|
||||
valid_mxs = serializers.CharField(allow_blank=True)
|
||||
domains_must_have_authorized_mx = serializers.BooleanField(default=False)
|
||||
enable_spf_checks = serializers.BooleanField(default=True)
|
||||
enable_dkim_checks = serializers.BooleanField(default=True)
|
||||
enable_dmarc_checks = serializers.BooleanField(default=True)
|
||||
enable_autoconfig_checks = serializers.BooleanField(default=True)
|
||||
custom_dns_server = serializers.IPAddressField(allow_blank=True)
|
||||
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)
|
||||
default_domain_quota = serializers.IntegerField(default=0)
|
||||
default_domain_message_limit = serializers.IntegerField(
|
||||
required=False, allow_null=True)
|
||||
|
||||
# Mailboxes settings
|
||||
handle_mailboxes = serializers.BooleanField(default=False)
|
||||
default_domain_quota = serializers.IntegerField(default=0)
|
||||
default_mailbox_quota = serializers.IntegerField(default=0)
|
||||
default_mailbox_message_limit = serializers.IntegerField(
|
||||
required=False, allow_null=True)
|
||||
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)
|
||||
@@ -147,11 +161,37 @@ class IdentitySerializer(serializers.Serializer):
|
||||
"""Serializer used for identities."""
|
||||
|
||||
pk = serializers.IntegerField()
|
||||
type = serializers.CharField()
|
||||
identity = serializers.CharField()
|
||||
name_or_rcpt = serializers.CharField()
|
||||
tags = TagSerializer(many=True)
|
||||
|
||||
|
||||
class MailboxSerializer(serializers.ModelSerializer):
|
||||
"""Base mailbox serializer."""
|
||||
|
||||
quota = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = models.Mailbox
|
||||
fields = (
|
||||
"pk", "use_domain_quota", "quota", "message_limit"
|
||||
)
|
||||
|
||||
def validate_quota(self, value):
|
||||
"""Convert quota to MB."""
|
||||
return web_utils.size2integer(value, output_unit="MB")
|
||||
|
||||
def validate(self, data):
|
||||
"""Check if quota is required."""
|
||||
if not data.get("use_domain_quota", False):
|
||||
if "quota" not in data:
|
||||
raise serializers.ValidationError({
|
||||
"quota": _("This field is required")
|
||||
})
|
||||
return data
|
||||
|
||||
|
||||
class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
|
||||
"""Add support for aliases and sender addresses."""
|
||||
|
||||
@@ -159,6 +199,7 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
|
||||
child=lib_fields.DRFEmailFieldUTF8(),
|
||||
required=False
|
||||
)
|
||||
mailbox = MailboxSerializer(required=False)
|
||||
|
||||
class Meta(v1_serializers.WritableAccountSerializer.Meta):
|
||||
fields = tuple(
|
||||
@@ -176,25 +217,107 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
|
||||
aliases.append({"localpart": localpart, "domain": domain})
|
||||
return aliases
|
||||
|
||||
def validate(self, data):
|
||||
"""Check constraints."""
|
||||
master_user = data.get("master_user", False)
|
||||
role = data.get("role")
|
||||
if master_user and role != "SuperAdmins":
|
||||
raise serializers.ValidationError({
|
||||
"master_user": _("Not allowed for this role.")
|
||||
})
|
||||
if role == "SimpleUsers":
|
||||
username = data.get("username")
|
||||
if username:
|
||||
try:
|
||||
validators.UTF8EmailValidator()(username)
|
||||
except ValidationError as err:
|
||||
raise ValidationError({"username": err.message})
|
||||
mailbox = data.get("mailbox")
|
||||
if mailbox is None:
|
||||
if not self.instance:
|
||||
data["mailbox"] = {
|
||||
"use_domain_quota": True
|
||||
}
|
||||
condition = (
|
||||
not data.get("random_password") and (
|
||||
data.get("password") or
|
||||
not self.partial
|
||||
)
|
||||
)
|
||||
if condition:
|
||||
password = data.get("password")
|
||||
if password:
|
||||
try:
|
||||
password_validation.validate_password(
|
||||
data["password"], self.instance)
|
||||
except ValidationError as exc:
|
||||
raise serializers.ValidationError({
|
||||
"password": exc.messages[0]})
|
||||
elif not self.instance:
|
||||
raise serializers.ValidationError({
|
||||
"password": _("This field is required.")
|
||||
})
|
||||
domain_names = data.get("domains")
|
||||
if not domain_names:
|
||||
return data
|
||||
domains = []
|
||||
for name in domain_names:
|
||||
domain = models.Domain.objects.filter(name=name).first()
|
||||
if domain:
|
||||
domains.append(domain)
|
||||
continue
|
||||
raise serializers.ValidationError({
|
||||
"domains": _("Local domain {} does not exist").format(name)
|
||||
})
|
||||
data["domains"] = domains
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Handle aliases."""
|
||||
user = self.context["request"].user
|
||||
"""Create account, mailbox and aliases."""
|
||||
creator = self.context["request"].user
|
||||
mailbox_data = validated_data.pop("mailbox", None)
|
||||
role = validated_data.pop("role")
|
||||
domains = validated_data.pop("domains", [])
|
||||
random_password = validated_data.pop("random_password", False)
|
||||
aliases = validated_data.pop("aliases", None)
|
||||
user = super().create(validated_data)
|
||||
user = core_models.User(**validated_data)
|
||||
if random_password:
|
||||
password = lib.make_password()
|
||||
else:
|
||||
password = validated_data.pop("password")
|
||||
user.set_password(password)
|
||||
if "language" not in validated_data:
|
||||
user.language = settings.LANGUAGE_CODE
|
||||
user.save(creator=creator)
|
||||
if mailbox_data:
|
||||
mailbox_data["full_address"] = user.username
|
||||
self._create_mailbox(creator, user, mailbox_data)
|
||||
user.role = role
|
||||
self.set_permissions(user, domains)
|
||||
if aliases:
|
||||
for alias in aliases:
|
||||
models.Alias.objects.create(
|
||||
creator=user,
|
||||
creator=creator,
|
||||
domain=alias.domain,
|
||||
address=alias.localpart,
|
||||
recipients=[validated_data["mailbox"]["full_address"]]
|
||||
recipients=[user.username]
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
class DeleteAccountSerializer(serializers.Serializer):
|
||||
"""Serializer used with delete operation."""
|
||||
|
||||
keepdir = serializers.BooleanField(default=False)
|
||||
|
||||
|
||||
class AliasSerializer(v1_serializers.AliasSerializer):
|
||||
"""Alias serializer for v2 API."""
|
||||
|
||||
class Meta(v1_serializers.AliasSerializer.Meta):
|
||||
# We remove 'internal' field
|
||||
fields = ("pk", "address", "enabled", "internal", "recipients")
|
||||
fields = tuple(
|
||||
field
|
||||
for field in v1_serializers.AliasSerializer.Meta.fields
|
||||
if field != "internal"
|
||||
) + ("expire_at", "description")
|
||||
|
||||
@@ -11,7 +11,7 @@ router.register(r"domains", viewsets.DomainViewSet, basename="domain")
|
||||
router.register(
|
||||
r"domainaliases", v1_viewsets.DomainAliasViewSet, basename="domain_alias")
|
||||
router.register(r"accounts", viewsets.AccountViewSet, basename="account")
|
||||
router.register(r"aliases", v1_viewsets.AliasViewSet, basename="alias")
|
||||
router.register(r"aliases", viewsets.AliasViewSet, basename="alias")
|
||||
router.register(
|
||||
r"senderaddresses", v1_viewsets.SenderAddressViewSet,
|
||||
basename="sender_address")
|
||||
|
||||
@@ -139,6 +139,8 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
|
||||
def get_serializer_class(self):
|
||||
if self.action in ["create", "validate"]:
|
||||
return serializers.WritableAccountSerializer
|
||||
if self.action == "delete":
|
||||
return serializers.DeleteAccountSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -153,7 +155,7 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
|
||||
|
||||
@action(methods=["post"], detail=False)
|
||||
def validate(self, request, **kwargs):
|
||||
"""Validate given alias without creating it."""
|
||||
"""Validate given account without creating it."""
|
||||
serializer = self.get_serializer(
|
||||
data=request.data,
|
||||
context=self.get_serializer_context(),
|
||||
@@ -167,6 +169,15 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
|
||||
password = lib.make_password()
|
||||
return response.Response({"password": password})
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
def delete(self, request, **kwargs):
|
||||
"""Custom delete method that accepts body arguments."""
|
||||
account = self.get_object()
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
account.delete()
|
||||
return response.Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class IdentityViewSet(viewsets.ViewSet):
|
||||
"""Viewset for identities."""
|
||||
@@ -178,3 +189,25 @@ class IdentityViewSet(viewsets.ViewSet):
|
||||
serializer = serializers.IdentitySerializer(
|
||||
lib.get_identities(request.user), many=True)
|
||||
return response.Response(serializer.data)
|
||||
|
||||
|
||||
class AliasViewSet(v1_viewsets.AliasViewSet):
|
||||
"""Viewset for Alias."""
|
||||
|
||||
serializer_class = serializers.AliasSerializer
|
||||
|
||||
@action(methods=["post"], detail=False)
|
||||
def validate(self, request, **kwargs):
|
||||
"""Validate given alias without creating it."""
|
||||
serializer = self.get_serializer(
|
||||
data=request.data,
|
||||
context=self.get_serializer_context(),
|
||||
partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return response.Response(status=204)
|
||||
|
||||
@action(methods=["get"], detail=False)
|
||||
def random_address(self, request, **kwargs):
|
||||
return response.Response({
|
||||
"address": models.Alias.generate_random_address()
|
||||
})
|
||||
|
||||
@@ -302,12 +302,45 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([
|
||||
"does not use one of the defined addresses."
|
||||
)
|
||||
}),
|
||||
("enable_spf_checks", {
|
||||
"label": ugettext_lazy("Enable SPF checks"),
|
||||
"help_text": ugettext_lazy(
|
||||
"Check if every domain has a valid SPF record"
|
||||
)
|
||||
}),
|
||||
("enable_dkim_checks", {
|
||||
"label": ugettext_lazy("Enable DKIM checks"),
|
||||
"help_text": ugettext_lazy(
|
||||
"Check if every domain with DKIM signin enabled "
|
||||
"has a valid DNS record"
|
||||
)
|
||||
}),
|
||||
("enable_dmarc_checks", {
|
||||
"label": ugettext_lazy("Enable DMARC checks"),
|
||||
"help_text": ugettext_lazy(
|
||||
"Check if every domain has a valid DMARC record"
|
||||
)
|
||||
}),
|
||||
("enable_autoconfig_checks", {
|
||||
"label": ugettext_lazy("Enable SPF checks"),
|
||||
"help_text": ugettext_lazy(
|
||||
"Check if every domain has a valid records for "
|
||||
"autoconfiguration"
|
||||
)
|
||||
}),
|
||||
("enable_dnsbl_checks", {
|
||||
"label": ugettext_lazy("Enable DNSBL checks"),
|
||||
"help_text": ugettext_lazy(
|
||||
"Check every domain against major DNSBL providers"
|
||||
)
|
||||
}),
|
||||
("custom_dns_server", {
|
||||
"label": ugettext_lazy("Custom DNS server"),
|
||||
"help_text": ugettext_lazy(
|
||||
"Use a custom DNS server instead of local server "
|
||||
"configuration"
|
||||
)
|
||||
}),
|
||||
("dkim_keys_storage_dir", {
|
||||
"label": ugettext_lazy("DKIM keys storage directory"),
|
||||
"help_text": ugettext_lazy(
|
||||
@@ -322,6 +355,20 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([
|
||||
"Default length in bits for newly generated DKIM keys."
|
||||
)
|
||||
}),
|
||||
("default_domain_quota", {
|
||||
"label": ugettext_lazy("Default domain quota"),
|
||||
"help_text": ugettext_lazy(
|
||||
"Default quota (in MB) applied to freshly created domains "
|
||||
"with no value specified. A value of 0 means no quota."
|
||||
)
|
||||
}),
|
||||
("default_domain_message_limit", {
|
||||
"label": ugettext_lazy("Default domain sending limit"),
|
||||
"help_text": ugettext_lazy(
|
||||
"Number of messages freshly created domains will be "
|
||||
"allowed to send per day. Leave empty for no limit."
|
||||
)
|
||||
}),
|
||||
])
|
||||
}),
|
||||
("mailboxes", {
|
||||
@@ -334,13 +381,6 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([
|
||||
" renamed or removed within Modoboa"
|
||||
)
|
||||
}),
|
||||
("default_domain_quota", {
|
||||
"label": ugettext_lazy("Default domain quota"),
|
||||
"help_text": ugettext_lazy(
|
||||
"Default quota (in MB) applied to freshly created domains "
|
||||
"with no value specified. A value of 0 means no quota."
|
||||
)
|
||||
}),
|
||||
("default_mailbox_quota", {
|
||||
"label": ugettext_lazy("Default mailbox quota"),
|
||||
"help_text": ugettext_lazy(
|
||||
@@ -349,6 +389,13 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([
|
||||
"quota."
|
||||
)
|
||||
}),
|
||||
("default_mailbox_message_limit", {
|
||||
"label": ugettext_lazy("Default mailbox sending limit"),
|
||||
"help_text": ugettext_lazy(
|
||||
"Number of messages freshly created mailboxes will be "
|
||||
"allowed to send per day. Leave empty for no limit."
|
||||
)
|
||||
}),
|
||||
("auto_account_removal", {
|
||||
"label": ugettext_lazy("Automatic account removal"),
|
||||
"help_text": ugettext_lazy(
|
||||
|
||||
@@ -111,11 +111,13 @@ def mailbox_deleted_handler(sender, **kwargs):
|
||||
alias.delete()
|
||||
models.Quota.objects.filter(username=mb.full_address).delete()
|
||||
request = lib_signals.get_request()
|
||||
print(request)
|
||||
if request:
|
||||
if not request.localconfig.parameters.get_value(
|
||||
"handle_mailboxes", raise_exception=False):
|
||||
return
|
||||
keepdir = request.POST.get("keepdir", "false") == "true"
|
||||
print(keepdir)
|
||||
if keepdir:
|
||||
return
|
||||
mb.delete_dir()
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import (
|
||||
force_bytes, force_str, python_2_unicode_compatible, smart_text
|
||||
force_bytes, force_str, smart_text
|
||||
)
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
@@ -24,7 +24,7 @@ from .domain import Domain
|
||||
from .mailbox import Mailbox
|
||||
|
||||
|
||||
def validate_alias_address(address, creator, internal=False):
|
||||
def validate_alias_address(address, creator, internal=False, instance=None):
|
||||
"""Check if the given alias address can be created by creator."""
|
||||
local_part, domain = split_mailbox(address)
|
||||
domain = Domain.objects.filter(name=domain).first()
|
||||
@@ -32,18 +32,20 @@ def validate_alias_address(address, creator, internal=False):
|
||||
raise ValidationError(_("Domain not found."))
|
||||
if not creator.can_access(domain):
|
||||
raise ValidationError(_("Permission denied."))
|
||||
if Alias.objects.filter(address=address, internal=internal).exists():
|
||||
raise ValidationError(_("An alias with this name already exists."))
|
||||
try:
|
||||
# Check creator limits
|
||||
core_signals.can_create_object.send(
|
||||
sender="validate_alias_address", context=creator, klass=Alias)
|
||||
# Check domain limits
|
||||
core_signals.can_create_object.send(
|
||||
sender="validate_alias_address", context=domain,
|
||||
object_type="mailbox_aliases")
|
||||
except ModoboaException as inst:
|
||||
raise ValidationError(str(inst))
|
||||
if instance and instance.address != address:
|
||||
if Alias.objects.filter(address=address, internal=internal).exists():
|
||||
raise ValidationError(_("An alias with this name already exists."))
|
||||
if instance is None:
|
||||
try:
|
||||
# Check creator limits
|
||||
core_signals.can_create_object.send(
|
||||
sender="validate_alias_address", context=creator, klass=Alias)
|
||||
# Check domain limits
|
||||
core_signals.can_create_object.send(
|
||||
sender="validate_alias_address", context=domain,
|
||||
object_type="mailbox_aliases")
|
||||
except ModoboaException as inst:
|
||||
raise ValidationError(str(inst))
|
||||
return local_part, domain
|
||||
|
||||
|
||||
@@ -116,7 +118,6 @@ class Alias(AdminObject):
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""FIXME: deprecated."""
|
||||
return "alias"
|
||||
|
||||
@property
|
||||
@@ -223,7 +224,6 @@ class Alias(AdminObject):
|
||||
reversion.register(Alias)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class AliasRecipient(models.Model):
|
||||
|
||||
"""An alias recipient."""
|
||||
|
||||
@@ -150,6 +150,10 @@ class User(AbstractUser):
|
||||
"""Return detail url for this user."""
|
||||
return reverse("admin:account_detail", args=[self.pk])
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return "account"
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return [{"name": "account", "label": _("account"), "type": "idt"},
|
||||
|
||||
Reference in New Issue
Block a user