mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Added account creation wizard.
This commit is contained in:
@@ -5,5 +5,11 @@ const resource = 'accounts'
|
||||
export default {
|
||||
getAll ({ domain, role }) {
|
||||
return repository.get(`/${resource}/`, { params: { domain, role } })
|
||||
},
|
||||
validate (data) {
|
||||
return repository.post(`${resource}/validate/`, data)
|
||||
},
|
||||
create (data) {
|
||||
return repository.post(`${resource}/`, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
vid="alias"
|
||||
v-slot="{ errors }"
|
||||
>
|
||||
<email-field
|
||||
ref="aliasField"
|
||||
v-model="currentAlias"
|
||||
:placeholder="'Start typing a name here...'|translate"
|
||||
@domain-selected="addAlias"
|
||||
:hint="'Alias(es) of this mailbox. To create a catchall alias, just enter the domain name (@domain.tld).'|translate"
|
||||
persistent-hint
|
||||
:error-messages="errors"
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-chip
|
||||
v-for="(alias, index) in account.aliases"
|
||||
:key="index"
|
||||
class="mr-2 mt-2"
|
||||
close
|
||||
@click:close="account.aliases.splice(index, 1)"
|
||||
>
|
||||
{{ alias }}
|
||||
</v-chip>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import accounts from '@/api/accounts'
|
||||
import EmailField from '@/components/tools/EmailField'
|
||||
|
||||
export default {
|
||||
props: ['account'],
|
||||
components: {
|
||||
EmailField
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentAlias: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async addAlias () {
|
||||
try {
|
||||
await accounts.validate({ aliases: [this.currentAlias] })
|
||||
this.account.aliases.push(this.currentAlias)
|
||||
this.$refs.aliasField.reset()
|
||||
} catch (error) {
|
||||
let errorMsg = null
|
||||
if (error.response.data.aliases) {
|
||||
errorMsg = error.response.data.aliases
|
||||
} else if (error.response.data.non_field_errors) {
|
||||
errorMsg = error.response.data.non_field_errors
|
||||
}
|
||||
this.$refs.observer.setErrors({ alias: errorMsg })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -7,11 +7,19 @@
|
||||
max-width="190"
|
||||
/>
|
||||
<v-stepper-step :complete="step > 1" step="1">
|
||||
<translate>General</translate>
|
||||
<translate>Role</translate>
|
||||
</v-stepper-step>
|
||||
|
||||
<v-stepper-step :complete="step > 2" step="2">
|
||||
<translate>Mail</translate>
|
||||
<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>
|
||||
@@ -21,10 +29,10 @@
|
||||
<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>General</translate>
|
||||
<translate>Role</translate>
|
||||
</div>
|
||||
<account-general-form ref="form_1" :account="account" />
|
||||
<div class="d-flex justify-end">
|
||||
<account-role-form ref="form_1" :account="account" />
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="goToNextStep(1, 2)"
|
||||
@@ -34,6 +42,79 @@
|
||||
</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"
|
||||
/>
|
||||
<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" />
|
||||
@@ -41,21 +122,98 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import accounts from '@/api/accounts'
|
||||
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'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountAliasForm,
|
||||
AccountGeneralForm,
|
||||
ConfirmDialog
|
||||
AccountMailboxForm,
|
||||
AccountRoleForm,
|
||||
ConfirmDialog,
|
||||
CreationSummary
|
||||
},
|
||||
computed: {
|
||||
summarySections () {
|
||||
const result = [
|
||||
{
|
||||
title: this.$gettext('Role'),
|
||||
items: [
|
||||
{ key: this.$gettext('Role'), value: this.account.role }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: this.$gettext('Identification'),
|
||||
items: [
|
||||
{ key: this.$gettext('Username'), value: this.account.username },
|
||||
{ key: this.$gettext('First name'), value: this.account.first_name },
|
||||
{ key: this.$gettext('Last name'), value: this.account.last_name },
|
||||
{
|
||||
key: this.$gettext('Random password'),
|
||||
value: this.account.random_password,
|
||||
type: 'yesno'
|
||||
},
|
||||
{
|
||||
key: this.$gettext('Enabled'),
|
||||
value: this.account.is_active,
|
||||
type: 'yesno'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: this.$gettext('Mailbox'),
|
||||
items: [
|
||||
{ key: this.$gettext('Email'), value: this.account.mailbox.full_address },
|
||||
{
|
||||
key: this.$gettext('Quota'),
|
||||
value: (this.account.mailbox.use_domain_quota)
|
||||
? this.$gettext('Domain default value')
|
||||
: this.account.mailbox.quota
|
||||
},
|
||||
{
|
||||
key: this.$gettext('Message sending limit'),
|
||||
value: this.account.mailbox.message_limit
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: this.$gettext('Aliases'),
|
||||
items: [
|
||||
{
|
||||
key: this.$gettext('Aliases'),
|
||||
value: this.account.aliases,
|
||||
type: 'list'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
return result
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
account: {},
|
||||
account: this.getInitialForm(),
|
||||
step: 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getInitialForm () {
|
||||
return {
|
||||
aliases: [],
|
||||
is_active: true,
|
||||
role: 'SimpleUsers',
|
||||
mailbox: {
|
||||
use_domain_quota: true
|
||||
}
|
||||
}
|
||||
},
|
||||
async close (withConfirm) {
|
||||
if (withConfirm) {
|
||||
const confirm = await this.$refs.confirm.open(
|
||||
@@ -69,6 +227,7 @@ export default {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.account = this.getInitialForm()
|
||||
this.$emit('close')
|
||||
this.step = 1
|
||||
},
|
||||
@@ -77,8 +236,24 @@ export default {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await accounts.validate(this.account)
|
||||
} catch (error) {
|
||||
this.$refs[`form_${current}`].$refs.observer.setErrors(error.response.data)
|
||||
return
|
||||
}
|
||||
this.step = next
|
||||
},
|
||||
submit () {
|
||||
accounts.create(this.account).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Account created') })
|
||||
this.$emit('created')
|
||||
this.close()
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('identities/fetchDomains')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,37 +3,25 @@
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
vid="username"
|
||||
>
|
||||
<choice-field v-model="account.role"
|
||||
:label="'Role' | translate"
|
||||
:choices="accountRoles"
|
||||
:error-messages="errors"
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<v-text-field
|
||||
<email-field
|
||||
v-model="account.username"
|
||||
:label="'Username' | translate"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
class="mt-4"
|
||||
dense
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-text-field
|
||||
v-model="account.first_name"
|
||||
:label="'First name' | translate"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="account.last_name"
|
||||
:label="'Last name' | translate"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
<v-switch
|
||||
v-model="account.random_password"
|
||||
@@ -51,7 +39,6 @@
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
type="password"
|
||||
dense
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
@@ -65,7 +52,6 @@
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
type="password"
|
||||
dense
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-switch
|
||||
@@ -77,34 +63,20 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChoiceField from '@/components/tools/ChoiceField'
|
||||
import EmailField from '@/components/tools/EmailField'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChoiceField
|
||||
EmailField
|
||||
},
|
||||
props: ['account'],
|
||||
data () {
|
||||
return {
|
||||
accountRoles: [
|
||||
{
|
||||
label: this.$gettext('Domain administrator'),
|
||||
value: 'DomainAdmins'
|
||||
},
|
||||
{
|
||||
label: this.$gettext('Reseller'),
|
||||
value: 'Resellers'
|
||||
},
|
||||
{
|
||||
label: this.$gettext('Simple user'),
|
||||
value: 'SimpleUsers'
|
||||
},
|
||||
{
|
||||
label: this.$gettext('Super administrator'),
|
||||
value: 'SuperAdmins'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
mounted () {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="account.mailbox.full_address"
|
||||
:label="'Email' | translate"
|
||||
:error-messages="errors"
|
||||
:disabled="account.role === 'SimpleUsers'"
|
||||
outlined
|
||||
/>
|
||||
</validation-provider>
|
||||
<div class="d-flex">
|
||||
<v-text-field
|
||||
v-model="account.mailbox.quota"
|
||||
:label="'Quota' | translate"
|
||||
:hint="'Quota for this mailbox, can be expressed in KB, MB (default) or GB. Define a custom value or use domain\'s default one. Leave empty to define an unlimited value (not allowed for domain administrators).' | translate"
|
||||
persistent-hint
|
||||
class="mr-4 quota"
|
||||
:disabled="account.mailbox.use_domain_quota"
|
||||
outlined
|
||||
/>
|
||||
<v-switch
|
||||
v-model="account.mailbox.use_domain_quota"
|
||||
:label="useDomainQuotaLabel"
|
||||
/>
|
||||
</div>
|
||||
<v-text-field
|
||||
v-model="account.mailbox.message_limit"
|
||||
:label="'Message sending limit' | translate"
|
||||
:hint="'Number of messages this mailbox can send per day. Leave empty for no limit.' | translate"
|
||||
persistent-hint
|
||||
outlined
|
||||
/>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: ['account'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getDomainByName: 'identities/getDomainByName'
|
||||
}),
|
||||
domainQuota () {
|
||||
const email = this.account.mailbox.full_address
|
||||
if (email && email.indexOf('@') !== -1) {
|
||||
const domain = this.getDomainByName(email.split('@')[1])
|
||||
if (domain) {
|
||||
return parseInt(domain.default_mailbox_quota)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
useDomainQuotaLabel () {
|
||||
let result = this.$gettext('Use domain default value')
|
||||
if (this.domainQuota !== undefined) {
|
||||
if (this.domainQuota === 0) {
|
||||
result += ` (${this.$gettext('unlimited')})`
|
||||
} else {
|
||||
result += ` (${this.domainQuota} MB)`
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
account: {
|
||||
handler (val) {
|
||||
if (val.role === 'SimpleUsers') {
|
||||
this.$set(val.mailbox, 'full_address', val.username)
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.quota {
|
||||
max-width: 50%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<choice-field v-model="account.role"
|
||||
:label="'Role' | translate"
|
||||
:choices="accountRoles"
|
||||
:error-messages="errors"
|
||||
:choices-per-line="2"
|
||||
/>
|
||||
</validation-provider>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChoiceField from '@/components/tools/ChoiceField'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChoiceField
|
||||
},
|
||||
props: ['account'],
|
||||
data () {
|
||||
return {
|
||||
accountRoles: [
|
||||
{
|
||||
label: this.$gettext('Domain administrator'),
|
||||
value: 'DomainAdmins'
|
||||
},
|
||||
{
|
||||
label: this.$gettext('Reseller'),
|
||||
value: 'Resellers'
|
||||
},
|
||||
{
|
||||
label: this.$gettext('Simple user'),
|
||||
value: 'SimpleUsers'
|
||||
},
|
||||
{
|
||||
label: this.$gettext('Super administrator'),
|
||||
value: 'SuperAdmins'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -76,10 +76,15 @@ export default {
|
||||
selected: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchIdentities () {
|
||||
identities.getAll().then(resp => {
|
||||
this.identities = resp.data
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
identities.getAll().then(resp => {
|
||||
this.identities = resp.data
|
||||
})
|
||||
this.fetchIdentities()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-subtitle-1 grey--text text--darken-1 mb-4" :class="{ 'label--disabled': disabled }">
|
||||
<translate>{{ label }}</translate>
|
||||
<label class="v-label">{{ label }}</label>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div v-for="(choice, index) in choices"
|
||||
:key="index"
|
||||
class="choice rounded pa-10 mr-4 text-center"
|
||||
<div class="d-flex" v-for="(lineChoices, index1) in formatedChoices" :key="index1">
|
||||
<div v-for="(choice, index2) in lineChoices"
|
||||
:key="index2"
|
||||
class="choice rounded pa-10 mr-4 text-center flex-grow-0 mb-4"
|
||||
:class="{ 'choice--disabled': disabled, 'choice--selected': !disabled && currentChoice === choice.value }"
|
||||
@click="selectChoice(choice.value)">
|
||||
@click="selectChoice(choice.value)"
|
||||
>
|
||||
<v-icon v-if="choice.icon" class="d-block mb-2" :color="iconColor(choice.value)" x-large>{{ choice.icon }}</v-icon>
|
||||
<translate class="grey--text text--darken-1">{{ choice.label }}</translate>
|
||||
</div>
|
||||
@@ -25,14 +26,28 @@ export default {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
choicesPerLine: Number
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentChoice: null
|
||||
currentChoice: null,
|
||||
formatedChoices: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatChoices () {
|
||||
if (this.choicesPerLine) {
|
||||
let sliceIndex = 0
|
||||
while (sliceIndex < this.choices.length) {
|
||||
const result = this.choices.slice(sliceIndex, sliceIndex + this.choicesPerLine)
|
||||
this.formatedChoices.push(result)
|
||||
sliceIndex += this.choicesPerLine
|
||||
}
|
||||
} else {
|
||||
this.formatedChoices.push(this.choices)
|
||||
}
|
||||
},
|
||||
iconColor (value) {
|
||||
return (!this.disabled && value === this.currentChoice) ? 'primary' : ''
|
||||
},
|
||||
@@ -44,6 +59,9 @@ export default {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.formatChoices()
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler (val) {
|
||||
@@ -57,7 +75,8 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.choice {
|
||||
width: 200px;
|
||||
// width: 200px;
|
||||
flex-basis: 200px;
|
||||
background-color: #F2F5F7;
|
||||
border: 1px solid #DBDDDF;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(section, index) in sections" :key="index">
|
||||
<div class="subtitle mt-4 d-flex">
|
||||
<span class="text-h6">{{ section.title }}</span>
|
||||
<a href="#" class="edit-link ml-auto" @click="$emit('modify-step', index + 1)"><translate>Modify</translate></a>
|
||||
</div>
|
||||
<v-row v-for="(item, itemIndex) in section.items"
|
||||
:key="itemIndex"
|
||||
dense
|
||||
>
|
||||
<template v-if="item.value !== undefined">
|
||||
<v-col><span class="grey--text">{{ item.key }}</span></v-col>
|
||||
<v-col class="text-right" v-if="item.type === 'yesno'">{{ item.value|yesno }}</v-col>
|
||||
<v-col class="text-right" v-else-if="item.type === 'list'">
|
||||
{{ displayList(item.value) }}
|
||||
</v-col>
|
||||
<v-col class="text-right" v-else>{{ item.value }}</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['sections'],
|
||||
methods: {
|
||||
displayList (value) {
|
||||
return (value.length) ? value.join(', ') : this.$gettext('Empty')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.subtitle {
|
||||
color: #000;
|
||||
border-bottom: 1px solid #DBDDDF;
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-text-field
|
||||
ref="input"
|
||||
v-model="input"
|
||||
outlined
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
@input="update"
|
||||
@keydown="onKeyDown"
|
||||
/>
|
||||
<div class="menu"
|
||||
v-if="showMenu"
|
||||
:style="`width: ${width}px;`"
|
||||
>
|
||||
<v-list class="list">
|
||||
<v-list-item
|
||||
v-for="(domain, index) in filteredDomains"
|
||||
:key="index"
|
||||
@click="selectDomain(domain)"
|
||||
:class="selectionIndex === index ? 'selected' : ''"
|
||||
>
|
||||
<v-list-item-title>{{ domain.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
domains: 'identities/domains'
|
||||
}),
|
||||
filteredDomains () {
|
||||
return this.domains.filter(domain => domain.name.startsWith(this.domainSearch))
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
domainSearch: '',
|
||||
input: this.value,
|
||||
selectionIndex: 0,
|
||||
showMenu: false,
|
||||
width: 500
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onKeyDown (e) {
|
||||
const keyCode = e.keyCode
|
||||
|
||||
if (keyCode === 40 || keyCode === 34) {
|
||||
// on arrow down or page down
|
||||
if (!this.showMenu) {
|
||||
this.domainSearch = ''
|
||||
this.showMenu = true
|
||||
} else {
|
||||
this.increaseSelectionIndex()
|
||||
}
|
||||
e.preventDefault()
|
||||
} else if (keyCode === 38 || keyCode === 33) {
|
||||
// on arrow up or page up
|
||||
if (!this.showMenu) {
|
||||
this.domainSearch = ''
|
||||
this.showMenu = true
|
||||
} else {
|
||||
this.decreaseSelectionIndex()
|
||||
}
|
||||
e.preventDefault()
|
||||
} else if (keyCode === 13 || keyCode === 9) {
|
||||
// on enter or tab
|
||||
if (this.filteredDomains.length > 0) {
|
||||
this.selectDomain(this.filteredDomains[this.selectionIndex])
|
||||
}
|
||||
e.preventDefault()
|
||||
} else if (keyCode === 27) {
|
||||
// on escape
|
||||
if (this.filteredDomains.length > 0) {
|
||||
this.showMenu = false
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
},
|
||||
increaseSelectionIndex () {
|
||||
if (this.selectionIndex >= this.filteredDomains.length - 1) {
|
||||
this.selectionIndex = 0
|
||||
} else {
|
||||
this.selectionIndex += 1
|
||||
}
|
||||
},
|
||||
decreaseSelectionIndex () {
|
||||
if (this.selectionIndex <= 0) {
|
||||
this.selectionIndex = this.filteredDomains.length - 1
|
||||
} else {
|
||||
this.selectionIndex -= 1
|
||||
}
|
||||
},
|
||||
selectDomain (domain) {
|
||||
this.input = this.input.split('@')[0] + '@' + domain.name
|
||||
this.showMenu = false
|
||||
this.$emit('input', this.input)
|
||||
this.$emit('domain-selected')
|
||||
},
|
||||
update (value) {
|
||||
if (this.input.indexOf('@') !== -1) {
|
||||
this.domainSearch = this.input.split('@')[1]
|
||||
this.showMenu = true
|
||||
} else {
|
||||
this.showMenu = false
|
||||
}
|
||||
this.$emit('input', this.input)
|
||||
},
|
||||
onResize () {
|
||||
this.updateMinWidthProperty()
|
||||
},
|
||||
updateMinWidthProperty () {
|
||||
this.width = this.$refs.input.$el.clientWidth
|
||||
},
|
||||
reset () {
|
||||
this.input = ''
|
||||
this.domainSearch = ''
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('resize', this.onResize)
|
||||
this.$nextTick(() => {
|
||||
this.updateMinWidthProperty()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu {
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
margin-top: -20px;
|
||||
padding: 5px 0;
|
||||
box-shadow: 0 1px 5px 1px rgba(0,0,0,0.3);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
contain: content;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 1.1em;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: cornsilk;
|
||||
}
|
||||
</style>
|
||||
@@ -3,13 +3,15 @@ import Vuex from 'vuex'
|
||||
|
||||
import auth from '@/store/modules/auth'
|
||||
import domains from '@/store/modules/domains'
|
||||
import identities from '@/store/modules/identities'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
auth,
|
||||
domains
|
||||
domains,
|
||||
identities
|
||||
},
|
||||
strict: process.env.NODE_ENV !== 'production'
|
||||
})
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import domainApi from '@/api/domains'
|
||||
|
||||
const state = {
|
||||
domains: []
|
||||
}
|
||||
|
||||
const getters = {
|
||||
domains: state => state.domains,
|
||||
getDomainByName: state => name => {
|
||||
return state.domains.find(domain => domain.name === name)
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
SET_DOMAINS (state, domains) {
|
||||
state.domains = domains
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
fetchDomains ({ commit }) {
|
||||
domainApi.getDomains().then(resp => {
|
||||
commit('SET_DOMAINS', resp.data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
||||
@@ -26,15 +26,16 @@
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-toolbar>
|
||||
<identity-list />
|
||||
<identity-list ref="identities" />
|
||||
|
||||
<v-dialog
|
||||
v-model="showCreationWizard"
|
||||
fullscreen
|
||||
scrollable
|
||||
transition="dialog-bottom-transition"
|
||||
persistent
|
||||
>
|
||||
<account-creation-form @close="closeCreationWizard" />
|
||||
<account-creation-form @close="closeCreationWizard" @created="updateIdentities" />
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -56,6 +57,9 @@ export default {
|
||||
methods: {
|
||||
closeCreationWizard () {
|
||||
this.showCreationWizard = false
|
||||
},
|
||||
updateIdentities () {
|
||||
this.$refs.identities.fetchIdentities()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ class MailboxSerializer(serializers.ModelSerializer):
|
||||
"""Base mailbox serializer."""
|
||||
|
||||
full_address = lib_fields.DRFEmailFieldUTF8()
|
||||
quota = serializers.CharField()
|
||||
quota = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = models.Mailbox
|
||||
@@ -169,6 +169,15 @@ class MailboxSerializer(serializers.ModelSerializer):
|
||||
"""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 AccountSerializer(serializers.ModelSerializer):
|
||||
"""Base account serializer."""
|
||||
@@ -262,7 +271,7 @@ class WritableAccountSerializer(AccountSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Adapt fields to current user."""
|
||||
super(WritableAccountSerializer, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
request = self.context.get("request")
|
||||
if not request:
|
||||
return
|
||||
@@ -292,11 +301,11 @@ class WritableAccountSerializer(AccountSerializer):
|
||||
"full_address": data["username"],
|
||||
"use_domain_quota": True
|
||||
}
|
||||
elif mailbox["full_address"] != data["username"]:
|
||||
elif "full_address" in mailbox and mailbox["full_address"] != data["username"]:
|
||||
raise serializers.ValidationError({
|
||||
"username": _("Must be equal to mailbox full_address")
|
||||
})
|
||||
if not data.get("random_password"):
|
||||
if not self.partial and not data.get("random_password"):
|
||||
password = data.get("password")
|
||||
if password:
|
||||
try:
|
||||
@@ -415,45 +424,32 @@ class WritableAccountSerializer(AccountSerializer):
|
||||
|
||||
|
||||
class AliasSerializer(serializers.ModelSerializer):
|
||||
"""Base Alias serializer."""
|
||||
"""Alias serializer."""
|
||||
|
||||
address = lib_fields.DRFEmailFieldUTF8AndEmptyUser()
|
||||
recipients = serializers.ListField(
|
||||
child=lib_fields.DRFEmailFieldUTF8AndEmptyUser(),
|
||||
allow_empty=False,
|
||||
help_text=ugettext_lazy("A list of recipient"))
|
||||
help_text=ugettext_lazy("A list of recipient")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = admin_models.Alias
|
||||
fields = ("pk", "address", "enabled", "internal", "recipients")
|
||||
fields = (
|
||||
"pk", "address", "enabled", "internal", "recipients"
|
||||
)
|
||||
|
||||
def validate_address(self, value):
|
||||
"""Check domain."""
|
||||
local_part, domain = email_utils.split_mailbox(value)
|
||||
self.domain = admin_models.Domain.objects.filter(name=domain).first()
|
||||
if self.domain is None:
|
||||
raise serializers.ValidationError(_("Domain not found."))
|
||||
if not self.context["request"].user.can_access(self.domain):
|
||||
raise serializers.ValidationError(_("Permission denied."))
|
||||
local_part, self.domain = admin_models.validate_alias_address(
|
||||
value, self.context["request"].user)
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create appropriate objects."""
|
||||
creator = self.context["request"].user
|
||||
try:
|
||||
core_signals.can_create_object.send(
|
||||
sender=self.__class__, context=creator,
|
||||
klass=admin_models.Alias)
|
||||
core_signals.can_create_object.send(
|
||||
sender=self.__class__, context=self.domain,
|
||||
object_type="mailbox_aliases")
|
||||
except lib_exceptions.ModoboaException as inst:
|
||||
raise serializers.ValidationError(force_text(inst))
|
||||
recipients = validated_data.pop("recipients", None)
|
||||
alias = admin_models.Alias(domain=self.domain, **validated_data)
|
||||
alias.save(creator=creator)
|
||||
alias.set_recipients(recipients)
|
||||
return alias
|
||||
return admin_models.Alias.objects.create(
|
||||
creator=creator, domain=self.domain, **validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Update objects."""
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.parameters import tools as param_tools
|
||||
|
||||
from ... import constants, lib, models
|
||||
@@ -148,3 +149,49 @@ class IdentitySerializer(serializers.Serializer):
|
||||
identity = serializers.CharField()
|
||||
name_or_rcpt = serializers.CharField()
|
||||
tags = TagSerializer(many=True)
|
||||
|
||||
|
||||
class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
|
||||
"""Add support for aliases and sender addresses."""
|
||||
|
||||
aliases = serializers.ListField(
|
||||
child=lib_fields.DRFEmailFieldUTF8(),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta(v1_serializers.WritableAccountSerializer.Meta):
|
||||
fields = v1_serializers.WritableAccountSerializer.Meta.fields + (
|
||||
"aliases",
|
||||
)
|
||||
|
||||
def validate_aliases(self, value):
|
||||
"""Check if required domains are locals and user can access them."""
|
||||
aliases = []
|
||||
for alias in value:
|
||||
localpart, domain = models.validate_alias_address(
|
||||
alias, self.context["request"].user)
|
||||
aliases.append({"localpart": localpart, "domain": domain})
|
||||
return aliases
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Handle aliases."""
|
||||
user = self.context["request"].user
|
||||
aliases = validated_data.pop("aliases", None)
|
||||
user = super().create(validated_data)
|
||||
if aliases:
|
||||
for alias in aliases:
|
||||
models.Alias.objects.create(
|
||||
creator=user,
|
||||
domain=alias.domain,
|
||||
address=alias.localpart,
|
||||
recipients=[validated_data["mailbox"]["full_address"]]
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
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")
|
||||
|
||||
@@ -136,6 +136,11 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
|
||||
filter_backends = (filters.SearchFilter, dj_filters.DjangoFilterBackend)
|
||||
filterset_class = AccountFilterSet
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action in ["create", "validate"]:
|
||||
return serializers.WritableAccountSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter queryset based on current user."""
|
||||
user = self.request.user
|
||||
@@ -146,6 +151,16 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
|
||||
)
|
||||
return core_models.User.objects.filter(pk__in=ids)
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
class IdentityViewSet(viewsets.ViewSet):
|
||||
"""Viewset for identities."""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Admin models."""
|
||||
|
||||
from .alarm import Alarm
|
||||
from .alias import Alias, AliasRecipient
|
||||
from .alias import Alias, AliasRecipient, validate_alias_address
|
||||
from .base import AdminObject
|
||||
from .domain import Domain
|
||||
from .domain_alias import DomainAlias
|
||||
@@ -21,4 +21,5 @@ __all__ = [
|
||||
"MXRecord",
|
||||
"Quota",
|
||||
"SenderAddress",
|
||||
"validate_alias_address",
|
||||
]
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import hashlib
|
||||
import random
|
||||
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import (
|
||||
@@ -12,10 +11,12 @@ from django.utils.encoding import (
|
||||
)
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from modoboa.core import signals as core_signals
|
||||
from modoboa.lib.email_utils import split_mailbox
|
||||
from modoboa.lib.exceptions import (
|
||||
BadRequest, Conflict, NotFound, PermDeniedException
|
||||
BadRequest, Conflict, NotFound, ModoboaException
|
||||
)
|
||||
from .. import signals
|
||||
from .base import AdminObject
|
||||
@@ -23,9 +24,43 @@ from .domain import Domain
|
||||
from .mailbox import Mailbox
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Alias(AdminObject):
|
||||
def validate_alias_address(address, creator, internal=False):
|
||||
"""Check if the given alias address can be created by creator."""
|
||||
local_part, domain = split_mailbox(address)
|
||||
domain = Domain.objects.filter(name=domain).first()
|
||||
if domain is None:
|
||||
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))
|
||||
return local_part, domain
|
||||
|
||||
|
||||
class AliasManager(models.Manager):
|
||||
"""Custom manager for Alias."""
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
"""Custom create method."""
|
||||
creator = kwargs.pop("creator")
|
||||
recipients = kwargs.pop("recipients")
|
||||
alias = super().create(*args, **kwargs)
|
||||
alias.post_create(creator)
|
||||
alias.set_recipients(recipients)
|
||||
return alias
|
||||
|
||||
|
||||
class Alias(AdminObject):
|
||||
"""Mailbox alias."""
|
||||
|
||||
address = models.CharField(
|
||||
@@ -47,13 +82,15 @@ class Alias(AdminObject):
|
||||
ugettext_lazy("Expire at"), blank=True, null=True)
|
||||
_objectname = "MailboxAlias"
|
||||
|
||||
objects = AliasManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ["address"]
|
||||
unique_together = (("address", "internal"), )
|
||||
app_label = "admin"
|
||||
|
||||
def __str__(self):
|
||||
return smart_text(self.address)
|
||||
return self.address
|
||||
|
||||
@classmethod
|
||||
def generate_random_address(cls):
|
||||
@@ -167,23 +204,11 @@ class Alias(AdminObject):
|
||||
def from_csv(self, user, row, expected_elements=5):
|
||||
"""Create a new alias from a CSV file entry."""
|
||||
if len(row) < expected_elements:
|
||||
raise BadRequest(_("Invalid line: %s" % row))
|
||||
address = row[1].strip().lower()
|
||||
localpart, domname = split_mailbox(address)
|
||||
try:
|
||||
domain = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
raise BadRequest(_("Domain '%s' does not exist" % domname))
|
||||
if not user.can_access(domain):
|
||||
raise PermDeniedException
|
||||
core_signals.can_create_object.send(
|
||||
sender="import", context=user, klass=Alias)
|
||||
core_signals.can_create_object.send(
|
||||
sender="import", context=domain, object_type="mailbox_aliases")
|
||||
if Alias.objects.filter(address=address, internal=False).exists():
|
||||
raise BadRequest(_("Invalid line: {}").format(row))
|
||||
self.address = row[1].strip().lower()
|
||||
local_part, self.domain = validate_alias_address(self.address, user)
|
||||
if Alias.objects.filter(address=self.address, internal=False).exists():
|
||||
raise Conflict
|
||||
self.address = address
|
||||
self.domain = domain
|
||||
self.enabled = (row[2].strip().lower() in ["true", "1", "yes", "y"])
|
||||
self.save()
|
||||
self.set_recipients([raddress.strip() for raddress in row[3:]])
|
||||
|
||||
Reference in New Issue
Block a user