mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Merge pull request #2250 from modoboa/refacto/vuejs_interface
New admin interface using vuejs 2 and vuetify
This commit is contained in:
@@ -90,7 +90,7 @@ jobs:
|
||||
run: |
|
||||
python ./tests.py
|
||||
cd test_project
|
||||
python3 manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.transport modoboa.relaydomains modoboa.dnstools modoboa.ldapsync modoboa.policyd modoboa.maillog
|
||||
python3 manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.transport modoboa.relaydomains modoboa.dnstools modoboa.ldapsync modoboa.policyd modoboa.maillog modoboa.parameters
|
||||
env:
|
||||
# use localhost for the host here because we are running the job on the VM.
|
||||
# If we were running the job on in a container this would be postgres
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
run: |
|
||||
python ./tests.py
|
||||
cd test_project
|
||||
coverage run manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.transport modoboa.relaydomains modoboa.dnstools modoboa.ldapsync modoboa.policyd modoboa.maillog
|
||||
coverage run manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.transport modoboa.relaydomains modoboa.dnstools modoboa.ldapsync modoboa.policyd modoboa.maillog modoboa.parameters
|
||||
coverage combine
|
||||
env:
|
||||
# use localhost for the host here because we are running the job on the VM.
|
||||
@@ -156,7 +156,15 @@ jobs:
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
- name: Build frontend
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
cd frontend
|
||||
nvm install 12
|
||||
yarn install
|
||||
yarn build
|
||||
cd ..
|
||||
- name: Build packages
|
||||
run: |
|
||||
sudo apt-get install librrd-dev rrdtool libssl-dev gettext
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
|
||||
+10
-1
@@ -14,7 +14,16 @@ modoboa.iml
|
||||
.eggs/
|
||||
modoboa_test
|
||||
.coverage
|
||||
build/
|
||||
./build/
|
||||
dist/
|
||||
|
||||
# JS stuff
|
||||
node_modules
|
||||
/static/dist
|
||||
/frontend/webpack-stats.json
|
||||
|
||||
# TOX stuff
|
||||
test_project/modoboa.db
|
||||
|
||||
/.project
|
||||
/.pydevproject
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
include README.rst CONTRIBUTING.rst LICENSE requirements.txt
|
||||
include modoboa/admin/tests/dovecot
|
||||
recursive-include modoboa *.html *.js *.css *.png *.po *.mo *.json *.gif *.ttf *.woff *.woff2
|
||||
recursive-include frontend/dist *.js *.json *.ico *.html *.css *.png *.map
|
||||
recursive-include modoboa/core/templates *.tpl
|
||||
recursive-include modoboa/core/commands/templates *.tpl
|
||||
recursive-include doc *
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
@@ -0,0 +1,5 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,28 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/essential',
|
||||
'@vue/standard'
|
||||
],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/*.{j,t}s?(x)',
|
||||
'**/tests/unit/**/*.spec.{j,t}s?(x)'
|
||||
],
|
||||
env: {
|
||||
mocha: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,57 @@
|
||||
# On OSX the PATH variable isn't exported unless "SHELL" is also set, see: http://stackoverflow.com/a/25506676
|
||||
SHELL = /bin/bash
|
||||
NODE_BINDIR = ./node_modules/.bin
|
||||
export PATH := $(NODE_BINDIR):$(PATH)
|
||||
|
||||
INPUT_DIR = src
|
||||
# Where to write the files generated by this makefile.
|
||||
OUTPUT_DIR = .
|
||||
|
||||
# Available locales for the app.
|
||||
LOCALES = en fr
|
||||
|
||||
# Name of the generated .po files for each available locale.
|
||||
LOCALE_FILES ?= $(patsubst %,$(OUTPUT_DIR)/locale/%/LC_MESSAGES/app.po,$(LOCALES))
|
||||
|
||||
GETTEXT_HTML_SOURCES = $(shell find $(INPUT_DIR) -name '*.vue' -o -name '*.html' 2> /dev/null)
|
||||
GETTEXT_VUE_SOURCES = $(shell find $(INPUT_DIR) -name '*.vue')
|
||||
GETTEXT_JS_SOURCES = $(shell find $(INPUT_DIR) -name '*.vue' -o -name '*.js')
|
||||
|
||||
# Makefile Targets
|
||||
.PHONY: clean makemessages translations
|
||||
|
||||
clean:
|
||||
rm -f /tmp/template.pot $(INPUT_DIR)/translations.json
|
||||
|
||||
makemessages: /tmp/template.pot
|
||||
|
||||
translations: ./$(INPUT_DIR)/translations.json
|
||||
|
||||
# Create a main .pot template, then generate .po files for each available language.
|
||||
# Thanx to Systematic: https://github.com/Polyconseil/systematic/blob/866d5a/mk/main.mk#L167-L183
|
||||
/tmp/template.pot: $(GETTEXT_HTML_SOURCES)
|
||||
# `dir` is a Makefile built-in expansion function which extracts the directory-part of `$@`.
|
||||
# `$@` is a Makefile automatic variable: the file name of the target of the rule.
|
||||
# => `mkdir -p /tmp/`
|
||||
mkdir -p $(dir $@)
|
||||
which gettext-extract
|
||||
# Extract gettext strings from templates files and create a POT dictionary template.
|
||||
gettext-extract --quiet --output $@ $(GETTEXT_HTML_SOURCES)
|
||||
gettext-extract --startDelimiter "" --endDelimiter "" --quiet --output $@ $(GETTEXT_VUE_SOURCES)
|
||||
# Extract gettext strings from JavaScript files.
|
||||
xgettext --language=JavaScript --keyword=npgettext:1c,2,3 \
|
||||
--from-code=utf-8 --join-existing --no-wrap \
|
||||
--package-name=$(shell node -e "console.log(require('./package.json').name);") \
|
||||
--package-version=$(shell node -e "console.log(require('./package.json').version);") \
|
||||
--output $@ $(GETTEXT_JS_SOURCES)
|
||||
# Generate .po files for each available language.
|
||||
@for lang in $(LOCALES); do \
|
||||
export PO_FILE=$(OUTPUT_DIR)/locale/$$lang/LC_MESSAGES/app.po; \
|
||||
echo "msgmerge --update $$PO_FILE $@"; \
|
||||
mkdir -p $$(dirname $$PO_FILE); \
|
||||
[ -f $$PO_FILE ] && msgmerge --lang=$$lang --update $$PO_FILE $@ || msginit --no-translator --locale=$$lang --input=$@ --output-file=$$PO_FILE; \
|
||||
msgattrib --no-wrap --no-obsolete -o $$PO_FILE $$PO_FILE; \
|
||||
done;
|
||||
|
||||
$(INPUT_DIR)/translations.json: clean /tmp/template.pot
|
||||
gettext-compile --output $@ $(LOCALE_FILES)
|
||||
@@ -0,0 +1,29 @@
|
||||
# frontend
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Run your unit tests
|
||||
```
|
||||
yarn test:unit
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-10-31 11:03+0100\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: easygettext\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: src/components/layout/Navbar.vue:9 src/components/layout/Navbar.vue:71
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/domains/DomainList.vue:20
|
||||
#: src/components/domains/DomainList.vue:4
|
||||
msgid "Delete this domain"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/domains/DomainList.vue:30
|
||||
#: src/components/domains/DomainList.vue:1
|
||||
msgid "Domain"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/Navbar.vue:13 src/components/layout/Navbar.vue:75
|
||||
msgid "Domains"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/Navbar.vue:18 src/components/layout/Navbar.vue:80
|
||||
msgid "Identities"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/Topbar.vue:6 src/components/layout/Topbar.vue:53
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/Navbar.vue:23 src/components/layout/Navbar.vue:85
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/ConfirmDialog.vue:10
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/Navbar.vue:28 src/components/layout/Navbar.vue:90
|
||||
msgid "Parameters"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/ConfirmDialog.vue:13
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,70 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR Free Software Foundation, Inc.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"PO-Revision-Date: 2020-10-31 11:05+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#~ msgid ""
|
||||
#~ msgstr ""
|
||||
#~ "Project-Id-Version: \n"
|
||||
#~ "Report-Msgid-Bugs-To: \n"
|
||||
#~ "POT-Creation-Date: 2020-10-31 11:03+0100\n"
|
||||
#~ "Last-Translator: Automatically generated\n"
|
||||
#~ "Language-Team: none\n"
|
||||
#~ "Language: fr\n"
|
||||
#~ "MIME-Version: 1.0\n"
|
||||
#~ "Content-Type: text/plain; charset=UTF-8\n"
|
||||
#~ "Content-Transfer-Encoding: 8bit\n"
|
||||
#~ "Generated-By: easygettext\n"
|
||||
#~ "Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: src/components/layout/Navbar.vue:9 src/components/layout/Navbar.vue:71
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/domains/DomainList.vue:20
|
||||
#: src/components/domains/DomainList.vue:4
|
||||
msgid "Delete this domain"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/domains/DomainList.vue:30
|
||||
#: src/components/domains/DomainList.vue:1
|
||||
msgid "Domain"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/Navbar.vue:13 src/components/layout/Navbar.vue:75
|
||||
msgid "Domains"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/Navbar.vue:18 src/components/layout/Navbar.vue:80
|
||||
msgid "Identities"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/Topbar.vue:6 src/components/layout/Topbar.vue:53
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/Navbar.vue:23 src/components/layout/Navbar.vue:85
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/ConfirmDialog.vue:10
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/Navbar.vue:28 src/components/layout/Navbar.vue:90
|
||||
msgid "Parameters"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/layout/ConfirmDialog.vue:13
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "Modoboa",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"apexcharts": "^3.25.0",
|
||||
"core-js": "^3.6.5",
|
||||
"js-cookie": "^2.2.1",
|
||||
"luxon": "^1.26.0",
|
||||
"vee-validate": "^3.4.5",
|
||||
"vue": "^2.6.11",
|
||||
"vue-apexcharts": "^1.6.0",
|
||||
"vue-cookie": "^1.1.4",
|
||||
"vue-gettext": "^2.1.10",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuetify": "^2.2.11",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-unit-mocha": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"@vue/test-utils": "^1.0.3",
|
||||
"axios": "^0.18.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chai": "^4.1.2",
|
||||
"easygettext": "^2.15.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue-cli-plugin-axios": "~0.0.4",
|
||||
"vue-cli-plugin-vuetify": "~2.0.7",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuetify-loader": "^1.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"API_BASE_URL": "http://localhost:8000/api/v2"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Rubik:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<v-app v-if="!isAuthenticated">
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</v-app>
|
||||
<v-app v-else>
|
||||
<navbar />
|
||||
<v-main>
|
||||
<v-container fluid>
|
||||
<router-view/>
|
||||
</v-container>
|
||||
</v-main>
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
:color="notificationColor"
|
||||
:timeout="notificationTimeout"
|
||||
top
|
||||
>
|
||||
{{ notification }}
|
||||
|
||||
<template v-slot:action="{ attrs }">
|
||||
<v-btn
|
||||
color="white"
|
||||
text
|
||||
v-bind="attrs"
|
||||
@click="snackbar = false"
|
||||
>
|
||||
<translate>Close</translate>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { bus } from '@/main'
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isAuthenticated: 'auth/isAuthenticated'
|
||||
})
|
||||
},
|
||||
components: {
|
||||
Navbar
|
||||
},
|
||||
created () {
|
||||
// this.$store.dispatch('auth/initialize')
|
||||
bus.$on('notification', this.showNotification)
|
||||
},
|
||||
data: () => ({
|
||||
snackbar: false,
|
||||
notification: '',
|
||||
notificationColor: 'success',
|
||||
notificationTimeout: 2000
|
||||
}),
|
||||
methods: {
|
||||
showNotification (options) {
|
||||
this.notification = options.msg
|
||||
this.notificationColor = (options.type) ? options.type : 'success'
|
||||
this.snackbar = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-main {
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
import repository from './repository'
|
||||
|
||||
const resource = 'account'
|
||||
|
||||
export default {
|
||||
getMe () {
|
||||
return repository.get(`/${resource}/me/`)
|
||||
},
|
||||
verifyTFACode (code) {
|
||||
const payload = { code }
|
||||
return repository.post(`${resource}/tfa/verify/`, payload)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import repository from './repository'
|
||||
|
||||
const resource = 'aliases'
|
||||
|
||||
export default {
|
||||
get (aliasId) {
|
||||
return repository.get(`${resource}/${aliasId}/`)
|
||||
},
|
||||
getRandomAddress () {
|
||||
return repository.get(`${resource}/random_address/`)
|
||||
},
|
||||
validate (data) {
|
||||
return repository.post(`${resource}/validate/`, data)
|
||||
},
|
||||
create (data) {
|
||||
return repository.post(`${resource}/`, data)
|
||||
},
|
||||
patch (aliasId, data) {
|
||||
return repository.patch(`${resource}/${aliasId}/`, data)
|
||||
},
|
||||
delete (aliasId) {
|
||||
return repository.delete(`${resource}/${aliasId}/`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import repository from './repository'
|
||||
|
||||
export default {
|
||||
requestToken (data) {
|
||||
return repository.post('/token/', data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import repository from './repository'
|
||||
|
||||
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/`)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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}/`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import repository from './repository'
|
||||
|
||||
const resource = 'identities'
|
||||
|
||||
export default {
|
||||
getAll () {
|
||||
return repository.get(`${resource}/`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import repository from './repository'
|
||||
|
||||
const resource = 'logs'
|
||||
|
||||
export default {
|
||||
getAll (params) {
|
||||
return repository.get(`/${resource}/`, { params })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import Vue from 'vue'
|
||||
import axios from 'axios'
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
import router from '../router'
|
||||
import store from '../store'
|
||||
|
||||
const _axios = axios.create()
|
||||
|
||||
_axios.interceptors.request.use(
|
||||
function (config) {
|
||||
// Do something before request is sent
|
||||
config.baseURL = Vue.prototype.$config.API_BASE_URL
|
||||
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 !== 401 || router.currentRoute.path === '/login/') {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
const refreshToken = Cookies.get('refreshToken')
|
||||
if (error.config.url.endsWith('/token/refresh/') || !refreshToken) {
|
||||
store.dispatch('auth/logout')
|
||||
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).then(resp => {
|
||||
resolve(resp)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}).catch(error => {
|
||||
Promise.reject(error)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default _axios
|
||||
@@ -0,0 +1,17 @@
|
||||
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 })
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="43" height="52" viewBox="0 0 43 52">
|
||||
<g fill="#FFF">
|
||||
<path d="M21.9964108,21 L40,10.0508068 C36.1185185,3.7978167 29.3170048,0 22,0 C14.6829952,0 7.88148146,3.7978167 4,10.0508068 L21.9964108,21 Z"/>
|
||||
<path d="M21.4961468,25.9648072 L1.22178013,14 C-1.09566722,20.4385601 -0.0784112625,27.5770277 3.95066026,33.1495782 C7.97973179,38.7221288 14.5202355,42.0366892 21.4961468,42.0411831 L21.4961468,52 L37.2852914,35.2976413 C42.7029462,29.5713312 44.4301418,21.3781287 41.7705135,14.0213403 L21.4961468,25.9648072 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 589 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="144" height="23" viewBox="0 0 144 23">
|
||||
<g fill="#4067F3">
|
||||
<path d="M36.4615385 23C31.618426 23 27.6923077 19.0869687 27.6923077 14.26 27.6923077 9.43303129 31.618426 5.52 36.4615385 5.52 41.3046509 5.52 45.2307692 9.43303129 45.2307692 14.26 45.2273536 19.0855579 41.3032354 22.9965957 36.4615385 23zM36.4615385 8.58764124C34.1590485 8.58514416 32.0818832 9.96558568 31.1990219 12.085002 30.3161605 14.2044183 30.8015567 16.6452127 32.4287774 18.268776 34.0559981 19.8923393 36.5044257 20.3787746 38.6318903 19.5011641 40.759355 18.6235536 42.146674 16.5548164 42.1466754 14.26 42.1466772 11.1296516 39.6023544 8.5910475 36.4615385 8.58764124zM75.2307692 23C70.3876568 23 66.4615385 19.0869687 66.4615385 14.26 66.4615385 9.43303129 70.3876568 5.52 75.2307692 5.52 80.0738817 5.52 84 9.43303129 84 14.26 83.9931779 19.0841495 80.071053 22.9932006 75.2307692 23zM75.2307692 8.58764124C72.9282793 8.58514416 70.851114 9.96558568 69.9682526 12.085002 69.0853913 14.2044183 69.5707875 16.6452127 71.1980082 18.268776 72.8252289 19.8923393 75.2736565 20.3787746 77.4011211 19.5011641 79.5285857 18.6235536 80.9159048 16.5548164 80.9159061 14.26 80.9125011 11.1310581 78.3701703 8.59444145 75.2307692 8.58764124zM114.916884 23C110.074579 22.9965917 106.151568 19.082052 106.153847 14.2558869 106.156127 9.42972191 110.082834 5.51886495 114.92514 5.52000025 119.767446 5.52113555 123.692306 9.43383334 123.692308 14.2599989 123.688889 19.0879665 119.760999 23.0000012 114.916884 23zM114.916884 8.58763945C112.614956 8.58763945 110.539691 9.96966546 109.658781 12.0892809 108.777871 14.2088964 109.264797 16.6486793 110.892506 18.2709628 112.520215 19.8932462 114.968157 20.3785492 117.094862 19.5005757 119.221566 18.6226023 120.608214 16.5542542 120.608214 14.2599989 120.608214 11.1272413 118.060119 8.58763945 114.916884 8.58763945zM55.8492514 23C51.0044283 23 47.0769231 18.8810197 47.0769231 13.8 47.0769231 8.7189803 51.0044283 4.6 55.8492514 4.6 56.8766002 4.5965405 57.8962614 4.78577094 58.8600929 5.15875706 59.4051364 5.33485793 59.8130608 5.81197803 59.9207598 6.39934362 60.0284588 6.98670921 59.8183572 7.58846885 59.3744629 7.96401239 58.9305687 8.33955594 58.3253202 8.42759892 57.8007228 8.19293785 55.4709655 7.29069588 52.8523972 8.07824875 51.332652 10.1382557 49.8129068 12.1982627 49.7434905 15.0542594 51.1611883 17.1927933 52.5788861 19.3313273 55.1557949 20.2577715 57.5263569 19.4811837 59.8969188 18.7045959 61.5128404 16.4045952 61.517811 13.8L61.517811 1.62429379C61.517811.727221098 62.2112265 0 63.0665978 0 63.9219692 0 64.6153846.727221098 64.6153846 1.62429379L64.6153846 13.8C64.6119731 18.877001 60.690239 22.9928354 55.8492514 23zM1.60193429 22.9999875C1.17655379 23.0016372.768099475 22.8401747.467307584 22.5514683.166515693 22.2627618-.00170568823 21.8707187.0000130419293 21.4624295L.0000130419293 11.3540664C-.00197082639 8.50221341 1.86380136 5.95694981 4.66108711 4.99548616 7.45837285 4.03402251 10.583378 4.86389073 12.4679784 7.06866384 14.3583384 4.88137793 17.47279 4.06353266 20.2599678 5.02251172 23.0471455 5.98149079 24.9109863 8.51220784 24.9230769 11.3540664L24.9230769 21.4006802C24.9230769 22.2532603 24.202992 22.9444131 23.3147223 22.9444131 22.4264525 22.9444131 21.7063676 22.2532603 21.7063676 21.4006802L21.7063676 11.3540664C21.4804587 9.48564675 19.8317477 8.07620561 17.8720501 8.07620561 15.9123524 8.07620561 14.2636415 9.48564675 14.0377326 11.3540664 14.0377326 12.2066465 13.3176476 12.8977993 12.4293779 12.8977993 11.5411081 12.8977993 10.8210232 12.2066465 10.8210232 11.3540664 11.0034574 9.93240119 10.3157373 8.53716424 9.05485875 7.77089671 7.7939802 7.00462918 6.18586463 7.00462918 4.92498608 7.77089671 3.66410753 8.53716424 2.97638739 9.93240119 3.1588216 11.3540664L3.1588216 21.4624295C3.15916444 22.2950016 2.46901563 22.9765819 1.60193429 22.9999875z"/>
|
||||
<path d="M12.9156923 22.9998804C11.9001681 22.9998804 11.0769231 22.2748918 11.0769231 21.3805731L11.0769231 10.8258106C11.0769231 9.9279002 11.9034743 9.2 12.9230769 9.2 13.9426795 9.2 14.7692308 9.9279002 14.7692308 10.8258106L14.7692308 21.406586C14.7633906 21.8343382 14.564818 22.2425241 14.2172043 22.5413315 13.8695906 22.8401388 13.4014162 23.0050861 12.9156923 22.9998804zM95.5446545 23C90.7019524 22.9964221 86.7760599 18.8805854 86.7692308 13.8L86.7692308 1.62429379C86.7692308.727221098 87.4624014 0 88.3174707 0 89.1725399 0 89.8657106.727221098 89.8657106 1.62429379L89.8657106 13.8C89.868798 16.4082726 91.4852597 18.7126023 93.8580458 19.4912311 96.2308319 20.2698598 98.8107647 19.3425752 100.229961 17.2010245 101.649157 15.0594737 101.579147 12.1993156 100.057048 10.1370089 98.5349498 8.07470227 95.9130515 7.28756489 93.5814863 8.19293785 92.7974423 8.44634809 91.9615409 8.01134264 91.679451 7.20311338 91.3973611 6.39488412 91.7678578 5.49643578 92.5224902 5.15875706 93.4879673 4.78514308 94.5094074 4.59590492 95.5384615 4.6 100.381574 4.6 104.307692 8.7189803 104.307692 13.8 104.307692 18.8810197 100.381574 23 95.5384615 23L95.5446545 23zM142.445556 22.9993388C141.593908 22.9993388 140.903511 22.3112896 140.903511 21.4625378L140.903511 14.2599403C140.901244 11.7851712 139.287553 9.59820438 136.917889 8.85839914 134.548226 8.11859391 131.970942 8.99714448 130.552311 11.0283119 129.133681 13.0594793 129.201983 15.7732422 130.721008 17.7309752 132.240032 19.6887083 134.858272 20.4373832 137.187739 19.5801109 137.708256 19.3810598 138.296049 19.4775172 138.725046 19.8323847 139.154043 20.1872523 139.35743 20.74526 139.256982 21.2917934 139.156534 21.8383268 138.767896 22.2882643 138.240541 22.4685562 134.644788 23.7780393 130.612248 22.6115365 128.278807 19.586901 125.945366 16.5622654 125.850397 12.3786386 128.044202 9.25193189 130.238007 6.12522521 134.213489 4.77817384 137.864984 5.92426076 141.516478 7.07034769 143.999946 10.4446561 143.999987 14.2599403L143.999987 21.4625378C144.001677 21.8727833 143.838048 22.2665335 143.545807 22.5554607 143.253565 22.8443878 142.857175 23.004305 142.445556 22.9993388z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">
|
||||
<translate v-if="dialogMode" class="headline">Administrators of {{ domain.name }}</translate>
|
||||
<translate v-else class="headline">Administrators</translate>
|
||||
</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="administrators"
|
||||
hide-default-footer
|
||||
>
|
||||
<template v-slot:item.username="{ item }">
|
||||
<span v-if="item.id">{{ item.username }}</span>
|
||||
<v-autocomplete
|
||||
v-else
|
||||
v-model="selectedAccount"
|
||||
:label="'Select an account'|translate"
|
||||
:items="accounts"
|
||||
item-text="username"
|
||||
return-object
|
||||
>
|
||||
<template v-slot:append-outer>
|
||||
<v-btn v-if="selectedAccount" icon color="primary" @click="addAdministrator">
|
||||
<v-icon>mdi-content-save</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
<template v-slot:item.name="{ item }">
|
||||
{{ item.first_name }} {{ item.last_name }}
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn v-if="item.id"
|
||||
icon
|
||||
color="red"
|
||||
@click="removeAdministrator(item)"
|
||||
:title="'Remove this administrator'|translate"
|
||||
>
|
||||
<v-icon>mdi-delete-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<v-btn v-if="!hideAddBtn"
|
||||
small
|
||||
color="primary"
|
||||
outlined
|
||||
@click="addRow"
|
||||
>
|
||||
<v-icon small>mdi-plus</v-icon> <translate>Add administrator</translate>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
<v-card-actions v-if="dialogMode">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="grey darken-1"
|
||||
text
|
||||
@click="close"
|
||||
>
|
||||
<translate>Close</translate>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import accounts from '@/api/accounts'
|
||||
import domains from '@/api/domains'
|
||||
import { bus } from '@/main'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
domain: Object,
|
||||
dialogMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
accounts: [],
|
||||
administrators: [],
|
||||
headers: [
|
||||
{ text: this.$gettext('Username'), value: 'username' },
|
||||
{ text: this.$gettext('Name'), value: 'name' },
|
||||
{ text: '', value: 'actions', align: 'right', sortable: false }
|
||||
],
|
||||
hideAddBtn: false,
|
||||
selectedAccount: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addRow () {
|
||||
this.administrators.push({})
|
||||
this.hideAddBtn = true
|
||||
},
|
||||
close () {
|
||||
this.$emit('close')
|
||||
this.hideAddBtn = false
|
||||
},
|
||||
fetchAdministrators (domain) {
|
||||
domains.getDomainAdministrators(domain.pk).then(resp => {
|
||||
this.administrators = resp.data
|
||||
})
|
||||
},
|
||||
fetchAccounts () {
|
||||
accounts.getAll({ role: 'DomainAdmins' }).then(resp => {
|
||||
this.accounts = resp.data.filter(
|
||||
account => this.administrators.find(admin => admin.id === account.pk) === undefined
|
||||
)
|
||||
})
|
||||
},
|
||||
addAdministrator () {
|
||||
domains.addDomainAdministrator(this.domain.pk, this.selectedAccount.pk).then(resp => {
|
||||
this.fetchAdministrators(this.domain)
|
||||
this.accounts = this.accounts.filter(account => account.pk !== this.selectedAccount.pk)
|
||||
this.selectedAccount = null
|
||||
this.hideAddBtn = false
|
||||
bus.$emit('notification', { msg: this.$gettext('Administrator added') })
|
||||
})
|
||||
},
|
||||
removeAdministrator (admin) {
|
||||
domains.removeDomainAdministrator(this.domain.pk, admin.id).then(resp => {
|
||||
this.fetchAdministrators(this.domain)
|
||||
this.fetchAccounts()
|
||||
bus.$emit('notification', { msg: this.$gettext('Administrator removed') })
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
domain: {
|
||||
handler: function (value) {
|
||||
this.fetchAdministrators(value)
|
||||
this.fetchAccounts()
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">DNS</span>
|
||||
<v-btn icon :title="'DNS configuration help'|translate" @click="showConfigHelp = true">
|
||||
<v-icon>mdi-information-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<translate class="overline">MX records</translate>
|
||||
<v-data-table
|
||||
:headers="mxRecordHeaders"
|
||||
:items="detail.mx_records"
|
||||
hide-default-footer
|
||||
>
|
||||
<template v-slot:item.updated="{ item }">
|
||||
{{ item.updated|date }}
|
||||
</template>
|
||||
</v-data-table>
|
||||
<translate class="overline">Auto configuration</translate>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-if="detail.autoconfig_record"
|
||||
color="success"
|
||||
>
|
||||
<translate>autoconfig record (Mozilla): {{ detail.autoconfig_record.value }}</translate>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="warning"
|
||||
>
|
||||
<translate>autoconfig record (Mozilla) not found</translate>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-if="detail.autodiscover_record"
|
||||
color="success"
|
||||
>
|
||||
<translate>autodiscover record (Microsoft): {{ detail.autodiscover_record.value }}</translate>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="warning"
|
||||
>
|
||||
<translate>autodiscover record (Microsoft) not found</translate>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<translate class="overline">Authentication</translate>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-if="detail.spf_record"
|
||||
color="success"
|
||||
>
|
||||
<translate>SPF record found</translate>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="error"
|
||||
>
|
||||
<translate>SPF record not found</translate>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-if="detail.dkim_record"
|
||||
color="success"
|
||||
>
|
||||
<translate>DKIM record found</translate>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="error"
|
||||
>
|
||||
<translate>DKIM record not found</translate>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-if="detail.dmarc_record"
|
||||
color="success"
|
||||
>
|
||||
<translate>DMARC record found</translate>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="error"
|
||||
>
|
||||
<translate>DMARC record not found</translate>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="6"><translate>DKIM key</translate></v-col>
|
||||
<v-col cols="6">
|
||||
<div v-if="domain.dkim_public_key">
|
||||
<v-btn color="primary" small>
|
||||
<translate>Show key</translate>
|
||||
</v-btn>
|
||||
<v-btn icon><v-icon>mdi-refresh</v-icon></v-btn>
|
||||
</div>
|
||||
<translate v-else>Not generated</translate>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-dialog v-model="showConfigHelp"
|
||||
max-width="800px"
|
||||
persistent
|
||||
>
|
||||
<domain-dns-config :domain="domain" @close="showConfigHelp = false" />
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import domains from '@/api/domains'
|
||||
import DomainDNSConfig from './DomainDNSConfig'
|
||||
|
||||
export default {
|
||||
props: ['domain'],
|
||||
components: {
|
||||
'domain-dns-config': DomainDNSConfig
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
detail: {},
|
||||
mxRecordHeaders: [
|
||||
{ text: this.$gettext('Name'), value: 'name' },
|
||||
{ text: this.$gettext('Address'), value: 'address' },
|
||||
{ text: this.$gettext('Updated'), value: 'updated' }
|
||||
],
|
||||
showConfigHelp: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
watch: {
|
||||
domain: {
|
||||
handler: function (val) {
|
||||
domains.getDomainDNSDetail(val.pk).then(resp => {
|
||||
this.detail = resp.data
|
||||
})
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<v-chip :color="chipType" small>
|
||||
{{ label }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['status'],
|
||||
computed: {
|
||||
label () {
|
||||
if (this.status === 'disabled') {
|
||||
return this.$gettext('Disabled')
|
||||
}
|
||||
if (this.status === 'pending') {
|
||||
return this.$gettext('Pending')
|
||||
}
|
||||
if (this.status === 'critical') {
|
||||
return this.$gettext('Problem')
|
||||
}
|
||||
if (this.status === 'ok') {
|
||||
return this.$gettext('Valid')
|
||||
}
|
||||
return this.$gettext('Unknown')
|
||||
},
|
||||
chipType () {
|
||||
if (this.status === 'disabled') {
|
||||
return ''
|
||||
}
|
||||
if (this.status === 'pending') {
|
||||
return 'info'
|
||||
}
|
||||
if (this.status === 'critical') {
|
||||
return 'error'
|
||||
}
|
||||
if (this.status === 'ok') {
|
||||
return 'success'
|
||||
}
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">{{ title }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
name="name"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label">{{ $gettext('Name') }}</label>
|
||||
<v-text-field
|
||||
v-model="form.name"
|
||||
dense
|
||||
outlined
|
||||
:error-messages="errors"
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
name="target"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label">{{ $gettext('Choose a domain') }}</label>
|
||||
<v-select
|
||||
v-model="form.target"
|
||||
:items="domains"
|
||||
item-text="name"
|
||||
item-value="pk"
|
||||
:error-messages="errors"
|
||||
single-line
|
||||
dense
|
||||
outlined
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-switch
|
||||
v-model="form.enabled"
|
||||
label="Enabled"
|
||||
color="primary"
|
||||
/>
|
||||
</validation-observer>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="grey darken-1"
|
||||
text
|
||||
@click="close"
|
||||
>
|
||||
<translate>Close</translate>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="domainAlias && domainAlias.pk"
|
||||
color="error"
|
||||
text
|
||||
@click="deleteAlias"
|
||||
>
|
||||
<translate>Delete</translate>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary darken-1"
|
||||
text
|
||||
@click="submit"
|
||||
>
|
||||
{{ submitLabel }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { bus } from '@/main'
|
||||
import domains from '@/api/domains'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
domainAlias: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
domains: 'domains/domains'
|
||||
}),
|
||||
submitLabel () {
|
||||
return (this.domainAlias) ? this.$gettext('Update') : this.$gettext('Add')
|
||||
},
|
||||
title () {
|
||||
return (this.domainAlias) ? this.$gettext('Edit domain alias') : this.$gettext('Add a new domain alias')
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
form: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$emit('close')
|
||||
this.form = {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
deleteAlias () {
|
||||
domains.deleteDomainAlias(this.domainAlias.pk).then(resp => {
|
||||
this.$emit('alias-deleted')
|
||||
bus.$emit('notification', { msg: this.$gettext('Domain alias deleted') })
|
||||
})
|
||||
},
|
||||
async submit () {
|
||||
const valid = await this.$refs.observer.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (!this.form.pk) {
|
||||
await this.$store.dispatch('domains/addDomainAlias', this.form).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Domain alias created') })
|
||||
this.close()
|
||||
})
|
||||
} else {
|
||||
const data = JSON.parse(JSON.stringify(this.form))
|
||||
data.target = data.target.pk
|
||||
await this.$store.dispatch('domains/updateDomainAlias', { id: this.form.pk, data: data }).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Domain alias updated') })
|
||||
this.close()
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
this.$refs.observer.setErrors(error.response.data)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.domainAlias) {
|
||||
this.form = JSON.parse(JSON.stringify(this.domainAlias))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
domainAlias (value) {
|
||||
if (value) {
|
||||
this.form = JSON.parse(JSON.stringify(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar flat color="white">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
append-icon="mdi-magnify"
|
||||
label="Search"
|
||||
single-line
|
||||
hide-details
|
||||
></v-text-field>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn class="mr-2" fab small>
|
||||
<v-icon>mdi-file-import-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn class="mr-2" fab small @click="exportList" title="Export data">
|
||||
<v-icon>mdi-file-export-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn fab small color="primary" @click="showForm = true">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-data-table :headers="headers" :items="domainAliases" :search="search" class="elevation-1">
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn icon small title="Edit this domain alias" @click="editDomainAlias(item)">
|
||||
<v-icon>mdi-circle-edit-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon small title="Remove" @click="confirmDelete(item)">
|
||||
<v-icon>mdi-delete-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.name="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template v-slot:item.dns_status="{ }">
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<confirm-dialog v-model="showConfirmDialog"
|
||||
:message="deleteDomainMsg"
|
||||
@confirm="deleteDomainAlias" />
|
||||
|
||||
<v-dialog v-model="showForm"
|
||||
persistent
|
||||
max-width="800px">
|
||||
<domain-alias-form @close="closeForm" :domain-alias="selectedDomainAlias" />
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog'
|
||||
import DomainAliasForm from '@/components/domains/DomainAliasForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
DomainAliasForm
|
||||
},
|
||||
computed: mapGetters({
|
||||
domainAliases: 'domains/domainAliases'
|
||||
}),
|
||||
data () {
|
||||
return {
|
||||
headers: [
|
||||
{ text: '', value: 'actions', sortable: false },
|
||||
{ text: 'Name', value: 'name' },
|
||||
{ text: 'Target', value: 'target.name' },
|
||||
{ text: 'DNS status', value: 'dns_status', sortable: false }
|
||||
],
|
||||
deleteDomainMsg: 'Confirm deletion?',
|
||||
showConfirmDialog: false,
|
||||
showForm: false,
|
||||
search: '',
|
||||
selectedDomainAlias: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('domains/getDomainAliases')
|
||||
},
|
||||
methods: {
|
||||
closeForm () {
|
||||
this.showForm = false
|
||||
this.selectedDomainAlias = null
|
||||
},
|
||||
editDomainAlias (domainAlias) {
|
||||
this.selectedDomainAlias = domainAlias
|
||||
this.showForm = true
|
||||
},
|
||||
confirmDelete (domainAlias) {
|
||||
this.selectedDomainAlias = domainAlias
|
||||
this.showConfirmDialog = true
|
||||
},
|
||||
deleteDomainAlias () {
|
||||
this.$store.dispatch('domains/deleteDomainAlias', this.selectedDomainAlias.pk)
|
||||
},
|
||||
getDNSTagType (value) {
|
||||
if (value === 'unknown') {
|
||||
return 'orange'
|
||||
}
|
||||
if (value === 'ok') {
|
||||
return 'warning'
|
||||
}
|
||||
return 'success'
|
||||
},
|
||||
exportList () {
|
||||
this.$axios.get('/domainaliases/', { headers: { Accept: 'text/csv' } }).then(resp => {
|
||||
const url = window.URL.createObjectURL(
|
||||
new Blob([resp.data], { type: 'text/csv' }))
|
||||
const link = document.createElement('a')
|
||||
|
||||
link.style.display = 'none'
|
||||
link.href = url
|
||||
link.setAttribute('download', 'domain_aliases.csv')
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(link.href)
|
||||
document.body.removeChild(link)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<creation-form
|
||||
:title="'New domain'|translate"
|
||||
:steps="steps"
|
||||
:form-observer-getter="getObserver"
|
||||
:validate-object="validateDomain"
|
||||
:summary-sections="summarySections"
|
||||
@close="close"
|
||||
@create="submit"
|
||||
>
|
||||
<template v-slot:form.general="{ step }">
|
||||
<domain-general-form :ref="`form_${step}`" :domain="domain" />
|
||||
</template>
|
||||
<template v-slot:form.dns="{ step }">
|
||||
<domain-dns-form :ref="`form_${step}`" :domain="domain" />
|
||||
</template>
|
||||
<template v-slot:form.limitations="{ step }">
|
||||
<domain-limitations-form :ref="`form_${step}`" :domain="domain" />
|
||||
</template>
|
||||
<template v-slot:form.options="{ step }">
|
||||
<domain-options-form :ref="`form_${step}`" :domain="domain" @createAdmin="updateCreateAdmin" />
|
||||
</template>
|
||||
<template v-slot:item.with_random_password="{ item }">
|
||||
<template v-if="item.value">
|
||||
<v-col cols="12" class="highligth white--text">
|
||||
<v-row>
|
||||
<v-col><span>{{ item.key }}</span></v-col>
|
||||
<v-col class="text-right">{{ item.value|yesno }}</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="text-right py-1">
|
||||
{{ domain.domain_admin.password }}
|
||||
<v-btn icon color="white" :title="'Copy to clipboard'|translate" @click="copyPassword">
|
||||
<v-icon>mdi-clipboard-multiple-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-col><span class="grey--text">{{ item.key }}</span></v-col>
|
||||
<v-col class="text-right">{{ item.value|yesno }}</v-col>
|
||||
</template>
|
||||
</template>
|
||||
</creation-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import CreationForm from '@/components/tools/CreationForm'
|
||||
import DomainDNSForm from './DomainDNSForm'
|
||||
import DomainGeneralForm from './DomainGeneralForm'
|
||||
import DomainLimitationsForm from './DomainLimitationsForm'
|
||||
import DomainOptionsForm from './DomainOptionsForm'
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
components: {
|
||||
CreationForm,
|
||||
'domain-dns-form': DomainDNSForm,
|
||||
DomainGeneralForm,
|
||||
DomainLimitationsForm,
|
||||
DomainOptionsForm
|
||||
},
|
||||
computed: {
|
||||
summarySections () {
|
||||
const result = [
|
||||
{
|
||||
title: this.$gettext('General'),
|
||||
items: [
|
||||
{ key: this.$gettext('Name'), value: this.domain.name },
|
||||
{ key: this.$gettext('Type'), value: this.domain.type },
|
||||
{ key: this.$gettext('Enabled'), value: this.domain.enabled, type: 'yesno' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: this.$gettext('DNS'),
|
||||
items: [
|
||||
{ key: this.$gettext('Enable DNS checks'), value: this.domain.enable_dns_checks, type: 'yesno' },
|
||||
{ key: this.$gettext('Enable DKIM signing'), value: this.domain.enable_dkim, type: 'yesno' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: this.$gettext('Limitations'),
|
||||
items: [
|
||||
{ key: this.$gettext('Quota'), value: this.domain.quota },
|
||||
{ key: this.$gettext('Default mailbox quota'), value: this.domain.default_mailbox_quota },
|
||||
{ key: this.$gettext('Message sending limit'), value: this.domain.message_sending_limit }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: this.$gettext('Options'),
|
||||
items: [
|
||||
{ key: this.$gettext('Create a domain administrator'), value: this.createAdmin, type: 'yesno' }
|
||||
]
|
||||
}
|
||||
]
|
||||
if (this.domain.enable_dkim) {
|
||||
result[1].items.push({ key: this.$gettext('DKIM key selector'), value: this.domain.dkim_key_selector })
|
||||
result[1].items.push({ key: this.$gettext('DKIM key length'), value: this.domain.dkim_key_length })
|
||||
}
|
||||
if (this.createAdmin) {
|
||||
result[3].items.push({ key: this.$gettext('Administrator name'), value: this.domain.domain_admin.username })
|
||||
result[3].items.push({
|
||||
name: 'with_random_password',
|
||||
key: this.$gettext('Random password'),
|
||||
value: this.domain.domain_admin.with_random_password,
|
||||
type: 'yesno'
|
||||
})
|
||||
result[3].items.push({
|
||||
key: this.$gettext('With mailbox'),
|
||||
value: this.domain.domain_admin.with_mailbox,
|
||||
type: 'yesno'
|
||||
})
|
||||
result[3].items.push({
|
||||
key: this.$gettext('Create aliases'),
|
||||
value: this.domain.domain_admin.with_aliases,
|
||||
type: 'yesno'
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
createAdmin: false,
|
||||
domain: {
|
||||
domain_admin: {}
|
||||
},
|
||||
formErrors: {},
|
||||
steps: [
|
||||
{ name: 'general', title: this.$gettext('General') },
|
||||
{ name: 'dns', title: this.$gettext('DNS') },
|
||||
{ name: 'limitations', title: this.$gettext('Limitations') },
|
||||
{ name: 'options', title: this.$gettext('Options') }
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.initDomain()
|
||||
},
|
||||
methods: {
|
||||
copyPassword () {
|
||||
navigator.clipboard.writeText(this.domain.domain_admin.password).then(() => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Password copied to clipboard') })
|
||||
})
|
||||
},
|
||||
initDomain () {
|
||||
this.domain = {
|
||||
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',
|
||||
with_random_password: false,
|
||||
with_mailbox: false,
|
||||
with_aliases: false
|
||||
}
|
||||
}
|
||||
},
|
||||
close () {
|
||||
this.initDomain()
|
||||
this.$emit('close')
|
||||
},
|
||||
getObserver (step) {
|
||||
return this.$refs[`form_${step}`].$refs.observer
|
||||
},
|
||||
validateDomain () {
|
||||
|
||||
},
|
||||
updateCreateAdmin (value) {
|
||||
this.createAdmin = value
|
||||
},
|
||||
submit () {
|
||||
const data = JSON.parse(JSON.stringify(this.domain))
|
||||
if (!this.createAdmin) {
|
||||
delete data.domain_admin
|
||||
}
|
||||
this.$store.dispatch('domains/createDomain', data).then(resp => {
|
||||
this.$router.push({ name: 'DomainDetail', params: { id: resp.data.pk } })
|
||||
bus.$emit('notification', { msg: this.$gettext('Domain created') })
|
||||
this.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.highligth {
|
||||
background-color: #515D78;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">
|
||||
<translate>DNS configuration for </translate> {{ domain.name }}
|
||||
</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p>Here is an example of DNS configuration (bind format). Replace values between [ ] by the appropriate ones.</p>
|
||||
<v-alert
|
||||
border="left"
|
||||
colored-border
|
||||
color="primary lighten-3"
|
||||
elevation="2"
|
||||
dense
|
||||
>
|
||||
<div class="title">MX</div>
|
||||
<pre>
|
||||
mail.{{ domain.name }}. IN A <strong>[<translate>IP address of your Modoboa server</translate>]</strong>
|
||||
@ IN MX 10 mail.{{ domain.name }}.</pre>
|
||||
</v-alert>
|
||||
<v-alert
|
||||
border="left"
|
||||
colored-border
|
||||
color="primary lighten-3"
|
||||
elevation="2"
|
||||
dense
|
||||
>
|
||||
<div class="title">SPF</div>
|
||||
<pre>
|
||||
@ IN TXT "v=spf1 mx ~all"</pre>
|
||||
</v-alert>
|
||||
|
||||
<v-alert
|
||||
border="left"
|
||||
colored-border
|
||||
color="primary lighten-3"
|
||||
elevation="2"
|
||||
dense
|
||||
>
|
||||
<div class="title">DMARC</div>
|
||||
<pre>
|
||||
_dmarc.{{ domain.name }}. IN TXT "v=DMARC1; p=quarantine; pct=100"</pre>
|
||||
</v-alert>
|
||||
<v-alert
|
||||
border="left"
|
||||
colored-border
|
||||
color="primary lighten-3"
|
||||
elevation="2"
|
||||
dense
|
||||
>
|
||||
<div class="title">Auto configuration</div>
|
||||
<pre>
|
||||
autoconfig.{{ domain.name }}. IN CNAME <strong>[<translate>hostname of your automx server</translate>]</strong>
|
||||
autodiscover.{{ domain.name }}. IN CNAME <strong>[<translate>hostname of your automx server</translate>]</strong></pre>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey darken-1" text @click="close">
|
||||
<translate>Close</translate>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['domain'],
|
||||
methods: {
|
||||
close () {
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-switch
|
||||
:label="'Enable DNS checks' | translate"
|
||||
v-model="domain.enable_dns_checks"
|
||||
/>
|
||||
<v-switch
|
||||
:label="'Enable DKIM signing' | translate"
|
||||
v-model="domain.enable_dkim"
|
||||
/>
|
||||
<v-text-field
|
||||
:label="'DKIM key selector' | translate"
|
||||
v-model="domain.dkim_key_selector"
|
||||
:disabled="!domain.enable_dkim"
|
||||
outlined
|
||||
/>
|
||||
<choice-field
|
||||
v-model="domain.dkim_key_length"
|
||||
:label="'DKIM key length' | translate"
|
||||
:choices="dkimKeyLengths"
|
||||
:disabled="!domain.enable_dkim"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChoiceField from '@/components/tools/ChoiceField'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChoiceField
|
||||
},
|
||||
props: ['domain'],
|
||||
data () {
|
||||
return {
|
||||
dkimKeyLengths: [
|
||||
{
|
||||
label: '1024',
|
||||
value: 1024
|
||||
},
|
||||
{
|
||||
label: '2048',
|
||||
value: 2048
|
||||
},
|
||||
{
|
||||
label: '4096',
|
||||
value: 4096
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-expansion-panels v-model="panel">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header v-slot="{ open }">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="4">
|
||||
<translate>General</translate>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="text--secondary"
|
||||
>
|
||||
<v-fade-transition leave-absolute>
|
||||
<span v-if="open"></span>
|
||||
<v-row
|
||||
v-else
|
||||
no-gutters
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-col cols="6">
|
||||
<translate>Name: </translate> {{ editedDomain.name }}
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<translate>Type: </translate> {{ editedDomain.type }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<domain-general-form ref="generalForm" :domain="editedDomain" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header v-slot="{ open }">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="4">
|
||||
<translate>DNS</translate>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="text--secondary"
|
||||
>
|
||||
<v-fade-transition leave-absolute>
|
||||
<span v-if="open"></span>
|
||||
<v-row
|
||||
v-else
|
||||
no-gutters
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-col cols="6">
|
||||
<translate class="mr-2">DNS checks</translate>
|
||||
<v-icon color="success" v-if="editedDomain.enable_dns_checks">mdi-check-circle-outline</v-icon>
|
||||
<v-icon v-else>mdi-close-circle-outline</v-icon>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<translate class="mr-2">DKIM signing</translate>
|
||||
<v-icon color="success" v-if="editedDomain.enable_dkim">mdi-check-circle-outline</v-icon>
|
||||
<v-icon v-else>mdi-close-circle-outline</v-icon>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<domain-dns-form ref="dnsForm" :domain="editedDomain" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header v-slot="{ open }">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="4">
|
||||
<translate>Limitations</translate>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="text--secondary"
|
||||
>
|
||||
<v-fade-transition leave-absolute>
|
||||
<span v-if="open"></span>
|
||||
<v-row
|
||||
v-else
|
||||
no-gutters
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-col cols="6">
|
||||
<translate class="mr-2">Quota: </translate> {{ domain.quota }}
|
||||
</v-col>
|
||||
<v-col cols="6" v-if="domain.message_sending_limit">
|
||||
<translate class="mr-2">Sending limit: </translate> {{ domain.message_sending_limit }}
|
||||
</v-col>
|
||||
<v-col cols="6" v-else>
|
||||
<translate class="mr-2">No sending limit</translate>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<domain-limitations-form :domain="editedDomain" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<div class="mt-4 d-flex justify-end">
|
||||
<v-btn color="grey lighten-1" @click="$router.go(-1)">
|
||||
<translate>Cancel</translate>
|
||||
</v-btn>
|
||||
<v-btn class="ml-4" color="primary darken-1" @click="save">
|
||||
<translate>Save</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import DomainDNSForm from './DomainDNSForm'
|
||||
import DomainGeneralForm from './DomainGeneralForm'
|
||||
import DomainLimitationsForm from './DomainLimitationsForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'domain-dns-form': DomainDNSForm,
|
||||
DomainGeneralForm,
|
||||
DomainLimitationsForm
|
||||
},
|
||||
props: ['domain'],
|
||||
data () {
|
||||
return {
|
||||
editedDomain: {},
|
||||
panel: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save () {
|
||||
if (this.$refs.generalForm !== undefined) {
|
||||
const valid = await this.$refs.generalForm.$refs.observer.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.$store.dispatch('domains/updateDomain', this.editedDomain).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Domain updated') })
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
domain: {
|
||||
handler: function (val) {
|
||||
if (val) {
|
||||
this.editedDomain = JSON.parse(JSON.stringify(val))
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<v-text-field
|
||||
:label="'Domain name (ex: domain.tld)' | translate"
|
||||
v-model="domain.name"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<choice-field v-model="domain.type"
|
||||
:label="'Type' | translate"
|
||||
:choices="domainTypes"
|
||||
:error-messages="errors"
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-switch
|
||||
:label="'Enabled' | translate"
|
||||
v-model="domain.enabled"
|
||||
:hint="'Control if this domain will be allowed to send and receive messages' | translate"
|
||||
persistent-hint
|
||||
/>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChoiceField from '@/components/tools/ChoiceField'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChoiceField
|
||||
},
|
||||
props: ['domain'],
|
||||
data () {
|
||||
return {
|
||||
domainTypes: [
|
||||
{
|
||||
label: 'Domain',
|
||||
icon: 'mdi-earth',
|
||||
value: 'domain'
|
||||
},
|
||||
{
|
||||
label: 'Relay domain',
|
||||
icon: 'mdi-earth',
|
||||
value: 'relaydomain'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<v-text-field
|
||||
:label="'Quota' | translate"
|
||||
:hint="'Quota shared between mailboxes. Can be expressed in KB, MB (default) or GB. A value of 0 means no quota.' | translate"
|
||||
persistent-hint
|
||||
v-model="domain.quota"
|
||||
:error-messages="errors"
|
||||
class="mb-4"
|
||||
outlined
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<v-text-field
|
||||
:label="'Default mailbox quota' | translate"
|
||||
:hint="'Default quota applied to mailboxes. Can be expressed in KB, MB (default) or GB. A value of 0 means no quota.' | translate"
|
||||
persistent-hint
|
||||
v-model="domain.default_mailbox_quota"
|
||||
:error-messages="errors"
|
||||
class="mb-4"
|
||||
outlined
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="numeric"
|
||||
>
|
||||
<v-text-field
|
||||
:label="'Message sending limit' | translate"
|
||||
:hint="'Number of messages this domain can send per day. Leave empty for no limit.' | translate"
|
||||
persistent-hint
|
||||
v-model="domain.message_sending_limit"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
/>
|
||||
</validation-provider>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['domain']
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<v-card class="mt-6">
|
||||
<v-toolbar flat>
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" small>
|
||||
Actions <v-icon right>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
placeholder="Search"
|
||||
filled
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-toolbar>
|
||||
<v-data-table v-model="selected"
|
||||
:headers="headers"
|
||||
:items="domains"
|
||||
:search="search"
|
||||
item-key="name"
|
||||
class="elevation-1"
|
||||
show-select
|
||||
single-expand
|
||||
:expanded.sync="expanded"
|
||||
@item-expanded="loadAliases"
|
||||
@click:row="showAliases"
|
||||
>
|
||||
<template v-slot:item="{ item, isExpanded, expand }">
|
||||
<tr>
|
||||
<td>
|
||||
<v-checkbox />
|
||||
</td>
|
||||
<td>
|
||||
<router-link :to="{ name: 'DomainDetail', params: { id: item.pk } }">
|
||||
{{ item.name }}
|
||||
</router-link>
|
||||
<translate v-if="!item.enabled" class="ml-2 grey--text">(disabled)</translate>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.domainalias_count }} aliases
|
||||
<v-btn v-if="item.domainalias_count"
|
||||
icon
|
||||
@click="expand(!isExpanded)"
|
||||
>
|
||||
<v-icon v-if="!isExpanded">mdi-chevron-down</v-icon>
|
||||
<v-icon v-else>mdi-chevron-up</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>
|
||||
<dns-status-chip :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 v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on">
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<menu-items :items="domainMenuItems" :object="item" />
|
||||
</v-menu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-slot:expanded-item="{ headers, item }">
|
||||
<tr class="grey lighten-4">
|
||||
<td :colspan="headers.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 x-small color="success">DNS OK</v-chip>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<confirm-dialog ref="confirm">
|
||||
<v-checkbox v-model="keepDomainFolder"
|
||||
:label="'Do not delete domain folder'|translate"
|
||||
hide-details
|
||||
/>
|
||||
</confirm-dialog>
|
||||
<v-dialog v-model="showAliasForm"
|
||||
persistent
|
||||
max-width="800px">
|
||||
<domain-alias-form
|
||||
@close="closeDomainAliasForm"
|
||||
:domain-alias="selectedDomainAlias"
|
||||
@alias-deleted="domainAliasDeleted"
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showAdminList"
|
||||
persistent
|
||||
max-width="800px"
|
||||
>
|
||||
<administrator-list
|
||||
:domain="selectedDomain"
|
||||
@close="showAdminList = false"
|
||||
dialog-mode
|
||||
/>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { bus } from '@/main'
|
||||
import domainApi from '@/api/domains'
|
||||
import AdministratorList from './AdministratorList'
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog'
|
||||
import DNSStatusChip from './DNSStatusChip'
|
||||
import DomainAliasForm from '@/components/domains/DomainAliasForm'
|
||||
import MenuItems from '@/components/tools/MenuItems'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AdministratorList,
|
||||
ConfirmDialog,
|
||||
'dns-status-chip': DNSStatusChip,
|
||||
DomainAliasForm,
|
||||
MenuItems
|
||||
},
|
||||
computed: mapGetters({
|
||||
domains: 'domains/domains'
|
||||
}),
|
||||
data () {
|
||||
return {
|
||||
domainMenuItems: [
|
||||
{ label: this.$gettext('Administrators'), icon: 'mdi-account-supervisor', onClick: this.openAdminList },
|
||||
{ label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editDomain },
|
||||
{ label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteDomain, color: 'red' }
|
||||
],
|
||||
headers: [
|
||||
{ text: this.$gettext('Name'), value: 'name' },
|
||||
{ text: this.$gettext('Aliases'), value: 'domainalias_count' },
|
||||
{ text: this.$gettext('DNS status'), value: 'dns_global_status', sortable: false, align: 'center' },
|
||||
{ text: this.$gettext('Sending limit'), value: 'message_limit' },
|
||||
{ text: this.$gettext('Quota'), value: 'allocated_quota_in_percent' },
|
||||
{ text: this.$gettext('Actions'), value: 'actions', sortable: false, align: 'right' }
|
||||
],
|
||||
keepDomainFolder: false,
|
||||
selectedDomain: null,
|
||||
selectedDomainAlias: null,
|
||||
showAdminList: false,
|
||||
showConfirmDialog: false,
|
||||
showAliasForm: false,
|
||||
search: '',
|
||||
selected: [],
|
||||
expanded: [],
|
||||
aliases: {}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('domains/getDomains')
|
||||
},
|
||||
methods: {
|
||||
closeDomainAliasForm () {
|
||||
this.showAliasForm = false
|
||||
this.selectedDomain = null
|
||||
this.selectedDomainAlias = null
|
||||
},
|
||||
async deleteDomain (domain) {
|
||||
const confirm = await this.$refs.confirm.open(
|
||||
this.$gettext('Warning'),
|
||||
this.$gettext(`Do you really want to delete the domain ${domain.name}?`),
|
||||
{
|
||||
color: 'error',
|
||||
cancelLabel: this.$gettext('No'),
|
||||
agreeLabel: this.$gettext('Yes')
|
||||
}
|
||||
)
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
const data = { keep_folder: this.keepDomainFolder }
|
||||
this.$store.dispatch('domains/deleteDomain', { id: domain.pk, data }).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Domain deleted') })
|
||||
this.keepDomainFolder = false
|
||||
})
|
||||
},
|
||||
domainAliasDeleted () {
|
||||
const newList = this.aliases[this.selectedDomain.name].filter(alias => {
|
||||
return alias.pk !== this.selectedDomainAlias.pk
|
||||
})
|
||||
this.$set(this.aliases, this.selectedDomain.name, newList)
|
||||
this.closeDomainAliasForm()
|
||||
},
|
||||
editDomain (domain) {
|
||||
this.$router.push({ name: 'DomainEdit', params: { id: domain.pk } })
|
||||
},
|
||||
editDomainAlias (domain, alias) {
|
||||
this.selectedDomain = domain
|
||||
this.selectedDomainAlias = alias
|
||||
this.showAliasForm = true
|
||||
},
|
||||
loadAliases ({ item, value }) {
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
domainApi.getDomainAliases(item.name).then(resp => {
|
||||
this.$set(this.aliases, item.name, resp.data)
|
||||
})
|
||||
},
|
||||
openAdminList (domain) {
|
||||
this.selectedDomain = domain
|
||||
this.showAdminList = true
|
||||
},
|
||||
showAliases (item) {
|
||||
this.expanded = [{ item }]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-text-field--outlined >>> fieldset {
|
||||
border-color: #BFC5D2;
|
||||
}
|
||||
.v-input--checkbox >>> .v-label {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<v-switch v-model="createAdmin"
|
||||
:label="'Create a domain administrator' | translate"
|
||||
@change="update"
|
||||
/>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
:rules="(createAdmin) ? 'required' : ''"
|
||||
>
|
||||
<v-text-field
|
||||
:label="'Name' | translate"
|
||||
:hint="'Name of the administrator' | translate"
|
||||
persistent-hint
|
||||
v-model="domain.domain_admin.username"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
:disabled="!createAdmin"
|
||||
:suffix="`@${domain.name}`"
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-switch v-model="domain.domain_admin.with_random_password"
|
||||
:label="'Random password' | translate"
|
||||
:disabled="!createAdmin"
|
||||
:hint="'Generate a random password for the administrator.' | translate"
|
||||
@change="updatePassword"
|
||||
persistent-hint
|
||||
/>
|
||||
<v-switch v-model="domain.domain_admin.with_mailbox"
|
||||
:label="'With a mailbox' | translate"
|
||||
:disabled="!createAdmin"
|
||||
:hint="'Create a mailbox for the administrator.' | translate"
|
||||
persistent-hint
|
||||
/>
|
||||
<v-switch v-model="domain.domain_admin.with_aliases"
|
||||
:label="'Create aliases' | translate"
|
||||
:disabled="!createAdmin"
|
||||
:hint="'Create standard aliases for the domain.' | translate"
|
||||
persistent-hint
|
||||
/>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import accounts from '@/api/accounts'
|
||||
|
||||
export default {
|
||||
props: ['domain'],
|
||||
data () {
|
||||
return {
|
||||
createAdmin: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updatePassword (value) {
|
||||
if (value) {
|
||||
accounts.getRandomPassword().then(resp => {
|
||||
this.$set(this.domain.domain_admin, 'password', resp.data.password)
|
||||
})
|
||||
} else {
|
||||
delete this.domain.domain_admin.password
|
||||
}
|
||||
},
|
||||
|
||||
update (val) {
|
||||
this.$emit('createAdmin', val)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<translate class="headline">General information</translate>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="6"><translate>Creation date</translate></v-col>
|
||||
<v-col cols="6">{{ domain.creation|date }}</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="6"><translate>Last modification date</translate></v-col>
|
||||
<v-col cols="6">{{ domain.last_modification|date }}</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="6"><translate>Quota</translate></v-col>
|
||||
<v-col cols="6">{{ domain.quota }} MB</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="6"><translate>Default mailbox quota</translate></v-col>
|
||||
<v-col cols="6">{{ domain.default_mailbox_quota }} MB</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="domain.sending_limit !== undefined">
|
||||
<v-col cols="6"><translate>Daily sending limit</translate></v-col>
|
||||
<v-col cols="6">{{ domain.sending_limit }} <translate>messages</translate></v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="6"><translate>Mailboxes</translate></v-col>
|
||||
<v-col cols="6">{{ domain.mailbox_count }}</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="6"><translate>Aliases</translate></v-col>
|
||||
<v-col cols="6">{{ domain.mbalias_count }}</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['domain']
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
vid="alias"
|
||||
v-slot="{ errors }"
|
||||
>
|
||||
<email-field
|
||||
ref="aliasField"
|
||||
v-model="currentAlias"
|
||||
:placeholder="'Start typing a name here...'|translate"
|
||||
@domain-selected="addAlias"
|
||||
:hint="'Alias(es) of this mailbox. To create a catchall alias, just enter the domain name (@domain.tld).'|translate"
|
||||
persistent-hint
|
||||
:error-messages="errors"
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-chip
|
||||
v-for="(alias, index) in account.aliases"
|
||||
:key="index"
|
||||
class="mr-2 mt-2"
|
||||
close
|
||||
@click:close="account.aliases.splice(index, 1)"
|
||||
>
|
||||
{{ alias }}
|
||||
</v-chip>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import accounts from '@/api/accounts'
|
||||
import EmailField from '@/components/tools/EmailField'
|
||||
|
||||
export default {
|
||||
props: ['account'],
|
||||
components: {
|
||||
EmailField
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentAlias: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async addAlias () {
|
||||
try {
|
||||
await accounts.validate({ aliases: [this.currentAlias] })
|
||||
this.account.aliases.push(this.currentAlias)
|
||||
this.$refs.aliasField.reset()
|
||||
} catch (error) {
|
||||
let errorMsg = null
|
||||
if (error.response.data.aliases) {
|
||||
errorMsg = error.response.data.aliases
|
||||
} else if (error.response.data.non_field_errors) {
|
||||
errorMsg = error.response.data.non_field_errors
|
||||
}
|
||||
this.$refs.observer.setErrors({ alias: errorMsg })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<creation-form
|
||||
:title="'New account'|translate"
|
||||
:steps="steps"
|
||||
:form-observer-getter="getObserver"
|
||||
:validate-object="validateAccount"
|
||||
:summary-sections="summarySections"
|
||||
@close="close"
|
||||
@create="submit"
|
||||
>
|
||||
<template v-slot:form.role="{ step }">
|
||||
<account-role-form :ref="`form_${step}`" :account="account" />
|
||||
</template>
|
||||
<template v-slot:form.identification="{ step }">
|
||||
<account-general-form v-if="step >= 2" :ref="`form_${step}`" :account="account" />
|
||||
</template>
|
||||
<template v-slot:form.mailbox="{ step }">
|
||||
<account-mailbox-form :ref="`form_${step}`" :account="account" />
|
||||
</template>
|
||||
<template v-slot:form.aliases="{ step }">
|
||||
<account-alias-form v-if="step >= 4" :ref="`form_${step}`" :account="account" />
|
||||
</template>
|
||||
<template v-slot:item.random_password="{ item }">
|
||||
<template v-if="item.value">
|
||||
<v-col cols="12" class="highligth white--text">
|
||||
<v-row>
|
||||
<v-col><span>{{ item.key }}</span></v-col>
|
||||
<v-col class="text-right" v-if="item.type === 'yesno'">
|
||||
{{ item.value|yesno }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="text-right py-1">
|
||||
{{ account.password }}
|
||||
<v-btn icon color="white" :title="'Copy to clipboard'|translate" @click="copyPassword">
|
||||
<v-icon>mdi-clipboard-multiple-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-col><span class="grey--text">{{ item.key }}</span></v-col>
|
||||
<v-col class="text-right" v-if="item.type === 'yesno'">{{ item.value|yesno }}</v-col>
|
||||
</template>
|
||||
</template>
|
||||
</creation-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import accounts from '@/api/accounts'
|
||||
import AccountAliasForm from './AccountAliasForm'
|
||||
import AccountGeneralForm from './AccountGeneralForm'
|
||||
import AccountMailboxForm from './AccountMailboxForm'
|
||||
import AccountRoleForm from './AccountRoleForm'
|
||||
import CreationForm from '@/components/tools/CreationForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountAliasForm,
|
||||
AccountGeneralForm,
|
||||
AccountMailboxForm,
|
||||
AccountRoleForm,
|
||||
CreationForm
|
||||
},
|
||||
computed: {
|
||||
summarySections () {
|
||||
const result = [
|
||||
{
|
||||
title: this.$gettext('Role'),
|
||||
items: [
|
||||
{ key: this.$gettext('Role'), value: this.account.role }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: this.$gettext('Identification'),
|
||||
items: [
|
||||
{ key: this.$gettext('Username'), value: this.account.username },
|
||||
{ key: this.$gettext('First name'), value: this.account.first_name },
|
||||
{ key: this.$gettext('Last name'), value: this.account.last_name },
|
||||
{
|
||||
name: 'random_password',
|
||||
key: this.$gettext('Random password'),
|
||||
value: this.account.random_password,
|
||||
type: 'yesno'
|
||||
},
|
||||
{
|
||||
key: this.$gettext('Enabled'),
|
||||
value: this.account.is_active,
|
||||
type: 'yesno'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: this.$gettext('Mailbox'),
|
||||
items: [
|
||||
{ key: this.$gettext('Email'), value: this.account.mailbox.full_address },
|
||||
{
|
||||
key: this.$gettext('Quota'),
|
||||
value: (this.account.mailbox.use_domain_quota)
|
||||
? this.$gettext('Domain default value')
|
||||
: this.account.mailbox.quota
|
||||
},
|
||||
{
|
||||
key: this.$gettext('Message sending limit'),
|
||||
value: this.account.mailbox.message_limit
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: this.$gettext('Aliases'),
|
||||
items: [
|
||||
{
|
||||
key: this.$gettext('Aliases'),
|
||||
value: this.account.aliases,
|
||||
type: 'list'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
return result
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
account: this.getInitialForm(),
|
||||
steps: [
|
||||
{
|
||||
name: 'role',
|
||||
title: this.$gettext('Role')
|
||||
},
|
||||
{
|
||||
name: 'identification',
|
||||
title: this.$gettext('Identification')
|
||||
},
|
||||
{
|
||||
name: 'mailbox',
|
||||
title: this.$gettext('Mailbox')
|
||||
},
|
||||
{
|
||||
name: 'aliases',
|
||||
title: this.$gettext('Aliases')
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyPassword () {
|
||||
navigator.clipboard.writeText(this.account.password).then(() => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Password copied to clipboard') })
|
||||
})
|
||||
},
|
||||
getInitialForm () {
|
||||
return {
|
||||
aliases: [],
|
||||
is_active: true,
|
||||
role: 'SimpleUsers',
|
||||
mailbox: {
|
||||
use_domain_quota: true
|
||||
},
|
||||
random_password: true
|
||||
}
|
||||
},
|
||||
getObserver (step) {
|
||||
return this.$refs[`form_${step}`].$refs.observer
|
||||
},
|
||||
validateAccount () {
|
||||
return accounts.validate(this.account)
|
||||
},
|
||||
close () {
|
||||
this.account = this.getInitialForm()
|
||||
this.$emit('close')
|
||||
},
|
||||
submit () {
|
||||
accounts.create(this.account).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Account created') })
|
||||
this.$emit('created')
|
||||
this.close()
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('identities/fetchDomains')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.highligth {
|
||||
background-color: #515D78;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-expansion-panels v-model="panel">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header v-slot="{ open }">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="4">
|
||||
<translate>Role</translate>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="text--secondary"
|
||||
>
|
||||
<v-fade-transition leave-absolute>
|
||||
<span v-if="open"></span>
|
||||
<v-row
|
||||
v-else
|
||||
no-gutters
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-col cols="6">
|
||||
{{ $refs.roleForm.roleLabel }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<account-role-form ref="roleForm" :account="editedAccount" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header v-slot="{ open }">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="4">
|
||||
<translate>Identification</translate>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="text--secondary"
|
||||
>
|
||||
<v-fade-transition leave-absolute>
|
||||
<span v-if="open"></span>
|
||||
<v-row
|
||||
v-else
|
||||
no-gutters
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-col cols="6">
|
||||
<translate class="mr-2">Username:</translate> {{ editedAccount.username }}
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<translate class="mr-2">Enabled</translate>
|
||||
<v-icon color="success" v-if="editedAccount.is_active">mdi-check-circle-outline</v-icon>
|
||||
<v-icon v-else>mdi-close-circle-outline</v-icon>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<account-general-form ref="generalForm" :account="editedAccount" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel v-if="usernameIsEmail">
|
||||
<v-expansion-panel-header v-slot="{ open }">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="4">
|
||||
<translate>Mailbox</translate>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="text--secondary"
|
||||
>
|
||||
<v-fade-transition leave-absolute>
|
||||
<span v-if="open"></span>
|
||||
<v-row
|
||||
v-else
|
||||
no-gutters
|
||||
style="width: 100%"
|
||||
>
|
||||
<template v-if="account.mailbox">
|
||||
<v-col cols="6" v-if="account.mailbox.use_domain_quota">
|
||||
<translate class="mr-2">Quota: </translate> <translate>domain's default value</translate>
|
||||
</v-col>
|
||||
<v-col cols="6" v-else>
|
||||
<translate class="mr-2">Quota: </translate> {{ account.mailbox.quota }}
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<account-mailbox-form ref="mailboxForm" :account="editedAccount" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<div class="mt-4 d-flex justify-end">
|
||||
<v-btn color="grey lighten-1" @click="$router.go(-1)">
|
||||
<translate>Cancel</translate>
|
||||
</v-btn>
|
||||
<v-btn class="ml-4" color="primary darken-1" @click="save">
|
||||
<translate>Save</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _isEmpty from 'lodash/isEmpty'
|
||||
import { bus } from '@/main'
|
||||
import accounts from '@/api/accounts'
|
||||
import AccountGeneralForm from './AccountGeneralForm'
|
||||
import AccountMailboxForm from './AccountMailboxForm'
|
||||
import AccountRoleForm from './AccountRoleForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountGeneralForm,
|
||||
AccountMailboxForm,
|
||||
AccountRoleForm
|
||||
},
|
||||
props: ['account'],
|
||||
computed: {
|
||||
usernameIsEmail () {
|
||||
return this.editedAccount.username.indexOf('@') !== -1
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
editedAccount: {},
|
||||
panel: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save () {
|
||||
if (this.$refs.generalForm !== undefined) {
|
||||
const valid = await this.$refs.generalForm.$refs.observer.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(JSON.stringify(this.editedAccount))
|
||||
if (this.usernameIsEmail) {
|
||||
data.mailbox.full_address = data.username
|
||||
} else {
|
||||
delete data.mailbox.full_address
|
||||
}
|
||||
if (_isEmpty(data.mailbox)) {
|
||||
delete data.mailbox
|
||||
}
|
||||
if (data.random_password || !data.password) {
|
||||
delete data.password
|
||||
delete data.password_confirmation
|
||||
}
|
||||
await accounts.patch(this.editedAccount.pk, data).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Account updated') })
|
||||
})
|
||||
} catch (error) {
|
||||
if (this.$refs.generalForm) {
|
||||
this.$refs.generalForm.$refs.observer.setErrors(error.response.data)
|
||||
}
|
||||
if (this.$refs.mailboxForm) {
|
||||
this.$refs.mailboxForm.$refs.observer.setErrors(error.response.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('identities/fetchDomains')
|
||||
},
|
||||
watch: {
|
||||
account: {
|
||||
handler: function (val) {
|
||||
if (val) {
|
||||
this.editedAccount = JSON.parse(JSON.stringify(val))
|
||||
delete this.editedAccount.domains
|
||||
if (this.editedAccount.mailbox === null) {
|
||||
this.editedAccount.mailbox = {}
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
vid="username"
|
||||
>
|
||||
<label class="m-label">{{ $gettext('Username') }}</label>
|
||||
<email-field
|
||||
ref="username"
|
||||
v-model="account.username"
|
||||
:error-messages="errors"
|
||||
:placeholder="usernamePlaceholder"
|
||||
:type="usernameInputType"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</validation-provider>
|
||||
<label class="m-label">{{ $gettext('First name') }}</label>
|
||||
<v-text-field
|
||||
v-model="account.first_name"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
<label class="m-label">{{ $gettext('Last name') }}</label>
|
||||
<v-text-field
|
||||
v-model="account.last_name"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
<div class="d-flex align-center">
|
||||
<v-switch
|
||||
v-model="account.random_password"
|
||||
:label="'Random password'|translate"
|
||||
@change="updatePassword"
|
||||
dense
|
||||
/>
|
||||
<template v-if="account.pk && account.random_password">
|
||||
<v-alert style="background-color: #515D78" class="ml-6" dense>
|
||||
<span class="white--text mr-4">{{ account.password }}</span>
|
||||
<v-btn small color="white" icon :title="'Copy to clipboard'|translate" @click="copyPassword">
|
||||
<v-icon>mdi-clipboard-multiple-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-alert>
|
||||
</template>
|
||||
</div>
|
||||
<validation-provider
|
||||
v-if="!account.random_password"
|
||||
v-slot="{ errors }"
|
||||
:rules="passwordRules"
|
||||
vid="password"
|
||||
>
|
||||
<label class="m-label">{{ $gettext('Password') }}</label>
|
||||
<v-text-field
|
||||
v-model="account.password"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
type="password"
|
||||
dense
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-if="!account.random_password"
|
||||
v-slot="{ errors }"
|
||||
:rules="passwordConfirmationRules"
|
||||
>
|
||||
<label class="m-label">{{ $gettext('Confirmation') }}</label>
|
||||
<v-text-field
|
||||
v-model="account.password_confirmation"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
type="password"
|
||||
dense
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-switch
|
||||
v-model="account.is_active"
|
||||
:label="'Enabled' | translate"
|
||||
dense
|
||||
/>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import accounts from '@/api/accounts'
|
||||
import EmailField from '@/components/tools/EmailField'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmailField
|
||||
},
|
||||
props: ['account'],
|
||||
computed: {
|
||||
passwordRules () {
|
||||
return (this.account.pk) ? '' : 'required'
|
||||
},
|
||||
passwordConfirmationRules () {
|
||||
return (this.account.password) ? 'required' : ''
|
||||
},
|
||||
usernamePlaceholder () {
|
||||
if (this.account.role === 'SimpleUsers') {
|
||||
return this.$gettext('Enter an email address')
|
||||
}
|
||||
return this.$gettext('Enter a simple username or an email address')
|
||||
},
|
||||
usernameInputType () {
|
||||
return (this.account.role === 'SimpleUsers') ? 'email' : 'text'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyPassword () {
|
||||
navigator.clipboard.writeText(this.account.password).then(() => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Password copied to clipboard') })
|
||||
})
|
||||
},
|
||||
updatePassword (value) {
|
||||
if (value) {
|
||||
accounts.getRandomPassword().then(resp => {
|
||||
this.$set(this.account, 'password', resp.data.password)
|
||||
})
|
||||
} else {
|
||||
this.$set(this.account, 'password', null)
|
||||
this.$set(this.account, 'password_confirmation', null)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.account.random_password) {
|
||||
this.updatePassword(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<label class="m-label">{{ $gettext('Quota') }}</label>
|
||||
<v-switch
|
||||
v-model="account.mailbox.use_domain_quota"
|
||||
:label="useDomainQuotaLabel"
|
||||
/>
|
||||
<validation-provider
|
||||
v-if="!account.mailbox.use_domain_quota"
|
||||
v-slot="{ errors }"
|
||||
rules=""
|
||||
vid="quota"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="account.mailbox.quota"
|
||||
:placeholder="'Ex: 10MB. Leave empty for no limit'|translate"
|
||||
:hint="'Quota for this mailbox, can be expressed in KB, MB (default) or GB. Define a custom value or use domain\'s default one. Leave empty to define an unlimited value (not allowed for domain administrators).' | translate"
|
||||
persistent-hint
|
||||
outlined
|
||||
:error-messages="errors"
|
||||
dense
|
||||
/>
|
||||
</validation-provider>
|
||||
<label class="m-label">{{ $gettext('Daily message sending limit') }}</label>
|
||||
<v-text-field
|
||||
v-model="account.mailbox.message_limit"
|
||||
:placeholder="'Leave empty for no limit' | translate"
|
||||
:hint="'Number of messages this mailbox can send per day. Leave empty for no limit.' | translate"
|
||||
persistent-hint
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: ['account'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getDomainByName: 'identities/getDomainByName'
|
||||
}),
|
||||
domainQuota () {
|
||||
const email = this.account.mailbox.full_address
|
||||
if (email && email.indexOf('@') !== -1) {
|
||||
const domain = this.getDomainByName(email.split('@')[1])
|
||||
if (domain) {
|
||||
return parseInt(domain.default_mailbox_quota)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
useDomainQuotaLabel () {
|
||||
let result = this.$gettext('Use domain default value')
|
||||
if (this.domainQuota !== undefined) {
|
||||
if (this.domainQuota === 0) {
|
||||
result += ` (${this.$gettext('unlimited')})`
|
||||
} else {
|
||||
result += ` (${this.domainQuota} MB)`
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
account: {
|
||||
handler (val) {
|
||||
if (val.role === 'SimpleUsers') {
|
||||
this.$set(val.mailbox, 'full_address', val.username)
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.quota {
|
||||
max-width: 50%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<v-row>
|
||||
<v-col cols="7">
|
||||
<choice-field v-model="account.role"
|
||||
:label="'Choose a role' | translate"
|
||||
:choices="accountRoles"
|
||||
:error-messages="errors"
|
||||
:choices-per-line="2"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="5">
|
||||
<v-alert text 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" large>mdi-help-circle-outline</v-icon>
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</validation-provider>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChoiceField from '@/components/tools/ChoiceField'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChoiceField
|
||||
},
|
||||
props: ['account'],
|
||||
computed: {
|
||||
roleLabel () {
|
||||
const role = this.accountRoles.find(role => role.value === this.account.role)
|
||||
return role !== undefined ? role.label : ''
|
||||
},
|
||||
roleHelp () {
|
||||
const role = this.accountRoles.find(role => role.value === this.account.role)
|
||||
return role !== undefined ? role.help : ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
accountRoles: [
|
||||
{
|
||||
label: this.$gettext('Domain administrator'),
|
||||
value: 'DomainAdmins',
|
||||
help: this.$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: this.$gettext('Reseller'),
|
||||
value: 'Resellers',
|
||||
help: this.$gettext('help')
|
||||
},
|
||||
{
|
||||
label: this.$gettext('Simple user'),
|
||||
value: 'SimpleUsers',
|
||||
help: this.$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: this.$gettext('Super administrator'),
|
||||
value: 'SuperAdmins',
|
||||
help: this.$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.')
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<creation-form
|
||||
:title="'New alias'|translate"
|
||||
:steps="steps"
|
||||
:form-observer-getter="getObserver"
|
||||
:validate-object="validateAlias"
|
||||
:summary-sections="summarySections"
|
||||
@close="close"
|
||||
@create="submit"
|
||||
>
|
||||
<template v-slot:form.general="{ step }">
|
||||
<alias-general-form :ref="`form_${step}`" :alias="alias" />
|
||||
</template>
|
||||
<template v-slot:form.recipients="{ step }">
|
||||
<alias-recipient-form :ref="`form_${step}`" :alias="alias" />
|
||||
</template>
|
||||
</creation-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import aliases from '@/api/aliases'
|
||||
import AliasGeneralForm from '@/components/identities/AliasGeneralForm'
|
||||
import AliasRecipientForm from './AliasRecipientForm'
|
||||
import CreationForm from '@/components/tools/CreationForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AliasGeneralForm,
|
||||
AliasRecipientForm,
|
||||
CreationForm
|
||||
},
|
||||
computed: {
|
||||
summarySections () {
|
||||
const result = [
|
||||
{
|
||||
title: this.$gettext('General'),
|
||||
items: [
|
||||
{ key: this.$gettext('Random address'), value: this.alias.random, type: 'yesno' },
|
||||
{ key: this.$gettext('Address'), value: this.alias.address }
|
||||
]
|
||||
}
|
||||
]
|
||||
if (this.alias.expire_at !== undefined) {
|
||||
result[0].items.push({
|
||||
key: this.$gettext('Expire at'), value: this.alias.expire_at
|
||||
})
|
||||
}
|
||||
if (this.alias.description !== undefined) {
|
||||
result[0].items.push({
|
||||
key: this.$gettext('Description'), value: this.alias.description
|
||||
})
|
||||
}
|
||||
result.push({
|
||||
title: this.$gettext('Recipients'),
|
||||
items: [
|
||||
{ key: this.$gettext('Recipients'), value: this.alias.recipients.join(', ') }
|
||||
]
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
alias: this.getInitialForm(),
|
||||
steps: [
|
||||
{
|
||||
name: 'general',
|
||||
title: this.$gettext('General')
|
||||
},
|
||||
{
|
||||
name: 'recipients',
|
||||
title: this.$gettext('Recipients')
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.alias = this.getInitialForm()
|
||||
this.$emit('close')
|
||||
},
|
||||
getInitialForm () {
|
||||
return {
|
||||
random: false,
|
||||
enabled: true,
|
||||
recipients: []
|
||||
}
|
||||
},
|
||||
getObserver (step) {
|
||||
return this.$refs[`form_${step}`].$refs.observer
|
||||
},
|
||||
validateAlias () {
|
||||
const data = { ...this.alias }
|
||||
if (data.recipients.length === 0) {
|
||||
delete data.recipients
|
||||
}
|
||||
return aliases.validate(data)
|
||||
},
|
||||
submit () {
|
||||
aliases.create(this.alias).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Alias created') })
|
||||
this.$emit('created')
|
||||
this.close()
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('identities/fetchDomains')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-expansion-panels v-model="panel">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header v-slot="{ open }">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="4">
|
||||
<translate>General</translate>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="text--secondary"
|
||||
>
|
||||
<v-fade-transition leave-absolute>
|
||||
<span v-if="open"></span>
|
||||
<v-row
|
||||
v-else
|
||||
no-gutters
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-col cols="6">
|
||||
<translate>Address: </translate> {{ editedAlias.address }}
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<translate class="mr-2">Enabled</translate>
|
||||
<v-icon color="success" v-if="editedAlias.enabled">mdi-check-circle-outline</v-icon>
|
||||
<v-icon v-else>mdi-close-circle-outline</v-icon>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<alias-general-form ref="generalForm" :alias="editedAlias" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-header v-slot="{ open }">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="4">
|
||||
<translate>Recipients</translate>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="text--secondary"
|
||||
>
|
||||
<v-fade-transition leave-absolute>
|
||||
<span v-if="open"></span>
|
||||
<v-row
|
||||
v-else
|
||||
no-gutters
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-chip
|
||||
v-for="(rcpt, index) in editedAlias.recipients"
|
||||
:key="index"
|
||||
small
|
||||
>
|
||||
{{ rcpt }}
|
||||
</v-chip>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<alias-recipient-form ref="recipientForm" :alias="editedAlias" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<div class="mt-4 d-flex justify-end">
|
||||
<v-btn color="grey lighten-1" @click="$router.go(-1)">
|
||||
<translate>Cancel</translate>
|
||||
</v-btn>
|
||||
<v-btn class="ml-4" color="primary darken-1" @click="save">
|
||||
<translate>Save</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import aliases from '@/api/aliases'
|
||||
import AliasGeneralForm from './AliasGeneralForm'
|
||||
import AliasRecipientForm from './AliasRecipientForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AliasGeneralForm,
|
||||
AliasRecipientForm
|
||||
},
|
||||
props: ['alias'],
|
||||
data () {
|
||||
return {
|
||||
editedAlias: {},
|
||||
panel: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save () {
|
||||
if (this.$refs.generalForm !== undefined) {
|
||||
const valid = await this.$refs.generalForm.$refs.observer.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
await aliases.patch(this.editedAlias.pk, this.editedAlias).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Alias updated') })
|
||||
})
|
||||
} catch (error) {
|
||||
if (this.$refs.generalForm) {
|
||||
this.$refs.generalForm.$refs.observer.setErrors(error.response.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
alias: {
|
||||
handler: function (val) {
|
||||
if (val) {
|
||||
this.editedAlias = { ...val }
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<v-switch
|
||||
v-if="!alias.pk"
|
||||
v-model="alias.random"
|
||||
:label="'Random address'|translate"
|
||||
/>
|
||||
<validation-provider
|
||||
v-if="!alias.random"
|
||||
v-slot="{ errors }"
|
||||
name="address"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label">{{ $gettext('Email address') }}</label>
|
||||
<email-field
|
||||
ref="address"
|
||||
v-model="alias.address"
|
||||
:error-messages="errors"
|
||||
type="email"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-if="alias.random"
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label">{{ $gettext('Domain') }}</label>
|
||||
<v-select
|
||||
v-model="alias.domain"
|
||||
:items="domains"
|
||||
:error-messages="errors"
|
||||
item-text="name"
|
||||
return-object
|
||||
outlined
|
||||
dense
|
||||
@change="updateAddress"
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-switch
|
||||
v-model="alias.enabled"
|
||||
:label="'Enabled'|translate"
|
||||
/>
|
||||
<label class="m-label">{{ $gettext('Expire at') }}</label>
|
||||
<v-text-field
|
||||
v-model="alias.expire"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
<label class="m-label">{{ $gettext('Description') }}</label>
|
||||
<v-textarea
|
||||
v-model="alias.description"
|
||||
rows="2"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import aliases from '@/api/aliases'
|
||||
import EmailField from '@/components/tools/EmailField'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmailField
|
||||
},
|
||||
props: ['alias'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
domains: 'identities/domains'
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
updateAddress (value) {
|
||||
aliases.getRandomAddress().then(resp => {
|
||||
this.$set(this.alias, 'address', `${resp.data.address}@${value.name}`)
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
alias (val) {
|
||||
console.log(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
vid="recipients"
|
||||
v-slot="{ errors }"
|
||||
>
|
||||
<email-field
|
||||
ref="recipientField"
|
||||
v-model="recipient"
|
||||
:placeholder="'Start typing a name here...'|translate"
|
||||
@domain-selected="addRecipient"
|
||||
:hint="'Alias(es) of this mailbox. To create a catchall alias, just enter the domain name (@domain.tld).'|translate"
|
||||
persistent-hint
|
||||
allow-add
|
||||
:error-messages="errors"
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-chip
|
||||
v-for="(recipient, index) in alias.recipients"
|
||||
:key="index"
|
||||
class="mr-2 mt-2"
|
||||
close
|
||||
@click:close="alias.recipients.splice(index, 1)"
|
||||
>
|
||||
{{ recipient }}
|
||||
</v-chip>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmailField from '@/components/tools/EmailField'
|
||||
|
||||
export default {
|
||||
props: ['alias'],
|
||||
components: {
|
||||
EmailField
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
recipient: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async addRecipient () {
|
||||
this.alias.recipients.push(this.recipient)
|
||||
this.$refs.recipientField.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<v-card class="mt-6">
|
||||
<v-toolbar flat>
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" small>
|
||||
Actions <v-icon right>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
placeholder="Search"
|
||||
filled
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
/>
|
||||
</v-toolbar>
|
||||
|
||||
<v-data-table
|
||||
v-model="selected"
|
||||
:headers="headers"
|
||||
:items="identities"
|
||||
:search="search"
|
||||
item-key="identity"
|
||||
class="elevation-1"
|
||||
show-select
|
||||
>
|
||||
<template v-slot: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' : ''"
|
||||
small
|
||||
>
|
||||
{{ tag.label }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div class="text-right">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on">
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<menu-items :items="getMenuItems(item)" :object="item" />
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<confirm-dialog ref="confirmAlias" />
|
||||
<confirm-dialog ref="confirmAccount">
|
||||
<v-checkbox v-model="keepAccountFolder"
|
||||
:label="'Do not delete account folder'|translate"
|
||||
hide-details
|
||||
/>
|
||||
</confirm-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import identities from '@/api/identities'
|
||||
import accounts from '@/api/accounts'
|
||||
import aliases from '@/api/aliases'
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog'
|
||||
import MenuItems from '@/components/tools/MenuItems'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
MenuItems
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
headers: [
|
||||
{ text: this.$gettext('Name'), value: 'identity' },
|
||||
{ text: this.$gettext('Fullname/recipient'), value: 'name_or_rcpt' },
|
||||
{ text: this.$gettext('Tags'), value: 'tags' },
|
||||
{ text: this.$gettext('Actions'), value: 'actions', sortable: false, align: 'right' }
|
||||
],
|
||||
identities: [],
|
||||
keepAccountFolder: false,
|
||||
search: '',
|
||||
selected: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async deleteAlias (alias) {
|
||||
const confirm = await this.$refs.confirmAlias.open(
|
||||
this.$gettext('Warning'),
|
||||
this.$gettext(`Do you really want to delete the alias ${alias.identity}?`),
|
||||
{
|
||||
color: 'error',
|
||||
cancelLabel: this.$gettext('No'),
|
||||
agreeLabel: this.$gettext('Yes')
|
||||
}
|
||||
)
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
aliases.delete(alias.pk).then(() => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Alias deleted') })
|
||||
this.fetchIdentities()
|
||||
})
|
||||
},
|
||||
async deleteAccount (account) {
|
||||
const confirm = await this.$refs.confirmAccount.open(
|
||||
this.$gettext('Warning'),
|
||||
this.$gettext(`Do you really want to delete the account ${account.identity}?`),
|
||||
{
|
||||
color: 'error',
|
||||
cancelLabel: this.$gettext('No'),
|
||||
agreeLabel: this.$gettext('Yes')
|
||||
}
|
||||
)
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
const data = { keepdir: this.keepAccountFolder }
|
||||
accounts.delete(account.pk, data).then(() => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Account deleted') })
|
||||
this.keepAccountFolder = false
|
||||
this.fetchIdentities()
|
||||
}).catch(error => {
|
||||
bus.$emit('notification', { msg: error.response.data, type: 'error' })
|
||||
})
|
||||
},
|
||||
editAccount (account) {
|
||||
this.$router.push({ name: 'AccountEdit', params: { id: account.pk } })
|
||||
},
|
||||
editAlias (alias) {
|
||||
this.$router.push({ name: 'AliasEdit', params: { id: alias.pk } })
|
||||
},
|
||||
fetchIdentities () {
|
||||
identities.getAll().then(resp => {
|
||||
this.identities = resp.data
|
||||
})
|
||||
},
|
||||
getMenuItems (item) {
|
||||
const result = []
|
||||
if (item.type === 'account') {
|
||||
result.push({ label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editAccount })
|
||||
result.push({ label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteAccount, color: 'red' })
|
||||
} else if (item.type === 'alias') {
|
||||
result.push({ label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editAlias })
|
||||
result.push({ label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteAlias, color: 'red' })
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.fetchIdentities()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
:max-width="options.width"
|
||||
:style="{ zIndex: options.zIndex }"
|
||||
@keydown.esc="cancel"
|
||||
>
|
||||
<v-card>
|
||||
<v-toolbar dark :color="options.color" dense 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.native="cancel"
|
||||
>
|
||||
{{ options.cancelLabel }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:color="this.options.color"
|
||||
@click.native="agree">
|
||||
{{ options.agreeLabel }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
dialog: false,
|
||||
resolve: null,
|
||||
reject: null,
|
||||
message: null,
|
||||
title: null,
|
||||
options: {
|
||||
color: 'primary',
|
||||
width: 400,
|
||||
zIndex: 200,
|
||||
noconfirm: false,
|
||||
cancelLabel: this.$gettext('Cancel'),
|
||||
agreeLabel: this.$gettext('OK')
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open (title, message, options) {
|
||||
this.dialog = true
|
||||
this.title = title
|
||||
this.message = message
|
||||
this.options = Object.assign(this.options, options)
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve
|
||||
this.reject = reject
|
||||
})
|
||||
},
|
||||
agree () {
|
||||
this.resolve(true)
|
||||
this.dialog = false
|
||||
},
|
||||
cancel () {
|
||||
this.resolve(false)
|
||||
this.dialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
color="primary"
|
||||
app
|
||||
dark
|
||||
>
|
||||
<div class="d-flex justify-start">
|
||||
<v-img
|
||||
src="../../assets/Modoboa_RVB-BLANC-SANS.png"
|
||||
max-width="190"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-list>
|
||||
<template v-for="item in mainMenuItems">
|
||||
<template v-if="item.roles === undefined || item.roles.indexOf(authUser.role) !== -1">
|
||||
<v-list-item
|
||||
v-if="!item.children"
|
||||
class="menu-item"
|
||||
:to="item.to"
|
||||
link
|
||||
:key="item.title"
|
||||
:exact="item.exact"
|
||||
>
|
||||
<v-list-item-icon v-if="item.icon">
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-group v-else
|
||||
color="white"
|
||||
:key="item.text"
|
||||
:prepend-icon="item.icon"
|
||||
no-action>
|
||||
<template v-slot:activator>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
</template>
|
||||
|
||||
<template v-for="subitem in item.children">
|
||||
<v-list-item v-if="!subitem.children"
|
||||
:key="subitem.text"
|
||||
:to="subitem.to"
|
||||
link>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ subitem.text }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-group v-else
|
||||
:key="subitem.text"
|
||||
:value="true"
|
||||
no-action
|
||||
sub-group>
|
||||
<template v-slot:activator>
|
||||
<v-list-item-title>{{ subitem.text }}</v-list-item-title>
|
||||
</template>
|
||||
<v-list-item v-for="subsubitem in subitem.children"
|
||||
:key="subsubitem.text"
|
||||
:to="subsubitem.to"
|
||||
link>
|
||||
<v-list-item-title>{{ subsubitem.text }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list-group>
|
||||
</template>
|
||||
</v-list-group>
|
||||
</template>
|
||||
</template>
|
||||
</v-list>
|
||||
<template v-slot:append>
|
||||
<v-menu rounded="lg" offset-y top>
|
||||
<template v-slot:activator="{ attrs, on }">
|
||||
<div class="d-flex user-box justify-center align-center white--text py-2" v-bind="attrs" v-on="on">
|
||||
<v-avatar size="40" color="primary">
|
||||
<span class="white--text headline">AN</span>
|
||||
</v-avatar>
|
||||
<span class="mx-2">{{ authUser.first_name }} {{ authUser.last_name }}</span>
|
||||
<v-icon class="float-right">mdi-chevron-up</v-icon>
|
||||
</div>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="item in userMenuItems"
|
||||
:key="item.text"
|
||||
@click="item.click"
|
||||
link
|
||||
>
|
||||
<v-list-item-icon v-if="item.icon">
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import parameters from '@/api/parameters'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
authUser: 'auth/authUser'
|
||||
})
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
drawer: true,
|
||||
mainMenuItems: [
|
||||
{
|
||||
text: this.$gettext('Dashboard'),
|
||||
to: { name: 'Dashboard' },
|
||||
icon: 'mdi-view-dashboard-outline',
|
||||
exact: true
|
||||
},
|
||||
{
|
||||
text: this.$gettext('Domains'),
|
||||
to: { name: 'DomainList' },
|
||||
icon: 'mdi-domain',
|
||||
roles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
|
||||
},
|
||||
{
|
||||
text: this.$gettext('Identities'),
|
||||
to: { name: 'Identities' },
|
||||
icon: 'mdi-account',
|
||||
roles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
|
||||
},
|
||||
{
|
||||
icon: 'mdi-history',
|
||||
text: this.$gettext('Logs'),
|
||||
to: { name: 'AuditTrail' },
|
||||
roles: ['SuperAdmins']
|
||||
},
|
||||
{
|
||||
icon: 'mdi-cog',
|
||||
text: this.$gettext('Parameters'),
|
||||
children: [],
|
||||
roles: ['SuperAdmins']
|
||||
}
|
||||
],
|
||||
userMenuItems: [
|
||||
{
|
||||
text: this.$gettext('Logout'),
|
||||
icon: 'mdi-logout',
|
||||
click: this.logout
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
created () {
|
||||
parameters.getApplications().then(response => {
|
||||
response.data.forEach(item => {
|
||||
this.mainMenuItems[4].children.push({
|
||||
text: item.label,
|
||||
to: { name: 'ParametersEdit', params: { app: item.name } }
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
logout () {
|
||||
this.$store.dispatch('auth/logout').then(() => {
|
||||
this.$router.push({ name: 'Login' })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-list-item {
|
||||
&--active {
|
||||
&::before {
|
||||
opacity: 0;
|
||||
}
|
||||
background-color: #034bad;
|
||||
color: white;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.user-box {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<v-app-bar
|
||||
color="primary"
|
||||
dark
|
||||
app
|
||||
clipped-left
|
||||
>
|
||||
<div class="d-flex justify-start">
|
||||
<v-img
|
||||
class="mx-2"
|
||||
src="../../assets/Modoboa_RVB-BLANC-SANS.png"
|
||||
contain
|
||||
width="200"
|
||||
/>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-menu rounded="lg" offset-y>
|
||||
<template v-slot:activator="{ attrs, on }">
|
||||
<v-btn
|
||||
color="white"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
icon
|
||||
>
|
||||
<v-icon>mdi-account-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="item in items"
|
||||
:key="item.text"
|
||||
@click="item.click"
|
||||
link
|
||||
>
|
||||
<v-list-item-icon v-if="item.icon">
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
text: this.$gettext('Logout'),
|
||||
icon: 'mdi-logout',
|
||||
click: this.logout
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
logout () {
|
||||
this.$store.dispatch('auth/logout').then(() => {
|
||||
this.$router.push('/login')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<v-card class="mt-6">
|
||||
<v-toolbar flat>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:placeholder="'Search'|translate"
|
||||
filled
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
/>
|
||||
</v-toolbar>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="logs"
|
||||
:search="search"
|
||||
class="elevation-1"
|
||||
:options.sync="options"
|
||||
:server-items-length="total"
|
||||
>
|
||||
<template v-slot:item.level="{ item }">
|
||||
<v-chip :color="getLevelColor(item.level)" text-color="white" small>
|
||||
{{ item.level }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import logs from '@/api/logs'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
logs: [],
|
||||
headers: [
|
||||
{ text: 'Date', value: 'date_created' },
|
||||
{ text: 'Level', value: 'level' },
|
||||
{ text: 'Logger', value: 'logger' },
|
||||
{ text: 'Message', value: 'message' }
|
||||
],
|
||||
loading: true,
|
||||
options: {},
|
||||
search: '',
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getLevelColor (level) {
|
||||
if (level === 'INFO') {
|
||||
return 'primary'
|
||||
}
|
||||
if (level === 'WARNING') {
|
||||
return 'orange'
|
||||
}
|
||||
if (level === 'CRITICAL') {
|
||||
return 'red'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
fetchLogs () {
|
||||
let order
|
||||
if (this.options.sortBy.length) {
|
||||
order = (this.options.sortDesc[0] ? '-' : '') + this.options.sortBy[0]
|
||||
}
|
||||
const params = {
|
||||
ordering: order,
|
||||
page: this.options.page,
|
||||
page_size: this.options.itemsPerPage,
|
||||
search: this.search
|
||||
}
|
||||
this.loading = true
|
||||
logs.getAll(params).then(response => {
|
||||
this.logs = response.data.results
|
||||
this.total = response.data.count
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
options: {
|
||||
handler () {
|
||||
this.fetchLogs()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
search (val) {
|
||||
this.fetchLogs()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>{{ title }}</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-tabs v-model="active">
|
||||
<v-tab v-for="(element, index) in displayableElements"
|
||||
:key="index"
|
||||
>
|
||||
{{ element.label }}
|
||||
</v-tab>
|
||||
<v-tabs-items v-model="active">
|
||||
<v-tab-item
|
||||
v-for="(element, index) in displayableElements"
|
||||
:key="index"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<template v-for="(param, index) in displayableParams(element.parameters)">
|
||||
<div class="my-4" :key="index">
|
||||
<template v-if="param.widget === 'SeparatorField'">
|
||||
<h2>{{ param.label }}</h2>
|
||||
</template>
|
||||
<template v-else-if="param.widget === 'BooleanField'">
|
||||
<v-switch
|
||||
v-model="parameters[param.name]"
|
||||
:label="param.label"
|
||||
:hint="param.help_text"
|
||||
persistent-hint
|
||||
v-if="param.widget === 'BooleanField'"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<label class="m-label">{{ param.label }}</label>
|
||||
<v-select
|
||||
v-model="parameters[param.name]"
|
||||
:items="param.choices"
|
||||
:hint="param.help_text"
|
||||
persistent-hint
|
||||
v-if="param.widget === 'ChoiceField'"
|
||||
dense
|
||||
outlined
|
||||
/>
|
||||
<v-text-field
|
||||
:hint="param.help_text"
|
||||
persistent-hint
|
||||
v-model="parameters[param.name]"
|
||||
:error="formErrors[param.name] !== undefined"
|
||||
:error-messages="formErrors[param.name]"
|
||||
:type="param.widget === 'PasswordField' ? 'password' : 'text'"
|
||||
v-else
|
||||
dense
|
||||
outlined
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-tabs>
|
||||
<v-btn fixed
|
||||
dark
|
||||
fab
|
||||
bottom
|
||||
right
|
||||
color="green"
|
||||
@click="save"
|
||||
>
|
||||
<v-icon>mdi-content-save</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import parameters from '@/api/parameters'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
active: 0,
|
||||
structure: [],
|
||||
parameters: {},
|
||||
formErrors: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayableElements () {
|
||||
const result = this.structure.filter(element => this.display(element))
|
||||
return result
|
||||
},
|
||||
title () {
|
||||
let result = this.$gettext('Parameters: ')
|
||||
if (this.$route.params.app === 'core') {
|
||||
result += this.$gettext('General')
|
||||
} else if (this.$route.params.app === 'admin') {
|
||||
result += this.$gettext('Administration')
|
||||
} else if (this.$route.params.app === 'limits') {
|
||||
result += this.$gettext('Limitations')
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.loadParams(this.$route.params.app)
|
||||
},
|
||||
watch: {
|
||||
'$route' (to, from) {
|
||||
this.loadParams(to.params.app)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadParams (app) {
|
||||
parameters.getApplicationStructure(app).then(response => {
|
||||
this.structure = response.data
|
||||
})
|
||||
parameters.getApplication(app).then(response => {
|
||||
this.parameters = response.data
|
||||
})
|
||||
},
|
||||
display (element) {
|
||||
if (element.display === '') {
|
||||
return true
|
||||
}
|
||||
let result = true
|
||||
element.display.split('&').forEach(rule => {
|
||||
var [field, value] = rule.split('=')
|
||||
if (value === 'true' || value === 'false') {
|
||||
value = Boolean(value)
|
||||
}
|
||||
result &= this.parameters[field] === value
|
||||
})
|
||||
return result
|
||||
},
|
||||
displayableParams (params) {
|
||||
return params.filter(param => this.display(param))
|
||||
},
|
||||
save () {
|
||||
parameters.saveApplication(this.$route.params.app, this.parameters).then(() => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Parameters updated') })
|
||||
}).catch(error => {
|
||||
this.formErrors = error.response.data
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-toolbar {
|
||||
background-color: #f7f8fa !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-subtitle-1 grey--text text--darken-1 mb-4" :class="{ 'label--disabled': disabled }">
|
||||
<label class="m-label">{{ label }}</label>
|
||||
</div>
|
||||
<div class="d-flex" v-for="(lineChoices, index1) in formatedChoices" :key="index1">
|
||||
<div v-for="(choice, index2) in lineChoices"
|
||||
:key="index2"
|
||||
class="choice rounded pa-10 mr-4 text-center flex-grow-0 mb-4"
|
||||
:class="{ 'choice--disabled': disabled, 'choice--selected': !disabled && currentChoice === choice.value }"
|
||||
@click="selectChoice(choice.value)"
|
||||
>
|
||||
<v-icon v-if="choice.icon" class="d-block mb-2" :color="iconColor(choice.value)" x-large>{{ choice.icon }}</v-icon>
|
||||
<translate class="grey--text text--darken-1">{{ choice.label }}</translate>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: [Number, String],
|
||||
label: String,
|
||||
choices: Array,
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
choicesPerLine: Number
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentChoice: null,
|
||||
formatedChoices: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatChoices () {
|
||||
if (this.choicesPerLine) {
|
||||
let sliceIndex = 0
|
||||
while (sliceIndex < this.choices.length) {
|
||||
const result = this.choices.slice(sliceIndex, sliceIndex + this.choicesPerLine)
|
||||
this.formatedChoices.push(result)
|
||||
sliceIndex += this.choicesPerLine
|
||||
}
|
||||
} else {
|
||||
this.formatedChoices.push(this.choices)
|
||||
}
|
||||
},
|
||||
iconColor (value) {
|
||||
return (!this.disabled && value === this.currentChoice) ? 'primary' : ''
|
||||
},
|
||||
selectChoice (value) {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
this.currentChoice = value
|
||||
this.$emit('input', value)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.formatChoices()
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler (val) {
|
||||
this.currentChoice = val
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.choice {
|
||||
// width: 200px;
|
||||
flex-basis: 200px;
|
||||
background-color: #F2F5F7;
|
||||
border: 1px solid #DBDDDF;
|
||||
cursor: pointer;
|
||||
|
||||
&--selected {
|
||||
border-color: #046BF8 !important;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
cursor: unset;
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
.label--disabled {
|
||||
opacity: .5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="d-flex justify-center inner">
|
||||
<v-stepper v-model="currentStep">
|
||||
<v-stepper-header class="align-center px-10">
|
||||
<v-img
|
||||
src="../../assets/Modoboa_RVB-BLEU-SANS.png"
|
||||
max-width="190"
|
||||
/>
|
||||
<v-stepper-step
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
:complete="currentStep > index + 1" :step="index + 1">
|
||||
{{ step.title }}
|
||||
</v-stepper-step>
|
||||
<v-stepper-step :step="steps.length + 1">
|
||||
<translate>Summary</translate>
|
||||
</v-stepper-step>
|
||||
<v-btn icon @click="close">
|
||||
<v-icon color="primary" x-large>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-stepper-header>
|
||||
<v-stepper-items class="mt-4 d-flex justify-center">
|
||||
<v-stepper-content
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
:step="index + 1"
|
||||
class="flex-grow-0"
|
||||
>
|
||||
<div class="mb-6 text-h5">
|
||||
<translate class="grey--text text--darken-1">{{ title }}</translate> /
|
||||
<translate>{{ step.title }}</translate>
|
||||
</div>
|
||||
<slot :name="`form.${step.name}`" v-bind:step="index + 1" />
|
||||
<div class="d-flex justify-end mt-4">
|
||||
<v-btn v-if="index + 1 > 1" @click="currentStep = index" class="mr-10" text>
|
||||
<translate>Back</translate>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="goToNextStep(index + 1, index + 2)"
|
||||
large
|
||||
>
|
||||
<translate>Next</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
<v-stepper-content :step="steps.length + 1" class="flex-grow-0">
|
||||
<div class="text-center text-h3"><translate>Summary</translate></div>
|
||||
<creation-summary
|
||||
:sections="summarySections"
|
||||
@modify-step="val => currentStep = val"
|
||||
>
|
||||
<template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
|
||||
<slot :name="slot" v-bind="scope" />
|
||||
</template>
|
||||
</creation-summary>
|
||||
<div class="d-flex justify-center mt-8">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="$emit('create')"
|
||||
large
|
||||
>
|
||||
<translate>Confirm and create</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-stepper-content>
|
||||
</v-stepper-items>
|
||||
</v-stepper>
|
||||
<confirm-dialog ref="confirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog'
|
||||
import CreationSummary from '@/components/tools/CreationSummary'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
CreationSummary
|
||||
},
|
||||
props: {
|
||||
title: String,
|
||||
steps: Array,
|
||||
summarySections: Array,
|
||||
formObserverGetter: Function,
|
||||
validateObject: Function
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentStep: 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async close (withConfirm) {
|
||||
if (withConfirm) {
|
||||
const confirm = await this.$refs.confirm.open(
|
||||
this.$gettext('Warning'),
|
||||
this.$gettext('If you close this form now, your modifications won\'t be saved. Do you confirm?'),
|
||||
{
|
||||
color: 'error'
|
||||
}
|
||||
)
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.$emit('close')
|
||||
this.currentStep = 1
|
||||
this.steps.forEach((item, index) => {
|
||||
const observer = this.formObserverGetter(index + 1)
|
||||
if (observer !== undefined) {
|
||||
observer.reset()
|
||||
}
|
||||
})
|
||||
},
|
||||
async goToNextStep (current, next) {
|
||||
const observer = this.formObserverGetter(current)
|
||||
if (observer !== undefined) {
|
||||
const valid = await observer.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.validateObject()
|
||||
} catch (error) {
|
||||
observer.setErrors(error.response.data)
|
||||
return
|
||||
}
|
||||
}
|
||||
this.currentStep = next
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.inner {
|
||||
background-color: #fff;
|
||||
}
|
||||
.v-stepper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&__content {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
&__items {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(section, index) in sections" :key="index">
|
||||
<div class="subtitle mt-4 d-flex">
|
||||
<span class="text-h6">{{ section.title }}</span>
|
||||
<a href="#" class="edit-link ml-auto" @click="$emit('modify-step', index + 1)"><translate>Modify</translate></a>
|
||||
</div>
|
||||
<v-row v-for="(item, itemIndex) in section.items"
|
||||
:key="itemIndex"
|
||||
dense
|
||||
>
|
||||
<template v-if="item.value !== undefined">
|
||||
<slot :name="`item.${item.name}`" v-bind:item="item" v-bind:displayList="displayList">
|
||||
<v-col><span class="grey--text">{{ item.key }}</span></v-col>
|
||||
<v-col class="text-right" v-if="item.type === 'yesno'">{{ item.value|yesno }}</v-col>
|
||||
<v-col class="text-right" v-else-if="item.type === 'list'">
|
||||
{{ displayList(item.value) }}
|
||||
</v-col>
|
||||
<v-col class="text-right" v-else>{{ item.value }}</v-col>
|
||||
</slot>
|
||||
</template>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['sections'],
|
||||
methods: {
|
||||
displayList (value) {
|
||||
return (value.length) ? value.join(', ') : this.$gettext('Empty')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.subtitle {
|
||||
color: #000;
|
||||
border-bottom: 1px solid #DBDDDF;
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<v-menu
|
||||
v-model="menu"
|
||||
:close-on-content-click="false"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
min-width="auto"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
:value="value"
|
||||
:label="label"
|
||||
prepend-icon="mdi-calendar"
|
||||
readonly
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<v-date-picker
|
||||
:value="value"
|
||||
@input="update"
|
||||
no-title
|
||||
>
|
||||
</v-date-picker>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['value', 'label'],
|
||||
data () {
|
||||
return {
|
||||
menu: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update (value) {
|
||||
this.$emit('input', value)
|
||||
this.menu = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-text-field
|
||||
ref="input"
|
||||
v-model="input"
|
||||
outlined
|
||||
dense
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
@input="update"
|
||||
@keydown="onKeyDown"
|
||||
/>
|
||||
<div class="menu"
|
||||
v-if="showMenu"
|
||||
:style="`width: ${width}px;`"
|
||||
>
|
||||
<v-list class="list">
|
||||
<v-list-item
|
||||
v-for="(domain, index) in filteredDomains"
|
||||
:key="index"
|
||||
@click="selectDomain(domain)"
|
||||
:class="selectionIndex === index ? 'selected' : ''"
|
||||
>
|
||||
<v-list-item-title>{{ domain.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
allowAdd: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
domains: 'identities/domains'
|
||||
}),
|
||||
filteredDomains () {
|
||||
return this.domains.filter(domain => domain.name.startsWith(this.domainSearch))
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
domainSearch: '',
|
||||
input: this.value,
|
||||
selectionIndex: 0,
|
||||
showMenu: false,
|
||||
width: 500
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onKeyDown (e) {
|
||||
const keyCode = e.keyCode
|
||||
|
||||
if (keyCode === 40 || keyCode === 34) {
|
||||
// on arrow down or page down
|
||||
if (!this.showMenu) {
|
||||
this.domainSearch = ''
|
||||
this.showMenu = true
|
||||
} else {
|
||||
this.increaseSelectionIndex()
|
||||
}
|
||||
e.preventDefault()
|
||||
} else if (keyCode === 38 || keyCode === 33) {
|
||||
// on arrow up or page up
|
||||
if (!this.showMenu) {
|
||||
this.domainSearch = ''
|
||||
this.showMenu = true
|
||||
} else {
|
||||
this.decreaseSelectionIndex()
|
||||
}
|
||||
e.preventDefault()
|
||||
} else if (keyCode === 13 || keyCode === 9) {
|
||||
// on enter or tab
|
||||
if (this.filteredDomains.length > 0) {
|
||||
this.selectDomain(this.filteredDomains[this.selectionIndex])
|
||||
} else if (this.allowAdd) {
|
||||
this.selectDomain()
|
||||
}
|
||||
e.preventDefault()
|
||||
} else if (keyCode === 27) {
|
||||
// on escape
|
||||
if (this.filteredDomains.length > 0) {
|
||||
this.showMenu = false
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
},
|
||||
increaseSelectionIndex () {
|
||||
if (this.selectionIndex >= this.filteredDomains.length - 1) {
|
||||
this.selectionIndex = 0
|
||||
} else {
|
||||
this.selectionIndex += 1
|
||||
}
|
||||
},
|
||||
decreaseSelectionIndex () {
|
||||
if (this.selectionIndex <= 0) {
|
||||
this.selectionIndex = this.filteredDomains.length - 1
|
||||
} else {
|
||||
this.selectionIndex -= 1
|
||||
}
|
||||
},
|
||||
selectDomain (domain) {
|
||||
if (domain !== undefined) {
|
||||
this.input = this.input.split('@')[0] + '@' + domain.name
|
||||
}
|
||||
this.showMenu = false
|
||||
this.$emit('input', this.input)
|
||||
this.$emit('domain-selected')
|
||||
},
|
||||
update (value) {
|
||||
if (this.input.indexOf('@') !== -1) {
|
||||
this.domainSearch = this.input.split('@')[1]
|
||||
if (this.allowAdd) {
|
||||
this.showMenu = this.filteredDomains.length > 0
|
||||
} else {
|
||||
this.showMenu = true
|
||||
}
|
||||
} else {
|
||||
this.showMenu = false
|
||||
}
|
||||
this.$emit('input', this.input)
|
||||
},
|
||||
onResize () {
|
||||
this.updateMinWidthProperty()
|
||||
},
|
||||
updateMinWidthProperty () {
|
||||
this.width = this.$refs.input.$el.clientWidth
|
||||
},
|
||||
reset () {
|
||||
this.input = ''
|
||||
this.domainSearch = ''
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// window.addEventListener('resize', this.onResize)
|
||||
},
|
||||
updated () {
|
||||
this.$nextTick(() => {
|
||||
this.updateMinWidthProperty()
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
value (newValue) {
|
||||
this.input = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu {
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
margin-top: -20px;
|
||||
padding: 5px 0;
|
||||
box-shadow: 0 1px 5px 1px rgba(0,0,0,0.3);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
contain: content;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 1.1em;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: cornsilk;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="(item, index) in items"
|
||||
:key="index"
|
||||
@click="item.onClick(object)"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon :color="item.color">{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ item.label }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['items', 'object']
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-toolbar dense elevation="0">
|
||||
<v-toolbar-title>{{ title }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-menu
|
||||
v-model="menu"
|
||||
:nudge-width="200"
|
||||
offset-y
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn
|
||||
small
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon small>mdi-calendar</v-icon>
|
||||
<translate>Period</translate>
|
||||
<span v-if="period">: {{ period }}</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-layout>
|
||||
<v-row>
|
||||
<v-col cols="6" class="d-flex flex-column align-start py-3 px-6">
|
||||
<translate class="subtitle">Absolute time range</translate>
|
||||
<date-field v-model="start" :label="'From'|translate" />
|
||||
<date-field v-model="end" :label="'To'|translate" />
|
||||
<v-btn color="primary" @click="setCustomPeriod"><translate>Apply</translate></v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<translate class="subtitle">Relative time ranges</translate>
|
||||
<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-layout>
|
||||
</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>
|
||||
import VueApexCharts from 'vue-apexcharts'
|
||||
import statistics from '@/api/statistics'
|
||||
import DateField from './DateField'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
apexchart: VueApexCharts,
|
||||
DateField
|
||||
},
|
||||
props: ['domain', 'graphicSet', 'graphicName'],
|
||||
computed: {
|
||||
title () {
|
||||
if (this.statistics) {
|
||||
return this.statistics.title
|
||||
}
|
||||
return ''
|
||||
},
|
||||
colors () {
|
||||
if (this.graphicName === 'averagetraffic') {
|
||||
return ['#ff6347', '#ffff00', '#4682B4', '#7cfc00', '#ffa500', '#d3d3d3']
|
||||
}
|
||||
return ['#ffa500', '#41d1cc']
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
dateMenu: false,
|
||||
menu: false,
|
||||
options: {
|
||||
colors: this.colors,
|
||||
chart: {
|
||||
type: 'area',
|
||||
stacked: false,
|
||||
zoom: {
|
||||
type: 'x',
|
||||
enabled: true,
|
||||
autoScaleYaxis: true
|
||||
},
|
||||
toolbar: {
|
||||
autoSelected: 'zoom'
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
yaxis: {
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime'
|
||||
}
|
||||
},
|
||||
end: null,
|
||||
start: null,
|
||||
statistics: null,
|
||||
period: 'day',
|
||||
periods: [
|
||||
{ value: 'day', name: this.$gettext('Day') },
|
||||
{ value: 'week', name: this.$gettext('Week') },
|
||||
{ value: 'month', name: this.$gettext('Month') },
|
||||
{ value: 'year', name: this.$gettext('Year') }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setCustomPeriod () {
|
||||
this.period = 'custom'
|
||||
this.fetchStatistics()
|
||||
},
|
||||
setPeriod (period) {
|
||||
if (period !== this.period) {
|
||||
this.period = period
|
||||
this.fetchStatistics()
|
||||
}
|
||||
},
|
||||
fetchStatistics () {
|
||||
const args = {
|
||||
graphSet: this.graphicSet,
|
||||
period: this.period,
|
||||
graphicName: this.graphicName,
|
||||
searchQuery: this.domain.name
|
||||
}
|
||||
if (this.period === 'custom') {
|
||||
args.start = this.start
|
||||
args.end = this.end
|
||||
}
|
||||
statistics.getStatistics(args).then(resp => {
|
||||
this.statistics = resp.data.graphs[this.graphicName]
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.fetchStatistics()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-card {
|
||||
z-index: 5;
|
||||
}
|
||||
.v-menu__content {
|
||||
background-color: white !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
import { DateTime } from 'luxon'
|
||||
import { translate } from 'vue-gettext'
|
||||
const { gettext: $gettext } = translate
|
||||
|
||||
Vue.filter('yesno', function (value) {
|
||||
return (value) ? $gettext('yes') : $gettext('no')
|
||||
})
|
||||
|
||||
/*
|
||||
** Return a human readable version of the given date string.
|
||||
*/
|
||||
Vue.filter('date', function (value) {
|
||||
return DateTime.fromISO(value).setLocale('en').toLocaleString(DateTime.DATETIME_MED)
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import Vue from 'vue'
|
||||
import './plugins/vee-validate'
|
||||
import './plugins/vue-cookie'
|
||||
import './plugins/vue-gettext'
|
||||
import './filters'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import sassStyles from './styles/global.scss' // eslint-disable-line no-unused-vars
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
export const bus = new Vue()
|
||||
|
||||
fetch(process.env.BASE_URL + 'config.json').then(resp => {
|
||||
resp.json().then((config) => {
|
||||
Vue.prototype.$config = config
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
vuetify,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import Vue from 'vue'
|
||||
import { ValidationObserver, ValidationProvider, extend } from 'vee-validate'
|
||||
import { numeric, required } from 'vee-validate/dist/rules'
|
||||
import { translate } from 'vue-gettext'
|
||||
|
||||
const { gettext: $gettext } = translate
|
||||
|
||||
// Declare builtin rules here
|
||||
extend('numeric', {
|
||||
...numeric,
|
||||
message: () => $gettext('This field must be a valid number')
|
||||
})
|
||||
extend('required', {
|
||||
...required,
|
||||
message: () => $gettext('This field is required')
|
||||
})
|
||||
|
||||
// Add custom rules here
|
||||
|
||||
// Register components globally
|
||||
Vue.component('ValidationObserver', ValidationObserver)
|
||||
Vue.component('ValidationProvider', ValidationProvider)
|
||||
@@ -0,0 +1,4 @@
|
||||
import Vue from 'vue'
|
||||
import VueCookie from 'vue-cookie'
|
||||
|
||||
Vue.use(VueCookie)
|
||||
@@ -0,0 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
import GetTextPlugin from 'vue-gettext'
|
||||
import translations from '@/translations.json'
|
||||
|
||||
Vue.use(GetTextPlugin, {
|
||||
availableLanguages: {
|
||||
en: 'English',
|
||||
fr: 'Français'
|
||||
},
|
||||
translations: translations
|
||||
})
|
||||
|
||||
Vue.filter('translate', value => {
|
||||
return !value ? '' : Vue.prototype.$gettext(value.toString())
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import Vue from 'vue'
|
||||
import Vuetify from 'vuetify/lib'
|
||||
|
||||
Vue.use(Vuetify)
|
||||
|
||||
export default new Vuetify({
|
||||
theme: {
|
||||
options: {
|
||||
customProperties: true
|
||||
},
|
||||
themes: {
|
||||
light: {
|
||||
primary: '#046BF8',
|
||||
secondary: '#F18429',
|
||||
label: '#616161'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
|
||||
import store from '@/store'
|
||||
|
||||
import ParametersForm from '@/components/parameters/ParametersForm'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
component: () => import(/* webpackChunkName: "dashboard" */ '../views/Dashboard.vue'),
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/twofa',
|
||||
name: 'TwoFA',
|
||||
component: () => import('../views/TwoFA.vue')
|
||||
},
|
||||
{
|
||||
path: '/domains',
|
||||
name: 'DomainList',
|
||||
component: () => import('../views/domains/Domains.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
allowedRoles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/domains/:id',
|
||||
name: 'DomainDetail',
|
||||
component: () => import('../views/domains/Domain.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
allowedRoles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/domains/:id/edit',
|
||||
name: 'DomainEdit',
|
||||
component: () => import('../views/domains/DomainEdit.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
allowedRoles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/identities',
|
||||
name: 'Identities',
|
||||
component: () => import('../views/identities/Identities.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
allowedRoles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/identities/accounts/:id/edit',
|
||||
name: 'AccountEdit',
|
||||
component: () => import('../views/identities/AccountEdit.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
allowedRoles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/identities/aliases/:id/edit',
|
||||
name: 'AliasEdit',
|
||||
component: () => import('../views/identities/AliasEdit.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
allowedRoles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/parameters/:app',
|
||||
name: 'ParametersEdit',
|
||||
component: ParametersForm,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
allowedRoles: ['SuperAdmins']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/logs/',
|
||||
name: 'AuditTrail',
|
||||
component: () => import('../views/logs/AuditTrail.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
allowedRoles: ['SuperAdmins']
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||
store.dispatch('auth/initialize').then(() => {
|
||||
if (!store.getters['auth/isAuthenticated']) {
|
||||
next({ name: 'Login' })
|
||||
} else {
|
||||
if (to.meta.allowedRoles !== undefined) {
|
||||
if (to.meta.allowedRoles.indexOf(store.getters['auth/authUser'].role) === -1) {
|
||||
next({ name: 'Dashboard' })
|
||||
return
|
||||
}
|
||||
}
|
||||
next()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,17 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import auth from '@/store/modules/auth'
|
||||
import domains from '@/store/modules/domains'
|
||||
import identities from '@/store/modules/identities'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
auth,
|
||||
domains,
|
||||
identities
|
||||
},
|
||||
strict: process.env.NODE_ENV !== 'production'
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
import repository from '@/api/repository'
|
||||
import account from '@/api/account'
|
||||
|
||||
function setupAxios (token) {
|
||||
repository.defaults.headers.common.Authorization = `Bearer ${token}`
|
||||
repository.defaults.headers.post['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
function fetchUser (commit) {
|
||||
return account.getMe().then(resp => {
|
||||
commit('SET_AUTH_USER', { authUser: resp.data, isAuthenticated: true })
|
||||
})
|
||||
}
|
||||
|
||||
const state = () => ({
|
||||
authUser: {},
|
||||
isAuthenticated: false
|
||||
})
|
||||
|
||||
const getters = {
|
||||
isAuthenticated: state => state.isAuthenticated,
|
||||
authUser: state => state.authUser
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
SET_AUTH_USER (state, { authUser, isAuthenticated }) {
|
||||
state.authUser = authUser
|
||||
state.isAuthenticated = isAuthenticated
|
||||
},
|
||||
LOGOUT_USER (state) {
|
||||
state.authUser = null
|
||||
state.isAuthenticated = false
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
initialize ({ commit, state }) {
|
||||
if (state.isAuthenticated) {
|
||||
return
|
||||
}
|
||||
const token = Cookies.get('token')
|
||||
if (!token) {
|
||||
return
|
||||
}
|
||||
setupAxios(token)
|
||||
return fetchUser(commit)
|
||||
},
|
||||
logout ({ commit }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
delete repository.defaults.headers.common.Authorization
|
||||
Cookies.remove('token')
|
||||
Cookies.remove('refreshToken')
|
||||
commit('LOGOUT_USER')
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import Vue from 'vue'
|
||||
import domainApi from '@/api/domains'
|
||||
|
||||
// mutation types
|
||||
const SET_DOMAINS = 'SET_DOMAINS'
|
||||
const ADD_DOMAIN = 'ADD_DOMAIN'
|
||||
const UPDATE_DOMAIN = 'UPDATE_DOMAIN'
|
||||
const DELETE_DOMAIN = 'DELETE_DOMAIN'
|
||||
const SET_DOMAIN_ALIASES = 'SET_DOMAIN_ALIASES'
|
||||
const ADD_DOMAIN_ALIAS = 'ADD_DOMAIN_ALIAS'
|
||||
const UPDATE_DOMAIN_ALIAS = 'UPDATE_DOMAIN_ALIAS'
|
||||
const REMOVE_DOMAIN_ALIAS = 'REMOVE_DOMAIN_ALIAS'
|
||||
|
||||
// initial state
|
||||
const state = {
|
||||
domainsLoaded: false,
|
||||
domains: [],
|
||||
domainAliases: []
|
||||
}
|
||||
|
||||
// getters
|
||||
const getters = {
|
||||
domains: state => state.domains,
|
||||
getDomainByPk: state => pk => {
|
||||
for (var domain of state.domains) {
|
||||
if (domain.pk === parseInt(pk, 10)) {
|
||||
return domain
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
domainAliases: state => state.domainAliases
|
||||
}
|
||||
|
||||
// actions
|
||||
const actions = {
|
||||
getDomains ({ commit }) {
|
||||
return domainApi.getDomains().then(response => {
|
||||
commit(SET_DOMAINS, { domains: response.data })
|
||||
})
|
||||
},
|
||||
createDomain ({ commit }, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
domainApi.createDomain(data).then(response => {
|
||||
commit(ADD_DOMAIN, { domain: response.data })
|
||||
resolve(response)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
updateDomain ({ commit }, data) {
|
||||
return domainApi.updateDomain(data.pk, data).then(response => {
|
||||
commit(UPDATE_DOMAIN, { domain: response.data })
|
||||
})
|
||||
},
|
||||
deleteDomain ({ commit }, { id, data }) {
|
||||
return domainApi.deleteDomain(id, data).then(resp => {
|
||||
commit(DELETE_DOMAIN, id)
|
||||
})
|
||||
},
|
||||
getDomainAliases ({ commit }) {
|
||||
return Vue.prototype.$axios.get('/domainaliases/?expand=target').then(response => {
|
||||
commit(SET_DOMAIN_ALIASES, response.data)
|
||||
})
|
||||
},
|
||||
addDomainAlias ({ commit }, data) {
|
||||
return domainApi.createDomainAlias(data).then(response => {
|
||||
commit(ADD_DOMAIN_ALIAS, response.data)
|
||||
})
|
||||
},
|
||||
updateDomainAlias ({ commit }, { id, data }) {
|
||||
return domainApi.updateDomainAlias(id, data).then(response => {
|
||||
commit(UPDATE_DOMAIN_ALIAS, response.data)
|
||||
})
|
||||
},
|
||||
deleteDomainAlias ({ commit }, domainAliasId) {
|
||||
return domainApi.deleteDomainAlias(domainAliasId).then(response => {
|
||||
commit(REMOVE_DOMAIN_ALIAS, domainAliasId)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mutations
|
||||
const mutations = {
|
||||
[SET_DOMAINS] (state, { domains }) {
|
||||
state.domainsLoaded = true
|
||||
state.domains = domains
|
||||
},
|
||||
[ADD_DOMAIN] (state, { domain }) {
|
||||
state.domains.push(domain)
|
||||
},
|
||||
[UPDATE_DOMAIN] (state, { domain }) {
|
||||
state.domains.filter(function (item, pos) {
|
||||
if (item.pk === domain.pk) {
|
||||
Vue.set(state.domains, pos, domain)
|
||||
}
|
||||
})
|
||||
},
|
||||
[DELETE_DOMAIN] (state, domainId) {
|
||||
state.domains = state.domains.filter(item => item.pk !== domainId)
|
||||
},
|
||||
[SET_DOMAIN_ALIASES] (state, domainAliases) {
|
||||
state.domainAliases = domainAliases
|
||||
},
|
||||
[ADD_DOMAIN_ALIAS] (state, domainAlias) {
|
||||
state.domainAliases.push(domainAlias)
|
||||
},
|
||||
[UPDATE_DOMAIN_ALIAS] (state, domainAlias) {
|
||||
state.domainAliases.filter((item, pos) => {
|
||||
if (item.pk === domainAlias.pk) {
|
||||
Vue.set(state.domainAliases, pos, domainAlias)
|
||||
}
|
||||
})
|
||||
},
|
||||
[REMOVE_DOMAIN_ALIAS] (state, domainAliasId) {
|
||||
state.domainAliases = state.domainAliases.filter(item => item.pk !== domainAliasId)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import domainApi from '@/api/domains'
|
||||
|
||||
const state = {
|
||||
domains: []
|
||||
}
|
||||
|
||||
const getters = {
|
||||
domains: state => state.domains,
|
||||
getDomainByName: state => name => {
|
||||
return state.domains.find(domain => domain.name === name)
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
SET_DOMAINS (state, domains) {
|
||||
state.domains = domains
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
fetchDomains ({ commit }) {
|
||||
domainApi.getDomains().then(resp => {
|
||||
commit('SET_DOMAINS', resp.data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.m-label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: var(--v-label-base);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Globals
|
||||
$body-font-family: 'Rubik', serif;
|
||||
@@ -0,0 +1 @@
|
||||
{"en":{},"fr":{}}
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title><translate>Welcome to Modoboa</translate></v-toolbar-title>
|
||||
</v-toolbar>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<v-row style="height: 100%; width: 80%; margin: 0 auto">
|
||||
<v-col cols="0" sm="6" class="primary" style="height: 100%">
|
||||
<v-img
|
||||
src="../assets/Modoboa_RVB-BLANC-SANS.png"
|
||||
max-width="300"
|
||||
class="mt-10 ml-8"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="d-flex flex-column justify-center pa-10" @keyup.enter="authenticate">
|
||||
<span class="primary--text mb-6 text-h4"><translate>Identification</translate></span>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label"><translate>Username</translate></label>
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
vid="password"
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label"><translate>Password</translate></label>
|
||||
<v-text-field
|
||||
type="password"
|
||||
v-model="password"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
/>
|
||||
</validation-provider>
|
||||
<a href="" class="float-right primary--text"><translate>Forgot password?</translate></a>
|
||||
<v-checkbox
|
||||
v-model="rememberMe"
|
||||
:label="'Remember me'|translate"
|
||||
/>
|
||||
</validation-observer>
|
||||
<div class="d-flex justify-center">
|
||||
<v-btn class="flex-grow-1" color="primary" large @click="authenticate">
|
||||
<translate>Connect</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Cookies from 'js-cookie'
|
||||
import auth from '@/api/auth'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
rememberMe: false,
|
||||
username: '',
|
||||
password: '',
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async authenticate () {
|
||||
const isValid = await this.$refs.observer.validate()
|
||||
if (!isValid) {
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
}
|
||||
auth.requestToken(payload).then(resp => {
|
||||
Cookies.set('token', resp.data.access, { sameSite: 'strict' })
|
||||
Cookies.set('refreshToken', resp.data.refresh, { sameSite: 'strict' })
|
||||
this.$store.dispatch('auth/initialize').then(() => {
|
||||
this.$router.push({ name: 'Dashboard' })
|
||||
})
|
||||
}).catch(err => {
|
||||
if (err.response.status === 401) {
|
||||
this.$refs.observer.setErrors({
|
||||
password: this.$gettext('Invalid username and/or password')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<v-row style="height: 100%; width: 80%; margin: 0 auto">
|
||||
<v-col cols="0" sm="6" class="primary" style="height: 100%">
|
||||
<v-img
|
||||
src="../assets/Modoboa_RVB-BLANC-SANS.png"
|
||||
max-width="300"
|
||||
class="mt-10 ml-8"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="d-flex flex-column justify-center pa-10" @keyup.enter="verifyCode">
|
||||
<span class="primary--text mb-6 text-h4"><translate>Verification</translate></span>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
vid="code"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label"><translate>Two-factor authentication code</translate></label>
|
||||
<v-text-field
|
||||
v-model="code"
|
||||
prepend-icon="mdi-numeric"
|
||||
:error-messages="errors"
|
||||
:hint="'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.'|translate"
|
||||
persistent-hint
|
||||
dense
|
||||
outlined
|
||||
/>
|
||||
</validation-provider>
|
||||
</validation-observer>
|
||||
<div class="d-flex justify-center mt-6">
|
||||
<v-btn class="flex-grow-1" color="primary" large @click="verifyCode">
|
||||
<translate>Verify code</translate>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Cookies from 'js-cookie'
|
||||
import account from '@/api/account'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
code: null,
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async verifyCode () {
|
||||
const isValid = await this.$refs.observer.validate()
|
||||
if (!isValid) {
|
||||
return
|
||||
}
|
||||
account.verifyTFACode(this.code).then(resp => {
|
||||
Cookies.set('token', resp.data.access, { sameSite: 'strict' })
|
||||
Cookies.set('refreshToken', resp.data.refresh, { sameSite: 'strict' })
|
||||
this.$store.dispatch('auth/initialize').then(() => {
|
||||
this.$router.push({ name: 'Dashboard' })
|
||||
})
|
||||
}).catch(error => {
|
||||
this.$refs.observer.setErrors(error.response.data)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title><translate>Domain</translate> {{ domain.name }}</v-toolbar-title>
|
||||
<v-btn color="primary" icon :to="{ name: 'DomainEdit', params: { id: domain.pk } }">
|
||||
<v-icon>mdi-circle-edit-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-layout>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<domain-summary :domain="domain" />
|
||||
<div class="mt-4" />
|
||||
<administrator-list
|
||||
:domain="domain"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<dns-detail :domain="domain" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-layout>
|
||||
<v-layout>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<time-serie-chart :domain="domain" graphic-set="mailtraffic" graphic-name="averagetraffic" />
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<time-serie-chart :domain="domain" graphic-set="mailtraffic" graphic-name="averagetrafficsize" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-layout>
|
||||
<v-dialog v-model="showEditForm"
|
||||
persistent
|
||||
max-width="800px"
|
||||
>
|
||||
<domain-form :domain="domain" @close="showEditForm = false" />
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import domains from '@/api/domains'
|
||||
import AdministratorList from '@/components/domains/AdministratorList'
|
||||
import DNSDetail from '@/components/domains/DNSDetail'
|
||||
import DomainForm from '@/components/domains/DomainForm'
|
||||
import DomainSummary from '@/components/domains/DomainSummary'
|
||||
import TimeSerieChart from '@/components/tools/TimeSerieChart'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AdministratorList,
|
||||
'dns-detail': DNSDetail,
|
||||
DomainForm,
|
||||
DomainSummary,
|
||||
TimeSerieChart
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
domain: { pk: this.$route.params.id },
|
||||
showEditForm: false,
|
||||
showAdminList: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
mounted () {
|
||||
domains.getDomain(this.$route.params.id).then(resp => {
|
||||
this.domain = resp.data
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-toolbar {
|
||||
background-color: #f7f8fa !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="text-h4"><translate>Edit domain</translate></span>
|
||||
<div class="mt-4" />
|
||||
<domain-form :domain="domain" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import domains from '@/api/domains'
|
||||
import DomainForm from '@/components/domains/DomainForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DomainForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
domain: {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
domains.getDomain(this.$route.params.id).then(resp => {
|
||||
this.domain = resp.data
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title><translate>Domains</translate></v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn class="mr-2">
|
||||
<v-icon>mdi-file-import-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn class="mr-2">
|
||||
<v-icon>mdi-file-export-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn color="primary" v-bind="attrs" v-on="on">
|
||||
<v-icon left>mdi-plus</v-icon>
|
||||
<translate>New</translate>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item @click="showDomainWizard = true">
|
||||
<v-list-item-title><translate>Domain</translate></v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showAliasForm = true">
|
||||
<v-list-item-title><translate>Alias</translate></v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-toolbar>
|
||||
<domain-list />
|
||||
|
||||
<v-dialog
|
||||
v-model="showDomainWizard"
|
||||
fullscreen
|
||||
scrollable
|
||||
transition="dialog-bottom-transition"
|
||||
>
|
||||
<domain-creation-form @close="showDomainWizard = false" />
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showAliasForm"
|
||||
persistent
|
||||
max-width="800px">
|
||||
<domain-alias-form @close="showAliasForm = false" />
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DomainAliasForm from '@/components/domains/DomainAliasForm'
|
||||
import DomainCreationForm from '@/components/domains/DomainCreationForm'
|
||||
import DomainList from '@/components/domains/DomainList'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DomainAliasForm,
|
||||
DomainCreationForm,
|
||||
DomainList
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showAliasForm: false,
|
||||
showDomainWizard: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-toolbar {
|
||||
background-color: #f7f8fa !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="text-h4"><translate>Edit account</translate></span>
|
||||
<div class="mt-4" />
|
||||
<account-form :account="account" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import accounts from '@/api/accounts'
|
||||
import AccountForm from '@/components/identities/AccountForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
account: {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
accounts.get(this.$route.params.id).then(resp => {
|
||||
this.account = resp.data
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="text-h4"><translate>Edit alias</translate></span>
|
||||
<div class="mt-4" />
|
||||
<alias-form :alias="alias" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import aliases from '@/api/aliases'
|
||||
import AliasForm from '@/components/identities/AliasForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AliasForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
alias: {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
aliases.get(this.$route.params.id).then(resp => {
|
||||
this.alias = resp.data
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title><translate>Identities</translate></v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn class="mr-2">
|
||||
<v-icon>mdi-file-import-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn class="mr-2">
|
||||
<v-icon>mdi-file-export-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn color="primary" v-bind="attrs" v-on="on">
|
||||
<v-icon left>mdi-plus</v-icon>
|
||||
<translate>New</translate>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item @click="showCreationWizard = true">
|
||||
<v-list-item-title><translate>Account</translate></v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showAliasCreationWizard = true">
|
||||
<v-list-item-title><translate>Alias</translate></v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-toolbar>
|
||||
<identity-list ref="identities" />
|
||||
|
||||
<v-dialog
|
||||
v-model="showCreationWizard"
|
||||
fullscreen
|
||||
scrollable
|
||||
transition="dialog-bottom-transition"
|
||||
persistent
|
||||
>
|
||||
<account-creation-form @close="closeCreationWizard" @created="updateIdentities" />
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-model="showAliasCreationWizard"
|
||||
fullscreen
|
||||
scrollable
|
||||
transition="dialog-bottom-transition"
|
||||
persistent
|
||||
>
|
||||
<alias-creation-form @close="closeAliasWizard" @created="updateIdentities" />
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AccountCreationForm from '@/components/identities/AccountCreationForm'
|
||||
import AliasCreationForm from '@/components/identities/AliasCreationForm'
|
||||
import IdentityList from '@/components/identities/IdentityList'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountCreationForm,
|
||||
AliasCreationForm,
|
||||
IdentityList
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showAliasCreationWizard: false,
|
||||
showCreationWizard: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeAliasWizard () {
|
||||
this.showAliasCreationWizard = false
|
||||
},
|
||||
closeCreationWizard () {
|
||||
this.showCreationWizard = false
|
||||
},
|
||||
updateIdentities () {
|
||||
this.$refs.identities.fetchIdentities()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-toolbar {
|
||||
background-color: #f7f8fa !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title><translate>Audit trail</translate></v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<audit-trail-list />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AuditTrailList from '@/components/logs/AuditTrailList.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AuditTrailList
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-toolbar {
|
||||
background-color: #f7f8fa !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { expect } from 'chai'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import HelloWorld from '@/components/HelloWorld.vue'
|
||||
|
||||
describe('HelloWorld.vue', () => {
|
||||
it('renders props.msg when passed', () => {
|
||||
const msg = 'new message'
|
||||
const wrapper = shallowMount(HelloWorld, {
|
||||
propsData: { msg }
|
||||
})
|
||||
expect(wrapper.text()).to.include(msg)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
transpileDependencies: [
|
||||
'vuetify'
|
||||
]
|
||||
}
|
||||
+10246
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
"""Admin serializers."""
|
||||
|
||||
from typing import List
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import password_validation
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -7,6 +9,7 @@ from django.shortcuts import get_object_or_404
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from rest_flex_fields import FlexFieldsModelSerializer
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
@@ -17,10 +20,11 @@ from modoboa.core import (
|
||||
)
|
||||
from modoboa.lib import (
|
||||
email_utils, exceptions as lib_exceptions, fields as lib_fields,
|
||||
permissions, web_utils
|
||||
permissions, validators, web_utils
|
||||
)
|
||||
from modoboa.parameters import tools as param_tools
|
||||
from . import lib, models
|
||||
|
||||
from ... import lib, models
|
||||
|
||||
|
||||
class DomainSerializer(serializers.ModelSerializer):
|
||||
@@ -48,10 +52,13 @@ class DomainSerializer(serializers.ModelSerializer):
|
||||
"enable_dkim", "dkim_key_selector", "dkim_key_length",
|
||||
"dkim_public_key", "dkim_private_key_path",
|
||||
"mailbox_count", "mbalias_count", "domainalias_count",
|
||||
"message_limit"
|
||||
"dns_global_status", "message_limit", "creation",
|
||||
"last_modification"
|
||||
)
|
||||
read_only_fields = (
|
||||
"pk", "mailbox_count", "mbalias_count", "domainalias_count"
|
||||
"pk", "dkim_public_key", "dns_global_status", "allocated_quota_in_percent"
|
||||
"mailbox_count", "mbalias_count", "domainalias_count",
|
||||
"enable_dns_checks", "creation", "last_modification"
|
||||
)
|
||||
|
||||
def validate_name(self, value):
|
||||
@@ -104,12 +111,15 @@ class DomainSerializer(serializers.ModelSerializer):
|
||||
return domain
|
||||
|
||||
|
||||
class DomainAliasSerializer(serializers.ModelSerializer):
|
||||
class DomainAliasSerializer(FlexFieldsModelSerializer):
|
||||
"""Base DomainAlias serializer."""
|
||||
|
||||
class Meta:
|
||||
model = admin_models.DomainAlias
|
||||
fields = ("pk", "name", "target", "enabled", )
|
||||
expandable_fields = {
|
||||
"target": DomainSerializer
|
||||
}
|
||||
|
||||
def validate_target(self, value):
|
||||
"""Check target domain."""
|
||||
@@ -143,7 +153,7 @@ class MailboxSerializer(serializers.ModelSerializer):
|
||||
"""Base mailbox serializer."""
|
||||
|
||||
full_address = lib_fields.DRFEmailFieldUTF8()
|
||||
quota = serializers.CharField()
|
||||
quota = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = models.Mailbox
|
||||
@@ -159,6 +169,16 @@ class MailboxSerializer(serializers.ModelSerializer):
|
||||
"""Convert quota to MB."""
|
||||
return web_utils.size2integer(value, output_unit="MB")
|
||||
|
||||
def validate(self, data):
|
||||
"""Check if quota is required."""
|
||||
method = self.context["request"].method
|
||||
if not data.get("use_domain_quota", False):
|
||||
if "quota" not in data and method != "PATCH":
|
||||
raise serializers.ValidationError({
|
||||
"quota": _("This field is required")
|
||||
})
|
||||
return data
|
||||
|
||||
|
||||
class AccountSerializer(serializers.ModelSerializer):
|
||||
"""Base account serializer."""
|
||||
@@ -175,7 +195,7 @@ class AccountSerializer(serializers.ModelSerializer):
|
||||
fields = (
|
||||
"pk", "username", "first_name", "last_name", "is_active",
|
||||
"master_user", "mailbox", "role", "language", "phone_number",
|
||||
"secondary_email", "domains",
|
||||
"secondary_email", "domains"
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -193,7 +213,7 @@ class AccountSerializer(serializers.ModelSerializer):
|
||||
"""Return role."""
|
||||
return account.role
|
||||
|
||||
def get_domains(self, account):
|
||||
def get_domains(self, account) -> List[str]:
|
||||
"""Return domains administered by this account."""
|
||||
if account.role not in ["DomainAdmins", "Resellers"]:
|
||||
return []
|
||||
@@ -252,7 +272,7 @@ class WritableAccountSerializer(AccountSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Adapt fields to current user."""
|
||||
super(WritableAccountSerializer, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
request = self.context.get("request")
|
||||
if not request:
|
||||
return
|
||||
@@ -275,18 +295,30 @@ class WritableAccountSerializer(AccountSerializer):
|
||||
"master_user": _("Not allowed for this role.")
|
||||
})
|
||||
if role == "SimpleUsers":
|
||||
username = data.get("username")
|
||||
if username:
|
||||
try:
|
||||
validators.UTF8EmailValidator()(username)
|
||||
except ValidationError as err:
|
||||
raise ValidationError({"username": err.message})
|
||||
mailbox = data.get("mailbox")
|
||||
if mailbox is None:
|
||||
if not self.instance:
|
||||
data["mailbox"] = {
|
||||
"full_address": data["username"],
|
||||
"full_address": username,
|
||||
"use_domain_quota": True
|
||||
}
|
||||
elif mailbox["full_address"] != data["username"]:
|
||||
elif "full_address" in mailbox and mailbox["full_address"] != username:
|
||||
raise serializers.ValidationError({
|
||||
"username": _("Must be equal to mailbox full_address")
|
||||
})
|
||||
if not data.get("random_password"):
|
||||
condition = (
|
||||
not data.get("random_password") and (
|
||||
data.get("password") or
|
||||
not self.partial
|
||||
)
|
||||
)
|
||||
if condition:
|
||||
password = data.get("password")
|
||||
if password:
|
||||
try:
|
||||
@@ -405,45 +437,32 @@ class WritableAccountSerializer(AccountSerializer):
|
||||
|
||||
|
||||
class AliasSerializer(serializers.ModelSerializer):
|
||||
"""Base Alias serializer."""
|
||||
"""Alias serializer."""
|
||||
|
||||
address = lib_fields.DRFEmailFieldUTF8AndEmptyUser()
|
||||
recipients = serializers.ListField(
|
||||
child=lib_fields.DRFEmailFieldUTF8AndEmptyUser(),
|
||||
allow_empty=False,
|
||||
help_text=ugettext_lazy("A list of recipient"))
|
||||
help_text=ugettext_lazy("A list of recipient")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = admin_models.Alias
|
||||
fields = ("pk", "address", "enabled", "internal", "recipients")
|
||||
fields = (
|
||||
"pk", "address", "enabled", "internal", "recipients"
|
||||
)
|
||||
|
||||
def validate_address(self, value):
|
||||
"""Check domain."""
|
||||
local_part, domain = email_utils.split_mailbox(value)
|
||||
self.domain = admin_models.Domain.objects.filter(name=domain).first()
|
||||
if self.domain is None:
|
||||
raise serializers.ValidationError(_("Domain not found."))
|
||||
if not self.context["request"].user.can_access(self.domain):
|
||||
raise serializers.ValidationError(_("Permission denied."))
|
||||
local_part, self.domain = admin_models.validate_alias_address(
|
||||
value, self.context["request"].user, instance=self.instance)
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create appropriate objects."""
|
||||
creator = self.context["request"].user
|
||||
try:
|
||||
core_signals.can_create_object.send(
|
||||
sender=self.__class__, context=creator,
|
||||
klass=admin_models.Alias)
|
||||
core_signals.can_create_object.send(
|
||||
sender=self.__class__, context=self.domain,
|
||||
object_type="mailbox_aliases")
|
||||
except lib_exceptions.ModoboaException as inst:
|
||||
raise serializers.ValidationError(force_text(inst))
|
||||
recipients = validated_data.pop("recipients", None)
|
||||
alias = admin_models.Alias(domain=self.domain, **validated_data)
|
||||
alias.save(creator=creator)
|
||||
alias.set_recipients(recipients)
|
||||
return alias
|
||||
return admin_models.Alias.objects.create(
|
||||
creator=creator, domain=self.domain, **validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Update objects."""
|
||||
@@ -12,11 +12,11 @@ from django.urls import reverse
|
||||
from rest_framework.authtoken.models import Token
|
||||
from reversion.models import Version
|
||||
|
||||
from modoboa.admin import models as admin_models
|
||||
from modoboa.admin import factories
|
||||
from modoboa.admin import models
|
||||
from modoboa.admin.tests import utils
|
||||
from modoboa.core import factories as core_factories, models as core_models
|
||||
from modoboa.lib.tests import ModoAPITestCase
|
||||
from . import utils
|
||||
from .. import factories, models
|
||||
|
||||
|
||||
class DomainAPITestCase(ModoAPITestCase):
|
||||
@@ -32,13 +32,13 @@ class DomainAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_get_domains(self):
|
||||
"""Retrieve a list of domains."""
|
||||
url = reverse("api:domain-list")
|
||||
url = reverse("v1:domain-list")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
domain = response.data[0]
|
||||
url = reverse(
|
||||
"api:domain-detail", args=[domain["pk"]])
|
||||
"v1:domain-detail", args=[domain["pk"]])
|
||||
response = self.client.get(url)
|
||||
data = response.json()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -47,7 +47,7 @@ class DomainAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_create_domain(self):
|
||||
"""Check domain creation."""
|
||||
url = reverse("api:domain-list")
|
||||
url = reverse("v1:domain-list")
|
||||
response = self.client.post(
|
||||
url, {"name": "test3.com", "quota": 0, "default_mailbox_quota": 10}
|
||||
)
|
||||
@@ -83,7 +83,7 @@ class DomainAPITestCase(ModoAPITestCase):
|
||||
token = Token.objects.create(user=reseller)
|
||||
self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
|
||||
|
||||
url = reverse("api:domain-list")
|
||||
url = reverse("v1:domain-list")
|
||||
mock_query.side_effect = utils.mock_dns_query_result
|
||||
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
|
||||
response = self.client.post(
|
||||
@@ -107,7 +107,7 @@ class DomainAPITestCase(ModoAPITestCase):
|
||||
models.Mailbox.objects.filter(
|
||||
domain__name="test.com", address="user").update(
|
||||
use_domain_quota=True)
|
||||
url = reverse("api:domain-detail", args=[domain.pk])
|
||||
url = reverse("v1:domain-detail", args=[domain.pk])
|
||||
response = self.client.put(
|
||||
url, {"name": "test.com", "default_mailbox_quota": 1000})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -127,7 +127,7 @@ class DomainAPITestCase(ModoAPITestCase):
|
||||
def test_patch_domain(self):
|
||||
"""Check domain partial update."""
|
||||
domain = models.Domain.objects.get(name="test.com")
|
||||
url = reverse("api:domain-detail", args=[domain.pk])
|
||||
url = reverse("v1:domain-detail", args=[domain.pk])
|
||||
response = self.client.put(url, {"name": "toto.test"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
domain.refresh_from_db()
|
||||
@@ -136,7 +136,7 @@ class DomainAPITestCase(ModoAPITestCase):
|
||||
def test_delete_domain(self):
|
||||
"""Try to delete a domain."""
|
||||
domain = models.Domain.objects.get(name="test.com")
|
||||
url = reverse("api:domain-detail", args=[domain.pk])
|
||||
url = reverse("v1:domain-detail", args=[domain.pk])
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertFalse(models.Domain.objects.filter(pk=domain.pk).exists())
|
||||
@@ -159,30 +159,30 @@ class DomainAliasAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_get(self):
|
||||
"""Retrieve a list of domain aliases."""
|
||||
url = reverse("api:domain_alias-list")
|
||||
url = reverse("v1:domain_alias-list")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
url = reverse(
|
||||
"api:domain_alias-detail", args=[response.data[0]["pk"]])
|
||||
"v1:domain_alias-detail", args=[response.data[0]["pk"]])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["name"], "dalias1.com")
|
||||
|
||||
url = reverse("api:domain_alias-list")
|
||||
url = reverse("v1:domain_alias-list")
|
||||
response = self.client.get("{}?domain=test.com".format(url))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION="Token " + self.da_token.key)
|
||||
response = self.client.get(reverse("api:domain_alias-list"))
|
||||
response = self.client.get(reverse("v1:domain_alias-list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.data), 0)
|
||||
|
||||
def test_post(self):
|
||||
"""Try to create a new domain alias."""
|
||||
url = reverse("api:domain_alias-list")
|
||||
url = reverse("v1:domain_alias-list")
|
||||
target = models.Domain.objects.get(name="test.com")
|
||||
data = {
|
||||
"name": "dalias3.com",
|
||||
@@ -205,7 +205,7 @@ class DomainAliasAPITestCase(ModoAPITestCase):
|
||||
def test_put(self):
|
||||
"""Try to update a domain alias."""
|
||||
dalias = models.DomainAlias.objects.get(name="dalias1.com")
|
||||
url = reverse("api:domain_alias-detail", args=[dalias.pk])
|
||||
url = reverse("v1:domain_alias-detail", args=[dalias.pk])
|
||||
data = {
|
||||
"name": "dalias3.com", "target": dalias.target.pk
|
||||
}
|
||||
@@ -219,7 +219,7 @@ class DomainAliasAPITestCase(ModoAPITestCase):
|
||||
def test_delete(self):
|
||||
"""Try to delete an existing domain alias."""
|
||||
dalias = models.DomainAlias.objects.get(name="dalias1.com")
|
||||
url = reverse("api:domain_alias-detail", args=[dalias.pk])
|
||||
url = reverse("v1:domain_alias-detail", args=[dalias.pk])
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertFalse(
|
||||
@@ -257,7 +257,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_get_accounts(self):
|
||||
"""Retrieve a list of accounts."""
|
||||
url = reverse("api:account-list")
|
||||
url = reverse("v1:account-list")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = response.json()
|
||||
@@ -280,7 +280,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_create_account(self):
|
||||
"""Try to create a new account."""
|
||||
url = reverse("api:account-list")
|
||||
url = reverse("v1:account-list")
|
||||
response = self.client.post(url, self.ACCOUNT_DATA, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
@@ -306,7 +306,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_create_account_with_random_password(self):
|
||||
"""Try to create a new account with random password."""
|
||||
url = reverse("api:account-list")
|
||||
url = reverse("v1:account-list")
|
||||
data = dict(self.ACCOUNT_DATA)
|
||||
data["random_password"] = True
|
||||
del data["password"]
|
||||
@@ -316,7 +316,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
@override_settings(LANGUAGE_CODE="fr")
|
||||
def test_create_account_default_lang(self):
|
||||
"""Try to create a new account with(out) language."""
|
||||
url = reverse("api:account-list")
|
||||
url = reverse("v1:account-list")
|
||||
data = dict(self.ACCOUNT_DATA)
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
@@ -337,16 +337,16 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
data = copy.deepcopy(self.ACCOUNT_DATA)
|
||||
data["domains"] = ["test.com"]
|
||||
data["role"] = "DomainAdmins"
|
||||
url = reverse("api:account-list")
|
||||
url = reverse("v1:account-list")
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
domain = admin_models.Domain.objects.get(name="test.com")
|
||||
domain = models.Domain.objects.get(name="test.com")
|
||||
admin = core_models.User.objects.get(
|
||||
pk=response.json()["pk"])
|
||||
self.assertIn(admin, domain.admins)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:account-detail", args=[admin.pk]))
|
||||
reverse("v1:account-detail", args=[admin.pk]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("domains", response.json())
|
||||
|
||||
@@ -359,7 +359,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
"""Try to create a new account."""
|
||||
data = copy.deepcopy(self.ACCOUNT_DATA)
|
||||
del data["mailbox"]
|
||||
url = reverse("api:account-list")
|
||||
url = reverse("v1:account-list")
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
account = response.json()
|
||||
@@ -373,7 +373,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
"""Check if unicity is respected."""
|
||||
data = copy.deepcopy(self.ACCOUNT_DATA)
|
||||
data["username"] = "user@test.com"
|
||||
url = reverse("api:account-list")
|
||||
url = reverse("v1:account-list")
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@@ -386,7 +386,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
"""Try to create a new account."""
|
||||
data = copy.deepcopy(self.ACCOUNT_DATA)
|
||||
data["password"] = "toto"
|
||||
url = reverse("api:account-list")
|
||||
url = reverse("v1:account-list")
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
errors = response.json()
|
||||
@@ -398,13 +398,13 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
HTTP_AUTHORIZATION="Token " + self.da_token.key)
|
||||
data = copy.deepcopy(self.ACCOUNT_DATA)
|
||||
data["mailbox"]["quota"] = 1000
|
||||
url = reverse("api:account-list")
|
||||
url = reverse("v1:account-list")
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
data["username"] = "fromapi@test2.com"
|
||||
data["mailbox"].update({"full_address": "fromapi@test2.com",
|
||||
"quota": 10})
|
||||
url = reverse("api:account-list")
|
||||
url = reverse("v1:account-list")
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
errors = response.json()
|
||||
@@ -414,7 +414,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
"""Try to create a new account."""
|
||||
data = copy.deepcopy(self.ACCOUNT_DATA)
|
||||
data["master_user"] = True
|
||||
url = reverse("api:account-list")
|
||||
url = reverse("v1:account-list")
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
errors = response.json()
|
||||
@@ -423,7 +423,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
def test_update_account(self):
|
||||
"""Try to update an account."""
|
||||
account = core_models.User.objects.get(username="user@test.com")
|
||||
url = reverse("api:account-detail", args=[account.pk])
|
||||
url = reverse("v1:account-detail", args=[account.pk])
|
||||
data = {
|
||||
"username": "fromapi@test.com",
|
||||
"role": account.role,
|
||||
@@ -450,7 +450,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
"""Try to disable an account."""
|
||||
account = core_factories.UserFactory(
|
||||
username="reseller", groups=("Resellers", ))
|
||||
url = reverse("api:account-detail", args=[account.pk])
|
||||
url = reverse("v1:account-detail", args=[account.pk])
|
||||
data = {
|
||||
"username": "reseller",
|
||||
"role": account.role,
|
||||
@@ -463,7 +463,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
"""Try to patch an account."""
|
||||
account = core_models.User.objects.get(username="user@test.com")
|
||||
old_quota = account.mailbox.quota
|
||||
url = reverse("api:account-detail", args=[account.pk])
|
||||
url = reverse("v1:account-detail", args=[account.pk])
|
||||
data = {
|
||||
"username": "fromapi@test.com",
|
||||
"mailbox": {
|
||||
@@ -479,7 +479,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
def test_update_domain_admin_account(self):
|
||||
"""Try to change administered domains."""
|
||||
account = core_models.User.objects.get(username="admin@test.com")
|
||||
url = reverse("api:account-detail", args=[account.pk])
|
||||
url = reverse("v1:account-detail", args=[account.pk])
|
||||
data = {
|
||||
"username": account.username,
|
||||
"role": account.role,
|
||||
@@ -492,21 +492,21 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
}
|
||||
response = self.client.put(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
domains = admin_models.Domain.objects.get_for_admin(account)
|
||||
domains = models.Domain.objects.get_for_admin(account)
|
||||
self.assertEqual(domains.count(), 2)
|
||||
self.assertTrue(domains.filter(name="test2.com").exists())
|
||||
|
||||
data["domains"] = ["test2.com"]
|
||||
response = self.client.put(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
domains = admin_models.Domain.objects.get_for_admin(account)
|
||||
domains = models.Domain.objects.get_for_admin(account)
|
||||
self.assertEqual(domains.count(), 1)
|
||||
self.assertTrue(domains.filter(name="test2.com").exists())
|
||||
|
||||
def test_update_account_wrong_address(self):
|
||||
"""Try to update an account."""
|
||||
account = core_models.User.objects.get(username="user@test.com")
|
||||
url = reverse("api:account-detail", args=[account.pk])
|
||||
url = reverse("v1:account-detail", args=[account.pk])
|
||||
data = {
|
||||
"username": "fromapi@test3.com",
|
||||
"role": account.role,
|
||||
@@ -524,7 +524,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
account = core_models.User.objects.get(username="user@test.com")
|
||||
domadmin = core_models.User.objects.get(username="admin@test.com")
|
||||
self.assertTrue(domadmin.can_access(account))
|
||||
url = reverse("api:account-detail", args=[account.pk])
|
||||
url = reverse("v1:account-detail", args=[account.pk])
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertFalse(
|
||||
@@ -533,7 +533,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_account_exists(self):
|
||||
"""Validate /exists/ service."""
|
||||
url = reverse("api:account-exists")
|
||||
url = reverse("v1:account-exists")
|
||||
response = self.client.get(
|
||||
"{}?email={}".format(url, "user@test.com"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -551,7 +551,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
"""Check the change password service."""
|
||||
account = core_models.User.objects.get(username="user@test.com")
|
||||
url = reverse(
|
||||
"api:account-password", args=[account.pk])
|
||||
"v1:account-password", args=[account.pk])
|
||||
response = self.client.put(
|
||||
url, {"password": "toto", "new_password": "pass"},
|
||||
format="json")
|
||||
@@ -568,7 +568,7 @@ class AccountAPITestCase(ModoAPITestCase):
|
||||
@mock.patch("ovh.Client.get")
|
||||
@mock.patch("ovh.Client.post")
|
||||
def test_reset_password(self, client_post, client_get):
|
||||
url = reverse("api:account-reset-password")
|
||||
url = reverse("v1:account-reset-password")
|
||||
# SMS password recovery not enabled
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
@@ -621,7 +621,7 @@ class AliasAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_get_aliases(self):
|
||||
"""Retrieve a list of aliases."""
|
||||
url = reverse("api:alias-list")
|
||||
url = reverse("v1:alias-list")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = response.json()
|
||||
@@ -635,7 +635,7 @@ class AliasAPITestCase(ModoAPITestCase):
|
||||
def test_get_alias(self):
|
||||
"""Retrieve an alias."""
|
||||
al = models.Alias.objects.get(address="alias@test.com")
|
||||
url = reverse("api:alias-detail", args=[al.pk])
|
||||
url = reverse("v1:alias-detail", args=[al.pk])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = response.json()
|
||||
@@ -643,7 +643,7 @@ class AliasAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_create_alias(self):
|
||||
"""Try to create a new alias."""
|
||||
url = reverse("api:alias-list")
|
||||
url = reverse("v1:alias-list")
|
||||
response = self.client.post(url, self.ALIAS_DATA, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
@@ -670,7 +670,7 @@ class AliasAPITestCase(ModoAPITestCase):
|
||||
"""As DomainAdmin, try to create a new alias."""
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION="Token " + self.da_token.key)
|
||||
url = reverse("api:alias-list")
|
||||
url = reverse("v1:alias-list")
|
||||
response = self.client.post(url, self.ALIAS_DATA, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
@@ -684,7 +684,7 @@ class AliasAPITestCase(ModoAPITestCase):
|
||||
def test_update_alias(self):
|
||||
"""Try to update an alias."""
|
||||
alias = models.Alias.objects.get(address="alias@test.com")
|
||||
url = reverse("api:alias-detail", args=[alias.pk])
|
||||
url = reverse("v1:alias-detail", args=[alias.pk])
|
||||
data = {
|
||||
"address": "alias@test.com",
|
||||
"recipients": ["user@test.com", "user@nonlocal.com"]
|
||||
@@ -715,7 +715,7 @@ class AliasAPITestCase(ModoAPITestCase):
|
||||
alias = models.Alias.objects.get(address="alias@test.com")
|
||||
domadmin = core_models.User.objects.get(username="admin@test.com")
|
||||
self.assertTrue(domadmin.can_access(alias))
|
||||
url = reverse("api:alias-detail", args=[alias.pk])
|
||||
url = reverse("v1:alias-detail", args=[alias.pk])
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertFalse(
|
||||
@@ -750,7 +750,7 @@ class SenderAddressAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_list(self):
|
||||
"""Retrieve a list of sender addresses."""
|
||||
url = reverse("api:sender_address-list")
|
||||
url = reverse("v1:sender_address-list")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = response.json()
|
||||
@@ -758,7 +758,7 @@ class SenderAddressAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_create(self):
|
||||
"""Create a new sender addresses."""
|
||||
url = reverse("api:sender_address-list")
|
||||
url = reverse("v1:sender_address-list")
|
||||
mbox = models.Mailbox.objects.get(
|
||||
address="admin", domain__name="test.com")
|
||||
data = {"address": "@extdomain.test", "mailbox": mbox.pk}
|
||||
@@ -785,7 +785,7 @@ class SenderAddressAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_patch(self):
|
||||
"""Patch an existing sender address."""
|
||||
url = reverse("api:sender_address-detail", args=[self.sa1.pk])
|
||||
url = reverse("v1:sender_address-detail", args=[self.sa1.pk])
|
||||
mbox = models.Mailbox.objects.get(
|
||||
address="user", domain__name="test.com")
|
||||
data = {"address": "@extdomain.test"}
|
||||
@@ -797,7 +797,7 @@ class SenderAddressAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_update(self):
|
||||
"""Update an existing sender address."""
|
||||
url = reverse("api:sender_address-detail", args=[self.sa1.pk])
|
||||
url = reverse("v1:sender_address-detail", args=[self.sa1.pk])
|
||||
mbox = models.Mailbox.objects.get(
|
||||
address="user", domain__name="test.com")
|
||||
data = {"address": "@extdomain.test", "mailbox": mbox.pk}
|
||||
@@ -809,7 +809,7 @@ class SenderAddressAPITestCase(ModoAPITestCase):
|
||||
|
||||
def test_delete(self):
|
||||
"""Delete an existing sender address."""
|
||||
url = reverse("api:sender_address-detail", args=[self.sa1.pk])
|
||||
url = reverse("v1:sender_address-detail", args=[self.sa1.pk])
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
with self.assertRaises(models.SenderAddress.DoesNotExist):
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user