Improved pre-commit and linters config

This commit is contained in:
Antoine Nguyen
2024-02-21 13:30:20 +01:00
parent 692871bedf
commit f1d89bd58f
141 changed files with 10460 additions and 8115 deletions
+12 -13
View File
@@ -9,20 +9,19 @@ repos:
language: system
types: [python]
args: ["-rn", "-sn", "--fail-under=5"]
- id: prettier
name: prettier
entry: prettier --config frontend/.prettierrc.json --write --ignore-unknown
language: node
types: [javascript]
additional_dependencies: ["prettier"]
- id: eslint
name: eslint
language: node
types: [javascript]
entry: eslint --fix
additional_dependencies: ["eslint-plugin-vue"]
- repo: https://github.com/psf/black
rev: "24.2.0"
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-eslint
rev: "v9.0.0-beta.0"
hooks:
- id: eslint
files: \.(js|vue)$
types: [file]
additional_dependencies:
- eslint
- eslint-plugin-vue
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v4.0.0-alpha.8" # Use the sha / tag you want to point at
hooks:
- id: prettier
+15 -15
View File
@@ -1,17 +1,17 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: ['plugin:vue/vue3-recommended', 'eslint:recommended', 'prettier'],
rules: {
'vue/component-name-in-template-casing': [
'error',
'PascalCase',
{
registeredComponentsOnly: true,
ignores: [],
},
],
},
root: true,
env: {
node: true,
},
extends: ['plugin:vue/vue3-recommended', 'eslint:recommended'],
rules: {
'vue/component-name-in-template-casing': [
'error',
'PascalCase',
{
registeredComponentsOnly: true,
ignores: [],
},
],
},
}
+22 -22
View File
@@ -1,24 +1,24 @@
module.exports = {
output: {
path: './src/locale',
locales: [
'en',
'br',
'cs',
'de',
'el',
'es',
'fi',
'fr',
'it',
'ja',
'nl',
'pl',
'pt',
'ru',
'sv',
'tr',
'zh',
],
},
output: {
path: './src/locale',
locales: [
'en',
'br',
'cs',
'de',
'el',
'es',
'fi',
'fr',
'it',
'ja',
'nl',
'pl',
'pt',
'ru',
'sv',
'tr',
'zh',
],
},
}
-1
View File
@@ -33,7 +33,6 @@
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.21.1",
"prettier": "^3.1.1",
"sass": "^1.69.5",
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<router-view />
<router-view />
</template>
<script setup></script>
+41 -41
View File
@@ -3,45 +3,45 @@ import repository from './repository'
const resource = 'account'
export default {
getMe() {
return repository.get(`/${resource}/me/`)
},
checkPassword(value) {
return repository.post(`/${resource}/me/password/`, { password: value })
},
verifyTFACode(code) {
const payload = { code }
return repository.post(`${resource}/tfa/verify/`, payload)
},
startTFASetup() {
return repository.post(`${resource}/tfa/setup/`)
},
getKeyForTFASetup() {
return repository.get(`${resource}/tfa/setup/key/`)
},
finalizeTFASetup(pinCode) {
const payload = { pin_code: pinCode }
return repository.post(`${resource}/tfa/setup/check/`, payload)
},
disableTFA(data) {
return repository.post(`${resource}/tfa/disable/`, data)
},
resetRecoveryCodes(data) {
return repository.post(`${resource}/tfa/reset_codes/`, data)
},
getForward() {
return repository.get(`${resource}/forward/`)
},
setForward(data) {
return repository.post(`${resource}/forward/`, data)
},
getAPIToken() {
return repository.get(`${resource}/api_token/`)
},
createAPIToken() {
return repository.post(`${resource}/api_token/`)
},
deleteAPIToken() {
return repository.delete(`${resource}/api_token/`)
},
getMe() {
return repository.get(`/${resource}/me/`)
},
checkPassword(value) {
return repository.post(`/${resource}/me/password/`, { password: value })
},
verifyTFACode(code) {
const payload = { code }
return repository.post(`${resource}/tfa/verify/`, payload)
},
startTFASetup() {
return repository.post(`${resource}/tfa/setup/`)
},
getKeyForTFASetup() {
return repository.get(`${resource}/tfa/setup/key/`)
},
finalizeTFASetup(pinCode) {
const payload = { pin_code: pinCode }
return repository.post(`${resource}/tfa/setup/check/`, payload)
},
disableTFA(data) {
return repository.post(`${resource}/tfa/disable/`, data)
},
resetRecoveryCodes(data) {
return repository.post(`${resource}/tfa/reset_codes/`, data)
},
getForward() {
return repository.get(`${resource}/forward/`)
},
setForward(data) {
return repository.post(`${resource}/forward/`, data)
},
getAPIToken() {
return repository.get(`${resource}/api_token/`)
},
createAPIToken() {
return repository.post(`${resource}/api_token/`)
},
deleteAPIToken() {
return repository.delete(`${resource}/api_token/`)
},
}
+21 -21
View File
@@ -3,25 +3,25 @@ import repository from './repository'
const resource = 'accounts'
export default {
getAll({ domain, role }) {
return repository.get(`/${resource}/`, { params: { domain, role } })
},
get(accountId) {
return repository.get(`${resource}/${accountId}/`)
},
getRandomPassword() {
return repository.get(`${resource}/random_password/`)
},
validate(data) {
return repository.post(`${resource}/validate/`, data)
},
create(data) {
return repository.post(`${resource}/`, data)
},
patch(accountId, data) {
return repository.patch(`${resource}/${accountId}/`, data)
},
delete(accountId, data) {
return repository.post(`${resource}/${accountId}/delete/`, data)
},
getAll({ domain, role }) {
return repository.get(`/${resource}/`, { params: { domain, role } })
},
get(accountId) {
return repository.get(`${resource}/${accountId}/`)
},
getRandomPassword() {
return repository.get(`${resource}/random_password/`)
},
validate(data) {
return repository.post(`${resource}/validate/`, data)
},
create(data) {
return repository.post(`${resource}/`, data)
},
patch(accountId, data) {
return repository.patch(`${resource}/${accountId}/`, data)
},
delete(accountId, data) {
return repository.post(`${resource}/${accountId}/delete/`, data)
},
}
+3 -3
View File
@@ -1,7 +1,7 @@
import repository from './repository'
export default {
getComponentsInformation() {
return repository.get('/admin/components/')
},
getComponentsInformation() {
return repository.get('/admin/components/')
},
}
+14 -14
View File
@@ -3,18 +3,18 @@ import repository from './repository'
const resource = 'alarms'
export default {
getAll(params) {
return repository.get(`${resource}/`, { params })
},
switchStatus(alarmId, data) {
return repository.patch(`${resource}/${alarmId}/switch/`, data)
},
delete(alarmId) {
return repository.delete(`${resource}/${alarmId}/`)
},
bulkDelete(alarmsIds) {
return repository.delete(`${resource}/bulk_delete/`, {
params: { ids: alarmsIds },
})
},
getAll(params) {
return repository.get(`${resource}/`, { params })
},
switchStatus(alarmId, data) {
return repository.patch(`${resource}/${alarmId}/switch/`, data)
},
delete(alarmId) {
return repository.delete(`${resource}/${alarmId}/`)
},
bulkDelete(alarmsIds) {
return repository.delete(`${resource}/bulk_delete/`, {
params: { ids: alarmsIds },
})
},
}
+21 -21
View File
@@ -3,25 +3,25 @@ import repository from './repository'
const resource = 'aliases'
export default {
getAll() {
return repository.get(`${resource}/`)
},
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}/`)
},
getAll() {
return repository.get(`${resource}/`)
},
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}/`)
},
}
+12 -12
View File
@@ -1,16 +1,16 @@
import repository from './repository'
export default {
requestToken(data) {
return repository.post('/token/', data)
},
recoverPassword(data) {
return repository.post('/password_reset/', data)
},
checkSmsTotp(data) {
return repository.post('/sms_totp/', data)
},
changePassword(data) {
return repository.post('/reset_confirm/', data)
},
requestToken(data) {
return repository.post('/token/', data)
},
recoverPassword(data) {
return repository.post('/password_reset/', data)
},
checkSmsTotp(data) {
return repository.post('/sms_totp/', data)
},
changePassword(data) {
return repository.post('/reset_confirm/', data)
},
}
+68 -68
View File
@@ -4,72 +4,72 @@ const domainResource = 'domains'
const domainAliasResource = 'domainaliases'
export default {
getDomains() {
return repository.get(`/${domainResource}/`)
},
getDomain(domainId) {
return repository.get(`/${domainResource}/${domainId}/`)
},
getDomainDNSDetail(domainId) {
return repository.get(`/${domainResource}/${domainId}/dns_detail/`)
},
getDomainDmarcAlignment(domainId, period) {
return repository.get(
`${domainResource}/${domainId}/dmarc/alignment_stats/`,
{ params: { period } }
)
},
getDomainAliases(domain) {
let url = `/${domainAliasResource}/`
if (domain !== undefined) {
url += `?domain=${domain}`
}
return repository.get(url)
},
getDomainAdministrators(domainId) {
return repository.get(`/${domainResource}/${domainId}/administrators/`)
},
addDomainAdministrator(domainId, accountId) {
return repository.post(
`/${domainResource}/${domainId}/administrators/add/`,
{ account: accountId }
)
},
removeDomainAdministrator(domainId, accountId) {
return repository.post(
`/${domainResource}/${domainId}/administrators/remove/`,
{ account: accountId }
)
},
createDomain(data) {
return repository.post(`/${domainResource}/`, data)
},
updateDomain(domainId, data) {
return repository.put(`/${domainResource}/${domainId}/`, data)
},
patchDomain(domainId, data) {
return repository.patch(`/${domainResource}/${domainId}/`, data)
},
deleteDomain(domainId, data) {
return repository.post(`/${domainResource}/${domainId}/delete/`, data)
},
createDomainAlias(data) {
return repository.post(`/${domainAliasResource}/`, data)
},
updateDomainAlias(aliasId, data) {
return repository.put(`/${domainAliasResource}/${aliasId}/`, data)
},
deleteDomainAlias(aliasId) {
return repository.delete(`/${domainAliasResource}/${aliasId}/`)
},
exportAll() {
return repository.get(`/${domainResource}/export/`)
},
importFromCSV(data) {
return repository.post(`/${domainResource}/import/`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
getDomains() {
return repository.get(`/${domainResource}/`)
},
getDomain(domainId) {
return repository.get(`/${domainResource}/${domainId}/`)
},
getDomainDNSDetail(domainId) {
return repository.get(`/${domainResource}/${domainId}/dns_detail/`)
},
getDomainDmarcAlignment(domainId, period) {
return repository.get(
`${domainResource}/${domainId}/dmarc/alignment_stats/`,
{ params: { period } }
)
},
getDomainAliases(domain) {
let url = `/${domainAliasResource}/`
if (domain !== undefined) {
url += `?domain=${domain}`
}
return repository.get(url)
},
getDomainAdministrators(domainId) {
return repository.get(`/${domainResource}/${domainId}/administrators/`)
},
addDomainAdministrator(domainId, accountId) {
return repository.post(
`/${domainResource}/${domainId}/administrators/add/`,
{ account: accountId }
)
},
removeDomainAdministrator(domainId, accountId) {
return repository.post(
`/${domainResource}/${domainId}/administrators/remove/`,
{ account: accountId }
)
},
createDomain(data) {
return repository.post(`/${domainResource}/`, data)
},
updateDomain(domainId, data) {
return repository.put(`/${domainResource}/${domainId}/`, data)
},
patchDomain(domainId, data) {
return repository.patch(`/${domainResource}/${domainId}/`, data)
},
deleteDomain(domainId, data) {
return repository.post(`/${domainResource}/${domainId}/delete/`, data)
},
createDomainAlias(data) {
return repository.post(`/${domainAliasResource}/`, data)
},
updateDomainAlias(aliasId, data) {
return repository.put(`/${domainAliasResource}/${aliasId}/`, data)
},
deleteDomainAlias(aliasId) {
return repository.delete(`/${domainAliasResource}/${aliasId}/`)
},
exportAll() {
return repository.get(`/${domainResource}/export/`)
},
importFromCSV(data) {
return repository.post(`/${domainResource}/import/`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
}
+13 -13
View File
@@ -3,17 +3,17 @@ import repository from './repository'
const resource = 'identities'
export default {
getAll() {
return repository.get(`${resource}/`)
},
exportAll() {
return repository.get(`/${resource}/export/`)
},
importFromCSV(data) {
return repository.post(`/${resource}/import/`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
getAll() {
return repository.get(`${resource}/`)
},
exportAll() {
return repository.get(`/${resource}/export/`)
},
importFromCSV(data) {
return repository.post(`/${resource}/import/`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
}
@@ -3,13 +3,13 @@ import repository from '@/api/repository'
const resource = 'migrations'
export default {
getMigrations(params) {
return repository.get(`/${resource}/`, { params })
},
getMigration(slug, query) {
return repository.get(`/${resource}/${slug}/`, query)
},
deleteMigration(slug) {
return repository.delete(`/${resource}/${slug}/`)
},
getMigrations(params) {
return repository.get(`/${resource}/`, { params })
},
getMigration(slug, query) {
return repository.get(`/${resource}/${slug}/`, query)
},
deleteMigration(slug) {
return repository.delete(`/${resource}/${slug}/`)
},
}
+24 -24
View File
@@ -3,28 +3,28 @@ import repository from '@/api/repository'
const resource = 'email-providers'
export default {
getProviders(params) {
return repository.get(`/${resource}/`, { params })
},
getProvider(providerId) {
return repository.get(`/${resource}/${providerId}/`)
},
createProvider(params) {
return repository.post(`/${resource}/`, params)
},
updateProvider(slug, params) {
return repository.put(`/${resource}/${slug}/`, params)
},
patchProvider(providerId, params) {
return repository.patch(`/${resource}/${providerId}/`, params)
},
deleteProvider(slug) {
return repository.delete(`/${resource}/${slug}/`)
},
checkProvider(params) {
return repository.post(`/${resource}/check_connection/`, params)
},
checkAssociatedDomain(params) {
return repository.post(`/${resource}/check_associated_domain/`, params)
},
getProviders(params) {
return repository.get(`/${resource}/`, { params })
},
getProvider(providerId) {
return repository.get(`/${resource}/${providerId}/`)
},
createProvider(params) {
return repository.post(`/${resource}/`, params)
},
updateProvider(slug, params) {
return repository.put(`/${resource}/${slug}/`, params)
},
patchProvider(providerId, params) {
return repository.patch(`/${resource}/${providerId}/`, params)
},
deleteProvider(slug) {
return repository.delete(`/${resource}/${slug}/`)
},
checkProvider(params) {
return repository.post(`/${resource}/check_connection/`, params)
},
checkAssociatedDomain(params) {
return repository.post(`/${resource}/check_associated_domain/`, params)
},
}
+3 -3
View File
@@ -3,7 +3,7 @@ import repository from './repository'
const resource = 'languages'
export default {
getAll() {
return repository.get(`/${resource}/`)
},
getAll() {
return repository.get(`/${resource}/`)
},
}
+6 -6
View File
@@ -3,10 +3,10 @@ import repository from './repository'
const resource = 'logs'
export default {
getAuditTrail(params) {
return repository.get(`/${resource}/audit-trail/`, { params })
},
getMessages(params) {
return repository.get(`/${resource}/messages/`, { params })
},
getAuditTrail(params) {
return repository.get(`/${resource}/audit-trail/`, { params })
},
getMessages(params) {
return repository.get(`/${resource}/messages/`, { params })
},
}
+12 -12
View File
@@ -3,16 +3,16 @@ import repository from './repository'
const resource = 'parameters'
export default {
getApplications() {
return repository.get(`/${resource}/applications/`)
},
getApplication(app) {
return repository.get(`/${resource}/${app}/`)
},
getApplicationStructure(app) {
return repository.get(`/${resource}/structure/?app=${app}`)
},
saveApplication(app, data) {
return repository.put(`/${resource}/${app}/`, data)
},
getApplications() {
return repository.get(`/${resource}/applications/`)
},
getApplication(app) {
return repository.get(`/${resource}/${app}/`)
},
getApplicationStructure(app) {
return repository.get(`/${resource}/structure/?app=${app}`)
},
saveApplication(app, data) {
return repository.put(`/${resource}/${app}/`, data)
},
}
+56 -53
View File
@@ -6,68 +6,71 @@ import { useAuthStore } from '@/stores'
const _axios = axios.create()
_axios.interceptors.request.use(
function (config) {
const authStore = useAuthStore()
// Do something before request is sent
if (authStore.isAuthenticated) {
config.headers['Accept-Language'] = authStore.authUser.language
function (config) {
const authStore = useAuthStore()
// Do something before request is sent
if (authStore.isAuthenticated) {
config.headers['Accept-Language'] = authStore.authUser.language
}
return config
},
function (error) {
// Do something with request error
return Promise.reject(error)
}
return config
},
function (error) {
// Do something with request error
return Promise.reject(error)
}
)
// Add a response interceptor
_axios.interceptors.response.use(
function (response) {
// Do something with response data
return response
},
function (error) {
if (error.response.status === 418) {
router.push({ name: 'TwoFA' })
return Promise.reject(error)
}
if (error.response.status === 429) {
return Promise.reject(error)
}
if (error.response.status !== 401 || router.currentRoute.name === 'Login') {
return Promise.reject(error)
}
const refreshToken = Cookies.get('refreshToken')
const authStore = useAuthStore()
if (error.config.url.endsWith('/token/refresh/') || !refreshToken) {
authStore.$reset()
if (router.currentRoute.name !== 'Login') {
router.push({ name: 'Login' })
}
return Promise.reject(error)
}
return _axios
.post('/token/refresh/', { refresh: refreshToken })
.then((resp) => {
Cookies.set('token', resp.data.access, { sameSite: 'strict' })
_axios.defaults.headers.common.Authorization = `Bearer ${resp.data.access}`
const config = error.config
config.headers.Authorization = `Bearer ${resp.data.access}`
return new Promise((resolve, reject) => {
_axios
.request(config)
function (response) {
// Do something with response data
return response
},
function (error) {
if (error.response.status === 418) {
router.push({ name: 'TwoFA' })
return Promise.reject(error)
}
if (error.response.status === 429) {
return Promise.reject(error)
}
if (
error.response.status !== 401 ||
router.currentRoute.name === 'Login'
) {
return Promise.reject(error)
}
const refreshToken = Cookies.get('refreshToken')
const authStore = useAuthStore()
if (error.config.url.endsWith('/token/refresh/') || !refreshToken) {
authStore.$reset()
if (router.currentRoute.name !== 'Login') {
router.push({ name: 'Login' })
}
return Promise.reject(error)
}
return _axios
.post('/token/refresh/', { refresh: refreshToken })
.then((resp) => {
resolve(resp)
Cookies.set('token', resp.data.access, { sameSite: 'strict' })
_axios.defaults.headers.common.Authorization = `Bearer ${resp.data.access}`
const config = error.config
config.headers.Authorization = `Bearer ${resp.data.access}`
return new Promise((resolve, reject) => {
_axios
.request(config)
.then((resp) => {
resolve(resp)
})
.catch((error) => {
reject(error)
})
})
})
.catch((error) => {
reject(error)
Promise.reject(error)
})
})
})
.catch((error) => {
Promise.reject(error)
})
}
}
)
export default _axios
+14 -12
View File
@@ -3,16 +3,18 @@ import repository from './repository'
const resource = 'senderaddresses'
export default {
getAll() {
return repository.get(`${resource}/`)
},
getAllForMailbox(mailboxId) {
return repository.get(`${resource}/`, { params: { mailbox: mailboxId } })
},
create(data) {
return repository.post(`${resource}/`, data)
},
delete(addressId) {
return repository.delete(`${resource}/${addressId}/`)
},
getAll() {
return repository.get(`${resource}/`)
},
getAllForMailbox(mailboxId) {
return repository.get(`${resource}/`, {
params: { mailbox: mailboxId },
})
},
create(data) {
return repository.post(`${resource}/`, data)
},
delete(addressId) {
return repository.delete(`${resource}/${addressId}/`)
},
}
+11 -11
View File
@@ -3,15 +3,15 @@ import repository from './repository'
const resource = 'statistics'
export default {
getStatistics({ graphSet, period, graphicName, searchQuery, start, end }) {
const args = {
gset: graphSet,
period,
graphic: graphicName,
searchquery: searchQuery,
start: start,
end: end,
}
return repository.get(`${resource}/`, { params: args })
},
getStatistics({ graphSet, period, graphicName, searchQuery, start, end }) {
const args = {
gset: graphSet,
period,
graphic: graphicName,
searchquery: searchQuery,
start: start,
end: end,
}
return repository.get(`${resource}/`, { params: args })
},
}
+3 -3
View File
@@ -3,7 +3,7 @@ import repository from './repository'
const resource = 'transports'
export default {
getAll() {
return repository.get(`/${resource}/`)
},
getAll() {
return repository.get(`/${resource}/`)
},
}
+44 -35
View File
@@ -1,31 +1,40 @@
<template>
<v-card flat>
<v-card-text>
<label class="m-label">{{ $gettext('Recipient(s)') }}</label>
<v-textarea
v-model="form.recipients"
:hint="
$gettext('Indicate one or more recipients separated by a comma (,)')
"
persistent-hint
rows="3"
variant="outlined"
/>
<v-checkbox
v-model="form.keepcopies"
:label="$gettext('Keep local copies')"
:hint="
$gettext('Forward messages and store copies into your local mailbox')
"
persistent-hint
/>
</v-card-text>
<v-card-actions class="pa-4">
<v-btn color="success" variant="flat" :loading="loading" @click="submit">
{{ $gettext('Update') }}
</v-btn>
</v-card-actions>
</v-card>
<v-card flat>
<v-card-text>
<label class="m-label">{{ $gettext('Recipient(s)') }}</label>
<v-textarea
v-model="form.recipients"
:hint="
$gettext(
'Indicate one or more recipients separated by a comma (,)'
)
"
persistent-hint
rows="3"
variant="outlined"
/>
<v-checkbox
v-model="form.keepcopies"
:label="$gettext('Keep local copies')"
:hint="
$gettext(
'Forward messages and store copies into your local mailbox'
)
"
persistent-hint
/>
</v-card-text>
<v-card-actions class="pa-4">
<v-btn
color="success"
variant="flat"
:loading="loading"
@click="submit"
>
{{ $gettext('Update') }}
</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup lang="js">
@@ -41,17 +50,17 @@ const form = ref({})
const loading = ref(false)
function submit() {
loading.value = true
accountApi.setForward(form.value).then(() => {
loading.value = false
busStore.displayNotification({
type: 'success',
msg: $gettext('Forward updated'),
loading.value = true
accountApi.setForward(form.value).then(() => {
loading.value = false
busStore.displayNotification({
type: 'success',
msg: $gettext('Forward updated'),
})
})
})
}
accountApi.getForward().then((resp) => {
form.value = resp.data
form.value = resp.data
})
</script>
+142 -123
View File
@@ -1,83 +1,102 @@
<template>
<v-expansion-panels v-model="panel" flat>
<v-expansion-panel>
<v-expansion-panel-title>
{{ $gettext('General settings') }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-form ref="profileForm" @submit.prevent="updateGeneralSettings">
<label class="m-label">{{ $gettext('First name') }}</label>
<v-text-field
v-model="form.first_name"
variant="outlined"
density="compact"
/>
<label class="m-label"> {{ $gettext('Last name') }} </label>
<v-text-field
v-model="form.last_name"
variant="outlined"
density="compact"
/>
<label class="m-label">
{{ $gettext('Language') }}
<p class="text-disabled">
{{ $gettext('Feel free to improve the translations') }}
<a
href="https://explore.transifex.com/tonio/modoboa/"
target="_blank"
>{{ $gettext('here') }}</a
>
{{ $gettext('or suggest a new language') }}
<a
href="https://github.com/modoboa/modoboa/issues/new/choose"
target="_blank"
>{{ $gettext('there') }}</a
>
</p>
</label>
<v-autocomplete
v-model="form.language"
:items="languages"
item-title="label"
item-value="code"
variant="outlined"
density="compact"
:error-messages="languageError"
/>
<label class="m-label">{{ $gettext('Phone number') }}</label>
<v-text-field
v-model="form.phone_number"
:label="$gettext('Phone number')"
variant="outlined"
density="compact"
/>
<label class="m-label">{{ $gettext('Secondary email') }}</label>
<v-text-field
v-model="form.secondary_email"
variant="outlined"
density="compact"
:rules="[rules.emailOrNull]"
/>
<v-btn color="success" type="submit" :loading="loadingUpdateProfile">
{{ $gettext('Update settings') }}
</v-btn>
</v-form>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel>
<v-expansion-panel-title>
{{ $gettext('Password') }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-form ref="passwordForm" @submit.prevent="updatePassword">
<AccountPasswordForm v-model="form" with-password-check />
<v-btn color="success" type="submit" :loading="loadingPasswordUpdate">
{{ $gettext('Update Password') }}
</v-btn>
</v-form>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<v-expansion-panels v-model="panel" flat>
<v-expansion-panel>
<v-expansion-panel-title>
{{ $gettext('General settings') }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-form
ref="profileForm"
@submit.prevent="updateGeneralSettings"
>
<label class="m-label">{{ $gettext('First name') }}</label>
<v-text-field
v-model="form.first_name"
variant="outlined"
density="compact"
/>
<label class="m-label"> {{ $gettext('Last name') }} </label>
<v-text-field
v-model="form.last_name"
variant="outlined"
density="compact"
/>
<label class="m-label">
{{ $gettext('Language') }}
<p class="text-disabled">
{{
$gettext(
'Feel free to improve the translations'
)
}}
<a
href="https://explore.transifex.com/tonio/modoboa/"
target="_blank"
>{{ $gettext('here') }}</a
>
{{ $gettext('or suggest a new language') }}
<a
href="https://github.com/modoboa/modoboa/issues/new/choose"
target="_blank"
>{{ $gettext('there') }}</a
>
</p>
</label>
<v-autocomplete
v-model="form.language"
:items="languages"
item-title="label"
item-value="code"
variant="outlined"
density="compact"
:error-messages="languageError"
/>
<label class="m-label">{{
$gettext('Phone number')
}}</label>
<v-text-field
v-model="form.phone_number"
:label="$gettext('Phone number')"
variant="outlined"
density="compact"
/>
<label class="m-label">{{
$gettext('Secondary email')
}}</label>
<v-text-field
v-model="form.secondary_email"
variant="outlined"
density="compact"
:rules="[rules.emailOrNull]"
/>
<v-btn
color="success"
type="submit"
:loading="loadingUpdateProfile"
>
{{ $gettext('Update settings') }}
</v-btn>
</v-form>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel>
<v-expansion-panel-title>
{{ $gettext('Password') }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-form ref="passwordForm" @submit.prevent="updatePassword">
<AccountPasswordForm v-model="form" with-password-check />
<v-btn
color="success"
type="submit"
:loading="loadingPasswordUpdate"
>
{{ $gettext('Update Password') }}
</v-btn>
</v-form>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</template>
<script setup lang="js">
@@ -104,62 +123,62 @@ const loadingPasswordUpdate = ref(false)
const languageError = ref('')
function initFormFromAccount(account) {
form.value = {
first_name: account.first_name,
last_name: account.last_name,
language: account.language,
phone_number: account.phone_number,
secondary_email: account.secondary_email,
}
form.value = {
first_name: account.first_name,
last_name: account.last_name,
language: account.language,
phone_number: account.phone_number,
secondary_email: account.secondary_email,
}
}
async function updateGeneralSettings() {
const { valid } = await profileForm.value.validate()
if (!valid) {
return
}
loadingUpdateProfile.value = true
authStore
.updateAccount(form.value)
.then(() => {
busStore.displayNotification({ msg: $gettext('Profile updated') })
if (!(authStore.authUser.language in available)) {
languageError.value =
'<a href="google.com">language does not exists</a>'
}
})
.finally(() => (loadingUpdateProfile.value = false))
const { valid } = await profileForm.value.validate()
if (!valid) {
return
}
loadingUpdateProfile.value = true
authStore
.updateAccount(form.value)
.then(() => {
busStore.displayNotification({ msg: $gettext('Profile updated') })
if (!(authStore.authUser.language in available)) {
languageError.value =
'<a href="google.com">language does not exists</a>'
}
})
.finally(() => (loadingUpdateProfile.value = false))
}
async function updatePassword() {
const { valid } = await passwordForm.value.validate()
if (!valid) {
return
}
loadingPasswordUpdate.value = true
authStore
.updateAccount({ password: form.value.password })
.then(() => {
busStore.displayNotification({ msg: $gettext('Password updated') })
})
.finally(() => (loadingPasswordUpdate.value = false))
const { valid } = await passwordForm.value.validate()
if (!valid) {
return
}
loadingPasswordUpdate.value = true
authStore
.updateAccount({ password: form.value.password })
.then(() => {
busStore.displayNotification({ msg: $gettext('Password updated') })
})
.finally(() => (loadingPasswordUpdate.value = false))
}
onMounted(() => {
initFormFromAccount(account.value)
languagesApi.getAll().then((resp) => {
languages.value = resp.data
})
initFormFromAccount(account.value)
languagesApi.getAll().then((resp) => {
languages.value = resp.data
})
})
watch(
() => account,
(newValue) => {
if (newValue) {
initFormFromAccount(newValue)
} else {
form.value = {}
() => account,
(newValue) => {
if (newValue) {
initFormFromAccount(newValue)
} else {
form.value = {}
}
}
}
)
</script>
@@ -1,29 +1,29 @@
<template>
<v-card>
<v-card-title>
<span class="headline"> {{ $gettext('New recovery codes') }} </span>
</v-card-title>
<v-card-text>
<v-alert type="info">
{{
$gettext(
"The following recovery codes can be used one time each to let you regain access to your account, in case you lose your phone for example. Make sure to save them in a safe place, otherwise you won't be able to access your account anymore."
)
}}
</v-alert>
<ul>
<li v-for="token in tokens" :key="token">
{{ token }}
</li>
</ul>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey darken-1" text @click="close">
{{ $gettext('Close') }}
</v-btn>
</v-card-actions>
</v-card>
<v-card>
<v-card-title>
<span class="headline"> {{ $gettext('New recovery codes') }} </span>
</v-card-title>
<v-card-text>
<v-alert type="info">
{{
$gettext(
"The following recovery codes can be used one time each to let you regain access to your account, in case you lose your phone for example. Make sure to save them in a safe place, otherwise you won't be able to access your account anymore."
)
}}
</v-alert>
<ul>
<li v-for="token in tokens" :key="token">
{{ token }}
</li>
</ul>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey darken-1" text @click="close">
{{ $gettext('Close') }}
</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup lang="js">
@@ -33,6 +33,6 @@ const { $gettext } = useGettext()
defineProps({ tokens: { type: Array, default: null } })
const emit = defineEmits(['close'])
function close() {
emit('close')
emit('close')
}
</script>
@@ -1,122 +1,135 @@
<template>
<div>
<v-card flat>
<v-card-title>
<span class="text-subtitle-1">
{{ $gettext('Two factor authentication') }}
</span>
</v-card-title>
<v-card-text>
<template v-if="qrURL">
<v-row>
<v-col cols="4">
<v-row rows="2">
<QrcodeVue
:value="qrURL"
:size="250"
render-as="svg"
level="H"
class="qrcode"
/>
</v-row>
<v-row>
<v-btn color="primary" class="key" @click="copyKey">
{{ $gettext('Click here to copy the key') }}
<v-icon v-if="clicked" color="success">
mdi-check-all
</v-icon>
</v-btn>
</v-row>
</v-col>
<v-col cols="6">
<v-alert type="info" class="ma-4">
{{
$gettext(
'Install a soft token authenticator like FreeOTP or Google Authenticator from your application repository and use that app to scan this QR code.'
)
}}
</v-alert>
<label class="m-label">{{ $gettext('Pin code') }}</label>
<v-otp-input v-model="pinCode" />
<v-btn color="primary" @click="finalizeTFASetup">
{{ $gettext('Register') }}
</v-btn>
</v-col>
</v-row>
</template>
<template v-else-if="tokens.length">
<v-alert type="success">
{{
$gettext(
'Congratulations! Two-Factor Authentication is now enabled for your account.'
)
}}
</v-alert>
<p>
{{
$gettext(
"The following recovery codes can be used one time each to let you regain access to your account, in case you lose your phone for example. Make sure to save them in a safe place, otherwise you won't be able to access your account anymore."
)
}}
</p>
<ul>
<li v-for="token in tokens" :key="token">
{{ token }}
</li>
</ul>
</template>
<template v-else-if="tfa_enabled">
<v-alert type="info" class="mb-2">
{{
$gettext('Two-Factor Authentication is enabled for your account.')
}}
</v-alert>
<v-form ref="editTFAForm">
<label class="m-label">{{ $gettext('Password') }} </label>
<v-text-field
v-model="password"
autocomplete="new-password"
type="password"
variant="outlined"
:rules="[rules.required]"
:error-messages="passwordError"
<div>
<v-card flat>
<v-card-title>
<span class="text-subtitle-1">
{{ $gettext('Two factor authentication') }}
</span>
</v-card-title>
<v-card-text>
<template v-if="qrURL">
<v-row>
<v-col cols="4">
<v-row rows="2">
<QrcodeVue
:value="qrURL"
:size="250"
render-as="svg"
level="H"
class="qrcode"
/>
</v-row>
<v-row>
<v-btn
color="primary"
class="key"
@click="copyKey"
>
{{ $gettext('Click here to copy the key') }}
<v-icon v-if="clicked" color="success">
mdi-check-all
</v-icon>
</v-btn>
</v-row>
</v-col>
<v-col cols="6">
<v-alert type="info" class="ma-4">
{{
$gettext(
'Install a soft token authenticator like FreeOTP or Google Authenticator from your application repository and use that app to scan this QR code.'
)
}}
</v-alert>
<label class="m-label">{{
$gettext('Pin code')
}}</label>
<v-otp-input v-model="pinCode" />
<v-btn color="primary" @click="finalizeTFASetup">
{{ $gettext('Register') }}
</v-btn>
</v-col>
</v-row>
</template>
<template v-else-if="tokens.length">
<v-alert type="success">
{{
$gettext(
'Congratulations! Two-Factor Authentication is now enabled for your account.'
)
}}
</v-alert>
<p>
{{
$gettext(
"The following recovery codes can be used one time each to let you regain access to your account, in case you lose your phone for example. Make sure to save them in a safe place, otherwise you won't be able to access your account anymore."
)
}}
</p>
<ul>
<li v-for="token in tokens" :key="token">
{{ token }}
</li>
</ul>
</template>
<template v-else-if="tfa_enabled">
<v-alert type="info" class="mb-2">
{{
$gettext(
'Two-Factor Authentication is enabled for your account.'
)
}}
</v-alert>
<v-form ref="editTFAForm">
<label class="m-label"
>{{ $gettext('Password') }}
</label>
<v-text-field
v-model="password"
autocomplete="new-password"
type="password"
variant="outlined"
:rules="[rules.required]"
:error-messages="passwordError"
/>
<div class="mt-5">
<v-btn
color="error"
class="mr-2"
:loading="loadingDisable"
@click="disableTFA"
>
{{ $gettext('Disable 2FA') }}
</v-btn>
<v-btn
:loading="loadingReset"
@click="resetRecoveryCodes"
>
{{ $gettext('Reset recovery codes') }}
</v-btn>
</div>
</v-form>
</template>
<template v-else>
<div tag="p" class="my-4">
{{
$gettext(
"Two-Factor Authentication (2FA) is not yet activated for your account. Enabling this feature will increase your account's security."
)
}}
</div>
<v-btn color="success" @click="startTFASetup">
{{ $gettext('Enable 2FA') }}
</v-btn>
</template>
</v-card-text>
</v-card>
<v-dialog v-model="showCodesResetDialog" max-width="800px" persistent>
<RecoveryCodesResetDialog
:tokens="newTokens"
@close="closeRecoveryCodesResetDialog"
/>
<div class="mt-5">
<v-btn
color="error"
class="mr-2"
:loading="loadingDisable"
@click="disableTFA"
>
{{ $gettext('Disable 2FA') }}
</v-btn>
<v-btn :loading="loadingReset" @click="resetRecoveryCodes">
{{ $gettext('Reset recovery codes') }}
</v-btn>
</div>
</v-form>
</template>
<template v-else>
<div tag="p" class="my-4">
{{
$gettext(
"Two-Factor Authentication (2FA) is not yet activated for your account. Enabling this feature will increase your account's security."
)
}}
</div>
<v-btn color="success" @click="startTFASetup">
{{ $gettext('Enable 2FA') }}
</v-btn>
</template>
</v-card-text>
</v-card>
<v-dialog v-model="showCodesResetDialog" max-width="800px" persistent>
<RecoveryCodesResetDialog
:tokens="newTokens"
@close="closeRecoveryCodesResetDialog"
/>
</v-dialog>
</div>
</v-dialog>
</div>
</template>
<script setup lang="js">
@@ -150,116 +163,116 @@ const tokens = ref([])
const tfa_enabled = computed(() => authStore.authUser.tfa_enabled)
function getKey() {
accountApi.getKeyForTFASetup().then((resp) => {
key.value = resp.data.key
qrURL.value = resp.data.url
})
accountApi.getKeyForTFASetup().then((resp) => {
key.value = resp.data.key
qrURL.value = resp.data.url
})
}
function copyKey() {
navigator.clipboard.writeText(key.value)
clicked.value = true
navigator.clipboard.writeText(key.value)
clicked.value = true
}
function startTFASetup() {
accountApi.startTFASetup().then(() => {
getKey()
})
accountApi.startTFASetup().then(() => {
getKey()
})
}
async function finalizeTFASetup() {
authStore
.finalizeTFASetup(pinCode.value)
.then((response) => {
key.value = null
qrURL.value = null
tokens.value = response.data.tokens
})
.catch((error) => {
if (error.response.status === 400) {
pinCodeErrors.value = error.response.data
}
})
authStore
.finalizeTFASetup(pinCode.value)
.then((response) => {
key.value = null
qrURL.value = null
tokens.value = response.data.tokens
})
.catch((error) => {
if (error.response.status === 400) {
pinCodeErrors.value = error.response.data
}
})
}
async function disableTFA() {
const isValid = await editTFAForm.value.validate()
if (!isValid) {
return
}
const payload = {
password: password.value,
}
loadingDisable.value = true
accountApi
.disableTFA(payload)
.then(() => {
authStore.fetchUser()
})
.catch((error) => {
if (error.response.status === 400) {
passwordError.value = error.response.data.password
} else {
busStore.displayNotification({
msg: $gettext(error.response.data),
type: 'error',
const isValid = await editTFAForm.value.validate()
if (!isValid) {
return
}
const payload = {
password: password.value,
}
loadingDisable.value = true
accountApi
.disableTFA(payload)
.then(() => {
authStore.fetchUser()
})
.catch((error) => {
if (error.response.status === 400) {
passwordError.value = error.response.data.password
} else {
busStore.displayNotification({
msg: $gettext(error.response.data),
type: 'error',
})
}
})
.finally(() => {
loadingDisable.value = false
editTFAForm.value.reset()
})
}
})
.finally(() => {
loadingDisable.value = false
editTFAForm.value.reset()
})
}
async function resetRecoveryCodes() {
const isValid = await editTFAForm.value.validate()
if (!isValid) {
return
}
const payload = {
password: password.value,
}
loadingReset.value = true
const isValid = await editTFAForm.value.validate()
if (!isValid) {
return
}
const payload = {
password: password.value,
}
loadingReset.value = true
accountApi
.resetRecoveryCodes(payload)
.then((response) => {
newTokens.value = response.data.tokens
showCodesResetDialog.value = true
})
.catch((error) => {
if (error.response.status === 400) {
passwordError.value = error.response.data.password
} else {
busStore.displayNotification({
msg: $gettext(error.response.data),
type: 'error',
accountApi
.resetRecoveryCodes(payload)
.then((response) => {
newTokens.value = response.data.tokens
showCodesResetDialog.value = true
})
.catch((error) => {
if (error.response.status === 400) {
passwordError.value = error.response.data.password
} else {
busStore.displayNotification({
msg: $gettext(error.response.data),
type: 'error',
})
}
})
.finally(() => {
loadingReset.value = false
editTFAForm.value.reset()
})
}
})
.finally(() => {
loadingReset.value = false
editTFAForm.value.reset()
})
}
function closeRecoveryCodesResetDialog() {
newTokens.value = []
showCodesResetDialog.value = false
newTokens.value = []
showCodesResetDialog.value = false
}
if (!tfa_enabled.value) {
getKey()
getKey()
}
</script>
<style>
.qrcode {
margin-left: 20px;
margin-top: 2em;
margin-left: 20px;
margin-top: 2em;
}
.key {
margin-left: 10px;
margin-top: 1.75em;
margin-left: 10px;
margin-top: 1.75em;
}
</style>
+158 -154
View File
@@ -1,73 +1,77 @@
<template>
<v-card class="mt-6">
<v-data-table-server
v-model="selected"
:headers="headers"
:items="alarms"
:search="search"
item-value="id"
:items-length="totalAlarms"
:loading="loading"
elevation="0"
show-select
@update:options="fetchAlarms"
>
<template #top>
<v-toolbar color="white" flat>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn v-bind="props" size="small">
{{ $gettext('Actions') }}
<v-icon right>mdi-chevron-down</v-icon>
</v-btn>
<v-card class="mt-6">
<v-data-table-server
v-model="selected"
:headers="headers"
:items="alarms"
:search="search"
item-value="id"
:items-length="totalAlarms"
:loading="loading"
elevation="0"
show-select
@update:options="fetchAlarms"
>
<template #top>
<v-toolbar color="white" flat>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn v-bind="props" size="small">
{{ $gettext('Actions') }}
<v-icon right>mdi-chevron-down</v-icon>
</v-btn>
</template>
<v-list density="compact">
<MenuItems :items="getActionMenuItems()" />
</v-list>
</v-menu>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
placeholder="Search"
density="compact"
variant="outlined"
hide-details
flat
single-line
></v-text-field>
</v-toolbar>
</template>
<v-list density="compact">
<MenuItems :items="getActionMenuItems()" />
</v-list>
</v-menu>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
placeholder="Search"
density="compact"
variant="outlined"
hide-details
flat
single-line
></v-text-field>
</v-toolbar>
</template>
<template #[`item.status`]="{ item }">
<v-chip v-if="item.status === 1" color="warning" size="small">
{{ $gettext('Opened') }}
</v-chip>
<v-chip v-else color="info" size="small">
{{ $gettext('Closed') }}
</v-chip>
</template>
<template #[`item.mailbox`]="{ item }">
<template v-if="item.mailbox">
{{ item.mailbox.address }}@{{ item.domain.name }}
</template>
<template v-else> / </template>
</template>
<template #[`item.created`]="{ item }">
{{ $date(item.created) }}
</template>
<template #[`item.actions`]="{ item }">
<div class="text-right">
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn icon="mdi-dots-horizontal" v-bind="props" variant="text">
</v-btn>
<template #[`item.status`]="{ item }">
<v-chip v-if="item.status === 1" color="warning" size="small">
{{ $gettext('Opened') }}
</v-chip>
<v-chip v-else color="info" size="small">
{{ $gettext('Closed') }}
</v-chip>
</template>
<MenuItems :items="getMenuItems(item)" :obj="item" />
</v-menu>
</div>
</template>
</v-data-table-server>
</v-card>
<template #[`item.mailbox`]="{ item }">
<template v-if="item.mailbox">
{{ item.mailbox.address }}@{{ item.domain.name }}
</template>
<template v-else> / </template>
</template>
<template #[`item.created`]="{ item }">
{{ $date(item.created) }}
</template>
<template #[`item.actions`]="{ item }">
<div class="text-right">
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
icon="mdi-dots-horizontal"
v-bind="props"
variant="text"
>
</v-btn>
</template>
<MenuItems :items="getMenuItems(item)" :obj="item" />
</v-menu>
</div>
</template>
</v-data-table-server>
</v-card>
</template>
<script setup lang="js">
@@ -89,113 +93,113 @@ const selected = ref([])
const totalAlarms = ref(0)
const headers = [
{ title: $gettext('Triggered'), key: 'created' },
{ title: $gettext('Status'), key: 'status' },
{ title: $gettext('Domain'), key: 'domain.name', sortable: false },
{ title: $gettext('Mailbox'), key: 'mailbox', sortable: false },
{ title: $gettext('Message'), key: 'title' },
{
title: $gettext('Actions'),
key: 'actions',
sortable: false,
align: 'right',
},
{ title: $gettext('Triggered'), key: 'created' },
{ title: $gettext('Status'), key: 'status' },
{ title: $gettext('Domain'), key: 'domain.name', sortable: false },
{ title: $gettext('Mailbox'), key: 'mailbox', sortable: false },
{ title: $gettext('Message'), key: 'title' },
{
title: $gettext('Actions'),
key: 'actions',
sortable: false,
align: 'right',
},
]
function getActionMenuItems() {
const result = []
if (selected.value.length > 0) {
const result = []
if (selected.value.length > 0) {
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteAlarms,
color: 'red',
})
}
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteAlarms,
color: 'red',
label: $gettext('Reload'),
icon: 'mdi-reload',
onClick: fetchAlarms,
})
}
result.push({
label: $gettext('Reload'),
icon: 'mdi-reload',
onClick: fetchAlarms,
})
return result
return result
}
function getMenuItems(item) {
const result = []
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteAlarm,
color: 'red',
})
if (item.status === constants.ALARM_OPENED) {
const result = []
result.push({
label: $gettext('Close alarm'),
icon: 'mdi-check',
onClick: closeAlarm,
color: 'green',
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteAlarm,
color: 'red',
})
} else {
result.push({
label: $gettext('Reopen alarm'),
icon: 'mdi-alert-circle-outline',
onClick: openAlarm,
color: 'orange',
})
}
return result
if (item.status === constants.ALARM_OPENED) {
result.push({
label: $gettext('Close alarm'),
icon: 'mdi-check',
onClick: closeAlarm,
color: 'green',
})
} else {
result.push({
label: $gettext('Reopen alarm'),
icon: 'mdi-alert-circle-outline',
onClick: openAlarm,
color: 'orange',
})
}
return result
}
function fetchAlarms({ page, itemsPerPage, sortBy }) {
const params = {
page: page || 1,
page_size: itemsPerPage || 10,
}
if (sortBy) {
params.ordering = sortBy
.map((item) => (this.options.sortDesc[0] ? `-${item}` : item))
.join(',')
}
if (search.value !== '') {
params.search = search.value
}
loading.value = true
alarmsApi.getAll(params).then((resp) => {
alarms.value = resp.data.results
totalAlarms.value = resp.data.count
loading.value = false
})
const params = {
page: page || 1,
page_size: itemsPerPage || 10,
}
if (sortBy) {
params.ordering = sortBy
.map((item) => (this.options.sortDesc[0] ? `-${item}` : item))
.join(',')
}
if (search.value !== '') {
params.search = search.value
}
loading.value = true
alarmsApi.getAll(params).then((resp) => {
alarms.value = resp.data.results
totalAlarms.value = resp.data.count
loading.value = false
})
}
async function deleteAlarm(alarm, single = true) {
alarmsApi.delete(alarm.id).then(() => {
if (single) {
busStore.displayNotification({ msg: $gettext('Alarm deleted') })
fetchAlarms({})
}
})
alarmsApi.delete(alarm.id).then(() => {
if (single) {
busStore.displayNotification({ msg: $gettext('Alarm deleted') })
fetchAlarms({})
}
})
}
async function deleteAlarms() {
await alarmsApi.bulkDelete(selected.value.map((alarm) => alarm.id))
busStore.displayNotification({ msg: this.$gettext('Alarms deleted') })
selected.value = []
fetchAlarms({})
await alarmsApi.bulkDelete(selected.value.map((alarm) => alarm.id))
busStore.displayNotification({ msg: this.$gettext('Alarms deleted') })
selected.value = []
fetchAlarms({})
}
async function closeAlarm(alarm) {
alarmsApi
.switchStatus(alarm.id, { status: constants.ALARM_CLOSED })
.then(() => {
busStore.displayNotification({ msg: $gettext('Alarm closed') })
fetchAlarms({})
})
alarmsApi
.switchStatus(alarm.id, { status: constants.ALARM_CLOSED })
.then(() => {
busStore.displayNotification({ msg: $gettext('Alarm closed') })
fetchAlarms({})
})
}
async function openAlarm(alarm) {
alarmsApi
.switchStatus(alarm.id, { status: constants.ALARM_OPENED })
.then(() => {
busStore.displayNotification({ msg: $gettext('Alarm re-opened') })
fetchAlarms({})
})
alarmsApi
.switchStatus(alarm.id, { status: constants.ALARM_OPENED })
.then(() => {
busStore.displayNotification({ msg: $gettext('Alarm re-opened') })
fetchAlarms({})
})
}
fetchAlarms = debounce(fetchAlarms, 500)
@@ -203,9 +207,9 @@ fetchAlarms = debounce(fetchAlarms, 500)
<style scoped>
.v-text-field--outlined :deep(fieldset) {
border-color: #bfc5d2;
border-color: #bfc5d2;
}
.v-input--checkbox :deep(.v-label) {
font-size: 0.875rem !important;
font-size: 0.875rem !important;
}
</style>
@@ -1,103 +1,117 @@
<template>
<div v-if="alignments">
<v-card>
<v-toolbar dense elevation="0" class="mr-2">
<v-toolbar-title>$gettext('Alignment')</v-toolbar-title>
<v-spacer />
<v-btn icon="mdi-arrow-left" size="x-small" @click="previousWeek">
</v-btn>
<span class="mx-2"
>{{ weekStart.toLocaleString() }} -
{{ weekEnd.toLocaleString() }}</span
<div v-if="alignments">
<v-card>
<v-toolbar dense elevation="0" class="mr-2">
<v-toolbar-title>$gettext('Alignment')</v-toolbar-title>
<v-spacer />
<v-btn
icon="mdi-arrow-left"
size="x-small"
@click="previousWeek"
>
</v-btn>
<span class="mx-2"
>{{ weekStart.toLocaleString() }} -
{{ weekEnd.toLocaleString() }}</span
>
<v-btn icon="mdi-arrow-right" size="x-small" @click="nextWeek">
</v-btn>
</v-toolbar>
<v-card-text>
<v-row>
<v-col cols="5">
<apexchart
v-if="series"
type="pie"
height="350"
:options="options"
:series="series"
/>
</v-col>
<v-col cols="7">
<v-row>
<v-col v-for="box in boxes" :key="box.key" cols="6">
<v-sheet outlined :color="box.color" rounded>
<v-card outlined class="pa-10 text-center">
<span
:class="
'text-h4 ' +
getTextColors(box.color)
"
>{{ stats[box.key] }}</span
><br />
<span class="text-subtitle-1">{{
box.label
}}</span>
</v-card>
</v-sheet>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-expansion-panels v-model="panel" class="mt-2">
<v-expansion-panel :title="$gettext('Detail')">
<v-expansion-panel-text>
<DmarcSourceTable
:title="$gettext('Trusted sources')"
color="green"
:total="stats.aligned"
:sources="alignments.aligned"
/>
<DmarcSourceTable
:title="
$gettext('Partially trusted sources / Forwarders')
"
color="orange"
:total="stats.trusted"
:sources="alignments.trusted"
class="mt-4"
/>
<DmarcSourceTable
:title="$gettext('Forwarders with ARC')"
color="blue"
:total="stats.forwarded"
:sources="alignments.forwarded"
class="mt-4"
/>
<DmarcSourceTable
:title="$gettext('Unknown sources / Threats')"
color="red"
:total="stats.failed"
:sources="alignments.failed"
class="mt-4"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</div>
<div v-else>
<v-alert
v-if="dmarcDisabled"
type="info"
variant="outlined"
prominent
border="start"
class="mt-6"
>
<v-btn icon="mdi-arrow-right" size="x-small" @click="nextWeek"> </v-btn>
</v-toolbar>
<v-card-text>
<v-row>
<v-col cols="5">
<apexchart
v-if="series"
type="pie"
height="350"
:options="options"
:series="series"
/>
</v-col>
<v-col cols="7">
<v-row>
<v-col v-for="box in boxes" :key="box.key" cols="6">
<v-sheet outlined :color="box.color" rounded>
<v-card outlined class="pa-10 text-center">
<span :class="'text-h4 ' + getTextColors(box.color)">{{
stats[box.key]
}}</span
><br />
<span class="text-subtitle-1">{{ box.label }}</span>
</v-card>
</v-sheet>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-expansion-panels v-model="panel" class="mt-2">
<v-expansion-panel :title="$gettext('Detail')">
<v-expansion-panel-text>
<DmarcSourceTable
:title="$gettext('Trusted sources')"
color="green"
:total="stats.aligned"
:sources="alignments.aligned"
/>
<DmarcSourceTable
:title="$gettext('Partially trusted sources / Forwarders')"
color="orange"
:total="stats.trusted"
:sources="alignments.trusted"
class="mt-4"
/>
<DmarcSourceTable
:title="$gettext('Forwarders with ARC')"
color="blue"
:total="stats.forwarded"
:sources="alignments.forwarded"
class="mt-4"
/>
<DmarcSourceTable
:title="$gettext('Unknown sources / Threats')"
color="red"
:total="stats.failed"
:sources="alignments.failed"
class="mt-4"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</div>
<div v-else>
<v-alert
v-if="dmarcDisabled"
type="info"
variant="outlined"
prominent
border="start"
class="mt-6"
>
<div>
{{
$gettext('DMARC support does not seem to be enabled for this domain.')
}}
</div>
<div>
{{
$gettext(
'If you configured it recently, please wait for the first report to be received and processed.'
)
}}
</div>
</v-alert>
</div>
<div>
{{
$gettext(
'DMARC support does not seem to be enabled for this domain.'
)
}}
</div>
<div>
{{
$gettext(
'If you configured it recently, please wait for the first report to be received and processed.'
)
}}
</div>
</v-alert>
</div>
</template>
<script setup lang="js">
@@ -113,43 +127,43 @@ const { $gettext } = useGettext()
const order = ['aligned', 'trusted', 'forwarded', 'failed']
const props = defineProps({
domain: { type: Object, default: () => null },
domain: { type: Object, default: () => null },
})
const options = ref({
colors: [
colors.green.lighten2,
colors.orange.lighten2,
colors.blue.lighten2,
colors.red.lighten2,
],
labels: [
$gettext('Fully aligned'),
$gettext('Partially aligned'),
$gettext('Forwarded'),
$gettext('Failed'),
],
legend: {
position: 'bottom',
},
colors: [
colors.green.lighten2,
colors.orange.lighten2,
colors.blue.lighten2,
colors.red.lighten2,
],
labels: [
$gettext('Fully aligned'),
$gettext('Partially aligned'),
$gettext('Forwarded'),
$gettext('Failed'),
],
legend: {
position: 'bottom',
},
})
const alignments = ref(null)
const boxes = ref([
{ key: 'total', label: $gettext('Total'), color: 'primary lighten-2' },
{
key: 'aligned',
label: $gettext('Fully aligned'),
color: 'green lighten-2',
},
{
key: 'trusted',
label: $gettext('Partially aligned'),
color: 'orange lighten-2',
},
{ key: 'forwarded', label: $gettext('Forwarded'), color: 'blue lighten-2' },
{ key: 'failed', label: $gettext('Failed'), color: 'red lighten-2' },
{ key: 'total', label: $gettext('Total'), color: 'primary lighten-2' },
{
key: 'aligned',
label: $gettext('Fully aligned'),
color: 'green lighten-2',
},
{
key: 'trusted',
label: $gettext('Partially aligned'),
color: 'orange lighten-2',
},
{ key: 'forwarded', label: $gettext('Forwarded'), color: 'blue lighten-2' },
{ key: 'failed', label: $gettext('Failed'), color: 'red lighten-2' },
])
const now = DateTime.now()
@@ -162,84 +176,84 @@ const series = ref([])
const stats = ref({})
const weekStart = computed(() => {
const dt = DateTime.fromObject({
weekYear: currentYear.value,
weekNumber: currentWeek.value,
})
return dt.startOf('week')
const dt = DateTime.fromObject({
weekYear: currentYear.value,
weekNumber: currentWeek.value,
})
return dt.startOf('week')
})
const weekEnd = computed(() => {
const dt = DateTime.fromObject({
weekYear: currentYear.value,
weekNumber: currentWeek.value,
})
return dt.endOf('week')
const dt = DateTime.fromObject({
weekYear: currentYear.value,
weekNumber: currentWeek.value,
})
return dt.endOf('week')
})
function getTextColors(value) {
let result = ''
for (const color of value.split(' ')) {
if (result !== '') {
result += ' '
let result = ''
for (const color of value.split(' ')) {
if (result !== '') {
result += ' '
}
result += 'text-' + color
}
result += 'text-' + color
}
return result
return result
}
function nextWeek() {
if (currentWeek.value === 52) {
currentYear.value += 1
currentWeek.value = 1
} else {
currentWeek.value += 1
}
if (currentWeek.value === 52) {
currentYear.value += 1
currentWeek.value = 1
} else {
currentWeek.value += 1
}
}
function previousWeek() {
if (currentWeek.value === 1) {
currentYear.value -= 1
currentWeek.value = 52
} else {
currentWeek.value -= 1
}
if (currentWeek.value === 1) {
currentYear.value -= 1
currentWeek.value = 52
} else {
currentWeek.value -= 1
}
}
function fetchAlignmentStats() {
if (loading.value) {
return
}
if (loading.value) {
return
}
const period = `${currentYear.value}-${currentWeek.value}`
loading.value = true
domainsApi.getDomainDmarcAlignment(props.domain.pk, period).then((resp) => {
loading.value = false
if (resp.status === 204) {
dmarcDisabled.value = true
return
}
stats.value = { total: 0 }
alignments.value = resp.data
series.value = []
for (const key of order) {
let total = 0
for (const name in resp.data[key]) {
for (const ip in resp.data[key][name]) {
total += resp.data[key][name][ip].total
const period = `${currentYear.value}-${currentWeek.value}`
loading.value = true
domainsApi.getDomainDmarcAlignment(props.domain.pk, period).then((resp) => {
loading.value = false
if (resp.status === 204) {
dmarcDisabled.value = true
return
}
}
stats.value.total += total
stats.value[key] = total
series.value.push(total)
}
})
stats.value = { total: 0 }
alignments.value = resp.data
series.value = []
for (const key of order) {
let total = 0
for (const name in resp.data[key]) {
for (const ip in resp.data[key][name]) {
total += resp.data[key][name][ip].total
}
}
stats.value.total += total
stats.value[key] = total
series.value.push(total)
}
})
}
onMounted(() => fetchAlignmentStats())
watch(currentWeek, () => {
fetchAlignmentStats()
fetchAlignmentStats()
})
watch(currentYear, () => {
fetchAlignmentStats()
fetchAlignmentStats()
})
</script>
@@ -1,50 +1,59 @@
<template>
<div>
<div class="text-h6">
{{ title }} <span :class="text - `${color}-lighten-2`">{{ total }}</span>
<div>
<div class="text-h6">
{{ title }}
<span :class="text - `${color}-lighten-2`">{{ total }}</span>
</div>
<v-simple-table>
<tbody>
<template v-for="(source, name) in sources" :key="name">
<tr>
<th width="40%">{{ name }}</th>
<th>{{ $gettext('Total') }}</th>
<th>SPF</th>
<th>DKIM</th>
</tr>
<tr v-for="(counters, ip) in source" :key="ip">
<td>{{ ip }}</td>
<td>{{ counters.total }}</td>
<td>
<v-avatar color="green lighten-2" size="24">
<span class="text-white">{{
counters.spf.success
}}</span>
</v-avatar>
<v-avatar color="red lighten-2 ml-1" size="24">
<span class="text-white">{{
counters.spf.failure
}}</span>
</v-avatar>
</td>
<td>
<v-avatar color="green lighten-2" size="24">
<span class="text-white">{{
counters.dkim.success
}}</span>
</v-avatar>
<v-avatar color="red lighten-2 ml-1" size="24">
<span class="text-white">{{
counters.dkim.failure
}}</span>
</v-avatar>
</td>
</tr>
</template>
</tbody>
</v-simple-table>
</div>
<v-simple-table>
<tbody>
<template v-for="(source, name) in sources" :key="name">
<tr>
<th width="40%">{{ name }}</th>
<th>{{ $gettext('Total') }}</th>
<th>SPF</th>
<th>DKIM</th>
</tr>
<tr v-for="(counters, ip) in source" :key="ip">
<td>{{ ip }}</td>
<td>{{ counters.total }}</td>
<td>
<v-avatar color="green lighten-2" size="24">
<span class="text-white">{{ counters.spf.success }}</span>
</v-avatar>
<v-avatar color="red lighten-2 ml-1" size="24">
<span class="text-white">{{ counters.spf.failure }}</span>
</v-avatar>
</td>
<td>
<v-avatar color="green lighten-2" size="24">
<span class="text-white">{{ counters.dkim.success }}</span>
</v-avatar>
<v-avatar color="red lighten-2 ml-1" size="24">
<span class="text-white">{{ counters.dkim.failure }}</span>
</v-avatar>
</td>
</tr>
</template>
</tbody>
</v-simple-table>
</div>
</template>
<script setup lang="js">
import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
defineProps({
title: { type: String, default: '' },
color: { type: String, default: '' },
total: { type: Number, default: null },
sources: { type: Object, default: null },
title: { type: String, default: '' },
color: { type: String, default: '' },
total: { type: Number, default: null },
sources: { type: Object, default: null },
})
</script>
@@ -1,37 +1,39 @@
<template>
<button class="dkimbutton" @click="copyDKIM()">
<pre id="dkimdns" class="dkim"
>{{ domain.dkim_key_selector }}._domainkey.{{ domain.name }}. IN TXT ({{
splitKey
}})
<button class="dkimbutton" @click="copyDKIM()">
<pre id="dkimdns" class="dkim"
>{{ domain.dkim_key_selector }}._domainkey.{{
domain.name
}}. IN TXT ({{ splitKey }})
}})</pre
>
</button>
>
</button>
</template>
<script setup lang="js">
import { computed } from 'vue'
const props = defineProps({
domain: { type: Object, default: null },
domain: { type: Object, default: null },
})
const splitKey = computed(() => {
let key = `v=DKIM1;k=rsa;p=${props.domain.dkim_public_key}`
const result = []
while (key.length > 0) {
if (key.length > 74) {
result.push(` "${key.substring(0, 74)}"`)
key = key.substring(74)
} else {
result.push(` "${key}"`)
break
let key = `v=DKIM1;k=rsa;p=${props.domain.dkim_public_key}`
const result = []
while (key.length > 0) {
if (key.length > 74) {
result.push(` "${key.substring(0, 74)}"`)
key = key.substring(74)
} else {
result.push(` "${key}"`)
break
}
}
}
return result.join('\n')
return result.join('\n')
})
function copyDKIM() {
navigator.clipboard.writeText(document.getElementById('dkimdns').textContent)
navigator.clipboard.writeText(
document.getElementById('dkimdns').textContent
)
}
</script>
@@ -1,149 +1,175 @@
<template>
<v-card v-if="domain">
<v-card-title>
<span> DNS </span>
<v-btn
class="ml-2"
icon="mdi-information-outline"
:title="$gettext('DNS configuration help')"
variant="text"
density="compact"
@click="showConfigHelp = true"
></v-btn>
</v-card-title>
<v-card-text>
<translate class="overline">MX records</translate>
<template v-if="domain.dns_global_status == 'pending'">
<v-row>
<v-col>
<v-chip color="info" size="small">
{{ $gettext('Pending') }}
</v-chip>
</v-col>
</v-row>
</template>
<template v-else>
<v-data-table-virtual
:headers="mxRecordHeaders"
:items="detail.mx_records"
>
<template #[`item.updated`]="{ item }">
{{ $date(item.updated) }}
</template>
</v-data-table-virtual>
</template>
<div class="overline">{{ $gettext('Auto configuration') }}</div>
<template v-if="domain.dns_global_status == 'pending'">
<v-row>
<v-col>
<v-chip color="info" size="small">
{{ $gettext('Pending') }}
</v-chip>
</v-col>
</v-row>
</template>
<template v-else>
<v-row>
<v-col>
<v-chip v-if="detail.autoconfig_record" color="success">
{{
$gettext('autoconfig record (Mozilla): %{ value }', {
value: detail.autoconfig_record.value,
})
}}
</v-chip>
<v-chip v-else color="warning">
{{ $gettext('autoconfig record (Mozilla) not found') }}
</v-chip>
</v-col>
<v-col>
<v-chip v-if="detail.autodiscover_record" color="success">
{{
$gettext('autodiscover record (Microsoft): %{ value }', {
value: detail.autodiscover_record.value,
})
}}
</v-chip>
<v-chip v-else color="warning">
{{ $gettext('autodiscover record (Microsoft) not found') }}
</v-chip>
</v-col>
</v-row>
<div class="overline">{{ $gettext('Authentication') }}</div>
<v-row>
<v-col>
<v-chip v-if="detail.spf_record" color="success">
{{ $gettext('SPF record found') }}
</v-chip>
<v-chip v-else color="error">
{{ $gettext('SPF record not found') }}
</v-chip>
</v-col>
<v-col>
<v-chip v-if="detail.dkim_record" color="success">
{{ $gettext('DKIM record found') }}
</v-chip>
<v-chip v-else color="error">
{{ $gettext('DKIM record not found') }}
</v-chip>
</v-col>
<v-col>
<v-chip v-if="detail.dmarc_record" color="success">
{{ $gettext('DMARC record found') }}
</v-chip>
<v-chip v-else color="error">
{{ $gettext('DMARC record not found') }}
</v-chip>
</v-col>
</v-row>
</template>
<template v-if="domain.enable_dkim">
<div class="d-flex align-center flex-nowrap mt-4">
<div class="text-subtitle-2">
{{ $gettext('DKIM key') }}
</div>
<div class="ml-6">
<div v-if="domain.dkim_private_key_path && domain.dkim_public_key">
<v-btn color="primary" size="small" @click="showDKIMKey = true">
{{ $gettext('Show key') }}
</v-btn>
<v-btn
icon="mdi-content-copy"
variant="text"
<v-card v-if="domain">
<v-card-title>
<span> DNS </span>
<v-btn
class="ml-2"
:title="$gettext('Copy key to clipboard')"
@click="copyPubKey"
></v-btn>
<v-btn
icon="mdi-refresh"
:title="$gettext('Generate a new DKIM key')"
icon="mdi-information-outline"
:title="$gettext('DNS configuration help')"
variant="text"
:loading="keyLoading"
@click="generateNewKey"
></v-btn>
</div>
<div v-else>
{{ $gettext('Not generated') }}
<v-btn
icon="mdi-reload"
color="primary"
variant="text"
:loading="keyLoading"
@click="retryKeyGeneration"
></v-btn>
</div>
</div>
</div>
</template>
</v-card-text>
<v-dialog v-model="showConfigHelp" max-width="800px" persistent>
<DomainDNSConfig :domain="domain" @close="showConfigHelp = false" />
</v-dialog>
<v-dialog v-model="showDKIMKey" max-width="800px" persistent>
<DomainDKIMKey :domain="domain" @close="showDKIMKey = false" />
</v-dialog>
<ConfirmDialog ref="dialog" />
</v-card>
density="compact"
@click="showConfigHelp = true"
></v-btn>
</v-card-title>
<v-card-text>
<translate class="overline">MX records</translate>
<template v-if="domain.dns_global_status == 'pending'">
<v-row>
<v-col>
<v-chip color="info" size="small">
{{ $gettext('Pending') }}
</v-chip>
</v-col>
</v-row>
</template>
<template v-else>
<v-data-table-virtual
:headers="mxRecordHeaders"
:items="detail.mx_records"
>
<template #[`item.updated`]="{ item }">
{{ $date(item.updated) }}
</template>
</v-data-table-virtual>
</template>
<div class="overline">{{ $gettext('Auto configuration') }}</div>
<template v-if="domain.dns_global_status == 'pending'">
<v-row>
<v-col>
<v-chip color="info" size="small">
{{ $gettext('Pending') }}
</v-chip>
</v-col>
</v-row>
</template>
<template v-else>
<v-row>
<v-col>
<v-chip v-if="detail.autoconfig_record" color="success">
{{
$gettext(
'autoconfig record (Mozilla): %{ value }',
{
value: detail.autoconfig_record.value,
}
)
}}
</v-chip>
<v-chip v-else color="warning">
{{
$gettext(
'autoconfig record (Mozilla) not found'
)
}}
</v-chip>
</v-col>
<v-col>
<v-chip
v-if="detail.autodiscover_record"
color="success"
>
{{
$gettext(
'autodiscover record (Microsoft): %{ value }',
{
value: detail.autodiscover_record.value,
}
)
}}
</v-chip>
<v-chip v-else color="warning">
{{
$gettext(
'autodiscover record (Microsoft) not found'
)
}}
</v-chip>
</v-col>
</v-row>
<div class="overline">{{ $gettext('Authentication') }}</div>
<v-row>
<v-col>
<v-chip v-if="detail.spf_record" color="success">
{{ $gettext('SPF record found') }}
</v-chip>
<v-chip v-else color="error">
{{ $gettext('SPF record not found') }}
</v-chip>
</v-col>
<v-col>
<v-chip v-if="detail.dkim_record" color="success">
{{ $gettext('DKIM record found') }}
</v-chip>
<v-chip v-else color="error">
{{ $gettext('DKIM record not found') }}
</v-chip>
</v-col>
<v-col>
<v-chip v-if="detail.dmarc_record" color="success">
{{ $gettext('DMARC record found') }}
</v-chip>
<v-chip v-else color="error">
{{ $gettext('DMARC record not found') }}
</v-chip>
</v-col>
</v-row>
</template>
<template v-if="domain.enable_dkim">
<div class="d-flex align-center flex-nowrap mt-4">
<div class="text-subtitle-2">
{{ $gettext('DKIM key') }}
</div>
<div class="ml-6">
<div
v-if="
domain.dkim_private_key_path &&
domain.dkim_public_key
"
>
<v-btn
color="primary"
size="small"
@click="showDKIMKey = true"
>
{{ $gettext('Show key') }}
</v-btn>
<v-btn
icon="mdi-content-copy"
variant="text"
class="ml-2"
:title="$gettext('Copy key to clipboard')"
@click="copyPubKey"
></v-btn>
<v-btn
icon="mdi-refresh"
:title="$gettext('Generate a new DKIM key')"
variant="text"
:loading="keyLoading"
@click="generateNewKey"
></v-btn>
</div>
<div v-else>
{{ $gettext('Not generated') }}
<v-btn
icon="mdi-reload"
color="primary"
variant="text"
:loading="keyLoading"
@click="retryKeyGeneration"
></v-btn>
</div>
</div>
</div>
</template>
</v-card-text>
<v-dialog v-model="showConfigHelp" max-width="800px" persistent>
<DomainDNSConfig :domain="domain" @close="showConfigHelp = false" />
</v-dialog>
<v-dialog v-model="showDKIMKey" max-width="800px" persistent>
<DomainDKIMKey :domain="domain" @close="showDKIMKey = false" />
</v-dialog>
<ConfirmDialog ref="dialog" />
</v-card>
</template>
<script setup lang="js">
@@ -159,7 +185,7 @@ const { $gettext } = useGettext()
const busStore = useBusStore()
const props = defineProps({
modelValue: { type: Object, default: null },
modelValue: { type: Object, default: null },
})
const domain = computed(() => props.modelValue)
@@ -167,9 +193,9 @@ const domain = computed(() => props.modelValue)
const dialog = ref()
const detail = ref({})
const mxRecordHeaders = ref([
{ title: $gettext('Name'), value: 'name' },
{ title: $gettext('Address'), value: 'address' },
{ title: $gettext('Updated'), value: 'updated' },
{ title: $gettext('Name'), value: 'name' },
{ title: $gettext('Address'), value: 'address' },
{ title: $gettext('Updated'), value: 'updated' },
])
const keyLoading = ref(false)
@@ -177,80 +203,80 @@ const showConfigHelp = ref(false)
const showDKIMKey = ref(false)
function copyPubKey() {
navigator.clipboard.writeText(domain.value.dkim_public_key)
busStore.displayNotification({
msg: $gettext('DKIM key copied to clipboard'),
type: 'success',
})
navigator.clipboard.writeText(domain.value.dkim_public_key)
busStore.displayNotification({
msg: $gettext('DKIM key copied to clipboard'),
type: 'success',
})
}
function confirmGenNewKey() {
const payload = {
dkim_private_key_path: '',
}
domainsApi
.patchDomain(domain.value.pk, payload)
.then(() => {
busStore.displayNotification({
msg: $gettext(
'A new key will be generated soon. Refresh the page in a moment to see it.'
),
type: 'success',
})
domain.value.dkim_private_key_path = ''
keyLoading.value = false
})
.catch(() => {
keyLoading.value = false
})
const payload = {
dkim_private_key_path: '',
}
domainsApi
.patchDomain(domain.value.pk, payload)
.then(() => {
busStore.displayNotification({
msg: $gettext(
'A new key will be generated soon. Refresh the page in a moment to see it.'
),
type: 'success',
})
domain.value.dkim_private_key_path = ''
keyLoading.value = false
})
.catch(() => {
keyLoading.value = false
})
}
function cancelDKIMGen() {
keyLoading.value = false
keyLoading.value = false
}
async function retryKeyGeneration() {
keyLoading.value = true
domainsApi.getDomainDNSDetail(domain.value.pk).then(async (resp) => {
detail.value = resp.data
const result = await dialog.value.open(
$gettext('warning'),
$gettext(
'DKIM key does not seem to be genrated yet or has failed. Do you want to requeue the job?'
),
{
color: 'warning',
cancelLabel: $gettext('No'),
agreeLabel: $gettext('Yes'),
}
)
if (result) {
confirmGenNewKey()
} else {
cancelDKIMGen()
}
})
keyLoading.value = true
domainsApi.getDomainDNSDetail(domain.value.pk).then(async (resp) => {
detail.value = resp.data
const result = await dialog.value.open(
$gettext('warning'),
$gettext(
'DKIM key does not seem to be genrated yet or has failed. Do you want to requeue the job?'
),
{
color: 'warning',
cancelLabel: $gettext('No'),
agreeLabel: $gettext('Yes'),
}
)
if (result) {
confirmGenNewKey()
} else {
cancelDKIMGen()
}
})
}
function generateNewKey() {
dialog.value.open(
$gettext('Warning'),
$gettext(
'DKIM keys already exist for this domain. Do you want to overwrite them?'
),
{
color: 'error',
cancelLabel: $gettext('No'),
agreeLabel: $gettext('Yes'),
}
)
dialog.value.open(
$gettext('Warning'),
$gettext(
'DKIM keys already exist for this domain. Do you want to overwrite them?'
),
{
color: 'error',
cancelLabel: $gettext('No'),
agreeLabel: $gettext('Yes'),
}
)
}
watch(domain, (newDomain) => {
if (props.modelValue) {
domainsApi.getDomainDNSDetail(newDomain.pk).then((resp) => {
detail.value = resp.data
})
}
if (props.modelValue) {
domainsApi.getDomainDNSDetail(newDomain.pk).then((resp) => {
detail.value = resp.data
})
}
})
</script>
@@ -1,7 +1,7 @@
<template>
<v-chip :color="chipType" size="small">
{{ label }}
</v-chip>
<v-chip :color="chipType" size="small">
{{ label }}
</v-chip>
</template>
<script setup>
@@ -9,39 +9,39 @@ import { computed } from 'vue'
import { useGettext } from 'vue3-gettext'
const props = defineProps({
status: { type: String, default: 'pending' },
status: { type: String, default: 'pending' },
})
const { $gettext } = useGettext()
const label = computed(() => {
if (props.status === 'disabled') {
return $gettext('Disabled')
}
if (props.status === 'pending') {
return $gettext('Pending')
}
if (props.status === 'critical') {
return $gettext('Problem')
}
if (props.status === 'ok') {
return $gettext('Valid')
}
return $gettext('Unknown')
if (props.status === 'disabled') {
return $gettext('Disabled')
}
if (props.status === 'pending') {
return $gettext('Pending')
}
if (props.status === 'critical') {
return $gettext('Problem')
}
if (props.status === 'ok') {
return $gettext('Valid')
}
return $gettext('Unknown')
})
const chipType = computed(() => {
if (props.status === 'disabled') {
return ''
}
if (props.status === 'pending') {
return 'info'
}
if (props.status === 'critical') {
return 'error'
}
if (props.status === 'ok') {
return 'success'
}
return 'warning'
if (props.status === 'disabled') {
return ''
}
if (props.status === 'pending') {
return 'info'
}
if (props.status === 'critical') {
return 'error'
}
if (props.status === 'ok') {
return 'success'
}
return 'warning'
})
</script>
@@ -1,78 +1,82 @@
<template>
<v-card>
<v-card-title>
<span class="headline">
<div v-if="dialogMode" class="headline">
{{ $gettext('Administrators of') }} {{ domain.name }}
</div>
<div v-else class="headline">
{{ $gettext('Administrators') }}
</div>
</span>
</v-card-title>
<v-card-text>
<v-data-table-virtual :headers="adminHeaders" :items="administrators">
<template #[`item.username`]="{ item }">
<span v-if="item.id">{{ item.username }}</span>
<v-autocomplete
v-else
v-model="selectedAccount"
:label="$gettext('Select an account')"
:items="accounts"
item-title="username"
class="mt-5"
return-object
>
<template #append>
<v-btn
v-if="selectedAccount"
variant="text"
color="primary"
size="x-large"
@click="addAdministrator"
>
<v-icon>mdi-content-save</v-icon>
</v-btn>
</template>
</v-autocomplete>
</template>
<template #[`item.name`]="{ item }">
{{ item.first_name }} {{ item.last_name }}
</template>
<template #[`item.actions`]="{ item }">
<v-btn
v-if="item.id"
variant="text"
color="red"
:title="$gettext('Remove this administrator')"
size="x-large"
@click="removeAdministrator(item)"
>
<v-icon>mdi-delete-outline</v-icon>
</v-btn>
</template>
</v-data-table-virtual>
</v-card-text>
<v-card-actions>
<v-row>
<v-col :align="dialogMode ? 'left' : 'right'">
<v-btn
v-if="!hideAddBtn"
color="primary"
variant="outlined"
@click="addRow"
>
<v-icon small>mdi-plus</v-icon> {{ $gettext('Add administrator') }}
</v-btn>
</v-col>
<v-col v-if="dialogMode" align="right">
<v-btn color="grey darken-1" variant="text" @click="close">
{{ $gettext('Close') }}
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
<v-card>
<v-card-title>
<span class="headline">
<div v-if="dialogMode" class="headline">
{{ $gettext('Administrators of') }} {{ domain.name }}
</div>
<div v-else class="headline">
{{ $gettext('Administrators') }}
</div>
</span>
</v-card-title>
<v-card-text>
<v-data-table-virtual
:headers="adminHeaders"
:items="administrators"
>
<template #[`item.username`]="{ item }">
<span v-if="item.id">{{ item.username }}</span>
<v-autocomplete
v-else
v-model="selectedAccount"
:label="$gettext('Select an account')"
:items="accounts"
item-title="username"
class="mt-5"
return-object
>
<template #append>
<v-btn
v-if="selectedAccount"
variant="text"
color="primary"
size="x-large"
@click="addAdministrator"
>
<v-icon>mdi-content-save</v-icon>
</v-btn>
</template>
</v-autocomplete>
</template>
<template #[`item.name`]="{ item }">
{{ item.first_name }} {{ item.last_name }}
</template>
<template #[`item.actions`]="{ item }">
<v-btn
v-if="item.id"
variant="text"
color="red"
:title="$gettext('Remove this administrator')"
size="x-large"
@click="removeAdministrator(item)"
>
<v-icon>mdi-delete-outline</v-icon>
</v-btn>
</template>
</v-data-table-virtual>
</v-card-text>
<v-card-actions>
<v-row>
<v-col :align="dialogMode ? 'left' : 'right'">
<v-btn
v-if="!hideAddBtn"
color="primary"
variant="outlined"
@click="addRow"
>
<v-icon small>mdi-plus</v-icon>
{{ $gettext('Add administrator') }}
</v-btn>
</v-col>
<v-col v-if="dialogMode" align="right">
<v-btn color="grey darken-1" variant="text" @click="close">
{{ $gettext('Close') }}
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</template>
<script setup>
@@ -86,14 +90,14 @@ const busStore = useBusStore()
const { $gettext } = useGettext()
const props = defineProps({
domain: {
type: Object,
default: () => {},
},
dialogMode: {
type: Boolean,
default: false,
},
domain: {
type: Object,
default: () => {},
},
dialogMode: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['close'])
@@ -102,62 +106,65 @@ const accounts = ref([])
const administrators = ref([])
const adminHeaders = ref([
{ title: $gettext('Username'), value: 'username' },
{ title: $gettext('Name'), value: 'name' },
{ title: '', value: 'actions', align: 'end', sortable: false },
{ title: $gettext('Username'), value: 'username' },
{ title: $gettext('Name'), value: 'name' },
{ title: '', value: 'actions', align: 'end', sortable: false },
])
const hideAddBtn = ref(false)
const selectedAccount = ref(null)
function addRow() {
administrators.value.push({})
hideAddBtn.value = true
administrators.value.push({})
hideAddBtn.value = true
}
function close() {
hideAddBtn.value = false
emit('close')
hideAddBtn.value = false
emit('close')
}
function fetchAdministrators(domain) {
domainApi.getDomainAdministrators(domain.pk).then((resp) => {
administrators.value = resp.data
})
}
function fetchAccounts() {
accountApi.getAll({ role: 'DomainAdmins' }).then((resp) => {
accounts.value = resp.data.filter(
(account) =>
administrators.value.find((admin) => admin.id === account.pk) ===
undefined
)
})
}
function addAdministrator() {
domainApi
.addDomainAdministrator(props.domain.pk, selectedAccount.value.pk)
.then(() => {
fetchAdministrators(props.domain)
accounts.value = accounts.value.filter(
(account) => account.pk !== selectedAccount.value.pk
)
selectedAccount.value = null
hideAddBtn.value = false
busStore.displayNotification({ msg: $gettext('Administrator added') })
domainApi.getDomainAdministrators(domain.pk).then((resp) => {
administrators.value = resp.data
})
}
function fetchAccounts() {
accountApi.getAll({ role: 'DomainAdmins' }).then((resp) => {
accounts.value = resp.data.filter(
(account) =>
administrators.value.find(
(admin) => admin.id === account.pk
) === undefined
)
})
}
function addAdministrator() {
domainApi
.addDomainAdministrator(props.domain.pk, selectedAccount.value.pk)
.then(() => {
fetchAdministrators(props.domain)
accounts.value = accounts.value.filter(
(account) => account.pk !== selectedAccount.value.pk
)
selectedAccount.value = null
hideAddBtn.value = false
busStore.displayNotification({
msg: $gettext('Administrator added'),
})
})
}
function removeAdministrator(admin) {
domainApi.removeDomainAdministrator(props.domain.pk, admin.id).then(() => {
fetchAdministrators(props.domain)
fetchAccounts()
busStore.displayNotification({ msg: $gettext('Administrator removed') })
})
domainApi.removeDomainAdministrator(props.domain.pk, admin.id).then(() => {
fetchAdministrators(props.domain)
fetchAccounts()
busStore.displayNotification({ msg: $gettext('Administrator removed') })
})
}
onMounted(() => {
fetchAdministrators(props.domain)
fetchAccounts()
fetchAdministrators(props.domain)
fetchAccounts()
})
</script>
@@ -1,48 +1,52 @@
<template>
<v-card>
<v-form ref="vFormRef" @submit.prevent="submit">
<v-card-title>
<span class="headline">{{ title }}</span>
</v-card-title>
<v-card-text>
<label class="m-label">{{ $gettext('Name') }}</label>
<v-text-field
v-model="form.name"
dense
variant="outlined"
:rules="[rules.required]"
/>
<label class="m-label">{{ $gettext('Choose a domain') }}</label>
<v-select
v-model="form.target"
:items="Object.values(domainsStore.domains)"
item-title="name"
item-value="pk"
:rules="[rules.required]"
single-line
dense
variant="outlined"
/>
<v-switch v-model="form.enabled" label="Enabled" color="primary" />
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="emit('close')">
{{ $gettext('Close') }}
</v-btn>
<v-btn
v-if="domainAlias && domainAlias.pk"
color="error"
@click="deleteAlias"
>
{{ $gettext('Delete') }}
</v-btn>
<v-btn color="primary" type="submit">
{{ submitLabel }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
<v-card>
<v-form ref="vFormRef" @submit.prevent="submit">
<v-card-title>
<span class="headline">{{ title }}</span>
</v-card-title>
<v-card-text>
<label class="m-label">{{ $gettext('Name') }}</label>
<v-text-field
v-model="form.name"
dense
variant="outlined"
:rules="[rules.required]"
/>
<label class="m-label">{{ $gettext('Choose a domain') }}</label>
<v-select
v-model="form.target"
:items="Object.values(domainsStore.domains)"
item-title="name"
item-value="pk"
:rules="[rules.required]"
single-line
dense
variant="outlined"
/>
<v-switch
v-model="form.enabled"
label="Enabled"
color="primary"
/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="emit('close')">
{{ $gettext('Close') }}
</v-btn>
<v-btn
v-if="domainAlias && domainAlias.pk"
color="error"
@click="deleteAlias"
>
{{ $gettext('Delete') }}
</v-btn>
<v-btn color="primary" type="submit">
{{ submitLabel }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</template>
<script setup lang="js">
@@ -57,7 +61,7 @@ const domainsStore = useDomainsStore()
const emit = defineEmits(['close'])
const props = defineProps({
domainAlias: { type: Object, default: () => null },
domainAlias: { type: Object, default: () => null },
})
const form = ref({ enabled: true })
@@ -65,44 +69,44 @@ const form = ref({ enabled: true })
const vFormRef = ref()
const submitLabel = computed(() => {
return props.domainAlias ? $gettext('Update') : $gettext('Add')
return props.domainAlias ? $gettext('Update') : $gettext('Add')
})
const title = computed(() =>
props.domainAlias
? $gettext('Edit domain alias')
: $gettext('Add a new domain alias')
props.domainAlias
? $gettext('Edit domain alias')
: $gettext('Add a new domain alias')
)
function deleteAlias() {
domainsStore.deleteAlias(props.domainAlias).then(() => emit('close'))
domainsStore.deleteAlias(props.domainAlias).then(() => emit('close'))
}
async function submit() {
const { valid } = await vFormRef.value.validate()
if (!valid) {
return
}
try {
if (!form.value.pk) {
// Create
domainsStore.createAlias(form.value).then(() => {
emit('close')
})
} else {
// Update
const data = JSON.parse(JSON.stringify(form.value))
domainsStore.updateAlias(data).then(() => emit('close'))
const { valid } = await vFormRef.value.validate()
if (!valid) {
return
}
try {
if (!form.value.pk) {
// Create
domainsStore.createAlias(form.value).then(() => {
emit('close')
})
} else {
// Update
const data = JSON.parse(JSON.stringify(form.value))
domainsStore.updateAlias(data).then(() => emit('close'))
}
} catch (error) {
//TODO
console.error(error)
}
} catch (error) {
//TODO
console.error(error)
}
}
onMounted(() => {
if (props.domainAlias) {
form.value = JSON.parse(JSON.stringify(props.domainAlias))
}
if (props.domainAlias) {
form.value = JSON.parse(JSON.stringify(props.domainAlias))
}
})
</script>
@@ -1,71 +1,77 @@
<template>
<CreationForm
ref="form"
:title="$gettext('New domain')"
:steps="steps"
:form-getter="getForm"
:get-v-form-ref="getVFormRef"
:validate-object="validateDomain"
:summary-sections="summarySections"
@close="close"
@create="submit"
>
<template #[`form.general`]>
<DomainGeneralForm ref="general" v-model="domain" class="ml-4" />
</template>
<CreationForm
ref="form"
:title="$gettext('New domain')"
:steps="steps"
:form-getter="getForm"
:get-v-form-ref="getVFormRef"
:validate-object="validateDomain"
:summary-sections="summarySections"
@close="close"
@create="submit"
>
<template #[`form.general`]>
<DomainGeneralForm ref="general" v-model="domain" class="ml-4" />
</template>
<template #[`form.dns`]>
<DomainDNSForm ref="dns" v-model="domain" class="ml-4" />
</template>
<template #[`form.limitations`]>
<DomainLimitationsForm ref="limitations" v-model="domain" class="ml-4" />
</template>
<template #[`form.options`]>
<DomainOptionsForm
ref="options"
v-model="domain"
class="ml-4"
@create-admin="updateCreateAdmin"
/>
</template>
<template #[`form.transport`]>
<DomainTransportForm
ref="transport"
v-model="domain.transport"
class="ml-4"
/>
</template>
<template #[`item.random_password`]="{ item }">
<template v-if="item.value">
<v-col cols="12" class="highligth text-white">
<v-row>
<v-col
><span>{{ item.key }}</span></v-col
>
<v-col class="text-right">{{ $yesno(item.value) }}</v-col>
</v-row>
<v-row>
<v-col class="text-right py-1">
{{ domain.domain_admin.password }}
<v-btn
icon="mdi-clipboard-multiple-outline"
color="white"
variant="text"
:title="$gettext('Copy to clipboard')"
@click="copyPassword"
></v-btn>
</v-col>
</v-row>
</v-col>
</template>
<template v-else>
<v-col
><span class="text-grey">{{ item.key }}</span></v-col
>
<v-col class="text-right">{{ $yesno(item.value) }}</v-col>
</template>
</template>
</CreationForm>
<template #[`form.dns`]>
<DomainDNSForm ref="dns" v-model="domain" class="ml-4" />
</template>
<template #[`form.limitations`]>
<DomainLimitationsForm
ref="limitations"
v-model="domain"
class="ml-4"
/>
</template>
<template #[`form.options`]>
<DomainOptionsForm
ref="options"
v-model="domain"
class="ml-4"
@create-admin="updateCreateAdmin"
/>
</template>
<template #[`form.transport`]>
<DomainTransportForm
ref="transport"
v-model="domain.transport"
class="ml-4"
/>
</template>
<template #[`item.random_password`]="{ item }">
<template v-if="item.value">
<v-col cols="12" class="highligth text-white">
<v-row>
<v-col
><span>{{ item.key }}</span></v-col
>
<v-col class="text-right">{{
$yesno(item.value)
}}</v-col>
</v-row>
<v-row>
<v-col class="text-right py-1">
{{ domain.domain_admin.password }}
<v-btn
icon="mdi-clipboard-multiple-outline"
color="white"
variant="text"
:title="$gettext('Copy to clipboard')"
@click="copyPassword"
></v-btn>
</v-col>
</v-row>
</v-col>
</template>
<template v-else>
<v-col
><span class="text-grey">{{ item.key }}</span></v-col
>
<v-col class="text-right">{{ $yesno(item.value) }}</v-col>
</template>
</template>
</CreationForm>
</template>
<script setup lang="js">
@@ -89,35 +95,35 @@ const router = useRouter()
const emit = defineEmits(['close'])
const defaultDomain = {
name: '',
type: 'domain',
enabled: true,
enable_dns_checks: true,
enable_dkim: false,
dkim_key_selector: 'modoboa',
quota: 0,
default_mailbox_quota: 0,
domain_admin: {
username: 'admin',
random_password: false,
with_mailbox: false,
with_aliases: false,
},
transport: { settings: {} },
name: '',
type: 'domain',
enabled: true,
enable_dns_checks: true,
enable_dkim: false,
dkim_key_selector: 'modoboa',
quota: 0,
default_mailbox_quota: 0,
domain_admin: {
username: 'admin',
random_password: false,
with_mailbox: false,
with_aliases: false,
},
transport: { settings: {} },
}
const domain = ref(defaultDomain)
onMounted(() => {
//TODO : Store this in Pinia
ParametersApi.getApplication('admin').then((resp) => {
const params = resp.data.params
domain.value.quota = params.default_domain_quota
domain.value.default_mailbox_quota = params.default_mailbox_quota
if (params.default_domain_message_limit !== null) {
domain.value.message_limit = params.default_domain_message_limit
}
})
//TODO : Store this in Pinia
ParametersApi.getApplication('admin').then((resp) => {
const params = resp.data.params
domain.value.quota = params.default_domain_quota
domain.value.default_mailbox_quota = params.default_mailbox_quota
if (params.default_domain_message_limit !== null) {
domain.value.message_limit = params.default_domain_message_limit
}
})
})
const createAdmin = ref(false)
@@ -131,193 +137,197 @@ const transport = ref()
// Object to reference
const formStepsComponents = {
general: general,
dns: dns,
limitations: limitations,
options: options,
transport: transport,
general: general,
dns: dns,
limitations: limitations,
options: options,
transport: transport,
}
const domainSteps = [
{ name: 'general', title: $gettext('General') },
{ name: 'dns', title: $gettext('DNS') },
{ name: 'limitations', title: $gettext('Limitations') },
{ name: 'options', title: $gettext('Options') },
{ name: 'general', title: $gettext('General') },
{ name: 'dns', title: $gettext('DNS') },
{ name: 'limitations', title: $gettext('Limitations') },
{ name: 'options', title: $gettext('Options') },
]
const relaySteps = [
{ name: 'general', title: $gettext('General') },
{ name: 'dns', title: $gettext('DNS') },
{ name: 'limitations', title: $gettext('Limitations') },
{ name: 'transport', title: $gettext('Transport') },
{ name: 'general', title: $gettext('General') },
{ name: 'dns', title: $gettext('DNS') },
{ name: 'limitations', title: $gettext('Limitations') },
{ name: 'transport', title: $gettext('Transport') },
]
const steps = computed(() => {
return domain.value.type === 'domain' ? domainSteps : relaySteps
return domain.value.type === 'domain' ? domainSteps : relaySteps
})
const summarySections = computed(() => {
const result = [
{
title: $gettext('General'),
items: [
{ key: $gettext('Name'), value: domain.value.name },
{ key: $gettext('Type'), value: domain.value.type },
const result = [
{
key: $gettext('Enabled'),
value: domain.value.enabled,
type: 'yesno',
},
],
},
{
title: $gettext('DNS'),
items: [
{
key: $gettext('Enable DNS checks'),
value: domain.value.enable_dns_checks,
type: 'yesno',
title: $gettext('General'),
items: [
{ key: $gettext('Name'), value: domain.value.name },
{ key: $gettext('Type'), value: domain.value.type },
{
key: $gettext('Enabled'),
value: domain.value.enabled,
type: 'yesno',
},
],
},
{
key: $gettext('Enable DKIM signing'),
value: domain.value.enable_dkim,
type: 'yesno',
},
],
},
{
title: $gettext('Limitations'),
items: [
{ key: $gettext('Quota'), value: domain.value.quota },
{
key: $gettext('Default mailbox quota'),
value: domain.value.default_mailbox_quota,
title: $gettext('DNS'),
items: [
{
key: $gettext('Enable DNS checks'),
value: domain.value.enable_dns_checks,
type: 'yesno',
},
{
key: $gettext('Enable DKIM signing'),
value: domain.value.enable_dkim,
type: 'yesno',
},
],
},
{
key: $gettext('Message sending limit'),
value: domain.value.message_limit,
title: $gettext('Limitations'),
items: [
{ key: $gettext('Quota'), value: domain.value.quota },
{
key: $gettext('Default mailbox quota'),
value: domain.value.default_mailbox_quota,
},
{
key: $gettext('Message sending limit'),
value: domain.value.message_limit,
},
],
},
],
},
]
if (domain.value.enable_dkim) {
result[1].items.push({
key: $gettext('DKIM key selector'),
value: domain.value.dkim_key_selector,
})
result[1].items.push({
key: $gettext('DKIM key length'),
value: domain.value.dkim_key_length,
})
}
if (domain.value.type === 'relaydomain' && domain.value.transport.service) {
const relayEntry = {
title: $gettext('Transport'),
items: [
{ key: $gettext('Service'), value: domain.value.transport.service },
],
]
if (domain.value.enable_dkim) {
result[1].items.push({
key: $gettext('DKIM key selector'),
value: domain.value.dkim_key_selector,
})
result[1].items.push({
key: $gettext('DKIM key length'),
value: domain.value.dkim_key_length,
})
}
for (const setting of transport.value.service.settings) {
const item = {
key: setting.label,
value:
domain.value.transport.settings[
`${domain.value.transport.service}_${setting.name}`
],
}
if (setting.type === 'boolean') {
item.type = 'yesno'
}
relayEntry.items.push(item)
}
result.push(relayEntry)
} else if (domain.value.type === 'domain') {
const options = {
title: $gettext('Options'),
items: [
{
key: $gettext('Create a domain administrator'),
value: createAdmin.value,
type: 'yesno',
},
],
}
if (createAdmin.value) {
options.items.push(
{
key: $gettext('Administrator name'),
value: domain.value.domain_admin.username,
},
{
name: 'random_password',
key: $gettext('Random password'),
value: domain.value.domain_admin.random_password,
type: 'yesno',
},
{
key: $gettext('With mailbox'),
value: domain.value.domain_admin.with_mailbox,
type: 'yesno',
},
{
key: $gettext('Create aliases'),
value: domain.value.domain_admin.with_aliases,
type: 'yesno',
if (domain.value.type === 'relaydomain' && domain.value.transport.service) {
const relayEntry = {
title: $gettext('Transport'),
items: [
{
key: $gettext('Service'),
value: domain.value.transport.service,
},
],
}
)
for (const setting of transport.value.service.settings) {
const item = {
key: setting.label,
value: domain.value.transport.settings[
`${domain.value.transport.service}_${setting.name}`
],
}
if (setting.type === 'boolean') {
item.type = 'yesno'
}
relayEntry.items.push(item)
}
result.push(relayEntry)
} else if (domain.value.type === 'domain') {
const options = {
title: $gettext('Options'),
items: [
{
key: $gettext('Create a domain administrator'),
value: createAdmin.value,
type: 'yesno',
},
],
}
if (createAdmin.value) {
options.items.push(
{
key: $gettext('Administrator name'),
value: domain.value.domain_admin.username,
},
{
name: 'random_password',
key: $gettext('Random password'),
value: domain.value.domain_admin.random_password,
type: 'yesno',
},
{
key: $gettext('With mailbox'),
value: domain.value.domain_admin.with_mailbox,
type: 'yesno',
},
{
key: $gettext('Create aliases'),
value: domain.value.domain_admin.with_aliases,
type: 'yesno',
}
)
}
result.push(options)
}
result.push(options)
}
return result
return result
})
function close() {
emit('close')
emit('close')
}
function copyPassword() {
navigator.clipboard.writeText(domain.value.domain_admin.password).then(() => {
busStore.displayNotification({
msg: $gettext('Password copied to clipboard'),
})
})
navigator.clipboard
.writeText(domain.value.domain_admin.password)
.then(() => {
busStore.displayNotification({
msg: $gettext('Password copied to clipboard'),
})
})
}
function getForm(step) {
return formStepsComponents.value[step.name]
return formStepsComponents.value[step.name]
}
function getVFormRef(step) {
return formStepsComponents[step.name].value.vFormRef
return formStepsComponents[step.name].value.vFormRef
}
async function validateDomain() {}
function updateCreateAdmin(value) {
createAdmin.value = value
createAdmin.value = value
}
function submit() {
const data = JSON.parse(JSON.stringify(domain.value))
if (!createAdmin.value) {
delete data.domain_admin
}
if (data.message_limit === '') {
delete data.message_limit
}
if (data.type === 'relaydomain') {
transport.value.checkSettingTypes(data)
} else {
delete data.transport
}
domainsStore.createDomain(data).then((resp) => {
router.push({ name: 'DomainDetail', params: { id: resp.data.pk } })
busStore.displayNotification({ msg: $gettext('Domain created') })
close()
})
const data = JSON.parse(JSON.stringify(domain.value))
if (!createAdmin.value) {
delete data.domain_admin
}
if (data.message_limit === '') {
delete data.message_limit
}
if (data.type === 'relaydomain') {
transport.value.checkSettingTypes(data)
} else {
delete data.transport
}
domainsStore.createDomain(data).then((resp) => {
router.push({ name: 'DomainDetail', params: { id: resp.data.pk } })
busStore.displayNotification({ msg: $gettext('Domain created') })
close()
})
}
</script>
<style lang="scss">
.highligth {
background-color: #515d78;
background-color: #515d78;
}
</style>
@@ -1,43 +1,45 @@
<template>
<v-card>
<v-card-title>
<span class="headline">
{{ $gettext('DKIM public key for') }} {{ domain.name }}
</span>
</v-card-title>
<v-card-text>
<p>{{ $gettext('Raw format. Click to copy') }}</p>
<v-alert
border="start"
colored-border
border-color="primary lighten-3"
elevation="2"
dense
>
<button class="dkimbutton" @click="copyPubDKIM()">
<pre id="dkimpub" class="dkim">{{ domain.dkim_public_key }}</pre>
</button>
</v-alert>
</v-card-text>
<v-card-text>
<p>{{ $gettext('Bind/named format. Click to copy') }}</p>
<v-alert
border="start"
colored-border
border-color="primary lighten-3"
elevation="2"
dense
>
<DKIMKeyViewer :domain="domain" />
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="emit('close')">
{{ $gettext('Close') }}
</v-btn>
</v-card-actions>
</v-card>
<v-card>
<v-card-title>
<span class="headline">
{{ $gettext('DKIM public key for') }} {{ domain.name }}
</span>
</v-card-title>
<v-card-text>
<p>{{ $gettext('Raw format. Click to copy') }}</p>
<v-alert
border="start"
colored-border
border-color="primary lighten-3"
elevation="2"
dense
>
<button class="dkimbutton" @click="copyPubDKIM()">
<pre id="dkimpub" class="dkim">{{
domain.dkim_public_key
}}</pre>
</button>
</v-alert>
</v-card-text>
<v-card-text>
<p>{{ $gettext('Bind/named format. Click to copy') }}</p>
<v-alert
border="start"
colored-border
border-color="primary lighten-3"
elevation="2"
dense
>
<DKIMKeyViewer :domain="domain" />
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="emit('close')">
{{ $gettext('Close') }}
</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup lang="js">
@@ -47,29 +49,31 @@ import DKIMKeyViewer from './DKIMKeyViewer.vue'
const { $gettext } = useGettext()
defineProps({
domain: { type: Object, default: null },
domain: { type: Object, default: null },
})
const emit = defineEmits(['close'])
function copyPubDKIM() {
navigator.clipboard.writeText(document.getElementById('dkimpub').textContent)
navigator.clipboard.writeText(
document.getElementById('dkimpub').textContent
)
}
</script>
<style>
.dkimbutton {
border-radius: 10px;
border-radius: 10px;
}
.dkimbutton:hover {
background-color: rgba(131, 131, 250, 0.283);
transition: all 0.8s;
background-color: rgba(131, 131, 250, 0.283);
transition: all 0.8s;
}
.dkimbutton:active {
background-color: rgba(60, 255, 60, 0.136);
transition: all 0.2s;
background-color: rgba(60, 255, 60, 0.136);
transition: all 0.2s;
}
.dkim {
white-space: normal;
word-break: break-all;
white-space: normal;
word-break: break-all;
}
</style>
@@ -1,86 +1,88 @@
<template>
<v-card>
<v-card-title class="text-h5">
<span> {{ $gettext('DNS configuration for ') }} {{ domain.name }} </span>
</v-card-title>
<v-card-text>
<p class="text-subtitle-1 text-grey-darken-2 mb-4">
{{
$gettext(
'Here is an example of DNS configuration (bind format). Replace values between [ ] by the appropriate ones.'
)
}}
</p>
<v-alert
class="mb-4"
border="start"
border-color="primary lighten-3"
elevation="2"
color="white"
>
<div class="text-h6">MX</div>
<pre>
<v-card>
<v-card-title class="text-h5">
<span>
{{ $gettext('DNS configuration for ') }} {{ domain.name }}
</span>
</v-card-title>
<v-card-text>
<p class="text-subtitle-1 text-grey-darken-2 mb-4">
{{
$gettext(
'Here is an example of DNS configuration (bind format). Replace values between [ ] by the appropriate ones.'
)
}}
</p>
<v-alert
class="mb-4"
border="start"
border-color="primary lighten-3"
elevation="2"
color="white"
>
<div class="text-h6">MX</div>
<pre>
mail.{{ domain.name }}. IN A <strong>[{{$gettext('IP address of your Modoboa server')}}]</strong>
@ IN MX 10 mail.{{ domain.name }}.</pre>
</v-alert>
<v-alert
class="mb-4"
border="start"
border-color="primary lighten-3"
elevation="2"
dense
color="white"
>
<div class="text-h6">SPF</div>
<pre>@ IN TXT "v=spf1 mx ~all"</pre>
</v-alert>
</v-alert>
<v-alert
class="mb-4"
border="start"
border-color="primary lighten-3"
elevation="2"
dense
color="white"
>
<div class="text-h6">SPF</div>
<pre>@ IN TXT "v=spf1 mx ~all"</pre>
</v-alert>
<v-alert
v-if="domain.enable_dkim && domain.dkim_public_key"
class="mb-4"
border="start"
border-color="primary lighten-3"
elevation="2"
dense
color="white"
>
<div class="text-h6">DKIM</div>
<DKIMKeyViewer :domain="domain" />
</v-alert>
<v-alert
class="mb-4"
border="start"
border-color="primary lighten-3"
elevation="2"
dense
color="white"
>
<div class="text-h6">DMARC</div>
<pre>
<v-alert
v-if="domain.enable_dkim && domain.dkim_public_key"
class="mb-4"
border="start"
border-color="primary lighten-3"
elevation="2"
dense
color="white"
>
<div class="text-h6">DKIM</div>
<DKIMKeyViewer :domain="domain" />
</v-alert>
<v-alert
class="mb-4"
border="start"
border-color="primary lighten-3"
elevation="2"
dense
color="white"
>
<div class="text-h6">DMARC</div>
<pre>
_dmarc.{{ domain.name }}. IN TXT "v=DMARC1; p=quarantine; pct=100;"</pre
>
</v-alert>
<v-alert
class="mb-4"
border="start"
border-color="primary lighten-3"
elevation="2"
dense
color="white"
>
<div class="text-h6">{{ $gettext('Auto configuration') }}</div>
<pre>
>
</v-alert>
<v-alert
class="mb-4"
border="start"
border-color="primary lighten-3"
elevation="2"
dense
color="white"
>
<div class="text-h6">{{ $gettext('Auto configuration') }}</div>
<pre>
autoconfig.{{ domain.name }}. IN CNAME <strong>[{{$gettext('hostname of your automx server')}}]</strong>
autodiscover.{{ domain.name }}. IN CNAME <strong>[{{$gettext('hostname of your automx server')}}]</strong></pre>
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="emit('close')">
{{ $gettext('Close') }}
</v-btn>
</v-card-actions>
</v-card>
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="emit('close')">
{{ $gettext('Close') }}
</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup lang="js">
@@ -88,12 +90,12 @@ import DKIMKeyViewer from './DKIMKeyViewer.vue'
const emit = defineEmits(['close'])
defineProps({
domain: { type: Object, default: null },
domain: { type: Object, default: null },
})
</script>
<style scoped>
pre {
overflow-x: scroll;
overflow-x: scroll;
}
</style>
@@ -1,169 +1,195 @@
<template>
<LoadingData v-if="!domainsStore.domainsLoaded || working" />
<div v-else>
<v-expansion-panels v-model="panel" :multiple="formErrors">
<v-expansion-panel eager value="generalForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('General') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
{{ $gettext('Name: ') }} {{ editedDomain.name }}
</v-col>
<v-col cols="6">
{{ $gettext('Type: ') }} {{ editedDomain.type }}
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<DomainGeneralForm ref="generalForm" v-model="editedDomain" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel eager value="dnsForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('DNS') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
{{ $gettext('DNS checks') }}
<v-icon
v-if="editedDomain.enable_dns_checks"
color="success"
icon="mdi-check-circle-outline"
<LoadingData v-if="!domainsStore.domainsLoaded || working" />
<div v-else>
<v-expansion-panels v-model="panel" :multiple="formErrors">
<v-expansion-panel eager value="generalForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('General') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
{{ $gettext('Name: ') }}
{{ editedDomain.name }}
</v-col>
<v-col cols="6">
{{ $gettext('Type: ') }}
{{ editedDomain.type }}
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<DomainGeneralForm
ref="generalForm"
v-model="editedDomain"
/>
<v-icon v-else icon="mdi-check-circle-outline" />
</v-col>
<v-col cols="6">
{{ $gettext('DKIM signing') }}
<v-icon
v-if="editedDomain.enable_dkim"
color="success"
icon="mdi-check-circle-outline"
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel eager value="dnsForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('DNS') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
{{ $gettext('DNS checks') }}
<v-icon
v-if="
editedDomain.enable_dns_checks
"
color="success"
icon="mdi-check-circle-outline"
/>
<v-icon
v-else
icon="mdi-check-circle-outline"
/>
</v-col>
<v-col cols="6">
{{ $gettext('DKIM signing') }}
<v-icon
v-if="editedDomain.enable_dkim"
color="success"
icon="mdi-check-circle-outline"
/>
<v-icon
v-else
icon="mdi-close-circle-outline"
/>
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<DomainDNSForm ref="dnsForm" v-model="editedDomain" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel eager value="limitationForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Limitations') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Quota: ') }}
{{ editedDomain.quota }}
</div>
</v-col>
<v-col
v-if="editedDomain.message_limit"
cols="6"
>
<div class="mr-2">
{{ $gettext('Sending limit: ') }}
{{ editedDomain.message_limit }}
</div>
</v-col>
<v-col v-else cols="6">
<div class="mr-2">
{{ $gettext('No sending limit') }}
</div>
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<DomainLimitationsForm
ref="limitationForm"
v-model="editedDomain"
/>
<v-icon v-else icon="mdi-close-circle-outline" />
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<DomainDNSForm ref="dnsForm" v-model="editedDomain" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel eager value="limitationForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Limitations') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Quota: ') }} {{ editedDomain.quota }}
</div>
</v-col>
<v-col v-if="editedDomain.message_limit" cols="6">
<div class="mr-2">
{{ $gettext('Sending limit: ') }}
{{ editedDomain.message_limit }}
</div>
</v-col>
<v-col v-else cols="6">
<div class="mr-2">{{ $gettext('No sending limit') }}</div>
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<DomainLimitationsForm ref="limitationForm" v-model="editedDomain" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel
v-if="editedDomain.type === 'relaydomain'"
value="transportForm"
eager
>
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Transport') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Service: ') }}
{{ editedDomain.transport.service }}
</div>
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<DomainTransportForm
ref="transportForm"
v-model="editedDomain.transport"
/>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel
v-if="
limitsConfig.params &&
limitsConfig.params.enable_domain_limits &&
(authUser.role === 'SuperAdmins' || authUser.role === 'Resellers')
"
value="resourcesForm"
eager
>
<v-expansion-panel-title>
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Resources') }}
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<ResourcesForm ref="resourcesForm" v-model="editedDomain.resources" />
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<div class="mt-4 d-flex justify-end">
<v-btn :loading="working" @click="router.go(-1)">
{{ $gettext('Cancel') }}
</v-btn>
<v-btn
class="ml-4"
color="primary darken-1"
:loading="working"
@click="save"
>
{{ $gettext('Save') }}
</v-btn>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel
v-if="editedDomain.type === 'relaydomain'"
value="transportForm"
eager
>
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Transport') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Service: ') }}
{{ editedDomain.transport.service }}
</div>
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<DomainTransportForm
ref="transportForm"
v-model="editedDomain.transport"
/>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel
v-if="
limitsConfig.params &&
limitsConfig.params.enable_domain_limits &&
(authUser.role === 'SuperAdmins' ||
authUser.role === 'Resellers')
"
value="resourcesForm"
eager
>
<v-expansion-panel-title>
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Resources') }}
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<ResourcesForm
ref="resourcesForm"
v-model="editedDomain.resources"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<div class="mt-4 d-flex justify-end">
<v-btn :loading="working" @click="router.go(-1)">
{{ $gettext('Cancel') }}
</v-btn>
<v-btn
class="ml-4"
color="primary darken-1"
:loading="working"
@click="save"
>
{{ $gettext('Save') }}
</v-btn>
</div>
</div>
</div>
</template>
<script setup lang="js">
@@ -203,61 +229,61 @@ const dnsForm = ref()
// Form map
const formMap = computed(() => {
const map = {
generalForm: generalForm,
limitationForm: limitationForm,
dnsForm: dnsForm,
}
if (editedDomain.value.type === 'relaydomain') {
map.transportForm = transportForm
}
if (
limitsConfig.value.params &&
limitsConfig.value.params.enable_domain_limits &&
(authUser.value.role === 'SuperAdmins' ||
authUser.value.role === 'Resellers')
) {
map.resourcesForm = resourcesForm
}
return map
const map = {
generalForm: generalForm,
limitationForm: limitationForm,
dnsForm: dnsForm,
}
if (editedDomain.value.type === 'relaydomain') {
map.transportForm = transportForm
}
if (
limitsConfig.value.params &&
limitsConfig.value.params.enable_domain_limits &&
(authUser.value.role === 'SuperAdmins' ||
authUser.value.role === 'Resellers')
) {
map.resourcesForm = resourcesForm
}
return map
})
const formErrors = ref(false)
async function save() {
formErrors.value = false
panel.value = []
for (const [panelName, formRef] of Object.entries(formMap.value)) {
if (formRef.value != null) {
const { valid } = await formRef.value.vFormRef.validate()
if (!valid) {
formErrors.value = true
panel.value.push(panelName)
}
formErrors.value = false
panel.value = []
for (const [panelName, formRef] of Object.entries(formMap.value)) {
if (formRef.value != null) {
const { valid } = await formRef.value.vFormRef.validate()
if (!valid) {
formErrors.value = true
panel.value.push(panelName)
}
}
}
}
if (formErrors.value) {
return
}
working.value = true
try {
const data = JSON.parse(JSON.stringify(editedDomain.value))
if (data.transport === null) {
delete data.transport
if (formErrors.value) {
return
}
if (data.type === 'relaydomain') {
transportForm.value.checkSettingTypes(data)
working.value = true
try {
const data = JSON.parse(JSON.stringify(editedDomain.value))
if (data.transport === null) {
delete data.transport
}
if (data.type === 'relaydomain') {
transportForm.value.checkSettingTypes(data)
}
domainsStore.updateDomain(data).then(() => {
router.go(-1)
})
} finally {
working.value = false
}
domainsStore.updateDomain(data).then(() => {
router.go(-1)
})
} finally {
working.value = false
}
}
onMounted(() => {
parametersApi.getApplication('limits').then((resp) => {
limitsConfig.value.data = resp.data
})
parametersApi.getApplication('limits').then((resp) => {
limitsConfig.value.data = resp.data
})
})
</script>
@@ -1,178 +1,203 @@
<template>
<v-card class="mt-6">
<v-data-table
v-model="selected"
:expanded="expanded"
:headers="domainHeaders"
:items="Object.values(domains)"
item-value="name"
:search="search"
show-select
expand-on-click
:loading="!domainsLoaded"
>
<template #top>
<v-toolbar color="white" flat>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
variant="flat"
append-icon="mdi-chevron-down"
v-bind="props"
size="small"
>
{{ $gettext('Actions') }}
</v-btn>
</template>
<v-list density="compact"> </v-list>
</v-menu>
<v-btn
variant="text"
icon="mdi-reload"
@click="reloadDomains"
></v-btn>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
:placeholder="$gettext('Search')"
variant="outlined"
single-line
flat
hide-details
></v-text-field>
</v-toolbar>
</template>
<template
#item="{
item,
internalItem,
isExpanded,
toggleExpand,
isSelected,
toggleSelect,
}"
>
<tr>
<td
class="v-data-table__td v-data-table-column--no-padding v-data-table-column--align-start"
>
<v-checkbox-btn
:value="!isSelected(internalItem)"
@click="toggleSelect(internalItem)"
/>
</td>
<td>
<router-link
:to="{ name: 'DomainDetail', params: { id: item.pk } }"
>
{{ item.name }}
</router-link>
<v-chip
v-if="item.type === 'relaydomain'"
size="small"
color="primary"
class="ml-2"
>
{{ $gettext('Relay') }}
</v-chip>
<span v-if="!item.enabled" class="ml-2">
({{ $gettext('disabled') }})
</span>
</td>
<td>
{{ item.domainalias_count }} aliases
<v-btn
v-if="item.domainalias_count"
variant="text"
:icon="
isExpanded(internalItem) ? 'mdi-chevron-up' : 'mdi-chevron-down'
"
@click="loadAliases(item, internalItem, isExpanded, toggleExpand)"
>
</v-btn>
</td>
<td>
<DNSStatusChip :status="item.dns_global_status" />
</td>
<td>
<v-progress-linear v-model="item.allocated_quota_in_percent" />
</td>
<td>
<v-progress-linear v-model="item.allocated_quota_in_percent" />
</td>
<td>
<div class="text-right">
<v-menu offset-y>
<template #activator="{ props }">
<v-badge
v-if="item.opened_alarms_count"
bordered
color="error"
icon="mdi-bell"
overlap
>
<v-card class="mt-6">
<v-data-table
v-model="selected"
:expanded="expanded"
:headers="domainHeaders"
:items="Object.values(domains)"
item-value="name"
:search="search"
show-select
expand-on-click
:loading="!domainsLoaded"
>
<template #top>
<v-toolbar color="white" flat>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
variant="flat"
append-icon="mdi-chevron-down"
v-bind="props"
size="small"
>
{{ $gettext('Actions') }}
</v-btn>
</template>
<v-list density="compact"> </v-list>
</v-menu>
<v-btn
icon="mdi-dots-horizontal"
variant="text"
v-bind="props"
>
</v-btn>
</v-badge>
<v-btn
v-else
icon="mdi-dots-horizontal"
variant="text"
v-bind="props"
>
</v-btn>
</template>
<MenuItems :items="getDomainMenuItems(item)" :obj="item" />
</v-menu>
</div>
</td>
</tr>
</template>
<template #expanded-row="{ item, columns }">
<tr class="grey lighten-4">
<td :colspan="columns.length">
<span
v-for="alias in aliases[item.name]"
:key="alias.name"
class="mr-4"
variant="text"
icon="mdi-reload"
@click="reloadDomains"
></v-btn>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
:placeholder="$gettext('Search')"
variant="outlined"
single-line
flat
hide-details
></v-text-field>
</v-toolbar>
</template>
<template
#item="{
item,
internalItem,
isExpanded,
toggleExpand,
isSelected,
toggleSelect,
}"
>
<a href="#" class="mr-2" @click="editDomainAlias(item, alias)">{{
alias.name
}}</a>
<v-chip variant="elevated" size="x-small" color="success"
>DNS OK</v-chip
>
</span>
</td>
</tr>
</template>
</v-data-table>
<ConfirmDialog ref="confirm">
<v-checkbox
v-model="keepDomainFolder"
:label="$gettext('Do not delete domain folder')"
hide-details
/>
</ConfirmDialog>
<v-dialog v-model="showAliasForm" persistent max-width="800px">
<DomainAliasForm
:domain-alias="selectedDomainAlias"
@close="closeDomainAliasForm"
/>
</v-dialog>
<v-dialog v-model="showAdminList" persistent max-width="800px">
<DomainAdminList
:domain="selectedDomain"
dialog-mode
@close="showAdminList = false"
/>
</v-dialog>
</v-card>
<tr>
<td
class="v-data-table__td v-data-table-column--no-padding v-data-table-column--align-start"
>
<v-checkbox-btn
:value="!isSelected(internalItem)"
@click="toggleSelect(internalItem)"
/>
</td>
<td>
<router-link
:to="{
name: 'DomainDetail',
params: { id: item.pk },
}"
>
{{ item.name }}
</router-link>
<v-chip
v-if="item.type === 'relaydomain'"
size="small"
color="primary"
class="ml-2"
>
{{ $gettext('Relay') }}
</v-chip>
<span v-if="!item.enabled" class="ml-2">
({{ $gettext('disabled') }})
</span>
</td>
<td>
{{ item.domainalias_count }} aliases
<v-btn
v-if="item.domainalias_count"
variant="text"
:icon="
isExpanded(internalItem)
? 'mdi-chevron-up'
: 'mdi-chevron-down'
"
@click="
loadAliases(
item,
internalItem,
isExpanded,
toggleExpand
)
"
>
</v-btn>
</td>
<td>
<DNSStatusChip :status="item.dns_global_status" />
</td>
<td>
<v-progress-linear
v-model="item.allocated_quota_in_percent"
/>
</td>
<td>
<v-progress-linear
v-model="item.allocated_quota_in_percent"
/>
</td>
<td>
<div class="text-right">
<v-menu offset-y>
<template #activator="{ props }">
<v-badge
v-if="item.opened_alarms_count"
bordered
color="error"
icon="mdi-bell"
overlap
>
<v-btn
icon="mdi-dots-horizontal"
variant="text"
v-bind="props"
>
</v-btn>
</v-badge>
<v-btn
v-else
icon="mdi-dots-horizontal"
variant="text"
v-bind="props"
>
</v-btn>
</template>
<MenuItems
:items="getDomainMenuItems(item)"
:obj="item"
/>
</v-menu>
</div>
</td>
</tr>
</template>
<template #expanded-row="{ item, columns }">
<tr class="grey lighten-4">
<td :colspan="columns.length">
<span
v-for="alias in aliases[item.name]"
:key="alias.name"
class="mr-4"
>
<a
href="#"
class="mr-2"
@click="editDomainAlias(item, alias)"
>{{ alias.name }}</a
>
<v-chip
variant="elevated"
size="x-small"
color="success"
>DNS OK</v-chip
>
</span>
</td>
</tr>
</template>
</v-data-table>
<ConfirmDialog ref="confirm">
<v-checkbox
v-model="keepDomainFolder"
:label="$gettext('Do not delete domain folder')"
hide-details
/>
</ConfirmDialog>
<v-dialog v-model="showAliasForm" persistent max-width="800px">
<DomainAliasForm
:domain-alias="selectedDomainAlias"
@close="closeDomainAliasForm"
/>
</v-dialog>
<v-dialog v-model="showAdminList" persistent max-width="800px">
<DomainAdminList
:domain="selectedDomain"
dialog-mode
@close="showAdminList = false"
/>
</v-dialog>
</v-card>
</template>
<script setup lang="js">
@@ -197,17 +222,21 @@ const domainsLoaded = computed(() => domainStore.domainsLoaded)
const aliases = computed(() => domainStore.domainAliases)
const domainHeaders = [
{ title: $gettext('Name'), key: 'name' },
{ title: $gettext('Aliases'), key: 'domainalias_count' },
{ title: $gettext('DNS status'), key: 'dns_global_status', sortable: false },
{ title: $gettext('Sending limit'), key: 'message_limit' },
{ title: $gettext('Quota'), key: 'allocated_quota_in_percent' },
{
title: $gettext('Actions'),
value: 'actions',
sortable: false,
align: 'end',
},
{ title: $gettext('Name'), key: 'name' },
{ title: $gettext('Aliases'), key: 'domainalias_count' },
{
title: $gettext('DNS status'),
key: 'dns_global_status',
sortable: false,
},
{ title: $gettext('Sending limit'), key: 'message_limit' },
{ title: $gettext('Quota'), key: 'allocated_quota_in_percent' },
{
title: $gettext('Actions'),
value: 'actions',
sortable: false,
align: 'end',
},
]
const confirm = ref()
@@ -221,105 +250,105 @@ const selected = ref([])
const expanded = ref([])
function reloadDomains() {
domainStore.getDomains()
domainStore.getDomains()
}
function closeDomainAliasForm() {
showAliasForm.value = false
selectedDomain.value = null
selectedDomainAlias.value = null
showAliasForm.value = false
selectedDomain.value = null
selectedDomainAlias.value = null
}
async function deleteDomain(domain) {
const result = await confirm.value.open(
$gettext('Warning'),
$gettext('Do you really want to delete the domain %{ domain }?', {
domain: domain.name,
}),
{
color: 'error',
cancelLabel: $gettext('No'),
agreeLabel: $gettext('Yes'),
const result = await confirm.value.open(
$gettext('Warning'),
$gettext('Do you really want to delete the domain %{ domain }?', {
domain: domain.name,
}),
{
color: 'error',
cancelLabel: $gettext('No'),
agreeLabel: $gettext('Yes'),
}
)
if (!result) {
return
}
)
if (!result) {
return
}
const data = { keep_folder: keepDomainFolder.value }
domainStore.deleteDomain({ id: domain.pk, data }).then(() => {
busStore.displayNotification({ msg: $gettext('Domain deleted') })
keepDomainFolder.value = false
})
const data = { keep_folder: keepDomainFolder.value }
domainStore.deleteDomain({ id: domain.pk, data }).then(() => {
busStore.displayNotification({ msg: $gettext('Domain deleted') })
keepDomainFolder.value = false
})
}
function editDomain(domain) {
router.push({ name: 'DomainEdit', params: { id: domain.pk } })
router.push({ name: 'DomainEdit', params: { id: domain.pk } })
}
function editDomainAlias(domain, alias) {
selectedDomain.value = domain
selectedDomainAlias.value = alias
showAliasForm.value = true
selectedDomain.value = domain
selectedDomainAlias.value = alias
showAliasForm.value = true
}
function loadAliases(item, internalItem, isItemExpanded, toggleExpand) {
const isExpand = isItemExpanded(internalItem)
if (isExpand) {
toggleExpand(internalItem)
return
}
domainStore.getAliases(item.name).then(() => {
toggleExpand(internalItem)
})
const isExpand = isItemExpanded(internalItem)
if (isExpand) {
toggleExpand(internalItem)
return
}
domainStore.getAliases(item.name).then(() => {
toggleExpand(internalItem)
})
}
function openAdminList(domain) {
selectedDomain.value = domain
showAdminList.value = true
selectedDomain.value = domain
showAdminList.value = true
}
function getDomainMenuItems(domain) {
const result = [
{
label: $gettext('Administrators'),
icon: 'mdi-account-supervisor',
onClick: openAdminList,
},
{
label: $gettext('Edit'),
icon: 'mdi-circle-edit-outline',
onClick: editDomain,
},
{
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteDomain,
color: 'red',
},
]
if (domain.opened_alarms_count) {
result.push({
label: $gettext('Alarms'),
icon: 'mdi-bell',
color: 'red',
onClick: () => router.push({ name: 'Alarms' }),
})
}
return result
const result = [
{
label: $gettext('Administrators'),
icon: 'mdi-account-supervisor',
onClick: openAdminList,
},
{
label: $gettext('Edit'),
icon: 'mdi-circle-edit-outline',
onClick: editDomain,
},
{
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteDomain,
color: 'red',
},
]
if (domain.opened_alarms_count) {
result.push({
label: $gettext('Alarms'),
icon: 'mdi-bell',
color: 'red',
onClick: () => router.push({ name: 'Alarms' }),
})
}
return result
}
onMounted(() => {
if (Object.values(domains.value).length < 1) {
reloadDomains()
}
if (Object.values(domains.value).length < 1) {
reloadDomains()
}
})
</script>
<style scoped>
.v-text-field--outlined :deep(fieldset) {
border-color: #bfc5d2;
border-color: #bfc5d2;
}
.v-input--checkbox :deep(.v-label) {
font-size: 0.875rem !important;
font-size: 0.875rem !important;
}
</style>
@@ -1,47 +1,52 @@
<template>
<v-card v-if="domain">
<v-card-title>
<div class="headline">{{ $gettext('General information') }}</div>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="6">{{ $gettext('Creation date') }}</v-col>
<v-col cols="6">{{ $date(domain.creation) }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Last modification date') }}</v-col>
<v-col cols="6">{{ $date(domain.last_modification) }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Quota') }}</v-col>
<v-col v-if="domain.quota === '0'" cols="6">{{
$gettext('Unlimited')
}}</v-col>
<v-col v-else cols="6">{{ domain.quota }} {{ $gettext('MB') }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Default mailbox quota') }}</v-col>
<v-col v-if="domain.default_mailbox_quota === '0'" cols="6">{{
$gettext('Unlimited')
}}</v-col>
<v-col v-else cols="6">{{ domain.default_mailbox_quota }} MB</v-col>
</v-row>
<v-row v-if="domain.message_limit !== undefined">
<v-col cols="6">{{ $gettext('Daily sending limit') }}</v-col>
<v-col cols="6"
>{{ domain.message_limit }} {{ $gettext('messages') }}</v-col
>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Mailboxes') }}</v-col>
<v-col cols="6">{{ domain.mailbox_count }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Aliases') }}</v-col>
<v-col cols="6">{{ domain.mbalias_count }}</v-col>
</v-row>
</v-card-text>
</v-card>
<v-card v-if="domain">
<v-card-title>
<div class="headline">{{ $gettext('General information') }}</div>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="6">{{ $gettext('Creation date') }}</v-col>
<v-col cols="6">{{ $date(domain.creation) }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Last modification date') }}</v-col>
<v-col cols="6">{{ $date(domain.last_modification) }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Quota') }}</v-col>
<v-col v-if="domain.quota === '0'" cols="6">{{
$gettext('Unlimited')
}}</v-col>
<v-col v-else cols="6"
>{{ domain.quota }} {{ $gettext('MB') }}</v-col
>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Default mailbox quota') }}</v-col>
<v-col v-if="domain.default_mailbox_quota === '0'" cols="6">{{
$gettext('Unlimited')
}}</v-col>
<v-col v-else cols="6"
>{{ domain.default_mailbox_quota }} MB</v-col
>
</v-row>
<v-row v-if="domain.message_limit !== undefined">
<v-col cols="6">{{ $gettext('Daily sending limit') }}</v-col>
<v-col cols="6"
>{{ domain.message_limit }}
{{ $gettext('messages') }}</v-col
>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Mailboxes') }}</v-col>
<v-col cols="6">{{ domain.mailbox_count }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Aliases') }}</v-col>
<v-col cols="6">{{ domain.mbalias_count }}</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script setup lang="js">
@@ -1,30 +1,30 @@
<template>
<v-form ref="vFormRef">
<div>
<v-switch
v-model="domain.enable_dns_checks"
:label="$gettext('Enable DNS checks')"
color="primary"
/>
<v-switch
v-model="domain.enable_dkim"
:label="$gettext('Enable DKIM signing')"
color="primary"
/>
<v-text-field
v-model="domain.dkim_key_selector"
:label="$gettext('DKIM key selector')"
:disabled="!domain.enable_dkim"
variant="outlined"
/>
<ChoiceField
v-model="domain.dkim_key_length"
:label="$gettext('DKIM key length')"
:choices="dkimKeyLengths"
:disabled="!domain.enable_dkim"
/>
</div>
</v-form>
<v-form ref="vFormRef">
<div>
<v-switch
v-model="domain.enable_dns_checks"
:label="$gettext('Enable DNS checks')"
color="primary"
/>
<v-switch
v-model="domain.enable_dkim"
:label="$gettext('Enable DKIM signing')"
color="primary"
/>
<v-text-field
v-model="domain.dkim_key_selector"
:label="$gettext('DKIM key selector')"
:disabled="!domain.enable_dkim"
variant="outlined"
/>
<ChoiceField
v-model="domain.dkim_key_length"
:label="$gettext('DKIM key length')"
:choices="dkimKeyLengths"
:disabled="!domain.enable_dkim"
/>
</div>
</v-form>
</template>
<script setup lang="js">
@@ -41,18 +41,18 @@ const vFormRef = ref()
const domain = computed(() => props.modelValue)
const dkimKeyLengths = ref([
{
label: '1024',
value: 1024,
},
{
label: '2048',
value: 2048,
},
{
label: '4096',
value: 4096,
},
{
label: '1024',
value: 1024,
},
{
label: '2048',
value: 2048,
},
{
label: '4096',
value: 4096,
},
])
defineExpose({ vFormRef })
@@ -1,32 +1,32 @@
<template>
<v-form ref="vFormRef">
<v-text-field
v-model="domain.name"
:label="$gettext('Domain name (ex: domain.tld)')"
:rules="[rules.required]"
variant="outlined"
class="mb-5"
/>
<v-form ref="vFormRef">
<v-text-field
v-model="domain.name"
:label="$gettext('Domain name (ex: domain.tld)')"
:rules="[rules.required]"
variant="outlined"
class="mb-5"
/>
<ChoiceField
v-model="domain.type"
:label="$gettext('Type')"
:choices="domainTypes"
@update:model-value="cleanTransport"
/>
<ChoiceField
v-model="domain.type"
:label="$gettext('Type')"
:choices="domainTypes"
@update:model-value="cleanTransport"
/>
<v-switch
v-model="domain.enabled"
:label="$gettext('Enabled')"
:hint="
$gettext(
'Control if this domain will be allowed to send and receive messages'
)
"
color="primary"
persistent-hint
/>
</v-form>
<v-switch
v-model="domain.enabled"
:label="$gettext('Enabled')"
:hint="
$gettext(
'Control if this domain will be allowed to send and receive messages'
)
"
color="primary"
persistent-hint
/>
</v-form>
</template>
<script setup lang="js">
@@ -44,21 +44,21 @@ const domain = computed(() => props.modelValue)
const vFormRef = ref()
function cleanTransport(value) {
if (value === 'relaydomain' && domain.value.transport == null) {
domain.value.transport = {}
}
if (value === 'relaydomain' && domain.value.transport == null) {
domain.value.transport = {}
}
}
const domainTypes = [
{
label: 'Domain',
icon: 'mdi-earth',
value: $gettext('domain'),
},
{
label: 'Relay domain',
icon: 'mdi-earth',
value: $gettext('relaydomain'),
},
{
label: 'Domain',
icon: 'mdi-earth',
value: $gettext('domain'),
},
{
label: 'Relay domain',
icon: 'mdi-earth',
value: $gettext('relaydomain'),
},
]
defineExpose({ vFormRef })
@@ -1,45 +1,45 @@
<template>
<v-form ref="vFormRef">
<v-text-field
v-model="domain.quota"
:label="$gettext('Quota')"
:hint="
$gettext(
'Quota shared between mailboxes. Can be expressed in KB, MB (default) or GB. A value of 0 means no quota.'
)
"
persistent-hint
class="mb-4"
variant="outlined"
:rules="[rules.required]"
/>
<v-form ref="vFormRef">
<v-text-field
v-model="domain.quota"
:label="$gettext('Quota')"
:hint="
$gettext(
'Quota shared between mailboxes. Can be expressed in KB, MB (default) or GB. A value of 0 means no quota.'
)
"
persistent-hint
class="mb-4"
variant="outlined"
:rules="[rules.required]"
/>
<v-text-field
v-model="domain.default_mailbox_quota"
:label="$gettext('Default mailbox quota')"
:hint="
$gettext(
'Default quota applied to mailboxes. Can be expressed in KB, MB (default) or GB. A value of 0 means no quota.'
)
"
persistent-hint
class="mb-4"
variant="outlined"
:rules="[rules.required]"
/>
<v-text-field
v-model="domain.message_limit"
:label="$gettext('Message sending limit')"
:hint="
$gettext(
'Number of messages this domain can send per day. Leave empty for no limit.'
)
"
persistent-hint
variant="outlined"
:rules="[rules.numericOrNull]"
/>
</v-form>
<v-text-field
v-model="domain.default_mailbox_quota"
:label="$gettext('Default mailbox quota')"
:hint="
$gettext(
'Default quota applied to mailboxes. Can be expressed in KB, MB (default) or GB. A value of 0 means no quota.'
)
"
persistent-hint
class="mb-4"
variant="outlined"
:rules="[rules.required]"
/>
<v-text-field
v-model="domain.message_limit"
:label="$gettext('Message sending limit')"
:hint="
$gettext(
'Number of messages this domain can send per day. Leave empty for no limit.'
)
"
persistent-hint
variant="outlined"
:rules="[rules.numericOrNull]"
/>
</v-form>
</template>
<script setup lang="js">
@@ -1,42 +1,42 @@
<template>
<v-form ref="vFormRef">
<v-switch
v-model="createAdmin"
:label="$gettext('Create a domain administrator')"
color="primary"
@update:model-value="updateCreateAdmin"
/>
<v-text-field
v-model="domain.domain_admin.username"
:label="$gettext('Name')"
:hint="$gettext('Name of the administrator')"
persistent-hint
variant="outlined"
:disabled="!createAdmin"
:suffix="`@${domain.name}`"
:rules="[!createAdmin || rules.required]"
/>
<AccountPasswordSubForm
v-model="domain.domain_admin"
:disabled="!createAdmin"
/>
<v-switch
v-model="domain.domain_admin.with_mailbox"
:label="$gettext('With a mailbox')"
:disabled="!createAdmin"
:hint="$gettext('Create a mailbox for the administrator.')"
persistent-hint
color="primary"
/>
<v-switch
v-model="domain.domain_admin.with_aliases"
:label="$gettext('Create aliases')"
:disabled="!createAdmin"
:hint="$gettext('Create standard aliases for the domain.')"
persistent-hint
color="primary"
/>
</v-form>
<v-form ref="vFormRef">
<v-switch
v-model="createAdmin"
:label="$gettext('Create a domain administrator')"
color="primary"
@update:model-value="updateCreateAdmin"
/>
<v-text-field
v-model="domain.domain_admin.username"
:label="$gettext('Name')"
:hint="$gettext('Name of the administrator')"
persistent-hint
variant="outlined"
:disabled="!createAdmin"
:suffix="`@${domain.name}`"
:rules="[!createAdmin || rules.required]"
/>
<AccountPasswordSubForm
v-model="domain.domain_admin"
:disabled="!createAdmin"
/>
<v-switch
v-model="domain.domain_admin.with_mailbox"
:label="$gettext('With a mailbox')"
:disabled="!createAdmin"
:hint="$gettext('Create a mailbox for the administrator.')"
persistent-hint
color="primary"
/>
<v-switch
v-model="domain.domain_admin.with_aliases"
:label="$gettext('Create aliases')"
:disabled="!createAdmin"
:hint="$gettext('Create standard aliases for the domain.')"
persistent-hint
color="primary"
/>
</v-form>
</template>
<script setup lang="js">
@@ -57,7 +57,7 @@ const createAdmin = ref(false)
const domain = computed(() => props.modelValue)
function updateCreateAdmin(val) {
emit('createAdmin', val)
emit('createAdmin', val)
}
defineExpose({ vFormRef })
@@ -1,35 +1,39 @@
<template>
<v-form ref="vFormRef">
<label class="m-label">{{ $gettext('Service') }}</label>
<v-autocomplete
v-model="service"
:items="backends"
item-title="name"
return-object
variant="outlined"
:rules="[rules.required]"
/>
<template v-if="service">
<template v-for="(setting, i) in service.settings" :key="i">
<template v-if="setting.type === 'str' || setting.type === 'int'">
<label class="m-label">{{ setting.label }}</label>
<v-text-field
v-model="form.settings[`${service.name}_${setting.name}`]"
<v-form ref="vFormRef">
<label class="m-label">{{ $gettext('Service') }}</label>
<v-autocomplete
v-model="service"
:items="backends"
item-title="name"
return-object
variant="outlined"
:type="form.type === 'int' ? 'number' : 'text'"
:rules="[!!setting.required || rules.required]"
@update:model-value="update"
/>
</template>
<v-checkbox
v-else-if="setting.type === 'boolean'"
v-model="form.settings[`${service.name}_${setting.name}`]"
:label="setting.label"
@update:model-value="update"
:rules="[rules.required]"
/>
</template>
</template>
</v-form>
<template v-if="service">
<template v-for="(setting, i) in service.settings" :key="i">
<template
v-if="setting.type === 'str' || setting.type === 'int'"
>
<label class="m-label">{{ setting.label }}</label>
<v-text-field
v-model="
form.settings[`${service.name}_${setting.name}`]
"
variant="outlined"
:type="form.type === 'int' ? 'number' : 'text'"
:rules="[!!setting.required || rules.required]"
@update:model-value="update"
/>
</template>
<v-checkbox
v-else-if="setting.type === 'boolean'"
v-model="form.settings[`${service.name}_${setting.name}`]"
:label="setting.label"
@update:model-value="update"
/>
</template>
</template>
</v-form>
</template>
<script setup lang="js">
@@ -48,31 +52,31 @@ const service = ref()
const backends = ref([])
function checkSettingTypes(data) {
for (const setting of service.value.settings) {
if (setting.type === 'int') {
const fullName = `${service.value.name}_${setting.name}`
data.transport.settings[fullName] = parseInt(
data.transport.settings[fullName]
)
for (const setting of service.value.settings) {
if (setting.type === 'int') {
const fullName = `${service.value.name}_${setting.name}`
data.transport.settings[fullName] = parseInt(
data.transport.settings[fullName]
)
}
}
}
}
function update() {
form.value.service = service.value.name
emit('update:modelValue', form.value)
form.value.service = service.value.name
emit('update:modelValue', form.value)
}
onMounted(() => {
form.value = { ...props.modelValue }
transports.getAll().then((resp) => {
backends.value = resp.data
if (form.value.service) {
service.value = backends.value.find(
(backend) => backend.name === form.value.service
)
}
})
form.value = { ...props.modelValue }
transports.getAll().then((resp) => {
backends.value = resp.data
if (form.value.service) {
service.value = backends.value.find(
(backend) => backend.name === form.value.service
)
}
})
})
defineExpose({ checkSettingTypes, service, vFormRef })
@@ -1,14 +1,18 @@
<template>
<v-card>
<v-card-title>
<span class="headline">{{ $gettext('Aliases') }}</span>
</v-card-title>
<v-card-text>
<v-chip v-for="alias in account.aliases" :key="alias" class="mr-2 mt-2">
{{ alias }}
</v-chip>
</v-card-text>
</v-card>
<v-card>
<v-card-title>
<span class="headline">{{ $gettext('Aliases') }}</span>
</v-card-title>
<v-card-text>
<v-chip
v-for="alias in account.aliases"
:key="alias"
class="mr-2 mt-2"
>
{{ alias }}
</v-chip>
</v-card-text>
</v-card>
</template>
<script setup lang="js">
@@ -1,64 +1,64 @@
<template>
<CreationForm
ref="form"
:title="$gettext('New account')"
:steps="steps"
:form-getter="getForm"
:get-v-form-ref="getVFormRef"
:validate-object="validateAccount"
:summary-sections="summarySections"
@close="close"
@create="submit"
@validation-error="setFormErrors"
>
<template #[`form.role`]>
<AccountRoleForm ref="role" v-model="account" />
</template>
<template #[`form.identification`]>
<AccountGeneralForm ref="identification" v-model="account" />
</template>
<template #[`form.mailbox`]>
<AccountMailboxForm ref="mailbox" v-model="account" />
</template>
<template #[`form.aliases`]>
<AccountAliasForm ref="aliases" v-model="account" />
</template>
<template #[`item.random_password`]="{ item }">
<template v-if="item.value">
<v-col cols="12" class="highligth text-white">
<v-row>
<v-col
><span>{{ item.key }}</span></v-col
>
<v-col v-if="item.type === 'yesno'" class="text-right">
{{ $yesno(item.value) }}
</v-col>
</v-row>
<v-row>
<v-col class="text-right py-1">
<v-btn
append-icon="mdi-clipboard-multiple-outline"
color="white"
variant="text"
:title="$gettext('Copy to clipboard')"
@click="copyPassword"
>
{{ account.password }}
</v-btn>
</v-col>
</v-row>
</v-col>
</template>
<template v-else>
<v-col
><span class="grey--text">{{ item.key }}</span></v-col
>
<v-col v-if="item.type === 'yesno'" class="text-right">{{
$yesno(item.value)
}}</v-col>
</template>
</template>
</CreationForm>
<CreationForm
ref="form"
:title="$gettext('New account')"
:steps="steps"
:form-getter="getForm"
:get-v-form-ref="getVFormRef"
:validate-object="validateAccount"
:summary-sections="summarySections"
@close="close"
@create="submit"
@validation-error="setFormErrors"
>
<template #[`form.role`]>
<AccountRoleForm ref="role" v-model="account" />
</template>
<template #[`form.identification`]>
<AccountGeneralForm ref="identification" v-model="account" />
</template>
<template #[`form.mailbox`]>
<AccountMailboxForm ref="mailbox" v-model="account" />
</template>
<template #[`form.aliases`]>
<AccountAliasForm ref="aliases" v-model="account" />
</template>
<template #[`item.random_password`]="{ item }">
<template v-if="item.value">
<v-col cols="12" class="highligth text-white">
<v-row>
<v-col
><span>{{ item.key }}</span></v-col
>
<v-col v-if="item.type === 'yesno'" class="text-right">
{{ $yesno(item.value) }}
</v-col>
</v-row>
<v-row>
<v-col class="text-right py-1">
<v-btn
append-icon="mdi-clipboard-multiple-outline"
color="white"
variant="text"
:title="$gettext('Copy to clipboard')"
@click="copyPassword"
>
{{ account.password }}
</v-btn>
</v-col>
</v-row>
</v-col>
</template>
<template v-else>
<v-col
><span class="grey--text">{{ item.key }}</span></v-col
>
<v-col v-if="item.type === 'yesno'" class="text-right">{{
$yesno(item.value)
}}</v-col>
</template>
</template>
</CreationForm>
</template>
<script setup lang="js">
@@ -80,110 +80,116 @@ const identitiesStore = useIdentitiesStore()
const emit = defineEmits(['close', 'created'])
const needsMailbox = computed(
() =>
account.value.role !== 'SuperAdmins' &&
account.value.username &&
account.value.username.indexOf('@') !== -1
() =>
account.value.role !== 'SuperAdmins' &&
account.value.username &&
account.value.username.indexOf('@') !== -1
)
const summarySections = computed(() => {
const result = [
{
title: $gettext('Role'),
items: [{ key: $gettext('Role'), value: account.value.role }],
},
{
title: $gettext('Identification'),
items: [
{ key: $gettext('Username'), value: account.value.username },
{ key: $gettext('First name'), value: account.value.first_name },
{ key: $gettext('Last name'), value: account.value.last_name },
const result = [
{
name: 'random_password',
key: $gettext('Random password'),
value: account.value.random_password,
type: 'yesno',
title: $gettext('Role'),
items: [{ key: $gettext('Role'), value: account.value.role }],
},
{
key: $gettext('Enabled'),
value: account.value.is_active,
type: 'yesno',
title: $gettext('Identification'),
items: [
{ key: $gettext('Username'), value: account.value.username },
{
key: $gettext('First name'),
value: account.value.first_name,
},
{ key: $gettext('Last name'), value: account.value.last_name },
{
name: 'random_password',
key: $gettext('Random password'),
value: account.value.random_password,
type: 'yesno',
},
{
key: $gettext('Enabled'),
value: account.value.is_active,
type: 'yesno',
},
],
},
],
},
]
if (needsMailbox.value) {
return result.concat([
{
title: $gettext('Mailbox'),
items: [
{ key: $gettext('Email'), value: account.value.mailbox.full_address },
{
key: $gettext('Quota'),
value: account.value.mailbox.use_domain_quota
? $gettext('Domain default value')
: account.value.mailbox.quota,
},
{
key: $gettext('Message sending limit'),
value: account.value.mailbox.message_limit,
},
{
key: $gettext('Send only account'),
value: account.value.mailbox.is_send_only,
type: 'yesno',
},
],
},
{
title: $gettext('Aliases'),
items: [
{
key: $gettext('Aliases'),
value: account.value.aliases,
type: 'list',
},
],
},
])
}
return result
]
if (needsMailbox.value) {
return result.concat([
{
title: $gettext('Mailbox'),
items: [
{
key: $gettext('Email'),
value: account.value.mailbox.full_address,
},
{
key: $gettext('Quota'),
value: account.value.mailbox.use_domain_quota
? $gettext('Domain default value')
: account.value.mailbox.quota,
},
{
key: $gettext('Message sending limit'),
value: account.value.mailbox.message_limit,
},
{
key: $gettext('Send only account'),
value: account.value.mailbox.is_send_only,
type: 'yesno',
},
],
},
{
title: $gettext('Aliases'),
items: [
{
key: $gettext('Aliases'),
value: account.value.aliases,
type: 'list',
},
],
},
])
}
return result
})
const steps = computed(() => {
const result = [
{
name: 'role',
title: $gettext('Role'),
},
{
name: 'identification',
title: $gettext('Identification'),
},
]
if (needsMailbox.value) {
return result.concat([
{
name: 'mailbox',
title: $gettext('Mailbox'),
},
{
name: 'aliases',
title: $gettext('Aliases'),
},
])
}
return result
const result = [
{
name: 'role',
title: $gettext('Role'),
},
{
name: 'identification',
title: $gettext('Identification'),
},
]
if (needsMailbox.value) {
return result.concat([
{
name: 'mailbox',
title: $gettext('Mailbox'),
},
{
name: 'aliases',
title: $gettext('Aliases'),
},
])
}
return result
})
const account = ref({
aliases: [],
is_active: true,
role: 'SimpleUsers',
mailbox: {
use_domain_quota: true,
},
random_password: false,
aliases: [],
is_active: true,
role: 'SimpleUsers',
mailbox: {
use_domain_quota: true,
},
random_password: false,
})
// Form refs
@@ -193,74 +199,77 @@ const mailbox = ref()
const aliases = ref()
const formStepsComponents = {
role: role,
identification: identification,
mailbox: mailbox,
aliases: aliases,
role: role,
identification: identification,
mailbox: mailbox,
aliases: aliases,
}
function copyPassword() {
navigator.clipboard.writeText(account.value.password).then(() => {
busStore.displayNotification({
msg: $gettext('Password copied to clipboard'),
navigator.clipboard.writeText(account.value.password).then(() => {
busStore.displayNotification({
msg: $gettext('Password copied to clipboard'),
})
})
})
}
function getForm(step) {
return formStepsComponents.step[step.name]
return formStepsComponents.step[step.name]
}
function getVFormRef(step) {
return formStepsComponents[step.name].value.vFormRef
return formStepsComponents[step.name].value.vFormRef
}
function setFormErrors(step, errors) {
const stepName = steps.value[step - 1].name
if (formStepsComponents[stepName].value.formErrors) {
formStepsComponents[stepName].value.formErrors.value = errors
}
const stepName = steps.value[step - 1].name
if (formStepsComponents[stepName].value.formErrors) {
formStepsComponents[stepName].value.formErrors.value = errors
}
}
function preparePayload(payload) {
const cleaned_payload = { ...payload }
if (
cleaned_payload.role === 'SuperAdmins' &&
cleaned_payload.username &&
cleaned_payload.username.indexOf('@') === -1
) {
delete cleaned_payload.mailbox
}
if (cleaned_payload.mailbox && cleaned_payload.mailbox.message_limit === '') {
cleaned_payload.mailbox.message_limit = null
}
return cleaned_payload
const cleaned_payload = { ...payload }
if (
cleaned_payload.role === 'SuperAdmins' &&
cleaned_payload.username &&
cleaned_payload.username.indexOf('@') === -1
) {
delete cleaned_payload.mailbox
}
if (
cleaned_payload.mailbox &&
cleaned_payload.mailbox.message_limit === ''
) {
cleaned_payload.mailbox.message_limit = null
}
return cleaned_payload
}
function validateAccount() {
const payload = preparePayload({ ...account.value })
return accountsApi.validate(payload)
const payload = preparePayload({ ...account.value })
return accountsApi.validate(payload)
}
function close() {
emit('close')
emit('close')
}
function submit() {
const data = preparePayload({ ...account.value })
identitiesStore.createIdentity('account', data).then(() => {
emit('created')
close()
})
const data = preparePayload({ ...account.value })
identitiesStore.createIdentity('account', data).then(() => {
emit('created')
close()
})
}
onMounted(() => {
domainsStore.getDomains()
domainsStore.getDomains()
})
</script>
<style lang="scss">
.highligth {
background-color: #515d78;
background-color: #515d78;
}
</style>
@@ -1,199 +1,248 @@
<template>
<LoadingData v-if="!accountsStore.accountsLoaded || working" />
<div v-else>
<v-expansion-panels v-model="panel" :multiple="formErrors">
<v-expansion-panel eager value="roleForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Role') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
{{ roleForm.roleLabel }}
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<AccountRoleForm ref="roleForm" v-model="editedAccount" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel eager value="generalForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Identification') }}
</v-col>
<v-col cols="8" class="text-secondary">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Username:') }} {{ editedAccount.username }}
</div>
</v-col>
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Enabled') }}
<v-icon v-if="editedAccount.is_active" color="success"
>mdi-check-circle-outline</v-icon
>
<v-icon v-else>mdi-close-circle-outline</v-icon>
</div>
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<AccountGeneralForm
ref="generalForm"
v-model="editedAccount"
editing
/>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel v-if="usernameIsEmail" eager value="mailboxForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Mailbox') }}
</v-col>
<v-col cols="8" class="text-secondary">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<template v-if="editedAccount.mailbox">
<v-col
v-if="editedAccount.mailbox.use_domain_quota"
cols="6"
>
<div class="mr-2">
{{ $gettext('Quota: ') }}
{{ $gettext("domain's default value") }}
</div>
</v-col>
<v-col v-else cols="6">
<div class="mr-2">
{{ $gettext('Quota: ') }}
{{ editedAccount.mailbox.quota }}
</div>
</v-col>
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Send only') }}
<v-icon
v-if="editedAccount.mailbox.is_send_only"
color="success"
>mdi-check-circle-outline</v-icon
>
<v-icon v-else>mdi-close-circle-outline</v-icon>
</div>
</v-col>
</template>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<AccountMailboxForm ref="mailboxForm" v-model="editedAccount" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel v-if="usernameIsEmail" eager value="aliasForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Alias') }}
</v-col>
<v-col cols="8" class="text-secondary">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<template v-if="editedAccount.aliases">
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Aliases:') }}
{{ $gettext('Number of associated alias:') }}
{{ editedAccount.aliases.length }}
</div>
</v-col>
</template>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<AccountAliasForm ref="aliasForm" v-model="editedAccount" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel v-if="needsResources" eager value="resourcesForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Resources') }}
</v-col>
<v-col cols="8" class="text-secondary">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<template
v-if="
editedAccount.resources != undefined &&
editedAccount.resources.length == 2
"
>
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Mailbox:') }}
{{ $gettext('Number of allowed mailboxes:') }}
{{ editedAccount.resources[0].max_value }}
</div>
</v-col>
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Mailbox aliases:') }}
{{ $gettext('Number of allowed mailbox aliases:') }}
{{ editedAccount.resources[1].max_value }}
</div>
</v-col>
</template>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<ResourcesForm
ref="resourcesForm"
v-model="editedAccount.resources"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<div class="mt-4 d-flex justify-end">
<v-btn :loading="working" @click="$router.go(-1)">
{{ $gettext('Cancel') }}
</v-btn>
<v-btn
class="ml-4"
color="primary darken-1"
:loading="working"
@click="save"
>
{{ $gettext('Save') }}
</v-btn>
<LoadingData v-if="!accountsStore.accountsLoaded || working" />
<div v-else>
<v-expansion-panels v-model="panel" :multiple="formErrors">
<v-expansion-panel eager value="roleForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Role') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
{{ roleForm.roleLabel }}
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<AccountRoleForm ref="roleForm" v-model="editedAccount" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel eager value="generalForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Identification') }}
</v-col>
<v-col cols="8" class="text-secondary">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Username:') }}
{{ editedAccount.username }}
</div>
</v-col>
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Enabled') }}
<v-icon
v-if="editedAccount.is_active"
color="success"
>mdi-check-circle-outline</v-icon
>
<v-icon v-else
>mdi-close-circle-outline</v-icon
>
</div>
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<AccountGeneralForm
ref="generalForm"
v-model="editedAccount"
editing
/>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel v-if="usernameIsEmail" eager value="mailboxForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Mailbox') }}
</v-col>
<v-col cols="8" class="text-secondary">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<template v-if="editedAccount.mailbox">
<v-col
v-if="
editedAccount.mailbox
.use_domain_quota
"
cols="6"
>
<div class="mr-2">
{{ $gettext('Quota: ') }}
{{
$gettext(
"domain's default value"
)
}}
</div>
</v-col>
<v-col v-else cols="6">
<div class="mr-2">
{{ $gettext('Quota: ') }}
{{
editedAccount.mailbox.quota
}}
</div>
</v-col>
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Send only') }}
<v-icon
v-if="
editedAccount.mailbox
.is_send_only
"
color="success"
>mdi-check-circle-outline</v-icon
>
<v-icon v-else
>mdi-close-circle-outline</v-icon
>
</div>
</v-col>
</template>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<AccountMailboxForm
ref="mailboxForm"
v-model="editedAccount"
/>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel v-if="usernameIsEmail" eager value="aliasForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Alias') }}
</v-col>
<v-col cols="8" class="text-secondary">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<template v-if="editedAccount.aliases">
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Aliases:') }}
{{
$gettext(
'Number of associated alias:'
)
}}
{{
editedAccount.aliases.length
}}
</div>
</v-col>
</template>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<AccountAliasForm ref="aliasForm" v-model="editedAccount" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel
v-if="needsResources"
eager
value="resourcesForm"
>
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Resources') }}
</v-col>
<v-col cols="8" class="text-secondary">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<template
v-if="
editedAccount.resources !=
undefined &&
editedAccount.resources.length == 2
"
>
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Mailbox:') }}
{{
$gettext(
'Number of allowed mailboxes:'
)
}}
{{
editedAccount.resources[0]
.max_value
}}
</div>
</v-col>
<v-col cols="6">
<div class="mr-2">
{{
$gettext('Mailbox aliases:')
}}
{{
$gettext(
'Number of allowed mailbox aliases:'
)
}}
{{
editedAccount.resources[1]
.max_value
}}
</div>
</v-col>
</template>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<ResourcesForm
ref="resourcesForm"
v-model="editedAccount.resources"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<div class="mt-4 d-flex justify-end">
<v-btn :loading="working" @click="$router.go(-1)">
{{ $gettext('Cancel') }}
</v-btn>
<v-btn
class="ml-4"
color="primary darken-1"
:loading="working"
@click="save"
>
{{ $gettext('Save') }}
</v-btn>
</div>
</div>
</div>
</template>
<script setup lang="js">
@@ -220,23 +269,23 @@ const router = useRouter()
const editedAccount = ref({ pk: route.params.id })
const usernameIsEmail = computed(() => {
return (
editedAccount.value.username &&
editedAccount.value.role !== 'SuperAdmins' &&
editedAccount.value.username.indexOf('@') !== -1
)
return (
editedAccount.value.username &&
editedAccount.value.role !== 'SuperAdmins' &&
editedAccount.value.username.indexOf('@') !== -1
)
})
const needsResources = computed(() => {
const isNeeded =
limitsConfig.value.params &&
limitsConfig.value.params.enable_admin_limits &&
editedAccount.value.role !== 'SimpleUsers' &&
editedAccount.value.role !== 'SuperAdmins'
if (isNeeded === undefined) {
return false
}
return isNeeded
const isNeeded =
limitsConfig.value.params &&
limitsConfig.value.params.enable_admin_limits &&
editedAccount.value.role !== 'SimpleUsers' &&
editedAccount.value.role !== 'SuperAdmins'
if (isNeeded === undefined) {
return false
}
return isNeeded
})
const limitsConfig = ref({})
@@ -253,79 +302,82 @@ const resourcesForm = ref()
//formMap
const formMap = computed(() => {
const map = {
roleForm: roleForm,
generalForm: generalForm,
}
if (usernameIsEmail.value) {
map.aliasForm = aliasForm
map.mailboxForm = mailboxForm
}
if (needsResources.value) {
map.resourcesForm = resourcesForm
}
return map
const map = {
roleForm: roleForm,
generalForm: generalForm,
}
if (usernameIsEmail.value) {
map.aliasForm = aliasForm
map.mailboxForm = mailboxForm
}
if (needsResources.value) {
map.resourcesForm = resourcesForm
}
return map
})
async function save() {
formErrors.value = false
panel.value = []
for (const [panelName, formRef] of Object.entries(formMap.value)) {
if (formRef.value != null) {
const { valid } = await formRef.value.vFormRef.validate()
if (!valid) {
formErrors.value = true
panel.value.push(panelName)
}
formErrors.value = false
panel.value = []
for (const [panelName, formRef] of Object.entries(formMap.value)) {
if (formRef.value != null) {
const { valid } = await formRef.value.vFormRef.validate()
if (!valid) {
formErrors.value = true
panel.value.push(panelName)
}
}
}
}
if (formErrors.value) {
return
}
working.value = true
try {
const data = { ...editedAccount.value }
if (usernameIsEmail.value) {
data.mailbox.full_address = data.username
} else {
delete data.mailbox.full_address
if (formErrors.value) {
return
}
if (data.random_password || !data.password) {
delete data.password
delete data.password_confirmation
working.value = true
try {
const data = { ...editedAccount.value }
if (usernameIsEmail.value) {
data.mailbox.full_address = data.username
} else {
delete data.mailbox.full_address
}
if (data.random_password || !data.password) {
delete data.password
delete data.password_confirmation
}
if (needsResources.value && resourcesForm.value !== null) {
data.resources = resourcesForm.value.getPayload()
}
if (data.aliases === null) {
delete data.aliases
}
identitiesStore.updateIdentity('account', data).then(() => {
router.push({
name: 'AccountDetail',
params: { id: route.params.id },
})
})
} finally {
working.value = false
}
if (needsResources.value && resourcesForm.value !== null) {
data.resources = resourcesForm.value.getPayload()
}
if (data.aliases === null) {
delete data.aliases
}
identitiesStore.updateIdentity('account', data).then(() => {
router.push({ name: 'AccountDetail', params: { id: route.params.id } })
})
} finally {
working.value = false
}
}
onMounted(() => {
accountsStore
.getAccount(route.params.id)
.then((response) => (editedAccount.value = { ...response.data }))
accountsStore
.getAccount(route.params.id)
.then((response) => (editedAccount.value = { ...response.data }))
parametersApi.getApplication('limits').then((response) => {
limitsConfig.value = response.data
})
parametersApi.getApplication('limits').then((response) => {
limitsConfig.value = response.data
})
})
watch(
editedAccount,
() => {
delete editedAccount.value.domains
if (editedAccount.value.mailbox === null) {
delete editedAccount.value.mailbox
}
},
{ deep: true }
editedAccount,
() => {
delete editedAccount.value.domains
if (editedAccount.value.mailbox === null) {
delete editedAccount.value.mailbox
}
},
{ deep: true }
)
</script>
@@ -1,59 +1,61 @@
Ac
<template>
<v-card>
<v-card-title>
<span class="headline">{{ $gettext('Sender addresses') }}</span>
</v-card-title>
<v-card-text>
<v-data-table
:headers="headers"
:items="addresses"
:no-data-text="$gettext('No sender address defined for this account')"
>
<template #[`item.address`]="{ item }">
<span v-if="item.pk">{{ item.address }}</span>
<v-text-field
v-else
v-model="newAddress"
:label="$gettext('Specify an email address')"
>
<template #append>
<v-btn
v-if="newAddress"
icon="mdi-content-save"
<v-card>
<v-card-title>
<span class="headline">{{ $gettext('Sender addresses') }}</span>
</v-card-title>
<v-card-text>
<v-data-table
:headers="headers"
:items="addresses"
:no-data-text="
$gettext('No sender address defined for this account')
"
>
<template #[`item.address`]="{ item }">
<span v-if="item.pk">{{ item.address }}</span>
<v-text-field
v-else
v-model="newAddress"
:label="$gettext('Specify an email address')"
>
<template #append>
<v-btn
v-if="newAddress"
icon="mdi-content-save"
color="primary"
variant="text"
@click="submit"
>
</v-btn>
</template>
</v-text-field>
</template>
<template #[`item.actions`]="{ item }">
<v-btn
v-if="item.pk"
icon="mdi-delete-outline"
color="red"
:title="$gettext('Remove this address')"
variant="text"
@click="deleteSenderAddress(item)"
>
</v-btn>
</template>
<template #bottom></template>
</v-data-table>
<v-btn
v-if="!hideAddBtn"
size="small"
color="primary"
variant="text"
@click="submit"
>
</v-btn>
</template>
</v-text-field>
</template>
<template #[`item.actions`]="{ item }">
<v-btn
v-if="item.pk"
icon="mdi-delete-outline"
color="red"
:title="$gettext('Remove this address')"
variant="text"
@click="deleteSenderAddress(item)"
>
</v-btn>
</template>
<template #bottom></template>
</v-data-table>
<v-btn
v-if="!hideAddBtn"
size="small"
color="primary"
variant="outlined"
@click="addRow"
>
<v-icon size="small" icon="mdi-plus"></v-icon>
{{ $gettext('Add sender address') }}
</v-btn>
</v-card-text>
</v-card>
variant="outlined"
@click="addRow"
>
<v-icon size="small" icon="mdi-plus"></v-icon>
{{ $gettext('Add sender address') }}
</v-btn>
</v-card-text>
</v-card>
</template>
<script setup lang="js">
@@ -70,39 +72,39 @@ const hideAddBtn = ref(false)
const newAddress = ref('')
const headers = [
{ title: $gettext('Address'), key: 'address' },
{ title: '', key: 'actions', align: 'end', sortable: false },
{ title: $gettext('Address'), key: 'address' },
{ title: '', key: 'actions', align: 'end', sortable: false },
]
function addRow() {
addresses.value.push({})
hideAddBtn.value = true
addresses.value.push({})
hideAddBtn.value = true
}
function fetchSenderAddresses() {
senderAddresses.getAllForMailbox(props.account.mailbox.pk).then((resp) => {
addresses.value = resp.data
})
senderAddresses.getAllForMailbox(props.account.mailbox.pk).then((resp) => {
addresses.value = resp.data
})
}
function submit() {
const data = {
address: newAddress.value,
mailbox: props.account.mailbox.pk,
}
senderAddresses.create(data).then(() => {
fetchSenderAddresses()
newAddress.value = null
hideAddBtn.value = false
// bus.$emit('notification', { msg: this.$gettext('Sender address added') })
})
const data = {
address: newAddress.value,
mailbox: props.account.mailbox.pk,
}
senderAddresses.create(data).then(() => {
fetchSenderAddresses()
newAddress.value = null
hideAddBtn.value = false
// bus.$emit('notification', { msg: this.$gettext('Sender address added') })
})
}
function deleteSenderAddress(senderAddress) {
senderAddresses.delete(senderAddress.pk).then(() => {
fetchSenderAddresses()
// bus.$emit('notification', { msg: this.$gettext('Sender address deleted') })
})
senderAddresses.delete(senderAddress.pk).then(() => {
fetchSenderAddresses()
// bus.$emit('notification', { msg: this.$gettext('Sender address deleted') })
})
}
fetchSenderAddresses()
@@ -1,63 +1,73 @@
<template>
<v-card>
<v-card-title>
<span class="headline">{{ $gettext('General information') }}</span>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="6">{{ $gettext('Creation date') }}</v-col>
<v-col cols="6">{{ $date(account.date_joined) }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Last login date') }}</v-col>
<v-col v-if="account.last_login" cols="6">
{{ $date(account.last_login) }}
</v-col>
<v-col v-else cols="6">{{ $gettext('Never logged-in') }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Full name') }}</v-col>
<v-col cols="6">{{ account.first_name }} {{ account.last_name }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Role') }} </v-col>
<v-col cols="6">{{ account.role }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Secondary email') }}</v-col>
<v-col cols="6">{{ account.secondary_email }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Phone number') }}</v-col>
<v-col cols="6">{{ account.phone_number }}</v-col>
</v-row>
<template v-if="account.mailbox">
<v-row>
<v-col cols="6">{{ $gettext('Quota') }} </v-col>
<v-col v-if="account.mailbox.quota === '0'" cols="6">
{{ $gettext('Unlimited') }}
</v-col>
<v-col v-else cols="6">
<v-progress-linear
:color="account.mailbox.quota_usage < 80 ? 'primary' : 'warning'"
:value="account.mailbox.quota_usage"
height="25"
class="white--text"
>
<template #default="{ value }">
{{ Math.ceil(value) }}% ({{ account.mailbox.quota }}
{{ $gettext('MB') }})
</template>
</v-progress-linear>
</v-col>
</v-row>
<v-row v-if="account.mailbox.message_limit">
<v-col cols="6">{{ $gettext('Daily sending limit') }}</v-col>
<v-col cols="6">{{ account.mailbox.message_limit }}</v-col>
</v-row>
</template>
</v-card-text>
</v-card>
<v-card>
<v-card-title>
<span class="headline">{{ $gettext('General information') }}</span>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="6">{{ $gettext('Creation date') }}</v-col>
<v-col cols="6">{{ $date(account.date_joined) }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Last login date') }}</v-col>
<v-col v-if="account.last_login" cols="6">
{{ $date(account.last_login) }}
</v-col>
<v-col v-else cols="6">{{ $gettext('Never logged-in') }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Full name') }}</v-col>
<v-col cols="6"
>{{ account.first_name }} {{ account.last_name }}</v-col
>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Role') }} </v-col>
<v-col cols="6">{{ account.role }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Secondary email') }}</v-col>
<v-col cols="6">{{ account.secondary_email }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Phone number') }}</v-col>
<v-col cols="6">{{ account.phone_number }}</v-col>
</v-row>
<template v-if="account.mailbox">
<v-row>
<v-col cols="6">{{ $gettext('Quota') }} </v-col>
<v-col v-if="account.mailbox.quota === '0'" cols="6">
{{ $gettext('Unlimited') }}
</v-col>
<v-col v-else cols="6">
<v-progress-linear
:color="
account.mailbox.quota_usage < 80
? 'primary'
: 'warning'
"
:value="account.mailbox.quota_usage"
height="25"
class="white--text"
>
<template #default="{ value }">
{{ Math.ceil(value) }}% ({{
account.mailbox.quota
}}
{{ $gettext('MB') }})
</template>
</v-progress-linear>
</v-col>
</v-row>
<v-row v-if="account.mailbox.message_limit">
<v-col cols="6">{{
$gettext('Daily sending limit')
}}</v-col>
<v-col cols="6">{{ account.mailbox.message_limit }}</v-col>
</v-row>
</template>
</v-card-text>
</v-card>
</template>
<script setup lang="js">
@@ -1,22 +1,22 @@
<template>
<CreationForm
ref="form"
:title="$gettext('New alias')"
:steps="steps"
:form-getter="getForm"
:get-v-form-ref="getVFormRef"
:validate-object="validateAlias"
:summary-sections="summarySections"
@close="close"
@create="submit"
>
<template #[`form.general`]>
<AliasGeneralForm ref="general" v-model="alias" />
</template>
<template #[`form.recipients`]>
<AliasRecipientForm ref="recipients" v-model="alias.recipients" />
</template>
</CreationForm>
<CreationForm
ref="form"
:title="$gettext('New alias')"
:steps="steps"
:form-getter="getForm"
:get-v-form-ref="getVFormRef"
:validate-object="validateAlias"
:summary-sections="summarySections"
@close="close"
@create="submit"
>
<template #[`form.general`]>
<AliasGeneralForm ref="general" v-model="alias" />
</template>
<template #[`form.recipients`]>
<AliasRecipientForm ref="recipients" v-model="alias.recipients" />
</template>
</CreationForm>
</template>
<script setup lang="js">
@@ -43,109 +43,112 @@ const general = ref()
const recipients = ref()
const formStepsComponenents = {
general: general,
recipients: recipients,
general: general,
recipients: recipients,
}
const summarySections = computed(() => {
const result = [
{
title: $gettext('General'),
items: [
const result = [
{
key: $gettext('Random address'),
value: alias.value.random,
type: 'yesno',
title: $gettext('General'),
items: [
{
key: $gettext('Random address'),
value: alias.value.random,
type: 'yesno',
},
{ key: $gettext('Address'), value: alias.value.address },
],
},
{ key: $gettext('Address'), value: alias.value.address },
],
},
]
if (alias.value.expire_at !== undefined) {
result[0].items.push({
key: $gettext('Expire at'),
value: alias.value.expire_at,
]
if (alias.value.expire_at !== undefined) {
result[0].items.push({
key: $gettext('Expire at'),
value: alias.value.expire_at,
})
}
if (alias.value.description !== undefined) {
result[0].items.push({
key: $gettext('Description'),
value: alias.value.description,
})
}
result.push({
title: $gettext('Recipients'),
items: [
{
key: $gettext('Recipients'),
value: alias.value.recipients.join(', '),
},
],
})
}
if (alias.value.description !== undefined) {
result[0].items.push({
key: $gettext('Description'),
value: alias.value.description,
})
}
result.push({
title: $gettext('Recipients'),
items: [
{
key: $gettext('Recipients'),
value: alias.value.recipients.join(', '),
},
],
})
return result
return result
})
const alias = ref(getInitialForm())
const steps = ref([
{
name: 'general',
title: $gettext('General'),
},
{
name: 'recipients',
title: $gettext('Recipients'),
},
{
name: 'general',
title: $gettext('General'),
},
{
name: 'recipients',
title: $gettext('Recipients'),
},
])
function close() {
alias.value = getInitialForm()
emit('close')
alias.value = getInitialForm()
emit('close')
}
function getInitialForm() {
return {
random: false,
enabled: true,
recipients: [],
}
return {
random: false,
enabled: true,
recipients: [],
}
}
function getForm(step) {
return formStepsComponenents.step[step.name]
return formStepsComponenents.step[step.name]
}
function getVFormRef(step) {
return formStepsComponenents[step.name].value.vFormRef
return formStepsComponenents[step.name].value.vFormRef
}
function validateAlias() {
const data = { ...alias.value }
if (data.recipients.length === 0) {
delete data.recipients
}
const validation = aliasesApi.validate(data)
validation.catch((error) => {
if (error.response.status === 409 && error.response.data.id !== undefined) {
busStore.displayNotification({
msg: $gettext('Alias already exists, redirecting to edit page'),
type: 'warning',
})
router.push({
name: 'AliasEdit',
params: { id: error.response.data.id },
})
const data = { ...alias.value }
if (data.recipients.length === 0) {
delete data.recipients
}
})
return validation
const validation = aliasesApi.validate(data)
validation.catch((error) => {
if (
error.response.status === 409 &&
error.response.data.id !== undefined
) {
busStore.displayNotification({
msg: $gettext('Alias already exists, redirecting to edit page'),
type: 'warning',
})
router.push({
name: 'AliasEdit',
params: { id: error.response.data.id },
})
}
})
return validation
}
function submit() {
if (alias.value.domain !== undefined) {
delete alias.value.domain
}
identitiesStore.createIdentity('alias', alias.value).then(() => {
emit('created')
close()
})
if (alias.value.domain !== undefined) {
delete alias.value.domain
}
identitiesStore.createIdentity('alias', alias.value).then(() => {
emit('created')
close()
})
}
</script>
@@ -1,80 +1,89 @@
<template>
<LoadingData v-if="!aliasesStore.aliasesLoaded || working" />
<div v-else>
<v-expansion-panels v-model="panel" :multiple="formErrors">
<v-expansion-panel eager value="generalForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('General') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
{{ $gettext('Address') }}: {{ editedAlias.address }}
</v-col>
<v-col cols="6">
<div class="mr-2">{{ $gettext('Enabled') }}</div>
<v-icon v-if="editedAlias.enabled" color="success"
>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-title>
<v-expansion-panel-text>
<AliasGeneralForm ref="generalForm" v-model="editedAlias" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel eager value="recipientForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Recipients') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-chip
v-for="(rcpt, index) in editedAlias.recipients"
:key="index"
size="small"
>
{{ rcpt }}
</v-chip>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<AliasRecipientForm
ref="recipientForm"
v-model="editedAlias.recipients"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<div class="mt-4 d-flex justify-end">
<v-btn :loading="working" @click="$router.go(-1)">
{{ $gettext('Cancel') }}
</v-btn>
<v-btn
class="ml-4"
color="primary darken-1"
:loading="working"
@click="save"
>
{{ $gettext('Save') }}
</v-btn>
<LoadingData v-if="!aliasesStore.aliasesLoaded || working" />
<div v-else>
<v-expansion-panels v-model="panel" :multiple="formErrors">
<v-expansion-panel eager value="generalForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('General') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-col cols="6">
{{ $gettext('Address') }}:
{{ editedAlias.address }}
</v-col>
<v-col cols="6">
<div class="mr-2">
{{ $gettext('Enabled') }}
</div>
<v-icon
v-if="editedAlias.enabled"
color="success"
>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-title>
<v-expansion-panel-text>
<AliasGeneralForm ref="generalForm" v-model="editedAlias" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel eager value="recipientForm">
<v-expansion-panel-title v-slot="{ expanded }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Recipients') }}
</v-col>
<v-col cols="8" class="text-medium-emphasis">
<v-fade-transition leave-absolute>
<span v-if="expanded"></span>
<v-row v-else no-gutters style="width: 100%">
<v-chip
v-for="(
rcpt, index
) in editedAlias.recipients"
:key="index"
size="small"
>
{{ rcpt }}
</v-chip>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<AliasRecipientForm
ref="recipientForm"
v-model="editedAlias.recipients"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<div class="mt-4 d-flex justify-end">
<v-btn :loading="working" @click="$router.go(-1)">
{{ $gettext('Cancel') }}
</v-btn>
<v-btn
class="ml-4"
color="primary darken-1"
:loading="working"
@click="save"
>
{{ $gettext('Save') }}
</v-btn>
</div>
</div>
</div>
</template>
<script setup lang="js">
@@ -106,34 +115,37 @@ const recipientForm = ref()
const formMap = { generalForm: generalForm, recipientForm: recipientForm }
async function save() {
formErrors.value = false
panel.value = []
for (const [panelName, formRef] of Object.entries(formMap)) {
if (formRef.value != null) {
const { valid } = await formRef.value.vFormRef.validate()
if (!valid) {
formErrors.value = true
panel.value.push(panelName)
}
formErrors.value = false
panel.value = []
for (const [panelName, formRef] of Object.entries(formMap)) {
if (formRef.value != null) {
const { valid } = await formRef.value.vFormRef.validate()
if (!valid) {
formErrors.value = true
panel.value.push(panelName)
}
}
}
if (formErrors.value) {
return
}
}
if (formErrors.value) {
return
}
working.value = true
working.value = true
identitiesStore
.updateIdentity('alias', editedAlias.value)
.then(() =>
router.push({ name: 'AliasDetail', params: { id: route.params.id } })
)
.finally(() => (working.value = false))
identitiesStore
.updateIdentity('alias', editedAlias.value)
.then(() =>
router.push({
name: 'AliasDetail',
params: { id: route.params.id },
})
)
.finally(() => (working.value = false))
}
onMounted(() => {
aliasesStore.getAlias(route.params.id).then((response) => {
editedAlias.value = { ...response.data }
})
aliasesStore.getAlias(route.params.id).then((response) => {
editedAlias.value = { ...response.data }
})
})
</script>
@@ -1,14 +1,14 @@
<template>
<v-card>
<v-card-title>
<div class="headline">{{ $gettext('Recipients') }}</div>
</v-card-title>
<v-card-text>
<p v-for="recipient in alias.recipients" :key="recipient">
{{ recipient }}
</p>
</v-card-text>
</v-card>
<v-card>
<v-card-title>
<div class="headline">{{ $gettext('Recipients') }}</div>
</v-card-title>
<v-card-text>
<p v-for="recipient in alias.recipients" :key="recipient">
{{ recipient }}
</p>
</v-card-text>
</v-card>
</template>
<script setup lang="js">
@@ -1,27 +1,27 @@
<template>
<v-card>
<v-card-title>
<div class="headline">{{ $gettext('General information') }}</div>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="6">{{ $gettext('Creation date') }}</v-col>
<v-col cols="6">{{ $date(alias.creation) }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Modified') }}</v-col>
<v-col cols="6">{{ $date(alias.last_modification) }}</v-col>
</v-row>
<v-row v-if="alias.expiry">
<v-col cols="6">{{ $gettext('Expire at') }}</v-col>
<v-col cols="6"></v-col>
</v-row>
<v-row v-if="alias.description">
<v-col cols="6">{{ $gettext('Description') }}</v-col>
<v-col cols="6">{{ alias.description }}</v-col>
</v-row>
</v-card-text>
</v-card>
<v-card>
<v-card-title>
<div class="headline">{{ $gettext('General information') }}</div>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="6">{{ $gettext('Creation date') }}</v-col>
<v-col cols="6">{{ $date(alias.creation) }}</v-col>
</v-row>
<v-row>
<v-col cols="6">{{ $gettext('Modified') }}</v-col>
<v-col cols="6">{{ $date(alias.last_modification) }}</v-col>
</v-row>
<v-row v-if="alias.expiry">
<v-col cols="6">{{ $gettext('Expire at') }}</v-col>
<v-col cols="6"></v-col>
</v-row>
<v-row v-if="alias.description">
<v-col cols="6">{{ $gettext('Description') }}</v-col>
<v-col cols="6">{{ alias.description }}</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script setup lang="js">
@@ -1,22 +1,24 @@
<template>
<v-card>
<v-card-title>
<span class="headline">{{ $gettext('Administered domains') }}</span>
</v-card-title>
<v-card-text>
<template v-if="account.domains !== undefined && account.domains.length">
<v-row v-for="domain in account.domains" :key="domain">
<v-col cols="12">{{ domain }}</v-col>
</v-row>
</template>
<div v-else>
{{ $gettext('No domain managed') }}
</div>
<div class="text-disabled">
{{ $gettext('Add/Remove from the domain page') }}
</div>
</v-card-text>
</v-card>
<v-card>
<v-card-title>
<span class="headline">{{ $gettext('Administered domains') }}</span>
</v-card-title>
<v-card-text>
<template
v-if="account.domains !== undefined && account.domains.length"
>
<v-row v-for="domain in account.domains" :key="domain">
<v-col cols="12">{{ domain }}</v-col>
</v-row>
</template>
<div v-else>
{{ $gettext('No domain managed') }}
</div>
<div class="text-disabled">
{{ $gettext('Add/Remove from the domain page') }}
</div>
</v-card-text>
</v-card>
</template>
<script setup lang="js">
@@ -1,104 +1,108 @@
<template>
<v-card class="mt-6">
<v-data-table
v-model="selected"
:headers="headers"
:items="identities"
:search="search"
:loading="!identitiesLoaded"
item-value="identity"
class="elevation-0"
show-select
>
<template #top>
<v-toolbar flat color="white">
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
append-icon="mdi-chevron-down"
v-bind="props"
size="small"
variant="flat"
>
{{ $gettext('Actions') }}
</v-btn>
<v-card class="mt-6">
<v-data-table
v-model="selected"
:headers="headers"
:items="identities"
:search="search"
:loading="!identitiesLoaded"
item-value="identity"
class="elevation-0"
show-select
>
<template #top>
<v-toolbar flat color="white">
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
append-icon="mdi-chevron-down"
v-bind="props"
size="small"
variant="flat"
>
{{ $gettext('Actions') }}
</v-btn>
</template>
<v-list density="compact">
<MenuItems :items="getActionMenuItems()" />
</v-list>
</v-menu>
<v-btn
variant="text"
icon="mdi-reload"
@click="fetchIdentities"
></v-btn>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
:placeholder="$gettext('Search')"
variant="outlined"
hide-details
single-line
flat
></v-text-field>
</v-toolbar>
</template>
<v-list density="compact">
<MenuItems :items="getActionMenuItems()" />
</v-list>
</v-menu>
<v-btn
variant="text"
icon="mdi-reload"
@click="fetchIdentities"
></v-btn>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
:placeholder="$gettext('Search')"
variant="outlined"
hide-details
single-line
flat
></v-text-field>
</v-toolbar>
</template>
<template #[`item.identity`]="{ item }">
<template v-if="item.type === 'account'">
<router-link :to="{ name: 'AccountDetail', params: { id: item.pk } }">
{{ item.identity }}
</router-link>
</template>
<template v-else>
<router-link :to="{ name: 'AliasDetail', params: { id: item.pk } }">
{{ item.identity }}
</router-link>
</template>
</template>
<template #[`item.tags`]="{ item }">
<v-chip
v-for="(tag, index) in item.tags"
:key="tag.name"
:color="tag.type !== 'idt' ? 'primary' : 'default'"
:class="index > 0 ? 'ml-2' : ''"
size="small"
>
{{ tag.label }}
</v-chip>
</template>
<template #[`item.actions`]="{ item }">
<template
v-if="
item.possible_actions !== undefined &&
item.possible_actions.length !== 0
"
>
<v-icon size="2.5em" color="blue">mdi-circle-small</v-icon>
</template>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
v-bind="props"
size="small"
icon="mdi-dots-horizontal"
variant="text"
>
</v-btn>
</template>
<MenuItems :items="getMenuItems(item)" :obj="item" />
</v-menu>
</template>
</v-data-table>
<ConfirmDialog ref="confirmAlias" />
<ConfirmDialog ref="confirmAccount">
<v-checkbox
v-model="keepAccountFolder"
:label="$gettext('Do not delete account folder')"
hide-details
/>
</ConfirmDialog>
</v-card>
<template #[`item.identity`]="{ item }">
<template v-if="item.type === 'account'">
<router-link
:to="{ name: 'AccountDetail', params: { id: item.pk } }"
>
{{ item.identity }}
</router-link>
</template>
<template v-else>
<router-link
:to="{ name: 'AliasDetail', params: { id: item.pk } }"
>
{{ item.identity }}
</router-link>
</template>
</template>
<template #[`item.tags`]="{ item }">
<v-chip
v-for="(tag, index) in item.tags"
:key="tag.name"
:color="tag.type !== 'idt' ? 'primary' : 'default'"
:class="index > 0 ? 'ml-2' : ''"
size="small"
>
{{ tag.label }}
</v-chip>
</template>
<template #[`item.actions`]="{ item }">
<template
v-if="
item.possible_actions !== undefined &&
item.possible_actions.length !== 0
"
>
<v-icon size="2.5em" color="blue">mdi-circle-small</v-icon>
</template>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
v-bind="props"
size="small"
icon="mdi-dots-horizontal"
variant="text"
>
</v-btn>
</template>
<MenuItems :items="getMenuItems(item)" :obj="item" />
</v-menu>
</template>
</v-data-table>
<ConfirmDialog ref="confirmAlias" />
<ConfirmDialog ref="confirmAccount">
<v-checkbox
v-model="keepAccountFolder"
:label="$gettext('Do not delete account folder')"
hide-details
/>
</ConfirmDialog>
</v-card>
</template>
<script setup lang="js">
@@ -114,15 +118,15 @@ const identitiesStore = useIdentitiesStore()
const router = useRouter()
const headers = ref([
{ title: $gettext('Name'), key: 'identity' },
{ title: $gettext('Fullname/recipient'), key: 'name_or_rcpt' },
{ title: $gettext('Tags'), key: 'tags' },
{
title: $gettext('Actions'),
key: 'actions',
sortable: false,
align: 'end',
},
{ title: $gettext('Name'), key: 'identity' },
{ title: $gettext('Fullname/recipient'), key: 'name_or_rcpt' },
{ title: $gettext('Tags'), key: 'tags' },
{
title: $gettext('Actions'),
key: 'actions',
sortable: false,
align: 'end',
},
])
const identities = computed(() => identitiesStore.identities)
@@ -134,90 +138,90 @@ const confirmAlias = ref()
const confirmAccount = ref()
function fetchIdentities() {
identitiesStore.getIdentities().then(() => {
selected.value = []
})
identitiesStore.getIdentities().then(() => {
selected.value = []
})
}
function getMenuItems(item) {
const result = []
if (item.type === 'account') {
item.possible_actions.forEach((element) => {
result.push({
label: element.label,
icon: element.icon,
onClick: () => {
if (element.type === 'download') {
//TODO : downloadFile(element)
}
fetchIdentities()
},
})
})
result.push({
label: $gettext('Edit'),
icon: 'mdi-circle-edit-outline',
onClick: editAccount,
})
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteAccount,
color: 'red',
})
} else if (item.type === 'alias') {
result.push({
label: $gettext('Edit'),
icon: 'mdi-circle-edit-outline',
onClick: editAlias,
})
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteAlias,
color: 'red',
})
}
return result
const result = []
if (item.type === 'account') {
item.possible_actions.forEach((element) => {
result.push({
label: element.label,
icon: element.icon,
onClick: () => {
if (element.type === 'download') {
//TODO : downloadFile(element)
}
fetchIdentities()
},
})
})
result.push({
label: $gettext('Edit'),
icon: 'mdi-circle-edit-outline',
onClick: editAccount,
})
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteAccount,
color: 'red',
})
} else if (item.type === 'alias') {
result.push({
label: $gettext('Edit'),
icon: 'mdi-circle-edit-outline',
onClick: editAlias,
})
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteAlias,
color: 'red',
})
}
return result
}
function getActionMenuItems() {
const result = []
if (selected.value.length > 0) {
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteIdentities,
color: 'red',
})
}
return result
const result = []
if (selected.value.length > 0) {
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteIdentities,
color: 'red',
})
}
return result
}
function editAccount(account) {
router.push({ name: 'AccountEdit', params: { id: account.pk } })
router.push({ name: 'AccountEdit', params: { id: account.pk } })
}
async function deleteAccount(account) {
const result = await confirmAccount.value.open(
$gettext('Warning'),
$gettext('Are you sure you want to delete this identity ?'),
{
color: 'error',
agreeLabel: $gettext('Yes'),
cancelLabel: $gettext('No'),
const result = await confirmAccount.value.open(
$gettext('Warning'),
$gettext('Are you sure you want to delete this identity ?'),
{
color: 'error',
agreeLabel: $gettext('Yes'),
cancelLabel: $gettext('No'),
}
)
if (!result) {
return
}
)
if (!result) {
return
}
identitiesStore
.deleteIdentity(account.type, account.pk, {
keepdir: keepAccountFolder.value,
})
.then(() => {
keepAccountFolder.value = false
})
identitiesStore
.deleteIdentity(account.type, account.pk, {
keepdir: keepAccountFolder.value,
})
.then(() => {
keepAccountFolder.value = false
})
}
function editAlias() {}
@@ -225,24 +229,24 @@ function editAlias() {}
function deleteIdentities() {}
async function deleteAlias(alias) {
const result = await confirmAlias.value.open(
$gettext('Warning'),
$gettext('Are you sure you want to delete this alias?'),
{
color: 'error',
agreeLabel: $gettext('Yes'),
cancelLabel: $gettext('No'),
const result = await confirmAlias.value.open(
$gettext('Warning'),
$gettext('Are you sure you want to delete this alias?'),
{
color: 'error',
agreeLabel: $gettext('Yes'),
cancelLabel: $gettext('No'),
}
)
if (!result) {
return
}
)
if (!result) {
return
}
identitiesStore.deleteIdentity(alias.type, alias.pk)
identitiesStore.deleteIdentity(alias.type, alias.pk)
}
onMounted(() => {
if (identities.value.length < 1) {
fetchIdentities()
}
if (identities.value.length < 1) {
fetchIdentities()
}
})
</script>
@@ -1,29 +1,29 @@
<template>
<v-form ref="vFormRef">
<EmailField
ref="aliasField"
v-model="currentAlias"
:placeholder="$gettext('Start typing a name here...')"
:hint="
$gettext(
'Alias(es) of this mailbox. To create a catchall alias, just enter the domain name (@domain.tld).'
)
"
persistent-hint
:loading="isValidating"
:error-msg="errors"
@domain-selected="addAlias"
/>
<v-chip
v-for="(alias, index) in account.aliases"
:key="index"
class="mr-2 mt-2"
closable
@click:close="removeAlias(index)"
>
{{ alias }}
</v-chip>
</v-form>
<v-form ref="vFormRef">
<EmailField
ref="aliasField"
v-model="currentAlias"
:placeholder="$gettext('Start typing a name here...')"
:hint="
$gettext(
'Alias(es) of this mailbox. To create a catchall alias, just enter the domain name (@domain.tld).'
)
"
persistent-hint
:loading="isValidating"
:error-msg="errors"
@domain-selected="addAlias"
/>
<v-chip
v-for="(alias, index) in account.aliases"
:key="index"
class="mr-2 mt-2"
closable
@click:close="removeAlias(index)"
>
{{ alias }}
</v-chip>
</v-form>
</template>
<script setup lang="js">
@@ -44,37 +44,37 @@ const isValidating = ref(false)
const errors = ref([])
async function addAlias() {
errors.value = []
isValidating.value = true
if (account.value.aliases.indexOf(currentAlias.value) !== -1) {
errors.value = [$gettext('Alias already added')]
currentAlias.value = ''
isValidating.value = false
return
}
try {
await accountsApi.validate({
aliases: [currentAlias.value],
mailbox: account.value.mailbox,
})
account.value.aliases.push(currentAlias.value)
currentAlias.value = ''
vFormRef.value.resetValidation()
} 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
errors.value = []
isValidating.value = true
if (account.value.aliases.indexOf(currentAlias.value) !== -1) {
errors.value = [$gettext('Alias already added')]
currentAlias.value = ''
isValidating.value = false
return
}
try {
await accountsApi.validate({
aliases: [currentAlias.value],
mailbox: account.value.mailbox,
})
account.value.aliases.push(currentAlias.value)
currentAlias.value = ''
vFormRef.value.resetValidation()
} 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
}
//TODO : v-alert !!
} finally {
isValidating.value = false
}
//TODO : v-alert !!
} finally {
isValidating.value = false
}
}
function removeAlias(index) {
account.value.aliases.splice(index, 1)
account.value.aliases.splice(index, 1)
}
defineExpose({ vFormRef })
@@ -1,49 +1,49 @@
<template>
<v-form ref="vFormRef">
<label class="m-label">{{ $gettext('Username') }}</label>
<EmailField
ref="username"
v-model="account.username"
:placeholder="usernamePlaceholder"
:type="usernameInputType"
:rules="
usernameInputType === 'email'
? [rules.required, rules.email]
: [rules.required]
"
:role="account.role"
:error-msg="formErrors.value ? formErrors.value.username : []"
@update:model-value="updateUsername"
/>
<label class="m-label">{{ $gettext('First name') }}</label>
<v-text-field
v-model="account.first_name"
autocomplete="new-password"
variant="outlined"
density="compact"
/>
<label class="m-label">{{ $gettext('Last name') }}</label>
<v-text-field
v-model="account.last_name"
autocomplete="new-password"
variant="outlined"
density="compact"
/>
<v-form ref="vFormRef">
<label class="m-label">{{ $gettext('Username') }}</label>
<EmailField
ref="username"
v-model="account.username"
:placeholder="usernamePlaceholder"
:type="usernameInputType"
:rules="
usernameInputType === 'email'
? [rules.required, rules.email]
: [rules.required]
"
:role="account.role"
:error-msg="formErrors.value ? formErrors.value.username : []"
@update:model-value="updateUsername"
/>
<label class="m-label">{{ $gettext('First name') }}</label>
<v-text-field
v-model="account.first_name"
autocomplete="new-password"
variant="outlined"
density="compact"
/>
<label class="m-label">{{ $gettext('Last name') }}</label>
<v-text-field
v-model="account.last_name"
autocomplete="new-password"
variant="outlined"
density="compact"
/>
<AccountPasswordSubForm
ref="passwordForm"
v-model="account"
:editing="editing"
:form-errors="formErrors"
/>
<AccountPasswordSubForm
ref="passwordForm"
v-model="account"
:editing="editing"
:form-errors="formErrors"
/>
<v-switch
v-model="account.is_active"
:label="$gettext('Enabled')"
density="compact"
color="primary"
/>
</v-form>
<v-switch
v-model="account.is_active"
:label="$gettext('Enabled')"
density="compact"
color="primary"
/>
</v-form>
</template>
<script setup lang="js">
@@ -55,8 +55,8 @@ import rules from '@/plugins/rules'
const { $gettext } = useGettext()
const props = defineProps({
modelValue: { type: Object, default: null },
editing: { type: Boolean, default: false },
modelValue: { type: Object, default: null },
editing: { type: Boolean, default: false },
})
const vFormRef = ref()
@@ -65,24 +65,24 @@ const formErrors = ref({})
const account = computed(() => props.modelValue)
const usernamePlaceholder = computed(() => {
if (account.value.role === 'SimpleUsers') {
return $gettext('Enter an email address')
}
return $gettext('Enter a simple username or an email address')
if (account.value.role === 'SimpleUsers') {
return $gettext('Enter an email address')
}
return $gettext('Enter a simple username or an email address')
})
const usernameInputType = computed(() => {
return account.value.role === 'SimpleUsers' ? 'email' : 'text'
return account.value.role === 'SimpleUsers' ? 'email' : 'text'
})
function updateUsername() {
if (
account.value.role != 'SuperAdmins' &&
account.value.username.indexOf('@') !== -1
) {
account.value.mailbox.full_address = account.value.username
account.value.mailbox.message_limit = null
}
if (
account.value.role != 'SuperAdmins' &&
account.value.username.indexOf('@') !== -1
) {
account.value.mailbox.full_address = account.value.username
account.value.mailbox.message_limit = null
}
}
defineExpose({ vFormRef, formErrors })
@@ -1,47 +1,49 @@
<template>
<v-form ref="vFormRef">
<label class="m-label">{{ $gettext('Quota') }}</label>
<v-switch
v-model="form.mailbox.use_domain_quota"
:label="useDomainQuotaLabel"
color="primary"
@update:model-value="cleanQuota"
/>
<v-text-field
v-if="!form.mailbox.use_domain_quota"
v-model="form.mailbox.quota"
:placeholder="$gettext('Ex: 10MB. Leave empty for no limit')"
:hint="
$gettext(
'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).'
)
"
persistent-hint
variant="outlined"
density="compact"
/>
<label class="m-label">{{ $gettext('Daily message sending limit') }}</label>
<v-text-field
v-model="form.mailbox.message_limit"
:placeholder="$gettext('Leave empty for no limit')"
:hint="
$gettext(
'Number of messages this mailbox can send per day. Leave empty for no limit.'
)
"
persistent-hint
variant="outlined"
density="compact"
:rules="[rules.numericOrNull]"
@update:model-value="cleanMessageLimit"
/>
<v-switch
v-model="form.mailbox.is_send_only"
:label="$gettext('Send only account')"
density="compact"
color="primary"
/>
</v-form>
<v-form ref="vFormRef">
<label class="m-label">{{ $gettext('Quota') }}</label>
<v-switch
v-model="form.mailbox.use_domain_quota"
:label="useDomainQuotaLabel"
color="primary"
@update:model-value="cleanQuota"
/>
<v-text-field
v-if="!form.mailbox.use_domain_quota"
v-model="form.mailbox.quota"
:placeholder="$gettext('Ex: 10MB. Leave empty for no limit')"
:hint="
$gettext(
'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).'
)
"
persistent-hint
variant="outlined"
density="compact"
/>
<label class="m-label">{{
$gettext('Daily message sending limit')
}}</label>
<v-text-field
v-model="form.mailbox.message_limit"
:placeholder="$gettext('Leave empty for no limit')"
:hint="
$gettext(
'Number of messages this mailbox can send per day. Leave empty for no limit.'
)
"
persistent-hint
variant="outlined"
density="compact"
:rules="[rules.numericOrNull]"
@update:model-value="cleanMessageLimit"
/>
<v-switch
v-model="form.mailbox.is_send_only"
:label="$gettext('Send only account')"
density="compact"
color="primary"
/>
</v-form>
</template>
<script setup lang="js">
@@ -57,73 +59,73 @@ const domainsStore = useDomainsStore()
const vFormRef = ref()
const form = ref({
mailbox: {},
mailbox: {},
})
watch(
props.modelValue,
(value) => {
if (value) {
form.value = { ...value }
if (form.value.role === 'SimpleUsers') {
if (!form.value.mailbox) {
form.value.mailbox = {}
props.modelValue,
(value) => {
if (value) {
form.value = { ...value }
if (form.value.role === 'SimpleUsers') {
if (!form.value.mailbox) {
form.value.mailbox = {}
}
form.value.mailbox.full_address = form.value.username
}
if (form.value.mailbox.message_limit === '') {
form.value.mailbox.message_limit = null
}
} else {
form.value = {
mailbox: {},
}
}
form.value.mailbox.full_address = form.value.username
}
if (form.value.mailbox.message_limit === '') {
form.value.mailbox.message_limit = null
}
} else {
form.value = {
mailbox: {},
}
}
},
{ immediate: true }
},
{ immediate: true }
)
watch(
form,
(value) => {
emit('update:modelValue', value)
},
{ deep: true }
form,
(value) => {
emit('update:modelValue', value)
},
{ deep: true }
)
const domainQuota = computed(() => {
const email = form.value.mailbox.full_address
if (email && email.indexOf('@') !== -1) {
const domain = domainsStore.getDomainByName(email.split('@')[1])
if (domain) {
return parseInt(domain.default_mailbox_quota)
const email = form.value.mailbox.full_address
if (email && email.indexOf('@') !== -1) {
const domain = domainsStore.getDomainByName(email.split('@')[1])
if (domain) {
return parseInt(domain.default_mailbox_quota)
}
}
}
return undefined
return undefined
})
const useDomainQuotaLabel = computed(() => {
let result = $gettext('Use domain default value')
if (domainQuota.value !== undefined) {
if (domainQuota.value === 0) {
result += ` (${$gettext('unlimited')})`
} else {
result += ` (${domainQuota.value} MB)`
let result = $gettext('Use domain default value')
if (domainQuota.value !== undefined) {
if (domainQuota.value === 0) {
result += ` (${$gettext('unlimited')})`
} else {
result += ` (${domainQuota.value} MB)`
}
}
}
return result
return result
})
function cleanQuota(value) {
if (value) {
delete form.value.mailbox.quota
}
if (value) {
delete form.value.mailbox.quota
}
}
function cleanMessageLimit(value) {
if (value === '') {
delete form.value.mailbox.message_limit
}
if (value === '') {
delete form.value.mailbox.message_limit
}
}
defineExpose({ vFormRef })
@@ -131,6 +133,6 @@ defineExpose({ vFormRef })
<style lang="scss" scoped>
.quota {
max-width: 50%;
max-width: 50%;
}
</style>
@@ -1,131 +1,137 @@
<template>
<div>
<template v-if="withPasswordCheck">
<label class="m-label">{{ $gettext("Current password") }}</label>
<v-text-field
v-model="account.currentPassword"
autocomplete="new-password"
variant="outlined"
type="password"
density="compact"
validate-on="submit"
:rules="disabled ? [] : [rules.required, validPassword]"
:disabled="disabled"
/>
</template>
<v-row justify="center">
<v-col cols="auto">
<v-switch
v-model="account.random_password"
:label="$gettext('Random password')"
density="compact"
color="primary"
class="text-center"
:disabled="disabled"
@update:model-value="updatePassword"
/>
</v-col>
<v-col>
<v-alert
v-if="account.random_password"
style="background-color: #515d78"
class="ml-6"
density="compact"
>
<span class="text-white mr-4">{{ account.password }}</span>
<v-btn
size="small"
color="white"
variant="text"
density="compact"
:disabled="disabled"
icon="mdi-clipboard-multiple-outline"
:title="$gettext('Copy to clipboard')"
@click="copyPassword"
/>
</v-alert>
</v-col>
</v-row>
<template v-if="!account.random_password">
<v-text-field
v-model="account.password"
variant="outlined"
:label="$gettext('Password')"
type="password"
autocomplete="new-password"
density="compact"
:disabled="disabled"
:rules="isRuleActive ? [rules.required] : []"
/>
<v-text-field
v-model="account.password_confirmation"
variant="outlined"
type="password"
:label="$gettext('Confirmation')"
density="compact"
:disabled="disabled"
:rules="isRuleActive ? [rules.required, passwordConfirmationRules] : []"
:error-messages="
formErrors && formErrors.value ? formErrors.value.password : []
"
/>
</template>
</div>
<div>
<template v-if="withPasswordCheck">
<label class="m-label">{{ $gettext('Current password') }}</label>
<v-text-field
v-model="account.currentPassword"
autocomplete="new-password"
variant="outlined"
type="password"
density="compact"
validate-on="submit"
:rules="disabled ? [] : [rules.required, validPassword]"
:disabled="disabled"
/>
</template>
<v-row justify="center">
<v-col cols="auto">
<v-switch
v-model="account.random_password"
:label="$gettext('Random password')"
density="compact"
color="primary"
class="text-center"
:disabled="disabled"
@update:model-value="updatePassword"
/>
</v-col>
<v-col>
<v-alert
v-if="account.random_password"
style="background-color: #515d78"
class="ml-6"
density="compact"
>
<span class="text-white mr-4">{{ account.password }}</span>
<v-btn
size="small"
color="white"
variant="text"
density="compact"
:disabled="disabled"
icon="mdi-clipboard-multiple-outline"
:title="$gettext('Copy to clipboard')"
@click="copyPassword"
/>
</v-alert>
</v-col>
</v-row>
<template v-if="!account.random_password">
<v-text-field
v-model="account.password"
variant="outlined"
:label="$gettext('Password')"
type="password"
autocomplete="new-password"
density="compact"
:disabled="disabled"
:rules="isRuleActive ? [rules.required] : []"
/>
<v-text-field
v-model="account.password_confirmation"
variant="outlined"
type="password"
:label="$gettext('Confirmation')"
density="compact"
:disabled="disabled"
:rules="
isRuleActive
? [rules.required, passwordConfirmationRules]
: []
"
:error-messages="
formErrors && formErrors.value
? formErrors.value.password
: []
"
/>
</template>
</div>
</template>
<script setup lang="js">
import { useBusStore } from "@/stores";
import accountsApi from "@/api/accounts";
import accountApi from "@/api/account";
import { computed } from "vue";
import rules from "@/plugins/rules";
import { useGettext } from "vue3-gettext";
import { useBusStore } from '@/stores'
import accountsApi from '@/api/accounts'
import accountApi from '@/api/account'
import { computed } from 'vue'
import rules from '@/plugins/rules'
import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext();
const busStore = useBusStore();
const { $gettext } = useGettext()
const busStore = useBusStore()
const props = defineProps({
modelValue: { type: Object, default: null },
withPasswordCheck: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
editing: { type: Boolean, default: false },
formErrors: { type: Object, required: false, default: () => {} },
});
modelValue: { type: Object, default: null },
withPasswordCheck: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
editing: { type: Boolean, default: false },
formErrors: { type: Object, required: false, default: () => {} },
})
const account = computed(() => props.modelValue);
const account = computed(() => props.modelValue)
const isRuleActive = computed(
() => !props.disabled && (!props.editing || account.value.password),
);
() => !props.disabled && (!props.editing || account.value.password)
)
const passwordConfirmationRules = (value) =>
value === account.value.password || $gettext("Password mismatch");
value === account.value.password || $gettext('Password mismatch')
function copyPassword() {
navigator.clipboard.writeText(account.value.password).then(() => {
busStore.displayNotification({
msg: $gettext("Password copied to clipboard"),
});
});
navigator.clipboard.writeText(account.value.password).then(() => {
busStore.displayNotification({
msg: $gettext('Password copied to clipboard'),
})
})
}
function updatePassword(value) {
if (value) {
accountsApi.getRandomPassword().then((resp) => {
account.value.password = resp.data.password;
});
} else {
account.value.password = null;
account.value.password_confirmation = null;
}
if (value) {
accountsApi.getRandomPassword().then((resp) => {
account.value.password = resp.data.password
})
} else {
account.value.password = null
account.value.password_confirmation = null
}
}
async function validPassword(value) {
if (value) {
return accountApi
.checkPassword(value)
.then(() => true)
.catch(() => $gettext("Invalid password"));
}
return true;
if (value) {
return accountApi
.checkPassword(value)
.then(() => true)
.catch(() => $gettext('Invalid password'))
}
return true
}
</script>
@@ -1,28 +1,28 @@
<template>
<v-form ref="vFormRef">
<v-row>
<v-col cols="7">
<ChoiceField
v-model="account.role"
:label="$gettext('Choose a role')"
:choices="accountRoles"
:choices-per-line="2"
/>
</v-col>
<v-col cols="5">
<v-alert color="primary" class="mt-11 ml-4 rounded-lg">
<h3 class="headline">{{ roleLabel }}</h3>
<p class="mt-4">{{ roleHelp }}</p>
<v-icon
color="white"
class="float-right"
size="large"
icon="mdi-help-circle-outline"
/>
</v-alert>
</v-col>
</v-row>
</v-form>
<v-form ref="vFormRef">
<v-row>
<v-col cols="7">
<ChoiceField
v-model="account.role"
:label="$gettext('Choose a role')"
:choices="accountRoles"
:choices-per-line="2"
/>
</v-col>
<v-col cols="5">
<v-alert color="primary" class="mt-11 ml-4 rounded-lg">
<h3 class="headline">{{ roleLabel }}</h3>
<p class="mt-4">{{ roleHelp }}</p>
<v-icon
color="white"
class="float-right"
size="large"
icon="mdi-help-circle-outline"
/>
</v-alert>
</v-col>
</v-row>
</v-form>
</template>
<script setup lang="js">
@@ -35,7 +35,7 @@ const authStore = useAuthStore()
const { $gettext } = useGettext()
const props = defineProps({
modelValue: { type: Object, default: null },
modelValue: { type: Object, default: null },
})
const vFormRef = ref()
@@ -45,70 +45,70 @@ const account = computed(() => props.modelValue)
const authUser = computed(() => authStore.authUser)
const roleLabel = computed(() => {
const role = accountRoles.value.find(
(roleTarget) => roleTarget.value === account.value.role
)
return role !== undefined ? role.label : ''
const role = accountRoles.value.find(
(roleTarget) => roleTarget.value === account.value.role
)
return role !== undefined ? role.label : ''
})
const roleHelp = computed(() => {
const role = accountRoles.value.find(
(roleTarget) => roleTarget.value === account.value.role
)
return role !== undefined ? role.help : ''
const role = accountRoles.value.find(
(roleTarget) => roleTarget.value === account.value.role
)
return role !== undefined ? role.help : ''
})
const accountRoles = computed(() => {
if (authUser.value.role === 'SuperAdmins') {
return [
...domainAdminsRole,
...resellerRole,
...simpleUserRole,
...superAdminsRole,
]
} else if (authUser.value.role === 'DomainAdmins') {
return [...simpleUserRole]
} else if (authUser.value.role === 'Resellers') {
return [...domainAdminsRole, ...simpleUserRole]
} else {
return []
}
if (authUser.value.role === 'SuperAdmins') {
return [
...domainAdminsRole,
...resellerRole,
...simpleUserRole,
...superAdminsRole,
]
} else if (authUser.value.role === 'DomainAdmins') {
return [...simpleUserRole]
} else if (authUser.value.role === 'Resellers') {
return [...domainAdminsRole, ...simpleUserRole]
} else {
return []
}
})
const simpleUserRole = [
{
label: $gettext('Simple user'),
value: 'SimpleUsers',
help: $gettext(
'A user with no privileges but with a mailbox. He will be allowed to use all end-user features: webmail, calendar, contacts, filters.'
),
},
{
label: $gettext('Simple user'),
value: 'SimpleUsers',
help: $gettext(
'A user with no privileges but with a mailbox. He will be allowed to use all end-user features: webmail, calendar, contacts, filters.'
),
},
]
const domainAdminsRole = [
{
label: $gettext('Domain administrator'),
value: 'DomainAdmins',
help: $gettext(
'A user with privileges on one or more domain. He will be allowed to administer mailboxes and he can also have a mailbox.'
),
},
{
label: $gettext('Domain administrator'),
value: 'DomainAdmins',
help: $gettext(
'A user with privileges on one or more domain. He will be allowed to administer mailboxes and he can also have a mailbox.'
),
},
]
const resellerRole = [
{
label: $gettext('Reseller'),
value: 'Resellers',
help: $gettext('help'),
},
{
label: $gettext('Reseller'),
value: 'Resellers',
help: $gettext('help'),
},
]
const superAdminsRole = [
{
label: $gettext('Super administrator'),
value: 'SuperAdmins',
help: $gettext(
"A user with all privileges, can do anything. By default, such a user does not have a mailbox so he can't access end-user features."
),
},
{
label: $gettext('Super administrator'),
value: 'SuperAdmins',
help: $gettext(
"A user with all privileges, can do anything. By default, such a user does not have a mailbox so he can't access end-user features."
),
},
]
defineExpose({ vFormRef, roleLabel })
@@ -1,67 +1,75 @@
<template>
<v-form ref="vFormRef">
<v-switch
v-if="!alias.pk"
v-model="alias.random"
:label="$gettext('Random address')"
color="primary"
/>
<template v-if="!alias.random">
<label class="m-label">{{ $gettext('Email address') }}</label>
<EmailField
ref="address"
v-model="alias.address"
type="email"
variant="outlined"
density="compact"
:rules="[rules.required]"
/>
</template>
<template v-else>
<label class="m-label">{{ $gettext('Domain') }}</label>
<v-select
v-model="alias.domain"
:items="Object.values(domains)"
item-title="name"
return-object
variant="outlined"
density="compact"
:rules="[rules.required]"
@update:model-value="updateAddress"
>
<template #append>
<v-btn variant="flat" icon="mdi-reload" @click="updateAddress" />
<v-form ref="vFormRef">
<v-switch
v-if="!alias.pk"
v-model="alias.random"
:label="$gettext('Random address')"
color="primary"
/>
<template v-if="!alias.random">
<label class="m-label">{{ $gettext('Email address') }}</label>
<EmailField
ref="address"
v-model="alias.address"
type="email"
variant="outlined"
density="compact"
:rules="[rules.required]"
/>
</template>
</v-select>
<v-text-field
v-model="alias.address"
:label="$gettext('Generated address')"
readonly
>
<template #append-inner>
<v-btn
variant="flat"
icon="mdi-clipboard-outline"
@click="copyAlias"
/>
<template v-else>
<label class="m-label">{{ $gettext('Domain') }}</label>
<v-select
v-model="alias.domain"
:items="Object.values(domains)"
item-title="name"
return-object
variant="outlined"
density="compact"
:rules="[rules.required]"
@update:model-value="updateAddress"
>
<template #append>
<v-btn
variant="flat"
icon="mdi-reload"
@click="updateAddress"
/>
</template>
</v-select>
<v-text-field
v-model="alias.address"
:label="$gettext('Generated address')"
readonly
>
<template #append-inner>
<v-btn
variant="flat"
icon="mdi-clipboard-outline"
@click="copyAlias"
/>
</template>
</v-text-field>
</template>
</v-text-field>
</template>
<v-switch
v-model="alias.enabled"
:label="$gettext('Enabled')"
color="primary"
/>
<label class="m-label">{{ $gettext('Expire at') }}</label>
<v-text-field v-model="alias.expire" variant="outlined" density="compact" />
<label class="m-label">{{ $gettext('Description') }}</label>
<v-textarea
v-model="alias.description"
rows="2"
variant="outlined"
density="compact"
/>
</v-form>
<v-switch
v-model="alias.enabled"
:label="$gettext('Enabled')"
color="primary"
/>
<label class="m-label">{{ $gettext('Expire at') }}</label>
<v-text-field
v-model="alias.expire"
variant="outlined"
density="compact"
/>
<label class="m-label">{{ $gettext('Description') }}</label>
<v-textarea
v-model="alias.description"
rows="2"
variant="outlined"
density="compact"
/>
</v-form>
</template>
<script setup lang="js">
@@ -81,16 +89,16 @@ const domains = computed(() => domainsStore.domains)
const vFormRef = ref()
function copyAlias() {
if (alias.value.address != null) {
navigator.clipboard.writeText(alias.value.address)
}
if (alias.value.address != null) {
navigator.clipboard.writeText(alias.value.address)
}
}
async function updateAddress() {
if (alias.value.domain != null)
aliasesApi.getRandomAddress().then((resp) => {
alias.value.address = `${resp.data.address}@${alias.value.domain.name}`
})
if (alias.value.domain != null)
aliasesApi.getRandomAddress().then((resp) => {
alias.value.address = `${resp.data.address}@${alias.value.domain.name}`
})
}
defineExpose({ vFormRef })
@@ -1,29 +1,29 @@
<template>
<v-form ref="vFormRef">
<EmailField
ref="recipientField"
v-model="recipient"
:placeholder="$gettext('Start typing a name here...')"
:hint="
$gettext(
'Alias(es) of this mailbox. To create a catchall alias, just enter the domain name (@domain.tld).'
)
"
persistent-hint
allow-add
:rules="requiredRecipient"
@domain-selected="addRecipient"
/>
<v-chip
v-for="(_recipient, index) in recipients"
:key="index"
class="mr-2 mt-2"
closable
@click:close="removeRecipient(index)"
>
{{ _recipient }}
</v-chip>
</v-form>
<v-form ref="vFormRef">
<EmailField
ref="recipientField"
v-model="recipient"
:placeholder="$gettext('Start typing a name here...')"
:hint="
$gettext(
'Alias(es) of this mailbox. To create a catchall alias, just enter the domain name (@domain.tld).'
)
"
persistent-hint
allow-add
:rules="requiredRecipient"
@domain-selected="addRecipient"
/>
<v-chip
v-for="(_recipient, index) in recipients"
:key="index"
class="mr-2 mt-2"
closable
@click:close="removeRecipient(index)"
>
{{ _recipient }}
</v-chip>
</v-form>
</template>
<script setup lang="js">
@@ -34,12 +34,12 @@ import { ref, computed } from 'vue'
const { $gettext } = useGettext()
const emit = defineEmits(['update:model-value'])
const props = defineProps({
modelValue: { type: Array, default: () => [] },
modelValue: { type: Array, default: () => [] },
})
const recipients = computed({
get: () => props.modelValue,
set: (value) => emit('update:model-value', value),
get: () => props.modelValue,
set: (value) => emit('update:model-value', value),
})
const recipient = ref('')
@@ -47,19 +47,19 @@ const recipientField = ref()
const vFormRef = ref()
async function addRecipient() {
recipients.value.push(recipient.value)
recipient.value = ''
vFormRef.value.resetValidation()
recipients.value.push(recipient.value)
recipient.value = ''
vFormRef.value.resetValidation()
}
function removeRecipient(index) {
recipients.value.splice(index, 1)
recipients.value.splice(index, 1)
}
const requiredRecipient = [
() =>
recipients.value.length > 0 ||
$gettext('Please add at least one recipient.'),
() =>
recipients.value.length > 0 ||
$gettext('Please add at least one recipient.'),
]
defineExpose({ vFormRef })
@@ -1,64 +1,67 @@
<template>
<v-card class="mt-6">
<v-data-table
v-model="selected"
v-model:options="options"
:headers="headers"
:items="migrations"
:search="search"
:server-items-length="totalMigrations"
:loading="loading"
show-select
>
<template #top>
<v-toolbar color="white" flat>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="flat"
append-icon="mdi-chevron-down"
size="small"
>
{{ $gettext('Actions') }}
</v-btn>
</template>
<v-list density="compact"> </v-list>
</v-menu>
<v-btn
variant="text"
icon="mdi-reload"
@click="getMigrations()"
></v-btn>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
placeholder="Search"
filled
variant="outlined"
density="compact"
hide-details
></v-text-field>
</v-toolbar>
</template>
<template #[`item.mailbox`]="{ item }">
<router-link
:to="{ name: 'AccountDetail', params: { id: item.mailbox.user } }"
<v-card class="mt-6">
<v-data-table
v-model="selected"
v-model:options="options"
:headers="headers"
:items="migrations"
:search="search"
:server-items-length="totalMigrations"
:loading="loading"
show-select
>
{{ item.mailbox.full_address }}
</router-link>
</template>
<template #[`item.actions`]="{ item }">
<v-menu offset-y>
<template #activator="{ props }">
<v-btn icon="mdi-dots-horizontal" v-bind="props" />
</template>
<MenuItems :items="getMenuItems()" :object="item" />
</v-menu>
</template>
</v-data-table>
</v-card>
<template #top>
<v-toolbar color="white" flat>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="flat"
append-icon="mdi-chevron-down"
size="small"
>
{{ $gettext('Actions') }}
</v-btn>
</template>
<v-list density="compact"> </v-list>
</v-menu>
<v-btn
variant="text"
icon="mdi-reload"
@click="getMigrations()"
></v-btn>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
placeholder="Search"
filled
variant="outlined"
density="compact"
hide-details
></v-text-field>
</v-toolbar>
</template>
<template #[`item.mailbox`]="{ item }">
<router-link
:to="{
name: 'AccountDetail',
params: { id: item.mailbox.user },
}"
>
{{ item.mailbox.full_address }}
</router-link>
</template>
<template #[`item.actions`]="{ item }">
<v-menu offset-y>
<template #activator="{ props }">
<v-btn icon="mdi-dots-horizontal" v-bind="props" />
</template>
<MenuItems :items="getMenuItems()" :object="item" />
</v-menu>
</template>
</v-data-table>
</v-card>
</template>
<script setup lang="js">
@@ -73,15 +76,15 @@ const busStore = useBusStore()
const migrations = ref([])
const headers = [
{ title: $gettext('Provider'), value: 'provider.name' },
{ title: $gettext('Old account'), value: 'username' },
{ title: $gettext('New account'), value: 'mailbox' },
{
title: $gettext('Actions'),
value: 'actions',
sortable: false,
align: 'right',
},
{ title: $gettext('Provider'), value: 'provider.name' },
{ title: $gettext('Old account'), value: 'username' },
{ title: $gettext('New account'), value: 'mailbox' },
{
title: $gettext('Actions'),
value: 'actions',
sortable: false,
align: 'right',
},
]
const selected = ref([])
@@ -91,38 +94,38 @@ const options = ref({})
const totalMigrations = ref(0)
function deleteMigration(migration) {
migrationApi.deleteMigration(migration.id).then(() => {
busStore.displayNotification({ msg: $gettext('Migration deleted') })
migrations.value = migrations.value.filter((item) => {
return item.id !== migration.id
migrationApi.deleteMigration(migration.id).then(() => {
busStore.displayNotification({ msg: $gettext('Migration deleted') })
migrations.value = migrations.value.filter((item) => {
return item.id !== migration.id
})
})
})
}
function getMigrations(filter) {
loading.value = true
let query = {}
if (filter !== undefined) {
query = { params: { search: filter } }
}
migrationApi
.getMigrations(query)
.then((response) => {
migrations.value = response.data
totalMigrations.value = migrations.value.length
})
.finally(() => (loading.value = false))
loading.value = true
let query = {}
if (filter !== undefined) {
query = { params: { search: filter } }
}
migrationApi
.getMigrations(query)
.then((response) => {
migrations.value = response.data
totalMigrations.value = migrations.value.length
})
.finally(() => (loading.value = false))
}
function getMenuItems() {
const result = []
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteMigration(),
color: 'red',
})
return result
const result = []
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteMigration(),
color: 'red',
})
return result
}
getMigrations()
@@ -1,22 +1,29 @@
<template>
<CreationForm
ref="form"
:title="$gettext('New provider')"
:steps="steps"
:form-getter="getForm"
:get-v-form-ref="getVFormRef"
:validate-object="validateProvider"
:summary-sections="summarySections"
@close="close"
@create="submit"
>
<template #[`form.general`]>
<ProviderGeneralForm ref="general" v-model="provider" class="ml-4" />
</template>
<template #[`form.associated`]>
<ProviderAssociatedForm ref="associated" v-model="provider.domains" />
</template>
</CreationForm>
<CreationForm
ref="form"
:title="$gettext('New provider')"
:steps="steps"
:form-getter="getForm"
:get-v-form-ref="getVFormRef"
:validate-object="validateProvider"
:summary-sections="summarySections"
@close="close"
@create="submit"
>
<template #[`form.general`]>
<ProviderGeneralForm
ref="general"
v-model="provider"
class="ml-4"
/>
</template>
<template #[`form.associated`]>
<ProviderAssociatedForm
ref="associated"
v-model="provider.domains"
/>
</template>
</CreationForm>
</template>
<script setup lang="js">
@@ -33,47 +40,47 @@ const busStore = useBusStore()
const providersStore = useProvidersStore()
const associatedDomainsSummary = computed(() => {
const result = []
let first = true
for (const domain of provider.value.domains) {
const value = domain.new_domain
? `${domain.name} => ${domain.new_domain.name}`
: `${domain.name} ${$gettext('(the domain will be created locally)')}`
result.push({ key: first ? $gettext('Associated domain') : '', value })
first = false
}
return result
const result = []
let first = true
for (const domain of provider.value.domains) {
const value = domain.new_domain
? `${domain.name} => ${domain.new_domain.name}`
: `${domain.name} ${$gettext('(the domain will be created locally)')}`
result.push({ key: first ? $gettext('Associated domain') : '', value })
first = false
}
return result
})
const summarySections = computed(() => {
return [
{
title: $gettext('General'),
items: [
{ key: $gettext('Name'), value: provider.value.name },
{ key: $gettext('Address'), value: provider.value.address },
{ key: $gettext('Port'), value: provider.value.port },
return [
{
key: $gettext('Secured'),
value: provider.value.secured,
type: 'yesno',
title: $gettext('General'),
items: [
{ key: $gettext('Name'), value: provider.value.name },
{ key: $gettext('Address'), value: provider.value.address },
{ key: $gettext('Port'), value: provider.value.port },
{
key: $gettext('Secured'),
value: provider.value.secured,
type: 'yesno',
},
],
},
],
},
{
title: $gettext('Associated domains'),
items: associatedDomainsSummary.value,
},
]
{
title: $gettext('Associated domains'),
items: associatedDomainsSummary.value,
},
]
})
const steps = [
{ name: 'general', title: $gettext('General') },
{ name: 'associated', title: $gettext('Associated domains') },
{ name: 'general', title: $gettext('General') },
{ name: 'associated', title: $gettext('Associated domains') },
]
const provider = ref({
domains: [],
domains: [],
})
// forms ref
@@ -85,53 +92,53 @@ const formStepsComponents = { general: general, associated: associated }
initProvider()
function initProvider() {
provider.value = {
name: '',
address: '',
port: 993,
secured: true,
domains: [],
}
provider.value = {
name: '',
address: '',
port: 993,
secured: true,
domains: [],
}
}
function close() {
initProvider()
emit('close')
initProvider()
emit('close')
}
function getForm(step) {
return formStepsComponents.value[step.name]
return formStepsComponents.value[step.name]
}
function getVFormRef(step) {
console.log(provider.value)
return formStepsComponents[step.name].value.vFormRef
console.log(provider.value)
return formStepsComponents[step.name].value.vFormRef
}
function validateProvider() {}
function submit() {
const data = { ...provider.value }
data.domains = []
for (const domain of provider.value.domains) {
if (domain.new_domain) {
data.domains.push({
name: domain.name,
new_domain: domain.new_domain.pk,
})
} else {
data.domains.push(domain)
const data = { ...provider.value }
data.domains = []
for (const domain of provider.value.domains) {
if (domain.new_domain) {
data.domains.push({
name: domain.name,
new_domain: domain.new_domain.pk,
})
} else {
data.domains.push(domain)
}
}
}
providersStore.createProvider(data).then(() => {
busStore.displayNotification({ msg: $gettext('Provider created') })
close()
})
providersStore.createProvider(data).then(() => {
busStore.displayNotification({ msg: $gettext('Provider created') })
close()
})
}
</script>
<style lang="scss">
.highligth {
background-color: #515d78;
background-color: #515d78;
}
</style>
@@ -1,65 +1,76 @@
<template>
<v-expansion-panels v-model="panel">
<v-expansion-panel>
<v-expansion-panel-title v-slot="{ open }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('General') }}
</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">
{{ $gettext('Name:') }} {{ editedProvider.name }}
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<ProviderGeneralForm ref="generalForm" v-model="editedProvider" />
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel>
<v-expansion-panel-title v-slot="{ open }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Associated domains') }}
</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 v-if="editedProvider.domains.length > 0" cols="6">
{{ $gettext('Number of associated domains:') }}
{{ editedProvider.domains.length }}
</v-col>
<v-col v-else cols="6">
{{ $gettext('No associated domains') }}
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<ProviderAssociatedForm
ref="associatedForm"
v-model="editedProvider.domains"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<div class="mt-4 d-flex justify-end">
<v-btn @click="$router.go(-1)">
{{ $gettext('Cancel') }}
</v-btn>
<v-btn class="ml-4" color="primary darken-1" @click="save">
{{ $gettext('Save') }}
</v-btn>
</div>
<v-expansion-panels v-model="panel">
<v-expansion-panel>
<v-expansion-panel-title v-slot="{ open }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('General') }}
</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">
{{ $gettext('Name:') }}
{{ editedProvider.name }}
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<ProviderGeneralForm
ref="generalForm"
v-model="editedProvider"
/>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel>
<v-expansion-panel-title v-slot="{ open }">
<v-row no-gutters>
<v-col cols="4">
{{ $gettext('Associated domains') }}
</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
v-if="editedProvider.domains.length > 0"
cols="6"
>
{{
$gettext(
'Number of associated domains:'
)
}}
{{ editedProvider.domains.length }}
</v-col>
<v-col v-else cols="6">
{{ $gettext('No associated domains') }}
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text>
<ProviderAssociatedForm
ref="associatedForm"
v-model="editedProvider.domains"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<div class="mt-4 d-flex justify-end">
<v-btn @click="$router.go(-1)">
{{ $gettext('Cancel') }}
</v-btn>
<v-btn class="ml-4" color="primary darken-1" @click="save">
{{ $gettext('Save') }}
</v-btn>
</div>
</template>
<script setup lang="js">
@@ -72,10 +83,10 @@ import ProviderGeneralForm from './form_steps/ProviderGeneralForm.vue'
import ProviderAssociatedForm from './form_steps/ProviderAssociatedForm.vue'
const props = defineProps({
provider: {
type: Object,
default: null,
},
provider: {
type: Object,
default: null,
},
})
const { $gettext } = useGettext()
@@ -84,63 +95,67 @@ const router = useRouter()
const providersStore = useProvidersStore()
const editedProvider = ref({
domains: [],
domains: [],
})
const panel = ref(0)
const generalForm = ref()
const associatedForm = ref()
watch(
() => props.provider,
(val) => {
if (val) {
editedProvider.value = { ...val }
if (!editedProvider.value.domains) {
editedProvider.value.domains = []
}
}
},
{ immediate: true }
() => props.provider,
(val) => {
if (val) {
editedProvider.value = { ...val }
if (!editedProvider.value.domains) {
editedProvider.value.domains = []
}
}
},
{ immediate: true }
)
async function save() {
if (generalForm.value !== undefined) {
const { valid } = await generalForm.value.vFormRef.validate()
if (!valid) {
return
if (generalForm.value !== undefined) {
const { valid } = await generalForm.value.vFormRef.validate()
if (!valid) {
return
}
}
}
if (associatedForm.value !== undefined) {
const { valid } = await associatedForm.value.vFormRef.validate()
if (!valid) {
return
if (associatedForm.value !== undefined) {
const { valid } = await associatedForm.value.vFormRef.validate()
if (!valid) {
return
}
}
}
try {
const data = { ...editedProvider.value }
data.domains = []
for (const domain of editedProvider.value.domains) {
if (domain.new_domain) {
data.domains.push({
name: domain.name,
new_domain: domain.new_domain.pk || domain.new_domain.id,
})
} else {
data.domains.push(domain)
}
try {
const data = { ...editedProvider.value }
data.domains = []
for (const domain of editedProvider.value.domains) {
if (domain.new_domain) {
data.domains.push({
name: domain.name,
new_domain: domain.new_domain.pk || domain.new_domain.id,
})
} else {
data.domains.push(domain)
}
}
await providers
.patchProvider(editedProvider.value.id, data)
.then(() => {
busStore.displayNotification({
msg: $gettext('Provider updated'),
})
providersStore.getProviders()
router.push({ name: 'ProvidersList' })
})
} catch (error) {
if (generalForm.value) {
generalForm.value.$refs.observer.setErrors(error.response.data)
}
if (associatedForm.value) {
associatedForm.value.$refs.observer.setErrors(error.response.data)
}
}
await providers.patchProvider(editedProvider.value.id, data).then(() => {
busStore.displayNotification({ msg: $gettext('Provider updated') })
providersStore.getProviders()
router.push({ name: 'ProvidersList' })
})
} catch (error) {
if (generalForm.value) {
generalForm.value.$refs.observer.setErrors(error.response.data)
}
if (associatedForm.value) {
associatedForm.value.$refs.observer.setErrors(error.response.data)
}
}
}
</script>
@@ -1,81 +1,90 @@
<template>
<v-card class="mt-6">
<v-data-table
v-model="selected"
v-model:expanded="expanded"
:headers="headers"
:items="Object.values(providers)"
:search="search"
:loading="!providersLoaded"
item-value="name"
single-expand
show-select
@click:row="(item, slot) => slot.expand(!slot.isExpanded)"
>
<template #top>
<v-toolbar color="white" flat>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="flat"
append-icon="mdi-chevron-down"
size="small"
>
{{ $gettext('Actions') }}
</v-btn>
<v-card class="mt-6">
<v-data-table
v-model="selected"
v-model:expanded="expanded"
:headers="headers"
:items="Object.values(providers)"
:search="search"
:loading="!providersLoaded"
item-value="name"
single-expand
show-select
@click:row="(item, slot) => slot.expand(!slot.isExpanded)"
>
<template #top>
<v-toolbar color="white" flat>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="flat"
append-icon="mdi-chevron-down"
size="small"
>
{{ $gettext('Actions') }}
</v-btn>
</template>
<v-list density="compact"> </v-list>
</v-menu>
<v-btn
variant="text"
icon="mdi-reload"
@click="reloadProviders"
></v-btn>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
:placeholder="$gettext('Search')"
single-line
variant="outlined"
hide-details
/>
</v-toolbar>
</template>
<v-list density="compact"> </v-list>
</v-menu>
<v-btn
variant="text"
icon="mdi-reload"
@click="reloadProviders"
></v-btn>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
:placeholder="$gettext('Search')"
single-line
variant="outlined"
hide-details
/>
</v-toolbar>
</template>
<template #[`item.secured`]="{ item }">
<v-icon v-if="item.secured === true" color="green"> mdi-lock </v-icon>
<v-icon v-else color="red"> mdi-lock-off </v-icon>
</template>
<template #[`item.domains`]="{ item }">
<v-chip v-if="item.domains.length > 0">
{{ item.domains.length }} associated domain(s)
</v-chip>
<v-chip v-else> No associated domain </v-chip>
</template>
<template #[`expanded-item`]="{ headers, item }">
<td :colspan="headers.length">
<v-chip v-for="(domain, index) in item.domains" :key="index">
<template v-if="domain.new_domain">
{{ domain.name }} --> {{ domain.new_domain.name }}
<template #[`item.secured`]="{ item }">
<v-icon v-if="item.secured === true" color="green">
mdi-lock
</v-icon>
<v-icon v-else color="red"> mdi-lock-off </v-icon>
</template>
<template v-else>
{{ domain.name }} --> {{ domain.name }}
<template #[`item.domains`]="{ item }">
<v-chip v-if="item.domains.length > 0">
{{ item.domains.length }} associated domain(s)
</v-chip>
<v-chip v-else> No associated domain </v-chip>
</template>
</v-chip>
</td>
</template>
<template #[`item.actions`]="{ item }">
<v-menu offset-y>
<template #activator="{ props }">
<v-btn icon="mdi-dots-horizontal" variant="text" v-bind="props" />
</template>
<MenuItems :items="getMenuItems()" :obj="item" />
</v-menu>
</template>
</v-data-table>
</v-card>
<template #[`expanded-item`]="{ headers, item }">
<td :colspan="headers.length">
<v-chip
v-for="(domain, index) in item.domains"
:key="index"
>
<template v-if="domain.new_domain">
{{ domain.name }} --> {{ domain.new_domain.name }}
</template>
<template v-else>
{{ domain.name }} --> {{ domain.name }}
</template>
</v-chip>
</td>
</template>
<template #[`item.actions`]="{ item }">
<v-menu offset-y>
<template #activator="{ props }">
<v-btn
icon="mdi-dots-horizontal"
variant="text"
v-bind="props"
/>
</template>
<MenuItems :items="getMenuItems()" :obj="item" />
</v-menu>
</template>
</v-data-table>
</v-card>
</template>
<script setup lang="js">
@@ -93,17 +102,17 @@ const providers = computed(() => providersStore.providers)
const providersLoaded = computed(() => providersStore.providersLoaded)
const headers = [
{ title: $gettext('Name'), key: 'name' },
{ title: $gettext('Address'), key: 'address' },
{ title: $gettext('Port'), key: 'port' },
{ title: $gettext('Secured'), key: 'secured' },
{ title: $gettext('Associated domains'), key: 'domains' },
{
title: $gettext('Actions'),
key: 'actions',
sortable: false,
align: 'right',
},
{ title: $gettext('Name'), key: 'name' },
{ title: $gettext('Address'), key: 'address' },
{ title: $gettext('Port'), key: 'port' },
{ title: $gettext('Secured'), key: 'secured' },
{ title: $gettext('Associated domains'), key: 'domains' },
{
title: $gettext('Actions'),
key: 'actions',
sortable: false,
align: 'right',
},
]
const selected = ref([])
@@ -111,34 +120,34 @@ const search = ref('')
const expanded = ref([])
function deleteProvider(provider) {
providersStore.deleteProvider(provider.id)
providersStore.deleteProvider(provider.id)
}
function editProvider(provider) {
router.push({ name: 'ProviderEdit', params: { id: provider.id } })
router.push({ name: 'ProviderEdit', params: { id: provider.id } })
}
function getMenuItems() {
const result = []
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteProvider,
color: 'red',
})
result.push({
label: $gettext('Edit'),
icon: 'mdi-circle-edit-outline',
onClick: editProvider,
})
return result
const result = []
result.push({
label: $gettext('Delete'),
icon: 'mdi-delete-outline',
onClick: deleteProvider,
color: 'red',
})
result.push({
label: $gettext('Edit'),
icon: 'mdi-circle-edit-outline',
onClick: editProvider,
})
return result
}
function reloadProviders() {
providersStore.getProviders()
providersStore.getProviders()
}
if (!providersLoaded.value) {
reloadProviders()
reloadProviders()
}
</script>
@@ -1,45 +1,45 @@
<template>
<v-form ref="formRef">
<v-text-field
ref="initialdomainField"
v-model="domain.name"
:placeholder="$gettext('Name of the domain to migrate')"
variant="outlined"
density="compact"
@keydown="onKeyDown"
/>
<v-autocomplete
v-model="domain.new_domain"
:label="$gettext('Local domain (optional)')"
:items="localDomains"
item-title="name"
return-object
variant="outlined"
density="compact"
clearable
@keydown="onKeyDown"
/>
</v-form>
<v-btn icon="mdi-plus" color="primary" @click="addDomain" />
<v-spacer />
<v-chip
v-for="(domain, index) in domains"
:key="index"
class="mr-2 mt-2"
closable
@click:close="removeDomain(index)"
>
<template v-if="domain.new_domain">
{{ domain.name }} --> {{ domain.new_domain.name }}
</template>
<template v-else>
{{ domain.name }}
</template>
</v-chip>
<v-form ref="formRef">
<v-text-field
ref="initialdomainField"
v-model="domain.name"
:placeholder="$gettext('Name of the domain to migrate')"
variant="outlined"
density="compact"
@keydown="onKeyDown"
/>
<v-autocomplete
v-model="domain.new_domain"
:label="$gettext('Local domain (optional)')"
:items="localDomains"
item-title="name"
return-object
variant="outlined"
density="compact"
clearable
@keydown="onKeyDown"
/>
</v-form>
<v-btn icon="mdi-plus" color="primary" @click="addDomain" />
<v-spacer />
<v-chip
v-for="(domain, index) in domains"
:key="index"
class="mr-2 mt-2"
closable
@click:close="removeDomain(index)"
>
<template v-if="domain.new_domain">
{{ domain.name }} --> {{ domain.new_domain.name }}
</template>
<template v-else>
{{ domain.name }}
</template>
</v-chip>
<v-form ref="vFormRef">
<v-input :rules="[atLeastOneAssDomain]" />
</v-form>
<v-form ref="vFormRef">
<v-input :rules="[atLeastOneAssDomain]" />
</v-form>
</template>
<script setup lang="js">
@@ -57,64 +57,64 @@ const domains = ref([])
const formRef = ref()
const vFormRef = ref()
const domain = ref({
name: '',
new_domain: '',
name: '',
new_domain: '',
})
const localDomains = ref([])
watch(
() => props.modelValue,
(value) => {
if (value) {
domains.value = [...value]
}
},
{ immediate: true }
() => props.modelValue,
(value) => {
if (value) {
domains.value = [...value]
}
},
{ immediate: true }
)
async function addDomain() {
const valid = await formRef.value.validate()
if (!valid) {
return
}
providerApi.checkAssociatedDomain(domain.value).then((response) => {
if (response.status === 200) {
domains.value.push({
name: domain.value.name,
new_domain: domain.value.new_domain,
})
emit('update:modelValue', domains.value)
domain.value = {
name: '',
new_domain: '',
}
const valid = await formRef.value.validate()
if (!valid) {
return
}
})
providerApi.checkAssociatedDomain(domain.value).then((response) => {
if (response.status === 200) {
domains.value.push({
name: domain.value.name,
new_domain: domain.value.new_domain,
})
emit('update:modelValue', domains.value)
domain.value = {
name: '',
new_domain: '',
}
}
})
}
function removeDomain(index) {
domains.value.splice(index, 1)
emit('update:modelValue', domains.value)
domains.value.splice(index, 1)
emit('update:modelValue', domains.value)
}
function onKeyDown(e) {
const keyCode = e.keyCode
if (keyCode === 13 || keyCode === 9) {
// on enter or tab
addDomain()
e.preventDefault()
}
const keyCode = e.keyCode
if (keyCode === 13 || keyCode === 9) {
// on enter or tab
addDomain()
e.preventDefault()
}
}
domainsApi.getDomains().then((resp) => {
localDomains.value = resp.data
localDomains.value = resp.data
})
function atLeastOneAssDomain() {
if (domains.value.length === 0) {
return $gettext('You need to bind at least one domain to the provider.')
}
return true
if (domains.value.length === 0) {
return $gettext('You need to bind at least one domain to the provider.')
}
return true
}
defineExpose({ vFormRef })
@@ -1,51 +1,58 @@
<template>
<v-form ref="vFormRef">
<v-text-field
v-model="provider.name"
:label="$gettext('Provider name')"
variant="outlined"
:rules="[rules.required]"
/>
<v-text-field
v-model="provider.address"
:label="$gettext('Address (hostname or IP)')"
:rules="[rules.required]"
variant="outlined"
/>
<v-text-field
v-model="provider.port"
:label="$gettext('IMAP Port')"
type="number"
variant="outlined"
:rules="[rules.required, rules.portNumber]"
/>
<v-switch
v-model="provider.secured"
:label="$gettext('Secured')"
color="primary"
:hint="
$gettext('Is the IMAP connection secured using SSL/TLS or StartTLS')
"
persistent-hint
/>
<div class="my-7">
<v-btn :loading="loading" color="primary" @click="checkConnection">
{{ $gettext('Check connection:') }}
<v-icon v-if="connectionStatus === 0" color="blue-grey-darken-2">
mdi-help-circle-outline
</v-icon>
<v-icon v-else-if="connectionStatus === 1" color="success">
mdi-check-all
</v-icon>
<v-icon v-else color="warning"> mdi-alert-circle-outline </v-icon>
<template #loader>
<span class="custom-loader">
<v-icon light>mdi-cached</v-icon>
</span>
</template>
</v-btn>
</div>
</v-form>
<v-form ref="vFormRef">
<v-text-field
v-model="provider.name"
:label="$gettext('Provider name')"
variant="outlined"
:rules="[rules.required]"
/>
<v-text-field
v-model="provider.address"
:label="$gettext('Address (hostname or IP)')"
:rules="[rules.required]"
variant="outlined"
/>
<v-text-field
v-model="provider.port"
:label="$gettext('IMAP Port')"
type="number"
variant="outlined"
:rules="[rules.required, rules.portNumber]"
/>
<v-switch
v-model="provider.secured"
:label="$gettext('Secured')"
color="primary"
:hint="
$gettext(
'Is the IMAP connection secured using SSL/TLS or StartTLS'
)
"
persistent-hint
/>
<div class="my-7">
<v-btn :loading="loading" color="primary" @click="checkConnection">
{{ $gettext('Check connection:') }}
<v-icon
v-if="connectionStatus === 0"
color="blue-grey-darken-2"
>
mdi-help-circle-outline
</v-icon>
<v-icon v-else-if="connectionStatus === 1" color="success">
mdi-check-all
</v-icon>
<v-icon v-else color="warning">
mdi-alert-circle-outline
</v-icon>
<template #loader>
<span class="custom-loader">
<v-icon light>mdi-cached</v-icon>
</span>
</template>
</v-btn>
</div>
</v-form>
</template>
<script setup lang="js">
@@ -63,25 +70,25 @@ const loading = ref(false)
const vFormRef = ref()
async function checkConnection() {
const valid = await vFormRef.value.validate()
if (!valid) {
return
}
loading.value = true
const params = {
address: provider.value.address,
port: provider.value.port,
secured: provider.value.secured,
}
ProviderApi.checkProvider(params)
.then(() => {
connectionStatus.value = 1
})
.catch((error) => {
console.log(error)
connectionStatus.value = 2
})
.finally(() => (loading.value = false))
const valid = await vFormRef.value.validate()
if (!valid) {
return
}
loading.value = true
const params = {
address: provider.value.address,
port: provider.value.port,
secured: provider.value.secured,
}
ProviderApi.checkProvider(params)
.then(() => {
connectionStatus.value = 1
})
.catch((error) => {
console.log(error)
connectionStatus.value = 2
})
.finally(() => (loading.value = false))
}
defineExpose({ vFormRef })
@@ -89,39 +96,39 @@ defineExpose({ vFormRef })
<style>
.custom-loader {
animation: loader 1s infinite;
display: flex;
animation: loader 1s infinite;
display: flex;
}
@-moz-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-webkit-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-o-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
</style>
@@ -1,36 +1,36 @@
<template>
<v-card class="mt-6">
<v-data-table-server
:headers="headers"
:items="logs"
:search="search"
class="elevation-0"
:items-length="total"
@update:options="fetchLogs"
>
<template #top>
<v-toolbar flat color="white">
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
:placeholder="$gettext('Search')"
variant="outlined"
hide-details
/>
</v-toolbar>
</template>
<template #[`item.level`]="{ item }">
<v-chip
:color="getLevelColor(item.level)"
text-color="white"
size="small"
<v-card class="mt-6">
<v-data-table-server
:headers="headers"
:items="logs"
:search="search"
class="elevation-0"
:items-length="total"
@update:options="fetchLogs"
>
{{ item.level }}
</v-chip>
</template>
</v-data-table-server>
</v-card>
<template #top>
<v-toolbar flat color="white">
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
:placeholder="$gettext('Search')"
variant="outlined"
hide-details
/>
</v-toolbar>
</template>
<template #[`item.level`]="{ item }">
<v-chip
:color="getLevelColor(item.level)"
text-color="white"
size="small"
>
{{ item.level }}
</v-chip>
</template>
</v-data-table-server>
</v-card>
</template>
<script setup lang="js">
@@ -46,42 +46,42 @@ const search = ref('')
const total = ref(0)
const headers = [
{ title: $gettext('Date'), key: 'date_created' },
{ title: $gettext('Level'), key: 'level' },
{ title: $gettext('Logger'), key: 'logger' },
{ title: $gettext('Message'), key: 'message' },
{ title: $gettext('Date'), key: 'date_created' },
{ title: $gettext('Level'), key: 'level' },
{ title: $gettext('Logger'), key: 'logger' },
{ title: $gettext('Message'), key: 'message' },
]
function getLevelColor(level) {
if (level === 'INFO') {
return 'primary'
}
if (level === 'WARNING') {
return 'orange'
}
if (level === 'CRITICAL' || level === 'ERROR') {
return 'red'
}
return ''
if (level === 'INFO') {
return 'primary'
}
if (level === 'WARNING') {
return 'orange'
}
if (level === 'CRITICAL' || level === 'ERROR') {
return 'red'
}
return ''
}
function fetchLogs({ page, itemsPerPage, sortBy, search }) {
let order
if (sortBy.length) {
order = (sortBy[0].order === 'desc' ? '-' : '') + sortBy[0].key
}
const params = {
ordering: order,
page: page,
page_size: itemsPerPage,
search: search,
}
loading.value = true
logsApi.getAuditTrail(params).then((response) => {
logs.value = response.data.results
total.value = response.data.count
loading.value = false
})
let order
if (sortBy.length) {
order = (sortBy[0].order === 'desc' ? '-' : '') + sortBy[0].key
}
const params = {
ordering: order,
page: page,
page_size: itemsPerPage,
search: search,
}
loading.value = true
logsApi.getAuditTrail(params).then((response) => {
logs.value = response.data.results
total.value = response.data.count
loading.value = false
})
}
fetchLogs = debounce(fetchLogs, 500)
@@ -1,33 +1,33 @@
<template>
<v-card class="mt-6">
<v-data-table-server
:headers="headers"
:items="messages"
:search="search"
class="elevation-0"
:items-length="total"
@update:options="fetchMessages"
>
<template #top>
<v-toolbar flat color="white">
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
:placeholder="$gettext('Search')"
variant="outlined"
hide-details
/>
</v-toolbar>
</template>
<template #[`item.date`]="{ item }">
{{ $date(item.date) }}
</template>
<template #[`item.sender`]="{ item }">
{{ $truncate(item.sender, 50) }}
</template>
</v-data-table-server>
</v-card>
<v-card class="mt-6">
<v-data-table-server
:headers="headers"
:items="messages"
:search="search"
class="elevation-0"
:items-length="total"
@update:options="fetchMessages"
>
<template #top>
<v-toolbar flat color="white">
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
:placeholder="$gettext('Search')"
variant="outlined"
hide-details
/>
</v-toolbar>
</template>
<template #[`item.date`]="{ item }">
{{ $date(item.date) }}
</template>
<template #[`item.sender`]="{ item }">
{{ $truncate(item.sender, 50) }}
</template>
</v-data-table-server>
</v-card>
</template>
<script setup lang="js">
@@ -39,11 +39,11 @@ import debounce from 'debounce'
const { $gettext } = useGettext()
const headers = [
{ title: $gettext('Queue ID'), key: 'queue_id' },
{ title: $gettext('Date'), key: 'date', width: '20%' },
{ title: $gettext('Status'), key: 'status' },
{ title: $gettext('To'), key: 'rcpt' },
{ title: $gettext('From'), key: 'sender' },
{ title: $gettext('Queue ID'), key: 'queue_id' },
{ title: $gettext('Date'), key: 'date', width: '20%' },
{ title: $gettext('Status'), key: 'status' },
{ title: $gettext('To'), key: 'rcpt' },
{ title: $gettext('From'), key: 'sender' },
]
const loading = ref(true)
@@ -52,22 +52,22 @@ const search = ref('')
const total = ref(0)
function fetchMessages({ page, itemsPerPage, sortBy, search }) {
let order
if (sortBy.length) {
order = (sortBy[0].order === 'desc' ? '-' : '') + sortBy[0].key
}
const params = {
ordering: order,
page: page,
page_size: itemsPerPage,
search: search,
}
loading.value = true
logsApi.getMessages(params).then((response) => {
messages.value = response.data.results
total.value = response.data.count
loading.value = false
})
let order
if (sortBy.length) {
order = (sortBy[0].order === 'desc' ? '-' : '') + sortBy[0].key
}
const params = {
ordering: order,
page: page,
page_size: itemsPerPage,
search: search,
}
loading.value = true
logsApi.getMessages(params).then((response) => {
messages.value = response.data.results
total.value = response.data.count
loading.value = false
})
}
fetchMessages = debounce(fetchMessages, 500)
+82 -73
View File
@@ -19,83 +19,92 @@ const router = useRouter()
const { $gettext } = useGettext()
async function authenticate() {
const { valid } = await loginForm.value.validate()
if (!valid) {
return
}
loading.value = true
const { valid } = await loginForm.value.validate()
if (!valid) {
return
}
loading.value = true
const payload = {
username: username.value,
password: password.value,
rememberMe: rememberMe.value,
}
authStore
.login(payload)
.then(() => {
router.push({ name: 'Dashboard' })
})
.catch((err) => {
loading.value = false
if (err.response.status === 401) {
errors.value = [$gettext('Invalid username and/or password')]
} else if (err.response.status === 429) {
errors.value = [
$gettext('Too many unsuccessful attempts, please try later.'),
]
}
})
const payload = {
username: username.value,
password: password.value,
rememberMe: rememberMe.value,
}
authStore
.login(payload)
.then(() => {
router.push({ name: 'Dashboard' })
})
.catch((err) => {
loading.value = false
if (err.response.status === 401) {
errors.value = [$gettext('Invalid username and/or password')]
} else if (err.response.status === 429) {
errors.value = [
$gettext(
'Too many unsuccessful attempts, please try later.'
),
]
}
})
}
</script>
<template>
<v-container class="fill-height" fluid>
<v-row align="center" justify="center">
<v-col cols="12" sm="10" md="8" lg="8" class="text-center">
<h4 class="text-primary mb-10 text-h4">
{{ $gettext('Identification') }}
</h4>
<v-form ref="loginForm" @submit.prevent="authenticate">
<v-text-field
v-model="username"
variant="outlined"
:label="$gettext('Username')"
prepend-inner-icon="mdi-account-outline"
:rules="[rules.required]"
/>
<v-text-field
v-model="password"
:type="isPasswordvisible ? 'text' : 'password'"
prepend-inner-icon="mdi-lock-outline"
:label="$gettext('Password')"
variant="outlined"
:append-inner-icon="isPasswordvisible ? 'mdi-eye-off' : 'mdi-eye'"
:rules="[rules.required, rules.minLength(6)]"
:error-messages="errors"
@click:append-inner="isPasswordvisible = !isPasswordvisible"
/>
<v-checkbox v-model="rememberMe" :label="$gettext('Remember me')" />
<v-container class="fill-height" fluid>
<v-row align="center" justify="center">
<v-col cols="12" sm="10" md="8" lg="8" class="text-center">
<h4 class="text-primary mb-10 text-h4">
{{ $gettext('Identification') }}
</h4>
<v-form ref="loginForm" @submit.prevent="authenticate">
<v-text-field
v-model="username"
variant="outlined"
:label="$gettext('Username')"
prepend-inner-icon="mdi-account-outline"
:rules="[rules.required]"
/>
<v-text-field
v-model="password"
:type="isPasswordvisible ? 'text' : 'password'"
prepend-inner-icon="mdi-lock-outline"
:label="$gettext('Password')"
variant="outlined"
:append-inner-icon="
isPasswordvisible ? 'mdi-eye-off' : 'mdi-eye'
"
:rules="[rules.required, rules.minLength(6)]"
:error-messages="errors"
@click:append-inner="
isPasswordvisible = !isPasswordvisible
"
/>
<v-checkbox
v-model="rememberMe"
:label="$gettext('Remember me')"
/>
<v-btn
block
color="primary"
size="large"
type="submit"
:loading="loading"
class="mb-5"
>
{{ $gettext('Connect') }}
</v-btn>
</v-form>
<v-btn
block
variant="outlined"
size="large"
:to="{ name: 'PasswordRecovery' }"
>
{{ $gettext('Forgot password') }}
</v-btn>
</v-col>
</v-row>
</v-container>
<v-btn
block
color="primary"
size="large"
type="submit"
:loading="loading"
class="mb-5"
>
{{ $gettext('Connect') }}
</v-btn>
</v-form>
<v-btn
block
variant="outlined"
size="large"
:to="{ name: 'PasswordRecovery' }"
>
{{ $gettext('Forgot password') }}
</v-btn>
</v-col>
</v-row>
</v-container>
</template>
@@ -1,53 +1,60 @@
<template>
<v-container class="fill-height" fluid>
<v-row align="center" justify="center">
<v-col
cols="12"
sm="6"
class="d-flex flex-column justify-center pa-10"
@keyup.enter="changePassword"
>
<h4 class="text-primary mb-6 text-h4">
{{ $gettext('Forgot password?') }}
</h4>
<v-form ref="observer">
<p id="password_validation">{{ password_validation_error }}</p>
<label class="m-label">{{ $gettext('Change password') }}</label>
<v-text-field
v-model="password"
type="password"
variant="outlined"
:rules="[rules.required]"
/>
<v-text-field
v-model="password_confirmed"
type="password"
variant="outlined"
:rules="[
rules.required,
(value) => rules.samePassword(value, password),
]"
/>
</v-form>
<div class="d-flex justify-center">
<v-btn
class="flex-grow-1"
color="primary"
size="large"
:loading="loading"
@click="changePassword"
>
{{ $gettext('Submit') }}
</v-btn>
</div>
<div>
<router-link :to="{ name: 'Login' }" class="float-right text-primary">
{{ $gettext('Return to login') }}
</router-link>
</div>
</v-col>
</v-row>
</v-container>
<v-container class="fill-height" fluid>
<v-row align="center" justify="center">
<v-col
cols="12"
sm="6"
class="d-flex flex-column justify-center pa-10"
@keyup.enter="changePassword"
>
<h4 class="text-primary mb-6 text-h4">
{{ $gettext('Forgot password?') }}
</h4>
<v-form ref="observer">
<p id="password_validation">
{{ password_validation_error }}
</p>
<label class="m-label">{{
$gettext('Change password')
}}</label>
<v-text-field
v-model="password"
type="password"
variant="outlined"
:rules="[rules.required]"
/>
<v-text-field
v-model="password_confirmed"
type="password"
variant="outlined"
:rules="[
rules.required,
(value) => rules.samePassword(value, password),
]"
/>
</v-form>
<div class="d-flex justify-center">
<v-btn
class="flex-grow-1"
color="primary"
size="large"
:loading="loading"
@click="changePassword"
>
{{ $gettext('Submit') }}
</v-btn>
</div>
<div>
<router-link
:to="{ name: 'Login' }"
class="float-right text-primary"
>
{{ $gettext('Return to login') }}
</router-link>
</div>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="js">
@@ -71,62 +78,63 @@ let token
let id
function checkUrl() {
if (route.params.token === undefined) {
if (token === '' || id === '') {
router.push({ name: 'PasswordRecovery' })
}
} else {
const decodedId = atob(route.params.id)
if (!/^\d+$/.test(decodedId)) {
router.push({ name: 'PasswordRecovery' })
if (route.params.token === undefined) {
if (token === '' || id === '') {
router.push({ name: 'PasswordRecovery' })
}
} else {
id = route.params.id
token = route.params.token
router.push({ name: 'PasswordRecoveryChangeForm' })
const decodedId = atob(route.params.id)
if (!/^\d+$/.test(decodedId)) {
router.push({ name: 'PasswordRecovery' })
} else {
id = route.params.id
token = route.params.token
router.push({ name: 'PasswordRecoveryChangeForm' })
}
}
}
}
async function changePassword() {
const { valid } = await observer.value.validate()
if (!valid) {
return
}
const payload = {
new_password1: password.value,
new_password2: password_confirmed.value,
token: token,
id: id,
}
loading.value = true
password_validation_error.value = ''
auth
.changePassword(payload)
.then((resp) => {
if (resp.status === 200) {
loading.value = false
router.push({ name: 'Login' })
}
})
.catch((err) => {
let message = ''
loading.value = false
if (err.response.status === 403) {
message = $gettext('Invalid reset token.')
} else if (err.response.status === 404) {
message = $gettext('User unknown.')
} else if (
err.response.status === 400 &&
err.response.data.type === 'password_requirement'
) {
err.response.data.errors.forEach((element) => {
message += $gettext(element) + '<br>'
const { valid } = await observer.value.validate()
if (!valid) {
return
}
const payload = {
new_password1: password.value,
new_password2: password_confirmed.value,
token: token,
id: id,
}
loading.value = true
password_validation_error.value = ''
auth.changePassword(payload)
.then((resp) => {
if (resp.status === 200) {
loading.value = false
router.push({ name: 'Login' })
}
})
.catch((err) => {
let message = ''
loading.value = false
if (err.response.status === 403) {
message = $gettext('Invalid reset token.')
} else if (err.response.status === 404) {
message = $gettext('User unknown.')
} else if (
err.response.status === 400 &&
err.response.data.type === 'password_requirement'
) {
err.response.data.errors.forEach((element) => {
message += $gettext(element) + '<br>'
})
} else if (err.response.status === 429) {
message = $gettext(
'Too many unsuccessful attempts, please try later.'
)
}
password_validation_error.value = message
})
} else if (err.response.status === 429) {
message = $gettext('Too many unsuccessful attempts, please try later.')
}
password_validation_error.value = message
})
}
checkUrl()
@@ -134,6 +142,6 @@ checkUrl()
<style>
#password_validation {
color: crimson;
color: crimson;
}
</style>
@@ -1,47 +1,49 @@
<template>
<v-container class="fill-height" fluid>
<v-row align="center" justify="center">
<v-col
cols="12"
sm="10"
md="8"
class="text-center"
@keyup.enter="recoverPassword"
>
<h4 class="text-primary mb-10 text-h4">
{{ $gettext('Forgot password?') }}
</h4>
<v-form ref="observer" class="my-4">
<label class="m-label">{{
$gettext('Please fill-in your primary email address')
}}</label>
<v-text-field
v-model="email"
variant="outlined"
:rules="[rules.required, rules.email]"
:error-messages="errors"
/>
</v-form>
<div class="d-flex justify-center">
<v-btn
class="flex-grow-1"
color="primary"
size="large"
:loading="loading"
@click="recoverPassword"
>
{{ $gettext('Submit') }}
</v-btn>
</div>
<div>
<a class="float-right text-primary" @click="returnToLogin">{{
$gettext('Return to login')
}}</a>
</div>
</v-col>
<ConfirmDialog ref="dialog" />
</v-row>
</v-container>
<v-container class="fill-height" fluid>
<v-row align="center" justify="center">
<v-col
cols="12"
sm="10"
md="8"
class="text-center"
@keyup.enter="recoverPassword"
>
<h4 class="text-primary mb-10 text-h4">
{{ $gettext('Forgot password?') }}
</h4>
<v-form ref="observer" class="my-4">
<label class="m-label">{{
$gettext('Please fill-in your primary email address')
}}</label>
<v-text-field
v-model="email"
variant="outlined"
:rules="[rules.required, rules.email]"
:error-messages="errors"
/>
</v-form>
<div class="d-flex justify-center">
<v-btn
class="flex-grow-1"
color="primary"
size="large"
:loading="loading"
@click="recoverPassword"
>
{{ $gettext('Submit') }}
</v-btn>
</div>
<div>
<a
class="float-right text-primary"
@click="returnToLogin"
>{{ $gettext('Return to login') }}</a
>
</div>
</v-col>
<ConfirmDialog ref="dialog" />
</v-row>
</v-container>
</template>
<script setup lang="js">
@@ -63,68 +65,69 @@ const observer = ref(null)
const errors = ref([])
function returnToLogin() {
router.push({ name: 'Login' })
router.push({ name: 'Login' })
}
async function showDialog(title, body, error = false) {
let color = 'info'
if (error) {
color = 'error'
}
const confirm = await dialog.value.open(title, body, {
color: color,
cancelLabel: $gettext('Ok'),
agreeLabel: $gettext('Return to login'),
})
if (!confirm) {
return
}
returnToLogin()
let color = 'info'
if (error) {
color = 'error'
}
const confirm = await dialog.value.open(title, body, {
color: color,
cancelLabel: $gettext('Ok'),
agreeLabel: $gettext('Return to login'),
})
if (!confirm) {
return
}
returnToLogin()
}
async function recoverPassword() {
const { valid } = await observer.value.validate()
if (!valid) {
return
}
const payload = {
email: email.value,
}
loading.value = true
auth
.recoverPassword(payload)
.then(async (resp) => {
loading.value = false
if (resp.status === 200) {
if (resp.data.type === 'sms') {
router.push({ name: 'PasswordRecoverySmsTotpForm' })
} else if (resp.data.type === 'email') {
await showDialog(
'Info',
$gettext('Email sent, please check your inbox')
)
}
}
})
.catch((err) => {
loading.value = false
if (err.response.status === 404) {
if (
err.response.data.type === 'sms' ||
err.response.data.type === 'email'
) {
errors.value = [$gettext(err.response.data.reason)]
}
} else if (
err.response.status === 503 &&
err.response.data.type === 'email'
) {
showDialog('Error', err.response.data.reason, true)
} else if (err.response.status === 429) {
errors.value = [
$gettext('Too many unsuccessful attempts, please try later.'),
]
}
})
const { valid } = await observer.value.validate()
if (!valid) {
return
}
const payload = {
email: email.value,
}
loading.value = true
auth.recoverPassword(payload)
.then(async (resp) => {
loading.value = false
if (resp.status === 200) {
if (resp.data.type === 'sms') {
router.push({ name: 'PasswordRecoverySmsTotpForm' })
} else if (resp.data.type === 'email') {
await showDialog(
'Info',
$gettext('Email sent, please check your inbox')
)
}
}
})
.catch((err) => {
loading.value = false
if (err.response.status === 404) {
if (
err.response.data.type === 'sms' ||
err.response.data.type === 'email'
) {
errors.value = [$gettext(err.response.data.reason)]
}
} else if (
err.response.status === 503 &&
err.response.data.type === 'email'
) {
showDialog('Error', err.response.data.reason, true)
} else if (err.response.status === 429) {
errors.value = [
$gettext(
'Too many unsuccessful attempts, please try later.'
),
]
}
})
}
</script>
@@ -1,52 +1,59 @@
<template>
<v-container class="fill-height" fluid>
<v-row align="center" justify="center">
<v-col
cols="12"
sm="6"
class="d-flex flex-column justify-center pa-10"
@keyup.enter="changePassword"
>
<ConfirmDialog ref="confirm" />
<h4 class="text-primary mb-6 text-h4">
{{ $gettext('Password recovery') }}
</h4>
<v-form ref="observer">
<label class="m-label"
>{{ $gettext("Enter the code you've just received by SMS") }}
</label>
<v-text-field
v-model="sms_totp"
type="totp"
variant="outlined"
:rules="[rules.required, (value) => rules.length(value, 6)]"
:error-messages="errors"
/>
</v-form>
<div class="d-flex justify-center">
<v-btn
class="flex-grow-1"
color="primary"
size="large"
:loading="loading"
@click="checkSmsTotp"
>
{{ $gettext('Submit') }}
</v-btn>
</div>
<div>
<a class="float-left text-primary" @click="resendSms">{{
$gettext('Resend SMS.')
}}</a>
<router-link
:to="{ name: 'Login' }"
class="float-right text-primary"
>{{ $gettext('Return to login') }}</router-link
>
</div>
</v-col>
</v-row>
</v-container>
<v-container class="fill-height" fluid>
<v-row align="center" justify="center">
<v-col
cols="12"
sm="6"
class="d-flex flex-column justify-center pa-10"
@keyup.enter="changePassword"
>
<ConfirmDialog ref="confirm" />
<h4 class="text-primary mb-6 text-h4">
{{ $gettext('Password recovery') }}
</h4>
<v-form ref="observer">
<label class="m-label"
>{{
$gettext(
"Enter the code you've just received by SMS"
)
}}
</label>
<v-text-field
v-model="sms_totp"
type="totp"
variant="outlined"
:rules="[
rules.required,
(value) => rules.length(value, 6),
]"
:error-messages="errors"
/>
</v-form>
<div class="d-flex justify-center">
<v-btn
class="flex-grow-1"
color="primary"
size="large"
:loading="loading"
@click="checkSmsTotp"
>
{{ $gettext('Submit') }}
</v-btn>
</div>
<div>
<a class="float-left text-primary" @click="resendSms">{{
$gettext('Resend SMS.')
}}</a>
<router-link
:to="{ name: 'Login' }"
class="float-right text-primary"
>{{ $gettext('Return to login') }}</router-link
>
</div>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="js">
@@ -67,74 +74,77 @@ const sms_totp = ref('')
const errors = ref([])
async function showErrorDialog(body) {
const confirm = await confirm.value.open($gettext('Error'), body, {
color: 'error',
cancelLabel: $gettext('Return to login'),
agreeLabel: $gettext('Retry reset'),
})
if (!confirm) {
router.push({ name: 'Login' })
} else {
router.push({ name: 'PasswordRecoveryForm' })
}
const confirm = await confirm.value.open($gettext('Error'), body, {
color: 'error',
cancelLabel: $gettext('Return to login'),
agreeLabel: $gettext('Retry reset'),
})
if (!confirm) {
router.push({ name: 'Login' })
} else {
router.push({ name: 'PasswordRecoveryForm' })
}
}
async function resendSms() {
auth
.checkSmsTotp({ type: 'resend' })
.then((resp) => {
loading.value = false
if (resp.status === 200) {
observer.value.setErrors({
sms_totp: this.$gettext('TOTP resent.'),
auth.checkSmsTotp({ type: 'resend' })
.then((resp) => {
loading.value = false
if (resp.status === 200) {
observer.value.setErrors({
sms_totp: this.$gettext('TOTP resent.'),
})
}
})
.catch(async (err) => {
if (err.response.status === 400) {
this.loading = false
await showErrorDialog(
$gettext(
'User seems wrong, return to login or restart reset the process?'
)
)
} else if (err.response.status === 429) {
errors.value = [
$gettext(
'Too many unsuccessful attempts, please try later.'
),
]
}
})
}
})
.catch(async (err) => {
if (err.response.status === 400) {
this.loading = false
await showErrorDialog(
$gettext(
'User seems wrong, return to login or restart reset the process?'
)
)
} else if (err.response.status === 429) {
errors.value = [
$gettext('Too many unsuccessful attempts, please try later.'),
]
}
})
}
async function checkSmsTotp() {
const { valid } = await observer.value.validate()
if (!valid) {
return
}
const payload = {
type: 'confirm',
sms_totp: this.sms_totp,
}
loading.value = true
auth
.checkSmsTotp(payload)
.then((resp) => {
loading.value = false
if (resp.status === 200) {
// Code is good, redirecting to change password form.
router.push({
name: 'PasswordRecoveryChangeForm',
data: resp.response.data,
const { valid } = await observer.value.validate()
if (!valid) {
return
}
const payload = {
type: 'confirm',
sms_totp: this.sms_totp,
}
loading.value = true
auth.checkSmsTotp(payload)
.then((resp) => {
loading.value = false
if (resp.status === 200) {
// Code is good, redirecting to change password form.
router.push({
name: 'PasswordRecoveryChangeForm',
data: resp.response.data,
})
}
})
.catch(async (err) => {
loading.value = false
if (err.response.status === 403 && 'reason' in err.response.data) {
await showErrorDialog(err.response.data.reason)
} else if (
err.response.status === 400 &&
'reason' in err.response.data
) {
errors.value = [$gettext(err.response.data.reason)]
}
})
}
})
.catch(async (err) => {
loading.value = false
if (err.response.status === 403 && 'reason' in err.response.data) {
await showErrorDialog(err.response.data.reason)
} else if (err.response.status === 400 && 'reason' in err.response.data) {
errors.value = [$gettext(err.response.data.reason)]
}
})
}
</script>
+55 -53
View File
@@ -1,39 +1,39 @@
<template>
<v-container class="fill-height" fuild>
<v-row align="center" justify="center" dense>
<v-col cols="12" sm="10" md="8" lg="8" class="text-center">
<h4 class="text-primary mb-10 text-h4">
{{ $gettext('Two-factor authentication') }}
</h4>
<v-form ref="loginForm" @submit.prevent="verifyCode">
<div class="text-body-2">
{{
$gettext(
'Enter the code from the two-factor app on your mobile device. If you have lost your device, you may enter one of your recovery codes.'
)
}}
</div>
<v-otp-input
ref="otpInput"
v-model="code"
length="6"
:rules="[rules.required]"
@finish="verifyCode"
/>
<v-btn
block
color="primary"
size="large"
type="submit"
:loading="loading"
class="mb-5"
>
{{ $gettext('Verify code') }}
</v-btn>
</v-form>
</v-col>
</v-row>
</v-container>
<v-container class="fill-height" fuild>
<v-row align="center" justify="center" dense>
<v-col cols="12" sm="10" md="8" lg="8" class="text-center">
<h4 class="text-primary mb-10 text-h4">
{{ $gettext('Two-factor authentication') }}
</h4>
<v-form ref="loginForm" @submit.prevent="verifyCode">
<div class="text-body-2">
{{
$gettext(
'Enter the code from the two-factor app on your mobile device. If you have lost your device, you may enter one of your recovery codes.'
)
}}
</div>
<v-otp-input
ref="otpInput"
v-model="code"
length="6"
:rules="[rules.required]"
@finish="verifyCode"
/>
<v-btn
block
color="primary"
size="large"
type="submit"
:loading="loading"
class="mb-5"
>
{{ $gettext('Verify code') }}
</v-btn>
</v-form>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="js">
@@ -54,23 +54,25 @@ const code = ref('')
const loginForm = ref()
async function verifyCode() {
const { valid } = await loginForm.value.validate()
if (!valid) {
return
}
loading.value = true
account
.verifyTFACode(code.value)
.then((resp) => {
Cookies.set('token', resp.data.access, { sameSite: 'strict' })
Cookies.set('refreshToken', resp.data.refresh, { sameSite: 'strict' })
authStore.initialize().then(() => {
// bus.$emit('loggedIn')
router.push({ name: 'DomainList' })
})
})
.catch(() => {
loading.value = false
})
const { valid } = await loginForm.value.validate()
if (!valid) {
return
}
loading.value = true
account
.verifyTFACode(code.value)
.then((resp) => {
Cookies.set('token', resp.data.access, { sameSite: 'strict' })
Cookies.set('refreshToken', resp.data.refresh, {
sameSite: 'strict',
})
authStore.initialize().then(() => {
// bus.$emit('loggedIn')
router.push({ name: 'DomainList' })
})
})
.catch(() => {
loading.value = false
})
}
</script>
+78 -78
View File
@@ -1,112 +1,112 @@
<template>
<div
class="text-subtitle-1 text-grey-darken-1 mb-5"
:class="{ 'label--disabled': disabled }"
>
<label class="m-label">{{ label }}</label>
</div>
<div
v-for="(lineChoices, index1) in formatedChoices"
:key="index1"
class="d-flex"
>
<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)"
class="text-subtitle-1 text-grey-darken-1 mb-5"
:class="{ 'label--disabled': disabled }"
>
<v-icon
v-if="choice.icon"
class="d-block mb-2 mx-auto"
:color="iconColor(choice.value)"
size="x-large"
:icon="choice.icon"
/>
<span class="text-grey-darken-1">{{ choice.label }}</span>
<label class="m-label">{{ label }}</label>
</div>
<div
v-for="(lineChoices, index1) in formatedChoices"
:key="index1"
class="d-flex"
>
<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)"
>
<v-icon
v-if="choice.icon"
class="d-block mb-2 mx-auto"
:color="iconColor(choice.value)"
size="x-large"
:icon="choice.icon"
/>
<span class="text-grey-darken-1">{{ choice.label }}</span>
</div>
</div>
</div>
</template>
<script setup lang="js">
import { ref, onMounted, computed } from "vue";
import { ref, onMounted, computed } from 'vue'
const props = defineProps({
modelValue: { type: [Number, String], default: null },
label: { type: String, default: "" },
choices: { type: Array, default: () => [] },
disabled: {
type: Boolean,
default: false,
},
choicesPerLine: { type: Number, default: undefined },
});
modelValue: { type: [Number, String], default: null },
label: { type: String, default: '' },
choices: { type: Array, default: () => [] },
disabled: {
type: Boolean,
default: false,
},
choicesPerLine: { type: Number, default: undefined },
})
const emit = defineEmits(["update:modelValue"]);
const emit = defineEmits(['update:modelValue'])
const currentChoice = computed({
get() {
return props.modelValue;
},
set(value) {
emit("update:modelValue", value);
},
});
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
},
})
const formatedChoices = ref([]);
const formatedChoices = ref([])
function formatChoices() {
if (props.choicesPerLine) {
let sliceIndex = 0;
while (sliceIndex < props.choices.length) {
const result = props.choices.slice(
sliceIndex,
sliceIndex + props.choicesPerLine,
);
formatedChoices.value.push(result);
sliceIndex += props.choicesPerLine;
if (props.choicesPerLine) {
let sliceIndex = 0
while (sliceIndex < props.choices.length) {
const result = props.choices.slice(
sliceIndex,
sliceIndex + props.choicesPerLine
)
formatedChoices.value.push(result)
sliceIndex += props.choicesPerLine
}
} else {
formatedChoices.value.push(props.choices)
}
} else {
formatedChoices.value.push(props.choices);
}
}
function iconColor(value) {
return !props.disabled && value === currentChoice.value ? "primary" : "";
return !props.disabled && value === currentChoice.value ? 'primary' : ''
}
function selectChoice(value) {
if (props.disabled) {
return;
}
currentChoice.value = value;
if (props.disabled) {
return
}
currentChoice.value = value
}
onMounted(() => {
formatChoices();
});
formatChoices()
})
</script>
<style lang="scss" scoped>
.choice {
flex-basis: 200px;
background-color: #f2f5f7;
border: 1px solid #dbdddf;
cursor: pointer;
flex-basis: 200px;
background-color: #f2f5f7;
border: 1px solid #dbdddf;
cursor: pointer;
&--selected {
border-color: #046bf8 !important;
}
&--selected {
border-color: #046bf8 !important;
}
&--disabled {
cursor: unset;
opacity: 0.5;
}
&--disabled {
cursor: unset;
opacity: 0.5;
}
}
.label--disabled {
opacity: 0.5;
opacity: 0.5;
}
</style>
+45 -45
View File
@@ -1,31 +1,31 @@
<template>
<v-dialog
v-model="dialog"
:max-width="options.width"
:z-index="options.zIndex"
@keydown.esc="cancel"
>
<v-card>
<v-toolbar :color="options.color" density="compact" flat>
<v-toolbar-title class="text-body-2 font-weight-bold">
{{ title }}
</v-toolbar-title>
</v-toolbar>
<v-card-text v-show="!!message" class="pa-4">
{{ message }}
<slot></slot>
</v-card-text>
<v-card-actions class="pt-3">
<v-spacer></v-spacer>
<v-btn v-if="!options.noconfirm" @click="cancel">
{{ options.cancelLabel }}
</v-btn>
<v-btn :color="options.color" @click="agree">
{{ options.agreeLabel }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="dialog"
:max-width="options.width"
:z-index="options.zIndex"
@keydown.esc="cancel"
>
<v-card>
<v-toolbar :color="options.color" density="compact" flat>
<v-toolbar-title class="text-body-2 font-weight-bold">
{{ title }}
</v-toolbar-title>
</v-toolbar>
<v-card-text v-show="!!message" class="pa-4">
{{ message }}
<slot></slot>
</v-card-text>
<v-card-actions class="pt-3">
<v-spacer></v-spacer>
<v-btn v-if="!options.noconfirm" @click="cancel">
{{ options.cancelLabel }}
</v-btn>
<v-btn :color="options.color" @click="agree">
{{ options.agreeLabel }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="js">
@@ -40,34 +40,34 @@ const title = ref('')
let storedResolve
const options = ref({
color: 'primary',
width: 400,
zIndex: 2400,
noconfirm: false,
cancelLabel: $gettext('Cancel'),
agreeLabel: $gettext('OK'),
color: 'primary',
width: 400,
zIndex: 2400,
noconfirm: false,
cancelLabel: $gettext('Cancel'),
agreeLabel: $gettext('OK'),
})
function open(_title, _message, _options) {
dialog.value = true
title.value = _title
message.value = _message
options.value = Object.assign(options.value, _options)
return new Promise((resolve) => {
storedResolve = resolve
})
dialog.value = true
title.value = _title
message.value = _message
options.value = Object.assign(options.value, _options)
return new Promise((resolve) => {
storedResolve = resolve
})
}
defineExpose({
open,
open,
})
function agree() {
storedResolve(true)
dialog.value = false
storedResolve(true)
dialog.value = false
}
function cancel() {
storedResolve(false)
dialog.value = false
storedResolve(false)
dialog.value = false
}
</script>
+149 -142
View File
@@ -1,98 +1,105 @@
<template>
<div class="d-flex justify-center inner fill-height">
<v-stepper v-model="currentStep" :mobile="!smAndDown">
<ConfirmDialog ref="confirm" />
<v-stepper-header class="align-center px-10">
<v-img
src="@/assets/Modoboa_RVB-BLEU-SANS.png"
max-width="190"
class="hidden-sm-and-down"
/>
<v-stepper-item
v-for="(step, index) in steps"
:key="step.name"
:value="index + 1"
:complete="currentStep > index + 1"
>
{{ step.title }}
</v-stepper-item>
<v-stepper-item :value="steps.length + 1">
{{ $gettext('Summary') }}
</v-stepper-item>
<v-btn
icon="mdi-close"
color="primary"
:size="smAndDown ? 'small' : 'x-large'"
variant="text"
@click="close"
>
</v-btn>
</v-stepper-header>
<v-stepper-window class="mt-4 d-flex justify-center">
<v-stepper-window-item
v-for="(step, index) in steps"
:key="step.name"
:value="index + 1"
class="flex-grow-1 ma-10"
>
<v-container fluid>
<v-row align="center" justify="center">
<v-col cols="8" align="start">
<div class="mb-5 text-h5">
<span class="text-grey-darken-1">{{ title }}</span>
/
{{ step.title }}
</div>
<slot
:name="`form.${step.name}`"
:step="step"
:current-step="index + 1"
<div class="d-flex justify-center inner fill-height">
<v-stepper v-model="currentStep" :mobile="!smAndDown">
<ConfirmDialog ref="confirm" />
<v-stepper-header class="align-center px-10">
<v-img
src="@/assets/Modoboa_RVB-BLEU-SANS.png"
max-width="190"
class="hidden-sm-and-down"
/>
</v-col>
</v-row>
</v-container>
<div class="d-flex justify-end mt-4">
<v-btn
v-if="index + 1 > 1"
class="mr-10"
variant="text"
@click="currentStep = index"
>
{{ $gettext('Back') }}
</v-btn>
<v-btn
color="primary"
size="large"
@click="goToNextStep(index + 1, index + 2)"
>
{{ $gettext('Next') }}
</v-btn>
</div>
</v-stepper-window-item>
<v-stepper-window-item :value="steps.length + 1" class="flex-grow-0">
<div class="text-center text-h3">{{ $gettext('Summary') }}</div>
<CreationSummary
:sections="summarySections"
@modify-step="(val) => (currentStep = val)"
>
<template v-for="(_, slot) of $slots" #[slot]="scope">
<slot :name="slot" v-bind="scope" />
</template>
</CreationSummary>
<div class="d-flex justify-center mt-8">
<v-btn
color="primary"
size="large"
:loading="working"
@click="create"
>
{{ $gettext('Confirm and create') }}
</v-btn>
</div>
</v-stepper-window-item>
</v-stepper-window>
</v-stepper>
</div>
<v-stepper-item
v-for="(step, index) in steps"
:key="step.name"
:value="index + 1"
:complete="currentStep > index + 1"
>
{{ step.title }}
</v-stepper-item>
<v-stepper-item :value="steps.length + 1">
{{ $gettext('Summary') }}
</v-stepper-item>
<v-btn
icon="mdi-close"
color="primary"
:size="smAndDown ? 'small' : 'x-large'"
variant="text"
@click="close"
>
</v-btn>
</v-stepper-header>
<v-stepper-window class="mt-4 d-flex justify-center">
<v-stepper-window-item
v-for="(step, index) in steps"
:key="step.name"
:value="index + 1"
class="flex-grow-1 ma-10"
>
<v-container fluid>
<v-row align="center" justify="center">
<v-col cols="8" align="start">
<div class="mb-5 text-h5">
<span class="text-grey-darken-1">{{
title
}}</span>
/
{{ step.title }}
</div>
<slot
:name="`form.${step.name}`"
:step="step"
:current-step="index + 1"
/>
</v-col>
</v-row>
</v-container>
<div class="d-flex justify-end mt-4">
<v-btn
v-if="index + 1 > 1"
class="mr-10"
variant="text"
@click="currentStep = index"
>
{{ $gettext('Back') }}
</v-btn>
<v-btn
color="primary"
size="large"
@click="goToNextStep(index + 1, index + 2)"
>
{{ $gettext('Next') }}
</v-btn>
</div>
</v-stepper-window-item>
<v-stepper-window-item
:value="steps.length + 1"
class="flex-grow-0"
>
<div class="text-center text-h3">
{{ $gettext('Summary') }}
</div>
<CreationSummary
:sections="summarySections"
@modify-step="(val) => (currentStep = val)"
>
<template v-for="(_, slot) of $slots" #[slot]="scope">
<slot :name="slot" v-bind="scope" />
</template>
</CreationSummary>
<div class="d-flex justify-center mt-8">
<v-btn
color="primary"
size="large"
:loading="working"
@click="create"
>
{{ $gettext('Confirm and create') }}
</v-btn>
</div>
</v-stepper-window-item>
</v-stepper-window>
</v-stepper>
</div>
</template>
<script setup lang="js">
@@ -105,12 +112,12 @@ import { useDisplay } from 'vuetify'
const { smAndDown } = useDisplay()
const props = defineProps({
title: { type: String, default: '' },
steps: { type: Array, default: () => [] },
summarySections: { type: Array, default: () => [] },
getVFormRef: { type: Function, default: null },
formGetter: { type: Function, default: null },
validateObject: { type: Function, default: null },
title: { type: String, default: '' },
steps: { type: Array, default: () => [] },
summarySections: { type: Array, default: () => [] },
getVFormRef: { type: Function, default: null },
formGetter: { type: Function, default: null },
validateObject: { type: Function, default: null },
})
const currentStep = ref(1)
@@ -122,65 +129,65 @@ const emit = defineEmits(['close', 'create', 'validationError'])
const { $gettext } = useGettext()
async function close() {
const confirmed = await confirm.value.open(
$gettext('Warning'),
$gettext(
"If you close this form now, your modifications won't be saved. Do you confirm?"
),
{
color: 'warning',
agreeLabel: $gettext('Yes'),
cancelLabel: $gettext('No'),
const confirmed = await confirm.value.open(
$gettext('Warning'),
$gettext(
"If you close this form now, your modifications won't be saved. Do you confirm?"
),
{
color: 'warning',
agreeLabel: $gettext('Yes'),
cancelLabel: $gettext('No'),
}
)
if (!confirmed) {
return
}
)
if (!confirmed) {
return
}
emit('close')
emit('close')
}
async function goToNextStep(current, next) {
const vform = props.getVFormRef(props.steps[current - 1])
if (vform !== undefined) {
const { valid } = await vform.validate()
if (!valid) {
return
const vform = props.getVFormRef(props.steps[current - 1])
if (vform !== undefined) {
const { valid } = await vform.validate()
if (!valid) {
return
}
if (props.validateObject) {
try {
await props.validateObject()
} catch (error) {
emit('validationError', current, error.response.data)
return
}
}
}
if (props.validateObject) {
try {
await props.validateObject()
} catch (error) {
emit('validationError', current, error.response.data)
return
}
}
}
currentStep.value = next
currentStep.value = next
}
function create() {
working.value = true
emit('create')
working.value = true
emit('create')
}
</script>
<style lang="scss">
.inner {
background-color: #fff;
background-color: #fff;
}
.v-stepper {
width: 100%;
overflow: auto;
width: 100%;
overflow: auto;
&__content {
width: 60%;
}
&__content {
width: 60%;
}
&__items {
overflow-y: auto;
}
&__items {
overflow-y: auto;
}
&__wrapper {
padding: 0 10px;
}
&__wrapper {
padding: 0 10px;
}
}
</style>
@@ -1,36 +1,40 @@
<template>
<template 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)"
>
{{ $gettext('Modify') }}</a
>
</div>
<v-row v-for="(item, itemIndex) in section.items" :key="itemIndex" dense>
<template v-if="item.value !== undefined">
<slot
:name="`item.${item.name}`"
:item="item"
:display-list="displayList"
<template 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)"
>
{{ $gettext('Modify') }}</a
>
</div>
<v-row
v-for="(item, itemIndex) in section.items"
:key="itemIndex"
dense
>
<v-col
><span class="text-grey">{{ item.key }}</span></v-col
>
<v-col v-if="item.type === 'yesno'" class="text-right">
{{ $yesno(item.value) }}
</v-col>
<v-col v-else-if="item.type === 'list'" class="text-right">
{{ displayList(item.value) }}
</v-col>
<v-col v-else class="text-right">{{ item.value }}</v-col>
</slot>
</template>
</v-row>
</template>
<template v-if="item.value !== undefined">
<slot
:name="`item.${item.name}`"
:item="item"
:display-list="displayList"
>
<v-col
><span class="text-grey">{{ item.key }}</span></v-col
>
<v-col v-if="item.type === 'yesno'" class="text-right">
{{ $yesno(item.value) }}
</v-col>
<v-col v-else-if="item.type === 'list'" class="text-right">
{{ displayList(item.value) }}
</v-col>
<v-col v-else class="text-right">{{ item.value }}</v-col>
</slot>
</template>
</v-row>
</template>
</template>
<script setup lang="js">
@@ -42,17 +46,17 @@ defineProps({ sections: { type: Array, default: () => [] } })
const emit = defineEmits(['modify-step'])
function displayList(value) {
return value.length ? value.join(', ') : $gettext('Empty')
return value.length ? value.join(', ') : $gettext('Empty')
}
</script>
<style scoped lang="scss">
.subtitle {
color: #000;
border-bottom: 1px solid #dbdddf;
color: #000;
border-bottom: 1px solid #dbdddf;
}
.edit-link {
text-decoration: none;
text-decoration: none;
}
</style>
+23 -23
View File
@@ -1,36 +1,36 @@
<template>
<v-menu
v-model="menu"
:close-on-content-click="false"
transition="scale-transition"
offset-y
min-width="auto"
>
<template #activator="{ props }">
<v-text-field
:value="modelValue"
:label="label"
prepend-icon="mdi-calendar"
readonly
variant="underlined"
v-bind="props"
></v-text-field>
</template>
<v-date-picker :value="modelValue" no-title @input="update">
</v-date-picker>
</v-menu>
<v-menu
v-model="menu"
:close-on-content-click="false"
transition="scale-transition"
offset-y
min-width="auto"
>
<template #activator="{ props }">
<v-text-field
:value="modelValue"
:label="label"
prepend-icon="mdi-calendar"
readonly
variant="underlined"
v-bind="props"
></v-text-field>
</template>
<v-date-picker :value="modelValue" no-title @input="update">
</v-date-picker>
</v-menu>
</template>
<script setup lang="js">
import { ref } from 'vue'
defineProps({
modelValue: { type: Object, default: null },
label: { type: String, default: '' },
modelValue: { type: Object, default: null },
label: { type: String, default: '' },
})
const menu = ref(false)
function update() {
menu.value = false
menu.value = false
}
</script>
+128 -128
View File
@@ -1,28 +1,28 @@
<template>
<v-text-field
ref="inputRef"
v-model="input"
variant="outlined"
v-bind="$attrs"
:error-messages="errors"
:error="errors.length > 0"
density="compact"
autocomplete="new-password"
@keydown="onKeyDown"
/>
<div v-if="showMenu" class="menu" :style="`width: ${width}px;`">
<v-list class="list">
<v-list-item
v-for="(domain, index) in filteredDomains"
:key="index"
:class="selectionIndex === index ? 'bg-primary-lighten-1' : ''"
@click="selectDomain(domain)"
@mouseenter="selectionIndex = index"
>
<v-list-item-title>{{ domain.name }}</v-list-item-title>
</v-list-item>
</v-list>
</div>
<v-text-field
ref="inputRef"
v-model="input"
variant="outlined"
v-bind="$attrs"
:error-messages="errors"
:error="errors.length > 0"
density="compact"
autocomplete="new-password"
@keydown="onKeyDown"
/>
<div v-if="showMenu" class="menu" :style="`width: ${width}px;`">
<v-list class="list">
<v-list-item
v-for="(domain, index) in filteredDomains"
:key="index"
:class="selectionIndex === index ? 'bg-primary-lighten-1' : ''"
@click="selectDomain(domain)"
@mouseenter="selectionIndex = index"
>
<v-list-item-title>{{ domain.name }}</v-list-item-title>
</v-list-item>
</v-list>
</div>
</template>
<script setup lang="js">
@@ -33,50 +33,50 @@ import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
const domainsStore = useDomainsStore()
const props = defineProps({
modelValue: { type: String, default: '' },
allowAdd: { type: Boolean, default: false },
role: { type: String, default: 'SimpleUsers' },
errorMsg: { type: Array, default: () => [] },
modelValue: { type: String, default: '' },
allowAdd: { type: Boolean, default: false },
role: { type: String, default: 'SimpleUsers' },
errorMsg: { type: Array, default: () => [] },
})
const emit = defineEmits(['domain-selected', 'update:model-value'])
const input = computed({
get() {
return props.modelValue
},
set(value) {
errors.value = []
if (value.indexOf('@') !== -1) {
if (props.role !== 'SuperAdmins') {
domainSearch.value = value.split('@')[1]
if (props.allowAdd) {
showMenu.value = filteredDomains.value.length > 0
get() {
return props.modelValue
},
set(value) {
errors.value = []
if (value.indexOf('@') !== -1) {
if (props.role !== 'SuperAdmins') {
domainSearch.value = value.split('@')[1]
if (props.allowAdd) {
showMenu.value = filteredDomains.value.length > 0
} else {
showMenu.value = true
}
} else {
errors.value = [$gettext('SuperAdmins cannot own a mailbox')]
}
} else {
showMenu.value = true
showMenu.value = false
}
} else {
errors.value = [$gettext('SuperAdmins cannot own a mailbox')]
}
} else {
showMenu.value = false
}
emit('update:model-value', value)
},
emit('update:model-value', value)
},
})
const filteredDomains = computed(() => {
return Object.values(domainsStore.domains).filter((domain) =>
domain.name.startsWith(domainSearch.value)
)
return Object.values(domainsStore.domains).filter((domain) =>
domain.name.startsWith(domainSearch.value)
)
})
const errors = computed({
get() {
return [...localErrors.value, ...props.errorMsg]
},
set(value) {
localErrors.value = value
},
get() {
return [...localErrors.value, ...props.errorMsg]
},
set(value) {
localErrors.value = value
},
})
const domainSearch = ref('')
@@ -87,106 +87,106 @@ const inputRef = ref()
const localErrors = ref([])
function onKeyDown(e) {
if (props.role === 'SuperAdmins') {
// No mailbox for SuperAdmins
return
}
const keyCode = e.keyCode
if (keyCode === 40 || keyCode === 34) {
// on arrow down or page down
if (!showMenu.value) {
domainSearch.value = ''
showMenu.value = true
} else {
increaseSelectionIndex()
if (props.role === 'SuperAdmins') {
// No mailbox for SuperAdmins
return
}
e.preventDefault()
} else if (keyCode === 38 || keyCode === 33) {
// on arrow up or page up
if (!showMenu.value) {
domainSearch.value = ''
showMenu.value = true
} else {
decreaseSelectionIndex()
const keyCode = e.keyCode
if (keyCode === 40 || keyCode === 34) {
// on arrow down or page down
if (!showMenu.value) {
domainSearch.value = ''
showMenu.value = true
} else {
increaseSelectionIndex()
}
e.preventDefault()
} else if (keyCode === 38 || keyCode === 33) {
// on arrow up or page up
if (!showMenu.value) {
domainSearch.value = ''
showMenu.value = true
} else {
decreaseSelectionIndex()
}
e.preventDefault()
} else if (keyCode === 13 || keyCode === 9) {
// on enter or tab
if (filteredDomains.value.length > 0) {
selectDomain(filteredDomains.value[selectionIndex.value])
} else if (props.allowAdd) {
selectDomain()
}
e.preventDefault()
} else if (keyCode === 27) {
// on escape
if (filteredDomains.value.length > 0) {
showMenu.value = false
e.preventDefault()
}
}
e.preventDefault()
} else if (keyCode === 13 || keyCode === 9) {
// on enter or tab
if (filteredDomains.value.length > 0) {
selectDomain(filteredDomains.value[selectionIndex.value])
} else if (props.allowAdd) {
selectDomain()
}
e.preventDefault()
} else if (keyCode === 27) {
// on escape
if (filteredDomains.value.length > 0) {
showMenu.value = false
e.preventDefault()
}
}
}
function increaseSelectionIndex() {
if (selectionIndex.value >= filteredDomains.value.length - 1) {
selectionIndex.value = 0
} else {
selectionIndex.value += 1
}
if (selectionIndex.value >= filteredDomains.value.length - 1) {
selectionIndex.value = 0
} else {
selectionIndex.value += 1
}
}
function decreaseSelectionIndex() {
if (selectionIndex.value <= 0) {
selectionIndex.value = filteredDomains.value.length - 1
} else {
selectionIndex.value -= 1
}
if (selectionIndex.value <= 0) {
selectionIndex.value = filteredDomains.value.length - 1
} else {
selectionIndex.value -= 1
}
}
function selectDomain(domain) {
if (domain !== undefined) {
input.value = input.value.split('@')[0] + '@' + domain.name
}
showMenu.value = false
emit('domain-selected')
if (domain !== undefined) {
input.value = input.value.split('@')[0] + '@' + domain.name
}
showMenu.value = false
emit('domain-selected')
}
function updateMinWidthProperty() {
width.value = inputRef.value.$el.clientWidth
width.value = inputRef.value.$el.clientWidth
}
onMounted(() => {
domainsStore.getDomains()
window.addEventListener('resize', updateMinWidthProperty)
domainsStore.getDomains()
window.addEventListener('resize', updateMinWidthProperty)
})
onUnmounted(() => {
window.removeEventListener('resize', updateMinWidthProperty)
window.removeEventListener('resize', updateMinWidthProperty)
})
onUpdated(() => {
nextTick(() => {
updateMinWidthProperty()
})
nextTick(() => {
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;
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;
width: 100%;
height: 100%;
font-size: 1.1em;
overflow-x: hidden;
padding: 0;
}
</style>
+80 -74
View File
@@ -1,61 +1,67 @@
<template>
<v-card>
<v-form ref="vform" @submit.prevent="submit">
<v-card-title>
<span class="headline">{{ title }}</span>
</v-card-title>
<v-card-text>
<v-alert variant="tonal" type="info" class="mb-2">
{{
$gettext(
'Provide a CSV file where lines respect one of the following formats:'
)
}}
<slot name="help" />
{{
$gettext(
'The first element of each line is mandatory and must be equal to one of the previous values.'
)
}}
{{ $gettext('You can use a different character as separator.') }}
</v-alert>
<label class="m-label">{{ $gettext('Select file') }}</label>
<v-file-input
v-model="form.sourcefile"
accept="text/csv"
truncate-length="15"
variant="outlined"
density="compact"
:rules="[rules.required]"
/>
<label class="m-label">{{ $gettext('Separator') }}</label>
<v-text-field
v-model="form.sepchar"
:rules="[rules.required]"
variant="outlined"
density="compact"
/>
<v-switch
v-model="form.continue_if_exists"
:label="$gettext('Continue on error')"
color="primary"
density="compact"
:hint="$gettext('Don\'t treat duplicated objects as errors')"
persistent-hint
/>
<slot name="extraFields" :form="form" />
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="close">
{{ $gettext('Close') }}
</v-btn>
<v-btn color="primary" type="submit">
{{ $gettext('Import') }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
<v-card>
<v-form ref="vform" @submit.prevent="submit">
<v-card-title>
<span class="headline">{{ title }}</span>
</v-card-title>
<v-card-text>
<v-alert variant="tonal" type="info" class="mb-2">
{{
$gettext(
'Provide a CSV file where lines respect one of the following formats:'
)
}}
<slot name="help" />
{{
$gettext(
'The first element of each line is mandatory and must be equal to one of the previous values.'
)
}}
{{
$gettext(
'You can use a different character as separator.'
)
}}
</v-alert>
<label class="m-label">{{ $gettext('Select file') }}</label>
<v-file-input
v-model="form.sourcefile"
accept="text/csv"
truncate-length="15"
variant="outlined"
density="compact"
:rules="[rules.required]"
/>
<label class="m-label">{{ $gettext('Separator') }}</label>
<v-text-field
v-model="form.sepchar"
:rules="[rules.required]"
variant="outlined"
density="compact"
/>
<v-switch
v-model="form.continue_if_exists"
:label="$gettext('Continue on error')"
color="primary"
density="compact"
:hint="
$gettext('Don\'t treat duplicated objects as errors')
"
persistent-hint
/>
<slot name="extraFields" :form="form" />
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="close">
{{ $gettext('Close') }}
</v-btn>
<v-btn color="primary" type="submit">
{{ $gettext('Import') }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</template>
<script setup lang="js">
@@ -64,8 +70,8 @@ import { useGettext } from 'vue3-gettext'
import rules from '@/plugins/rules.js'
defineProps({
helpText: { type: String, default: '' },
title: { type: String, default: '' },
helpText: { type: String, default: '' },
title: { type: String, default: '' },
})
const emit = defineEmits(['close', 'submit'])
@@ -76,24 +82,24 @@ const vform = ref()
const { $gettext } = useGettext()
function close() {
form.value = {}
emit('close')
form.value = {}
emit('close')
}
async function submit() {
const { valid } = await vform.value.validate()
if (!valid) {
return
}
const data = new FormData()
data.append('sourcefile', form.value.sourcefile[0])
if (form.value.sepchar) {
data.append('sepchar', form.value.sepchar)
}
if (form.value.continue_if_exists) {
data.append('continue_if_exists', form.value.continue_if_exists)
}
emit('submit', data, form.value)
const { valid } = await vform.value.validate()
if (!valid) {
return
}
const data = new FormData()
data.append('sourcefile', form.value.sourcefile[0])
if (form.value.sepchar) {
data.append('sepchar', form.value.sepchar)
}
if (form.value.continue_if_exists) {
data.append('continue_if_exists', form.value.continue_if_exists)
}
emit('submit', data, form.value)
}
defineExpose({ close })
+11 -11
View File
@@ -1,13 +1,13 @@
<template>
<v-container class="fill-height" fuild>
<v-row align="center" justify="center" dense>
<v-col cols="12" sm="12" md="12" lg="12" class="text-center">
<v-progress-circular
color="primary"
indeterminate
:size="108"
></v-progress-circular>
</v-col>
</v-row>
</v-container>
<v-container class="fill-height" fuild>
<v-row align="center" justify="center" dense>
<v-col cols="12" sm="12" md="12" lg="12" class="text-center">
<v-progress-circular
color="primary"
indeterminate
:size="108"
></v-progress-circular>
</v-col>
</v-row>
</v-container>
</template>
+21 -21
View File
@@ -1,28 +1,28 @@
<template>
<v-list density="compact">
<v-list-item
v-for="(item, index) in items"
:key="index"
:value="item"
@click="item.onClick(obj)"
>
<template #prepend>
<v-icon :color="item.color" :icon="item.icon"></v-icon>
</template>
<v-list-item-title>{{ item.label }}</v-list-item-title>
</v-list-item>
</v-list>
<v-list density="compact">
<v-list-item
v-for="(item, index) in items"
:key="index"
:value="item"
@click="item.onClick(obj)"
>
<template #prepend>
<v-icon :color="item.color" :icon="item.icon"></v-icon>
</template>
<v-list-item-title>{{ item.label }}</v-list-item-title>
</v-list-item>
</v-list>
</template>
<script setup lang="js">
defineProps({
items: {
type: Object,
default: () => {},
},
obj: {
type: Object,
default: () => {},
},
items: {
type: Object,
default: () => {},
},
obj: {
type: Object,
default: () => {},
},
})
</script>
+15 -15
View File
@@ -1,16 +1,16 @@
<template>
<v-form ref="vFormRef">
<template v-for="resource in resources" :key="resource.name">
<label class="m-label">{{ resource.label }}</label>
<v-text-field
v-model="resource.max_value"
type="number"
variant="outlined"
dense
:rules="[rules.required]"
/>
</template>
</v-form>
<v-form ref="vFormRef">
<template v-for="resource in resources" :key="resource.name">
<label class="m-label">{{ resource.label }}</label>
<v-text-field
v-model="resource.max_value"
type="number"
variant="outlined"
dense
:rules="[rules.required]"
/>
</template>
</v-form>
</template>
<script setup lang="js">
@@ -24,9 +24,9 @@ const resources = computed(() => props.modelValue)
const vFormRef = ref()
function getPayload() {
return resources.value.map((r) => {
return { name: r.name, max_value: r.max_value }
})
return resources.value.map((r) => {
return { name: r.name, max_value: r.max_value }
})
}
defineExpose({ vFormRef, getPayload })
+26 -22
View File
@@ -1,32 +1,36 @@
<template>
<v-card>
<v-card-title>
<span class="headline">{{ $gettext('Resources usage') }}</span>
</v-card-title>
<v-card-text>
<div v-for="resource in resources" :key="resource.name" class="mt-4">
{{ resource.label }} ({{ Math.ceil(resource.usage) }}%)
<v-progress-linear
:value="resource.usage"
:color="getProgressColor(resource.usage)"
/>
</div>
</v-card-text>
</v-card>
<v-card>
<v-card-title>
<span class="headline">{{ $gettext('Resources usage') }}</span>
</v-card-title>
<v-card-text>
<div
v-for="resource in resources"
:key="resource.name"
class="mt-4"
>
{{ resource.label }} ({{ Math.ceil(resource.usage) }}%)
<v-progress-linear
:value="resource.usage"
:color="getProgressColor(resource.usage)"
/>
</div>
</v-card-text>
</v-card>
</template>
<script setup lang="js">
defineProps({
resources: { type: Array, default: null },
resources: { type: Array, default: null },
})
function getProgressColor(value) {
if (value < 50) {
return 'primary'
}
if (value < 80) {
return 'warning'
}
return 'error'
if (value < 50) {
return 'primary'
}
if (value < 80) {
return 'warning'
}
return 'error'
}
</script>
+131 -115
View File
@@ -1,55 +1,64 @@
<template>
<v-card>
<v-toolbar dense elevation="0">
<v-toolbar-title>{{ title }}</v-toolbar-title>
<v-spacer />
<v-menu v-model="menu" offset-y>
<template #activator="{ props }">
<v-btn size="small" v-bind="props" prepend-icon="mdi-calendar">
{{ $gettext('Period') }}
<span v-if="period">: {{ period }}</span>
</v-btn>
</template>
<v-container class="bg-white" fluid>
<v-row no-gutters>
<v-col cols="12" md="6" class="pr-2">
<div class="text-subtitle-1">
{{ $gettext('Absolute time range') }}
</div>
<DateField v-model="start" :label="$gettext('From')" />
<DateField v-model="end" :label="$gettext('To')" />
<v-btn color="primary" @click="setCustomPeriod">{{
$gettext('Apply')
}}</v-btn>
</v-col>
<v-col cols="12" md="6">
<div class="text-subtitle-1">
{{ $gettext('Relative time ranges') }}
</div>
<v-list>
<v-list-item
v-for="_period in periods"
:key="_period.value"
@click="setPeriod(_period.value)"
>
<v-list-item-title> {{ _period.name }} </v-list-item-title>
</v-list-item>
</v-list>
</v-col>
</v-row>
</v-container>
</v-menu>
</v-toolbar>
<v-card-text>
<apexchart
v-if="statistics"
type="area"
height="350"
:options="options"
:series="statistics.series"
/>
</v-card-text>
</v-card>
<v-card>
<v-toolbar dense elevation="0">
<v-toolbar-title>{{ title }}</v-toolbar-title>
<v-spacer />
<v-menu v-model="menu" offset-y>
<template #activator="{ props }">
<v-btn
size="small"
v-bind="props"
prepend-icon="mdi-calendar"
>
{{ $gettext('Period') }}
<span v-if="period">: {{ period }}</span>
</v-btn>
</template>
<v-container class="bg-white" fluid>
<v-row no-gutters>
<v-col cols="12" md="6" class="pr-2">
<div class="text-subtitle-1">
{{ $gettext('Absolute time range') }}
</div>
<DateField
v-model="start"
:label="$gettext('From')"
/>
<DateField v-model="end" :label="$gettext('To')" />
<v-btn color="primary" @click="setCustomPeriod">{{
$gettext('Apply')
}}</v-btn>
</v-col>
<v-col cols="12" md="6">
<div class="text-subtitle-1">
{{ $gettext('Relative time ranges') }}
</div>
<v-list>
<v-list-item
v-for="_period in periods"
:key="_period.value"
@click="setPeriod(_period.value)"
>
<v-list-item-title>
{{ _period.name }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-col>
</v-row>
</v-container>
</v-menu>
</v-toolbar>
<v-card-text>
<apexchart
v-if="statistics"
type="area"
height="350"
:options="options"
:series="statistics.series"
/>
</v-card-text>
</v-card>
</template>
<script setup lang="js">
@@ -61,43 +70,43 @@ import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
const _props = defineProps({
domain: {
type: Object,
required: false,
default: null,
},
graphicSet: { type: String, default: null },
graphicName: { type: String, default: null },
domain: {
type: Object,
required: false,
default: null,
},
graphicSet: { type: String, default: null },
graphicName: { type: String, default: null },
})
const title = computed(() => {
if (statistics.value) {
return statistics.value.title
}
return ''
if (statistics.value) {
return statistics.value.title
}
return ''
})
const menu = ref(false)
const options = ref({
chart: {
type: 'area',
stacked: false,
zoom: {
type: 'x',
enabled: true,
autoScaleYaxis: true,
chart: {
type: 'area',
stacked: false,
zoom: {
type: 'x',
enabled: true,
autoScaleYaxis: true,
},
toolbar: {
autoSelected: 'zoom',
},
},
toolbar: {
autoSelected: 'zoom',
dataLabels: {
enabled: false,
},
yaxis: {},
xaxis: {
type: 'datetime',
},
},
dataLabels: {
enabled: false,
},
yaxis: {},
xaxis: {
type: 'datetime',
},
})
const end = ref(null)
@@ -105,63 +114,70 @@ const start = ref(null)
const statistics = ref(null)
const period = ref('day')
const periods = ref([
{ value: 'day', name: $gettext('Day') },
{ value: 'week', name: $gettext('Week') },
{ value: 'month', name: $gettext('Month') },
{ value: 'year', name: $gettext('Year') },
{ value: 'day', name: $gettext('Day') },
{ value: 'week', name: $gettext('Week') },
{ value: 'month', name: $gettext('Month') },
{ value: 'year', name: $gettext('Year') },
])
function getColors() {
if (_props.graphicName === 'averagetraffic') {
return ['#ff6347', '#ffff00', '#4682B4', '#7cfc00', '#ffa500', '#d3d3d3']
}
return ['#ffa500', '#41d1cc']
if (_props.graphicName === 'averagetraffic') {
return [
'#ff6347',
'#ffff00',
'#4682B4',
'#7cfc00',
'#ffa500',
'#d3d3d3',
]
}
return ['#ffa500', '#41d1cc']
}
function setCustomPeriod() {
period.value = 'custom'
fetchStatistics()
period.value = 'custom'
fetchStatistics()
}
function setPeriod(newPeriod) {
if (newPeriod !== period.value) {
period.value = newPeriod
fetchStatistics()
}
if (newPeriod !== period.value) {
period.value = newPeriod
fetchStatistics()
}
}
function fetchStatistics() {
const args = {
graphSet: _props.graphicSet,
period: period.value,
graphicName: _props.graphicName,
}
if (_props.domain) {
args.searchQuery = _props.domain.name
}
if (period.value === 'custom') {
args.start = start.value
args.end = end.value
}
statisticsApi.getStatistics(args).then((resp) => {
statistics.value = resp.data.graphs[_props.graphicName]
})
const args = {
graphSet: _props.graphicSet,
period: period.value,
graphicName: _props.graphicName,
}
if (_props.domain) {
args.searchQuery = _props.domain.name
}
if (period.value === 'custom') {
args.start = start.value
args.end = end.value
}
statisticsApi.getStatistics(args).then((resp) => {
statistics.value = resp.data.graphs[_props.graphicName]
})
}
onMounted(() => {
fetchStatistics()
fetchStatistics()
})
watch(
() => _props.graphicName,
() => {
options.value.colors = getColors()
}
() => _props.graphicName,
() => {
options.value.colors = getColors()
}
)
</script>
<style scoped>
.v-card {
z-index: 5;
z-index: 5;
}
.v-menu__content {
background-color: white !important;
background-color: white !important;
}
</style>
@@ -1,21 +1,21 @@
<template>
<v-app>
<NavBar />
<DashboardView />
<v-snackbar
v-model="snackbar"
:color="notificationColor"
:timeout="notificationTimeout"
location="top"
>
{{ notification }}
<template #actions>
<v-btn color="white" text @click="snackbar = false">
{{ $gettext('Close') }}
</v-btn>
</template>
</v-snackbar>
</v-app>
<v-app>
<NavBar />
<DashboardView />
<v-snackbar
v-model="snackbar"
:color="notificationColor"
:timeout="notificationTimeout"
location="top"
>
{{ notification }}
<template #actions>
<v-btn color="white" text @click="snackbar = false">
{{ $gettext('Close') }}
</v-btn>
</template>
</v-snackbar>
</v-app>
</template>
<script setup>
@@ -35,10 +35,10 @@ const notification = computed(() => busStore.notification)
const snackbar = ref(false)
busStore.$onAction(({ name, after }) => {
if (name === 'displayNotification') {
after(() => {
snackbar.value = true
})
}
if (name === 'displayNotification') {
after(() => {
snackbar.value = true
})
}
}, true)
</script>
@@ -1,15 +1,15 @@
<template>
<v-main>
<v-container fluid>
<router-view v-slot="{ Component }">
<transition name="fade">
<div v-show="true">
<component :is="Component" />
</div>
</transition>
</router-view>
</v-container>
</v-main>
<v-main>
<v-container fluid>
<router-view v-slot="{ Component }">
<transition name="fade">
<div v-show="true">
<component :is="Component" />
</div>
</transition>
</router-view>
</v-container>
</v-main>
</template>
<script setup>
@@ -18,6 +18,6 @@
<style scoped>
.v-main {
background-color: #f7f8fa;
background-color: #f7f8fa;
}
</style>
+120 -116
View File
@@ -1,117 +1,121 @@
<template>
<v-navigation-drawer
v-model="drawer"
:rail="rail"
permanent
:color="mainColor"
app
>
<div class="d-flex align-center">
<v-img
src="@/assets/Modoboa_RVB-BLANC-SANS.png"
max-width="190"
class="logo"
@click="router.push({ name: 'Dashboard' })"
/>
<v-btn
:icon="rail ? 'mdi-chevron-right' : 'mdi-chevron-left'"
variant="text"
@click.stop="rail = !rail"
>
</v-btn>
</div>
<v-list nav>
<template v-for="item in menuItems">
<template v-if="displayMenuItem(item)">
<v-list-item
v-if="!item.children"
:key="item.text"
:value="item"
:to="item.to"
link
:exact="item.exact"
:title="item.text"
:prepend-icon="item.icon"
>
</v-list-item>
<v-list-group v-else :key="item.text" :value="item.text">
<template #activator="{ props }">
<v-list-item
v-bind="props"
:key="item.text"
:title="item.text"
color="white"
:prepend-icon="item.icon"
>
</v-list-item>
</template>
<v-list-item
v-for="subitem in item.children"
:key="subitem.text"
:to="subitem.to"
link
:title="subitem.text"
:value="subitem"
></v-list-item>
</v-list-group>
</template>
</template>
</v-list>
<template #append>
<v-menu v-if="isAuthenticated" rounded="lg" offset-y top>
<template #activator="{ props }">
<div
:class="[
backgroundColor,
'd-flex',
'justify-center',
'align-center',
'pa-2',
]"
>
<v-navigation-drawer
v-model="drawer"
:rail="rail"
permanent
:color="mainColor"
app
>
<div class="d-flex align-center">
<v-img
src="@/assets/Modoboa_RVB-BLANC-SANS.png"
max-width="190"
class="logo"
@click="router.push({ name: 'Dashboard' })"
/>
<v-btn
v-bind="props"
:class="[backgroundColor, 'text-white']"
rounded
density="compact"
height="48"
elevation="0"
:icon="rail ? 'mdi-chevron-right' : 'mdi-chevron-left'"
variant="text"
@click.stop="rail = !rail"
>
<v-avatar size="40" color="primary">
<span class="text-white headline">{{ userInitials }}</span>
</v-avatar>
<template v-if="!rail">
<span class="mx-2">{{ displayName }}</span>
<v-icon class="float-right">mdi-chevron-up</v-icon>
</template>
</v-btn>
</div>
</template>
<v-list>
<v-list-item
v-for="item in userMenuItems"
:key="item.text"
:to="item.to"
link
@click="item.click"
>
<template #prepend>
<v-icon :icon="item.icon"></v-icon>
</div>
<v-list nav>
<template v-for="item in menuItems">
<template v-if="displayMenuItem(item)">
<v-list-item
v-if="!item.children"
:key="item.text"
:value="item"
:to="item.to"
link
:exact="item.exact"
:title="item.text"
:prepend-icon="item.icon"
>
</v-list-item>
<v-list-group v-else :key="item.text" :value="item.text">
<template #activator="{ props }">
<v-list-item
v-bind="props"
:key="item.text"
:title="item.text"
color="white"
:prepend-icon="item.icon"
>
</v-list-item>
</template>
<v-list-item
v-for="subitem in item.children"
:key="subitem.text"
:to="subitem.to"
link
:title="subitem.text"
:value="subitem"
></v-list-item>
</v-list-group>
</template>
</template>
<v-list-item-title>{{ item.text }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-navigation-drawer>
<template #append>
<v-menu v-if="isAuthenticated" rounded="lg" offset-y top>
<template #activator="{ props }">
<div
:class="[
backgroundColor,
'd-flex',
'justify-center',
'align-center',
'pa-2',
]"
>
<v-btn
v-bind="props"
:class="[backgroundColor, 'text-white']"
rounded
density="compact"
height="48"
elevation="0"
>
<v-avatar size="40" color="primary">
<span class="text-white headline">{{
userInitials
}}</span>
</v-avatar>
<template v-if="!rail">
<span class="mx-2">{{ displayName }}</span>
<v-icon class="float-right"
>mdi-chevron-up</v-icon
>
</template>
</v-btn>
</div>
</template>
<v-list>
<v-list-item
v-for="item in userMenuItems"
:key="item.text"
:to="item.to"
link
@click="item.click"
>
<template #prepend>
<v-icon :icon="item.icon"></v-icon>
</template>
<v-list-item-title>{{ item.text }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-navigation-drawer>
</template>
<script setup lang="js">
import { useRoute, useRouter } from 'vue-router'
import { ref, computed } from 'vue'
import { useGettext } from 'vue3-gettext'
import { getActivePinia } from "pinia"
import { getActivePinia } from 'pinia'
import parametersApi from '@/api/parameters'
import { useAuthStore, useParametersStore } from '@/stores'
@@ -139,8 +143,8 @@ const userInitials = computed(() => {
}
if (authUser.value.last_name) {
initials = initials
? initials + authUser.value.last_name[0]
: authUser.value.last_name[0]
? initials + authUser.value.last_name[0]
: authUser.value.last_name[0]
}
if (!initials) {
initials = authUser.value.username[0]
@@ -149,8 +153,8 @@ const userInitials = computed(() => {
})
const displayName = computed(() => {
return authUser.value.first_name || authUser.value.last_name
? `${authUser.value.first_name} ${authUser.value.last_name}`
: authUser.value.username
? `${authUser.value.first_name} ${authUser.value.last_name}`
: authUser.value.username
})
const menuItems = computed(() => {
@@ -308,7 +312,7 @@ function displayMenuItem(item) {
if (isAuthenticated.value) {
const condition =
(item.roles === undefined ||
item.roles.indexOf(authUser.value.role) !== -1) &&
item.roles.indexOf(authUser.value.role) !== -1) &&
(item.condition === undefined || item.condition()) &&
item.activated !== false
if (item.icon === 'mdi-email-sync-outline') {
@@ -320,7 +324,7 @@ function displayMenuItem(item) {
}
async function logout() {
getActivePinia()._s.forEach(async (store) => await store.$reset());
getActivePinia()._s.forEach(async (store) => await store.$reset())
router.push({ name: 'Login' })
}
@@ -336,17 +340,17 @@ onMounted(() => {
<style lang="scss" scoped>
.v-list-item {
&--active {
&::before {
opacity: 0;
&--active {
&::before {
opacity: 0;
}
background-color: #034bad !important;
color: white;
opacity: 1;
}
background-color: #034bad !important;
color: white;
opacity: 1;
}
}
.logo {
cursor: pointer;
cursor: pointer;
}
</style>
@@ -1,7 +1,7 @@
<template>
<v-app>
<DefaultView />
</v-app>
<v-app>
<DefaultView />
</v-app>
</template>
<script setup>
+3 -3
View File
@@ -1,7 +1,7 @@
<template>
<v-main>
<router-view />
</v-main>
<v-main>
<router-view />
</v-main>
</template>
<script setup>
File diff suppressed because one or more lines are too long
+10 -10
View File
@@ -17,18 +17,18 @@ import { createApp } from 'vue'
import repository from '@/api/repository.js'
fetch(import.meta.env.BASE_URL + 'config.json').then((resp) => {
resp.json().then((config) => {
if (!config.API_BASE_URL) {
throw Error('API_BASE_URL is not defined in config.json')
}
repository.defaults.baseURL = config.API_BASE_URL
resp.json().then((config) => {
if (!config.API_BASE_URL) {
throw Error('API_BASE_URL is not defined in config.json')
}
repository.defaults.baseURL = config.API_BASE_URL
const app = createApp(App)
const app = createApp(App)
registerPlugins(app)
registerPlugins(app)
app.mount('#app')
app.mount('#app')
app.provide('$config', config)
})
app.provide('$config', config)
})
})
+39 -40
View File
@@ -1,45 +1,44 @@
import { useBusStore } from '@/stores'
export const importExportMixin = () => {
function exportContent(content, name, $gettext) {
if (!content && !content.length) {
const busStore = useBusStore()
busStore.displayNotification({
msg: $gettext('No data to export'),
type: 'warning',
})
return
}
const blob = new Blob([content], { type: 'text/csv' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `modoboa-${name}.csv`
link.click()
URL.revokeObjectURL(link.href)
}
function importContent(api, data, importForm, $gettext) {
api
.importFromCSV(data)
.then((resp) => {
const busStore = useBusStore()
if (resp.data.status) {
busStore.displayNotification({ msg: resp.data.message })
importForm.value.close()
} else {
busStore.displayNotification({
msg: resp.data.message,
type: 'error',
})
function exportContent(content, name, $gettext) {
if (!content && !content.length) {
const busStore = useBusStore()
busStore.displayNotification({
msg: $gettext('No data to export'),
type: 'warning',
})
return
}
})
.catch(() => {
const busStore = useBusStore()
busStore.displayNotification({
msg: $gettext('CSV seems to be badly formatted'),
type: 'error',
})
})
}
return { exportContent, importContent }
const blob = new Blob([content], { type: 'text/csv' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `modoboa-${name}.csv`
link.click()
URL.revokeObjectURL(link.href)
}
function importContent(api, data, importForm, $gettext) {
api.importFromCSV(data)
.then((resp) => {
const busStore = useBusStore()
if (resp.data.status) {
busStore.displayNotification({ msg: resp.data.message })
importForm.value.close()
} else {
busStore.displayNotification({
msg: resp.data.message,
type: 'error',
})
}
})
.catch(() => {
const busStore = useBusStore()
busStore.displayNotification({
msg: $gettext('CSV seems to be badly formatted'),
type: 'error',
})
})
}
return { exportContent, importContent }
}
+16 -14
View File
@@ -4,18 +4,20 @@ import { DateTime } from 'luxon'
const { $gettext } = gettext
export default {
install: (app) => {
app.config.globalProperties.$yesno = (value) => {
return value ? $gettext('yes') : $gettext('no')
}
app.config.globalProperties.$date = (value) => {
return DateTime.fromISO(value)
.setLocale('en')
.toLocaleString(DateTime.DATETIME_MED)
}
app.config.globalProperties.$truncate = (value, length, clamp) => {
clamp = clamp || '...'
return value.length > length ? value.slice(0, length) + clamp : value
}
},
install: (app) => {
app.config.globalProperties.$yesno = (value) => {
return value ? $gettext('yes') : $gettext('no')
}
app.config.globalProperties.$date = (value) => {
return DateTime.fromISO(value)
.setLocale('en')
.toLocaleString(DateTime.DATETIME_MED)
}
app.config.globalProperties.$truncate = (value, length, clamp) => {
clamp = clamp || '...'
return value.length > length
? value.slice(0, length) + clamp
: value
}
},
}
+22 -22
View File
@@ -2,26 +2,26 @@ import { createGettext } from 'vue3-gettext'
import translations from '@/locale/translations.json'
export default createGettext({
availableLanguages: {
en: 'English',
cs: 'Czech',
de: 'German',
br: 'Breton',
el: 'Greek',
es: 'Spain',
fi: 'Finnish',
fr: 'French',
it: 'Italian',
ja: 'Japanese',
nl: 'Dutch',
pl: 'Polish',
pt: 'Portuguese',
ru: 'Russian',
sv: 'Swedish',
tr: 'tr',
zh: 'Chinese',
},
defaultLanguage: 'en',
translations: translations,
silent: true, // stop warnings
availableLanguages: {
en: 'English',
cs: 'Czech',
de: 'German',
br: 'Breton',
el: 'Greek',
es: 'Spain',
fi: 'Finnish',
fr: 'French',
it: 'Italian',
ja: 'Japanese',
nl: 'Dutch',
pl: 'Polish',
pt: 'Portuguese',
ru: 'Russian',
sv: 'Swedish',
tr: 'tr',
zh: 'Chinese',
},
defaultLanguage: 'en',
translations: translations,
silent: true, // stop warnings
})
+6 -7
View File
@@ -13,11 +13,10 @@ import pinia from '@/stores'
import VueApexCharts from 'vue3-apexcharts'
export function registerPlugins(app) {
app
.use(gettext)
.use(vuetify)
.use(pinia)
.use(router)
.use(filters)
.use(VueApexCharts)
app.use(gettext)
.use(vuetify)
.use(pinia)
.use(router)
.use(filters)
.use(VueApexCharts)
}

Some files were not shown because too many files have changed in this diff Show More