mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Improved pre-commit and linters config
This commit is contained in:
+12
-13
@@ -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
@@ -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
@@ -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',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
<template>
|
||||
<router-view />
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
+41
-41
@@ -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/`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import repository from './repository'
|
||||
|
||||
export default {
|
||||
getComponentsInformation() {
|
||||
return repository.get('/admin/components/')
|
||||
},
|
||||
getComponentsInformation() {
|
||||
return repository.get('/admin/components/')
|
||||
},
|
||||
}
|
||||
|
||||
+14
-14
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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}/`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,7 +3,7 @@ import repository from './repository'
|
||||
const resource = 'languages'
|
||||
|
||||
export default {
|
||||
getAll() {
|
||||
return repository.get(`/${resource}/`)
|
||||
},
|
||||
getAll() {
|
||||
return repository.get(`/${resource}/`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}/`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,7 +3,7 @@ import repository from './repository'
|
||||
const resource = 'transports'
|
||||
|
||||
export default {
|
||||
getAll() {
|
||||
return repository.get(`/${resource}/`)
|
||||
},
|
||||
getAll() {
|
||||
return repository.get(`/${resource}/`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+78
-78
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user