Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2528220
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
159 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
index a35b5f5d..cb710a1d 100644
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -1,578 +1,506 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap')
import AppComponent from '../vue/App'
import MenuComponent from '../vue/Widgets/Menu'
import SupportForm from '../vue/Widgets/SupportForm'
import store from './store'
import { Tab } from 'bootstrap'
import { loadLangAsync, i18n } from './locale'
const loader = '<div class="app-loader"><div class="spinner-border" role="status"><span class="visually-hidden">Loading</span></div></div>'
let isLoading = 0
// Lock the UI with the 'loading...' element
const startLoading = () => {
isLoading++
let loading = $('#app > .app-loader').removeClass('fadeOut')
if (!loading.length) {
$('#app').append($(loader))
}
}
// Hide "loading" overlay
const stopLoading = () => {
if (isLoading > 0) {
$('#app > .app-loader').addClass('fadeOut')
isLoading--;
}
}
let loadingRoute
// Note: This has to be before the app is created
// Note: You cannot use app inside of the function
window.router.beforeEach((to, from, next) => {
// check if the route requires authentication and user is not logged in
if (to.meta.requiresAuth && !store.state.isLoggedIn) {
// remember the original request, to use after login
store.state.afterLogin = to;
// redirect to login page
next({ name: 'login' })
return
}
if (to.meta.loading) {
startLoading()
loadingRoute = to.name
}
next()
})
window.router.afterEach((to, from) => {
if (to.name && loadingRoute === to.name) {
stopLoading()
loadingRoute = null
}
// When changing a page remove old:
// - error page
// - modal backdrop
$('#error-page,.modal-backdrop.show').remove()
$('body').css('padding', 0) // remove padding added by unclosed modal
})
const app = new Vue({
components: {
AppComponent,
MenuComponent,
},
i18n,
store,
router: window.router,
data() {
return {
isUser: !window.isAdmin && !window.isReseller,
appName: window.config['app.name'],
appUrl: window.config['app.url'],
themeDir: '/themes/' + window.config['app.theme']
}
},
methods: {
// Clear (bootstrap) form validation state
clearFormValidation(form) {
$(form).find('.is-invalid').removeClass('is-invalid')
$(form).find('.invalid-feedback').remove()
},
hasPermission(type) {
const authInfo = store.state.authInfo
const key = 'enable' + type.charAt(0).toUpperCase() + type.slice(1)
return !!(authInfo && authInfo.statusInfo[key])
},
hasRoute(name) {
return this.$router.resolve({ name: name }).resolved.matched.length > 0
},
hasSKU(name) {
const authInfo = store.state.authInfo
return authInfo.statusInfo.skus && authInfo.statusInfo.skus.indexOf(name) != -1
},
isController(wallet_id) {
if (wallet_id && store.state.authInfo) {
let i
for (i = 0; i < store.state.authInfo.wallets.length; i++) {
if (wallet_id == store.state.authInfo.wallets[i].id) {
return true
}
}
for (i = 0; i < store.state.authInfo.accounts.length; i++) {
if (wallet_id == store.state.authInfo.accounts[i].id) {
return true
}
}
}
return false
},
// Set user state to "logged in"
loginUser(response, dashboard, update) {
if (!update) {
store.commit('logoutUser') // destroy old state data
store.commit('loginUser')
}
localStorage.setItem('token', response.access_token)
localStorage.setItem('refreshToken', response.refresh_token)
axios.defaults.headers.common.Authorization = 'Bearer ' + response.access_token
if (response.email) {
store.state.authInfo = response
}
if (dashboard !== false) {
this.$router.push(store.state.afterLogin || { name: 'dashboard' })
}
store.state.afterLogin = null
// Refresh the token before it expires
let timeout = response.expires_in || 0
// We'll refresh 60 seconds before the token expires
if (timeout > 60) {
timeout -= 60
}
// TODO: We probably should try a few times in case of an error
// TODO: We probably should prevent axios from doing any requests
// while the token is being refreshed
this.refreshTimeout = setTimeout(() => {
axios.post('/api/auth/refresh', {'refresh_token': response.refresh_token}).then(response => {
this.loginUser(response.data, false, true)
})
}, timeout * 1000)
},
// Set user state to "not logged in"
logoutUser(redirect) {
store.commit('logoutUser')
localStorage.setItem('token', '')
localStorage.setItem('refreshToken', '')
delete axios.defaults.headers.common.Authorization
if (redirect !== false) {
this.$router.push({ name: 'login' })
}
clearTimeout(this.refreshTimeout)
},
logo(mode) {
let src = this.appUrl + this.themeDir + '/images/logo_' + (mode || 'header') + '.png'
return `<img src="${src}" alt="${this.appName}">`
},
// Display "loading" overlay inside of the specified element
addLoader(elem, small = true, style = null) {
if (style) {
$(elem).css(style)
} else {
$(elem).css('position', 'relative')
}
$(elem).append(small ? $(loader).addClass('small') : $(loader))
},
// Create an object copy with specified properties only
pick(obj, properties) {
let result = {}
properties.forEach(prop => {
if (prop in obj) {
result[prop] = obj[prop]
}
})
return result
},
// Remove loader element added in addLoader()
removeLoader(elem) {
$(elem).find('.app-loader').remove()
},
startLoading,
stopLoading,
isLoading() {
return isLoading > 0
},
tab(e) {
e.preventDefault()
new Tab(e.target).show()
},
errorPage(code, msg, hint) {
// Until https://github.com/vuejs/vue-router/issues/977 is implemented
// we can't really use router to display error page as it has two side
// effects: it changes the URL and adds the error page to browser history.
// For now we'll be replacing current view with error page "manually".
if (!msg) msg = this.$te('error.' + code) ? this.$t('error.' + code) : this.$t('error.unknown')
if (!hint) hint = ''
const error_page = '<div id="error-page" class="error-page">'
+ `<div class="code">${code}</div><div class="message">${msg}</div><div class="hint">${hint}</div>`
+ '</div>'
$('#error-page').remove()
$('#app').append(error_page)
app.updateBodyClass('error')
},
errorHandler(error) {
this.stopLoading()
if (!error.response) {
// TODO: probably network connection error
} else if (error.response.status === 401) {
// Remember requested route to come back to it after log in
if (this.$route.meta.requiresAuth) {
store.state.afterLogin = this.$route
this.logoutUser()
} else {
this.logoutUser(false)
}
} else {
this.errorPage(error.response.status, error.response.statusText)
}
},
downloadFile(url) {
// TODO: This might not be a best way for big files as the content
// will be stored (temporarily) in browser memory
// TODO: This method does not show the download progress in the browser
// but it could be implemented in the UI, axios has 'progress' property
axios.get(url, { responseType: 'blob' })
.then(response => {
const link = document.createElement('a')
const contentDisposition = response.headers['content-disposition']
let filename = 'unknown'
if (contentDisposition) {
const match = contentDisposition.match(/filename="(.+)"/);
if (match.length === 2) {
filename = match[1];
}
}
link.href = window.URL.createObjectURL(response.data)
link.download = filename
link.click()
})
},
price(price, currency) {
// TODO: Set locale argument according to the currently used locale
return ((price || 0) / 100).toLocaleString('de-DE', { style: 'currency', currency: currency || 'CHF' })
},
priceLabel(cost, discount, currency) {
let index = ''
if (discount) {
cost = Math.floor(cost * ((100 - discount) / 100))
index = '\u00B9'
}
return this.price(cost, currency) + '/' + this.$t('wallet.month') + index
},
clickRecord(event) {
if (!/^(a|button|svg|path)$/i.test(event.target.nodeName)) {
$(event.target).closest('tr').find('a').trigger('click')
}
},
- domainStatusClass(domain) {
- if (domain.isDeleted) {
- return 'text-muted'
- }
-
- if (domain.isSuspended) {
- return 'text-warning'
- }
-
- if (!domain.isVerified || !domain.isLdapReady || !domain.isConfirmed) {
- return 'text-danger'
- }
-
- return 'text-success'
- },
- domainStatusText(domain) {
- if (domain.isDeleted) {
- return this.$t('status.deleted')
- }
-
- if (domain.isSuspended) {
- return this.$t('status.suspended')
- }
-
- if (!domain.isVerified || !domain.isLdapReady || !domain.isConfirmed) {
- return this.$t('status.notready')
- }
-
- return this.$t('status.active')
- },
- distlistStatusClass(list) {
- if (list.isDeleted) {
- return 'text-muted'
- }
-
- if (list.isSuspended) {
- return 'text-warning'
- }
-
- if (!list.isLdapReady) {
- return 'text-danger'
- }
-
- return 'text-success'
- },
- distlistStatusText(list) {
- if (list.isDeleted) {
- return this.$t('status.deleted')
- }
-
- if (list.isSuspended) {
- return this.$t('status.suspended')
- }
-
- if (!list.isLdapReady) {
- return this.$t('status.notready')
- }
-
- return this.$t('status.active')
- },
- folderStatusClass(folder) {
- return this.userStatusClass(folder)
- },
- folderStatusText(folder) {
- return this.userStatusText(folder)
- },
isDegraded() {
return store.state.authInfo.isAccountDegraded
},
pageName(path) {
let page = this.$route.path
// check if it is a "menu page", find the page name
// otherwise we'll use the real path as page name
window.config.menu.every(item => {
if (item.location == page && item.page) {
page = item.page
return false
}
})
page = page.replace(/^\//, '')
return page ? page : '404'
},
- resourceStatusClass(resource) {
- return this.userStatusClass(resource)
- },
- resourceStatusText(resource) {
- return this.userStatusText(resource)
- },
supportDialog(container) {
let dialog = $('#support-dialog')[0]
if (!dialog) {
// FIXME: Find a nicer way of doing this
SupportForm.i18n = i18n
let form = new Vue(SupportForm)
form.$mount($('<div>').appendTo(container)[0])
form.$root = this
form.$toast = this.$toast
dialog = form.$el
}
dialog.__vue__.showDialog()
},
- userStatusClass(user) {
- if (user.isDeleted) {
+ statusClass(obj) {
+ if (obj.isDeleted) {
return 'text-muted'
}
- if (user.isDegraded || user.isAccountDegraded || user.isSuspended) {
+ if (obj.isDegraded || obj.isAccountDegraded || obj.isSuspended) {
return 'text-warning'
}
- if (!user.isImapReady || !user.isLdapReady) {
+ if (obj.isImapReady === false || obj.isLdapReady === false || obj.isVerified === false || obj.isConfirmed === false) {
return 'text-danger'
}
return 'text-success'
},
- userStatusText(user) {
- if (user.isDeleted) {
+ statusText(obj) {
+ if (obj.isDeleted) {
return this.$t('status.deleted')
}
- if (user.isDegraded || user.isAccountDegraded) {
+ if (obj.isDegraded || obj.isAccountDegraded) {
return this.$t('status.degraded')
}
- if (user.isSuspended) {
+ if (obj.isSuspended) {
return this.$t('status.suspended')
}
- if (!user.isImapReady || !user.isLdapReady) {
+ if (obj.isImapReady === false || obj.isLdapReady === false || obj.isVerified === false || obj.isConfirmed === false) {
return this.$t('status.notready')
}
return this.$t('status.active')
},
// Append some wallet properties to the object
userWalletProps(object) {
let wallet = store.state.authInfo.accounts[0]
if (!wallet) {
wallet = store.state.authInfo.wallets[0]
}
if (wallet) {
object.currency = wallet.currency
if (wallet.discount) {
object.discount = wallet.discount
object.discount_description = wallet.discount_description
}
}
},
updateBodyClass(name) {
// Add 'class' attribute to the body, different for each page
// so, we can apply page-specific styles
document.body.className = 'page-' + (name || this.pageName()).replace(/\/.*$/, '')
}
}
})
// Fetch the locale file and the start the app
loadLangAsync().then(() => app.$mount('#app'))
// Add a axios request interceptor
window.axios.interceptors.request.use(
config => {
// This is the only way I found to change configuration options
// on a running application. We need this for browser testing.
config.headers['X-Test-Payment-Provider'] = window.config.paymentProvider
return config
},
error => {
// Do something with request error
return Promise.reject(error)
}
)
// Add a axios response interceptor for general/validation error handler
window.axios.interceptors.response.use(
response => {
if (response.config.onFinish) {
response.config.onFinish()
}
return response
},
error => {
// Do not display the error in a toast message, pass the error as-is
if (error.config.ignoreErrors) {
return Promise.reject(error)
}
if (error.config.onFinish) {
error.config.onFinish()
}
let error_msg
const status = error.response ? error.response.status : 200
const data = error.response ? error.response.data : {}
if (status == 422 && data.errors) {
error_msg = app.$t('error.form')
const modal = $('div.modal.show')
$(modal.length ? modal : 'form').each((i, form) => {
form = $(form)
$.each(data.errors, (idx, msg) => {
const input_name = (form.data('validation-prefix') || form.find('form').first().data('validation-prefix') || '') + idx
let input = form.find('#' + input_name)
if (!input.length) {
input = form.find('[name="' + input_name + '"]');
}
if (input.length) {
// Create an error message
// API responses can use a string, array or object
let msg_text = ''
if (typeof(msg) !== 'string') {
$.each(msg, (index, str) => {
msg_text += str + ' '
})
}
else {
msg_text = msg
}
let feedback = $('<div class="invalid-feedback">').text(msg_text)
if (input.is('.list-input')) {
// List input widget
let controls = input.children(':not(:first-child)')
if (!controls.length && typeof msg == 'string') {
// this is an empty list (the main input only)
// and the error message is not an array
input.find('.main-input').addClass('is-invalid')
} else {
controls.each((index, element) => {
if (msg[index]) {
$(element).find('input').addClass('is-invalid')
}
})
}
input.addClass('is-invalid').next('.invalid-feedback').remove()
input.after(feedback)
} else {
// a special case, e.g. the invitation policy widget
if (input.is('select') && input.parent().is('.input-group-select.selected')) {
input = input.next()
}
// Standard form element
input.addClass('is-invalid')
input.parent().find('.invalid-feedback').remove()
input.parent().append(feedback)
}
}
})
form.find('.is-invalid:not(.listinput-widget)').first().focus()
})
}
else if (data.status == 'error') {
error_msg = data.message
}
else {
error_msg = error.request ? error.request.statusText : error.message
}
app.$toast.error(error_msg || app.$t('error.server'))
// Pass the error as-is
return Promise.reject(error)
}
)
diff --git a/src/resources/vue/Admin/Distlist.vue b/src/resources/vue/Admin/Distlist.vue
index 4134631f..19734767 100644
--- a/src/resources/vue/Admin/Distlist.vue
+++ b/src/resources/vue/Admin/Distlist.vue
@@ -1,116 +1,116 @@
<template>
<div v-if="list.id" class="container">
<div class="card" id="distlist-info">
<div class="card-body">
<div class="card-title">{{ list.email }}</div>
<div class="card-text">
<form class="read-only short">
<div class="row plaintext">
<label for="distlistid" class="col-sm-4 col-form-label">
{{ $t('form.id') }} <span class="text-muted">({{ $t('form.created') }})</span>
</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="distlistid">
{{ list.id }} <span class="text-muted">({{ list.created_at }})</span>
</span>
</div>
</div>
<div class="row plaintext">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
- <span :class="$root.distlistStatusClass(list) + ' form-control-plaintext'" id="status">{{ $root.distlistStatusText(list) }}</span>
+ <span :class="$root.statusClass(list) + ' form-control-plaintext'" id="status">{{ $root.statusText(list) }}</span>
</div>
</div>
<div class="row plaintext">
<label for="name" class="col-sm-4 col-form-label">{{ $t('distlist.name') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="name">{{ list.name }}</span>
</div>
</div>
<div class="row plaintext">
<label for="members" class="col-sm-4 col-form-label">{{ $t('distlist.recipients') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="members">
<span v-for="member in list.members" :key="member">{{ member }}<br></span>
</span>
</div>
</div>
</form>
<div class="mt-2">
<button v-if="!list.isSuspended" id="button-suspend" class="btn btn-warning" type="button" @click="suspendList">
{{ $t('btn.suspend') }}
</button>
<button v-if="list.isSuspended" id="button-unsuspend" class="btn btn-warning" type="button" @click="unsuspendList">
{{ $t('btn.unsuspend') }}
</button>
</div>
</div>
</div>
</div>
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-settings" href="#distlist-settings" role="tab" aria-controls="distlist-settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="distlist-settings" role="tabpanel" aria-labelledby="tab-settings">
<div class="card-body">
<div class="card-text">
<form class="read-only short">
<div class="row plaintext">
<label for="sender_policy" class="col-sm-4 col-form-label">{{ $t('distlist.sender-policy') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="sender_policy">
{{ list.config.sender_policy && list.config.sender_policy.length ? list.config.sender_policy.join(', ') : $t('form.none') }}
</span>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
list: { members: [], config: {} }
}
},
created() {
this.$root.startLoading()
axios.get('/api/v4/groups/' + this.$route.params.list)
.then(response => {
this.$root.stopLoading()
this.list = response.data
})
.catch(this.$root.errorHandler)
},
methods: {
suspendList() {
axios.post('/api/v4/groups/' + this.list.id + '/suspend', {})
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.list = Object.assign({}, this.list, { isSuspended: true })
}
})
},
unsuspendList() {
axios.post('/api/v4/groups/' + this.list.id + '/unsuspend', {})
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.list = Object.assign({}, this.list, { isSuspended: false })
}
})
}
}
}
</script>
diff --git a/src/resources/vue/Admin/Domain.vue b/src/resources/vue/Admin/Domain.vue
index 725f68d7..afe28ea3 100644
--- a/src/resources/vue/Admin/Domain.vue
+++ b/src/resources/vue/Admin/Domain.vue
@@ -1,118 +1,118 @@
<template>
<div v-if="domain" class="container">
<div class="card" id="domain-info">
<div class="card-body">
<div class="card-title">{{ domain.namespace }}</div>
<div class="card-text">
<form class="read-only short">
<div class="row plaintext">
<label for="domainid" class="col-sm-4 col-form-label">
{{ $t('form.id') }} <span class="text-muted">({{ $t('form.created') }})</span>
</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="domainid">
{{ domain.id }} <span class="text-muted">({{ domain.created_at }})</span>
</span>
</div>
</div>
<div class="row plaintext">
<label for="first_name" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="status">
- <span :class="$root.domainStatusClass(domain)">{{ $root.domainStatusText(domain) }}</span>
+ <span :class="$root.statusClass(domain)">{{ $root.statusText(domain) }}</span>
</span>
</div>
</div>
</form>
<div class="mt-2">
<button v-if="!domain.isSuspended" id="button-suspend" class="btn btn-warning" type="button" @click="suspendDomain">
{{ $t('btn.suspend') }}
</button>
<button v-if="domain.isSuspended" id="button-unsuspend" class="btn btn-warning" type="button" @click="unsuspendDomain">
{{ $t('btn.unsuspend') }}
</button>
</div>
</div>
</div>
</div>
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-config" href="#domain-config" role="tab" aria-controls="domain-config" aria-selected="true" @click="$root.tab">
{{ $t('form.config') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-settings" href="#domain-settings" role="tab" aria-controls="domain-settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="domain-config" role="tabpanel" aria-labelledby="tab-config">
<div class="card-body">
<div class="card-text">
<p>{{ $t('domain.dns-verify') }}</p>
<p><pre id="dns-verify">{{ domain.dns.join("\n") }}</pre></p>
<p>{{ $t('domain.dns-config') }}</p>
<p><pre id="dns-config">{{ domain.mx.join("\n") }}</pre></p>
</div>
</div>
</div>
<div class="tab-pane" id="domain-settings" role="tabpanel" aria-labelledby="tab-settings">
<div class="card-body">
<div class="card-text">
<form class="read-only short">
<div class="row plaintext">
<label for="spf_whitelist" class="col-sm-4 col-form-label">{{ $t('domain.spf-whitelist') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="spf_whitelist">
{{ domain.config && domain.config.spf_whitelist.length ? domain.config.spf_whitelist.join(', ') : $t('form.none') }}
</span>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
domain: null
}
},
created() {
const domain_id = this.$route.params.domain;
axios.get('/api/v4/domains/' + domain_id)
.then(response => {
this.domain = response.data
})
.catch(this.$root.errorHandler)
},
methods: {
suspendDomain() {
axios.post('/api/v4/domains/' + this.domain.id + '/suspend', {})
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.domain = Object.assign({}, this.domain, { isSuspended: true })
}
})
},
unsuspendDomain() {
axios.post('/api/v4/domains/' + this.domain.id + '/unsuspend', {})
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.domain = Object.assign({}, this.domain, { isSuspended: false })
}
})
}
}
}
</script>
diff --git a/src/resources/vue/Admin/Resource.vue b/src/resources/vue/Admin/Resource.vue
index 5d8a804d..64c37eff 100644
--- a/src/resources/vue/Admin/Resource.vue
+++ b/src/resources/vue/Admin/Resource.vue
@@ -1,80 +1,80 @@
<template>
<div v-if="resource.id" class="container">
<div class="card" id="resource-info">
<div class="card-body">
<div class="card-title">{{ resource.email }}</div>
<div class="card-text">
<form class="read-only short">
<div class="row plaintext">
<label for="resourceid" class="col-sm-4 col-form-label">
{{ $t('form.id') }} <span class="text-muted">({{ $t('form.created') }})</span>
</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="resourceid">
{{ resource.id }} <span class="text-muted">({{ resource.created_at }})</span>
</span>
</div>
</div>
<div class="row plaintext">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
- <span :class="$root.resourceStatusClass(resource) + ' form-control-plaintext'" id="status">{{ $root.resourceStatusText(resource) }}</span>
+ <span :class="$root.statusClass(resource) + ' form-control-plaintext'" id="status">{{ $root.statusText(resource) }}</span>
</div>
</div>
<div class="row plaintext">
<label for="name" class="col-sm-4 col-form-label">{{ $t('form.name') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="name">{{ resource.name }}</span>
</div>
</div>
</form>
</div>
</div>
</div>
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-settings" href="#resource-settings" role="tab" aria-controls="resource-settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="resource-settings" role="tabpanel" aria-labelledby="tab-settings">
<div class="card-body">
<div class="card-text">
<form class="read-only short">
<div class="row plaintext">
<label for="invitation_policy" class="col-sm-4 col-form-label">{{ $t('resource.invitation-policy') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="invitation_policy">
{{ resource.config.invitation_policy || $t('form.none') }}
</span>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
resource: { config: {} }
}
},
created() {
this.$root.startLoading()
axios.get('/api/v4/resources/' + this.$route.params.resource)
.then(response => {
this.$root.stopLoading()
this.resource = response.data
})
.catch(this.$root.errorHandler)
}
}
</script>
diff --git a/src/resources/vue/Admin/SharedFolder.vue b/src/resources/vue/Admin/SharedFolder.vue
index d712c284..b18b0235 100644
--- a/src/resources/vue/Admin/SharedFolder.vue
+++ b/src/resources/vue/Admin/SharedFolder.vue
@@ -1,91 +1,91 @@
<template>
<div v-if="folder.id" class="container">
<div class="card" id="folder-info">
<div class="card-body">
<div class="card-title">{{ folder.email }}</div>
<div class="card-text">
<form class="read-only short">
<div class="row plaintext">
<label for="folderid" class="col-sm-4 col-form-label">
{{ $t('form.id') }} <span class="text-muted">({{ $t('form.created') }})</span>
</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="folderid">
{{ folder.id }} <span class="text-muted">({{ folder.created_at }})</span>
</span>
</div>
</div>
<div class="row plaintext">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
- <span :class="$root.folderStatusClass(folder) + ' form-control-plaintext'" id="status">{{ $root.folderStatusText(folder) }}</span>
+ <span :class="$root.statusClass(folder) + ' form-control-plaintext'" id="status">{{ $root.statusText(folder) }}</span>
</div>
</div>
<div class="row plaintext">
<label for="name" class="col-sm-4 col-form-label">{{ $t('form.name') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="name">{{ folder.name }}</span>
</div>
</div>
<div class="row plaintext">
<label for="type" class="col-sm-4 col-form-label">{{ $t('form.type') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="type">{{ $t('shf.type-' + folder.type) }}</span>
</div>
</div>
</form>
</div>
</div>
</div>
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-settings" href="#folder-settings" role="tab" aria-controls="folder-settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="folder-settings" role="tabpanel" aria-labelledby="tab-settings">
<div class="card-body">
<div class="card-text">
<form class="read-only short">
<div class="row plaintext">
<label for="acl" class="col-sm-4 col-form-label">{{ $t('form.acl') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="acl">
<span v-if="folder.config.acl.length">
<span v-for="(entry, index) in folder.config.acl" :key="index">
{{ entry.replace(',', ':') }}<br>
</span>
</span>
<span v-else>{{ $t('form.none') }}</span>
</span>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
folder: { config: {} }
}
},
created() {
this.$root.startLoading()
axios.get('/api/v4/shared-folders/' + this.$route.params.folder)
.then(response => {
this.$root.stopLoading()
this.folder = response.data
})
.catch(this.$root.errorHandler)
}
}
</script>
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
index d67b7605..a7a69130 100644
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -1,842 +1,842 @@
<template>
<div class="container">
<div class="card" id="user-info">
<div class="card-body">
<h1 class="card-title">{{ user.email }}</h1>
<div class="card-text">
<form class="read-only short">
<div v-if="user.wallet.user_id != user.id" class="row plaintext">
<label for="manager" class="col-sm-4 col-form-label">{{ $t('user.managed-by') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="manager">
<router-link :to="{ path: '/user/' + user.wallet.user_id }">{{ user.wallet.user_email }}</router-link>
</span>
</div>
</div>
<div class="row plaintext">
<label for="userid" class="col-sm-4 col-form-label">ID <span class="text-muted">({{ $t('form.created') }})</span></label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="userid">
{{ user.id }} <span class="text-muted">({{ user.created_at }})</span>
</span>
</div>
</div>
<div class="row plaintext">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="status">
- <span :class="$root.userStatusClass(user)">{{ $root.userStatusText(user) }}</span>
+ <span :class="$root.statusClass(user)">{{ $root.statusText(user) }}</span>
</span>
</div>
</div>
<div class="row plaintext" v-if="user.first_name">
<label for="first_name" class="col-sm-4 col-form-label">{{ $t('form.firstname') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="first_name">{{ user.first_name }}</span>
</div>
</div>
<div class="row plaintext" v-if="user.last_name">
<label for="last_name" class="col-sm-4 col-form-label">{{ $t('form.lastname') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="last_name">{{ user.last_name }}</span>
</div>
</div>
<div class="row plaintext" v-if="user.organization">
<label for="organization" class="col-sm-4 col-form-label">{{ $t('user.org') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="organization">{{ user.organization }}</span>
</div>
</div>
<div class="row plaintext" v-if="user.phone">
<label for="phone" class="col-sm-4 col-form-label">{{ $t('form.phone') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="phone">{{ user.phone }}</span>
</div>
</div>
<div class="row plaintext">
<label for="external_email" class="col-sm-4 col-form-label">{{ $t('user.ext-email') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="external_email">
<a v-if="user.external_email" :href="'mailto:' + user.external_email">{{ user.external_email }}</a>
<button type="button" class="btn btn-secondary btn-sm" @click="emailEdit">{{ $t('btn.edit') }}</button>
</span>
</div>
</div>
<div class="row plaintext" v-if="user.billing_address">
<label for="billing_address" class="col-sm-4 col-form-label">{{ $t('user.address') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" style="white-space:pre" id="billing_address">{{ user.billing_address }}</span>
</div>
</div>
<div class="row plaintext">
<label for="country" class="col-sm-4 col-form-label">{{ $t('user.country') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="country">{{ user.country }}</span>
</div>
</div>
</form>
<div class="mt-2">
<button v-if="!user.isSuspended" id="button-suspend" class="btn btn-warning" type="button" @click="suspendUser">
{{ $t('btn.suspend') }}
</button>
<button v-if="user.isSuspended" id="button-unsuspend" class="btn btn-warning" type="button" @click="unsuspendUser">
{{ $t('btn.unsuspend') }}
</button>
</div>
</div>
</div>
</div>
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-finances" href="#user-finances" role="tab" aria-controls="user-finances" aria-selected="true">
{{ $t('user.finances') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-aliases" href="#user-aliases" role="tab" aria-controls="user-aliases" aria-selected="false">
{{ $t('user.aliases') }} ({{ user.aliases.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-subscriptions" href="#user-subscriptions" role="tab" aria-controls="user-subscriptions" aria-selected="false">
{{ $t('user.subscriptions') }} ({{ skus.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-domains" href="#user-domains" role="tab" aria-controls="user-domains" aria-selected="false">
{{ $t('user.domains') }} ({{ domains.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-users" href="#user-users" role="tab" aria-controls="user-users" aria-selected="false">
{{ $t('user.users') }} ({{ users.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-distlists" href="#user-distlists" role="tab" aria-controls="user-distlists" aria-selected="false">
{{ $t('user.distlists') }} ({{ distlists.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-resources" href="#user-resources" role="tab" aria-controls="user-resources" aria-selected="false">
{{ $t('user.resources') }} ({{ resources.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-shared-folders" href="#user-shared-folders" role="tab" aria-controls="user-shared-folders" aria-selected="false">
{{ $t('dashboard.shared-folders') }} ({{ folders.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-settings" href="#user-settings" role="tab" aria-controls="user-settings" aria-selected="false">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="user-finances" role="tabpanel" aria-labelledby="tab-finances">
<div class="card-body">
<h2 class="card-title">
{{ $t('wallet.title') }}
<span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'"><strong>{{ $root.price(wallet.balance, wallet.currency) }}</strong></span>
</h2>
<div class="card-text">
<form class="read-only short">
<div class="row">
<label class="col-sm-4 col-form-label">{{ $t('user.discount') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="discount">
<span>{{ wallet.discount ? (wallet.discount + '% - ' + wallet.discount_description) : 'none' }}</span>
<button type="button" class="btn btn-secondary btn-sm" @click="discountEdit">{{ $t('btn.edit') }}</button>
</span>
</div>
</div>
<div class="row" v-if="wallet.mandate && wallet.mandate.id">
<label class="col-sm-4 col-form-label">{{ $t('user.auto-payment') }}</label>
<div class="col-sm-8">
<span id="autopayment" :class="'form-control-plaintext' + (wallet.mandateState ? ' text-danger' : '')"
v-html="$t('user.auto-payment-text', {
amount: wallet.mandate.amount + ' ' + wallet.currency,
balance: wallet.mandate.balance + ' ' + wallet.currency,
method: wallet.mandate.method
})"
>
<span v-if="wallet.mandateState">({{ wallet.mandateState }})</span>.
</span>
</div>
</div>
<div class="row" v-if="wallet.providerLink">
<label class="col-sm-4 col-form-label">{{ capitalize(wallet.provider) }} {{ $t('form.id') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" v-html="wallet.providerLink"></span>
</div>
</div>
</form>
<div class="mt-2">
<button id="button-award" class="btn btn-success" type="button" @click="awardDialog">{{ $t('user.add-bonus') }}</button>
<button id="button-penalty" class="btn btn-danger" type="button" @click="penalizeDialog">{{ $t('user.add-penalty') }}</button>
</div>
</div>
<h2 class="card-title mt-4">{{ $t('wallet.transactions') }}</h2>
<transaction-log v-if="wallet.id && !walletReload" class="card-text" :wallet-id="wallet.id" :is-admin="true"></transaction-log>
</div>
</div>
<div class="tab-pane" id="user-aliases" role="tabpanel" aria-labelledby="tab-aliases">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(alias, index) in user.aliases" :id="'alias' + index" :key="index">
<td>{{ alias }}</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td>{{ $t('user.aliases-none') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user-subscriptions" role="tabpanel" aria-labelledby="tab-subscriptions">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('user.subscription') }}</th>
<th scope="col">{{ $t('user.price') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(sku, sku_id) in skus" :id="'sku' + sku.id" :key="sku_id">
<td>{{ sku.name }}</td>
<td class="price">{{ sku.price }}</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('user.subscriptions-none') }}</td>
</tr>
</tfoot>
</table>
<small v-if="discount > 0" class="hint">
<hr class="m-0">
¹ {{ $t('user.discount-hint') }}: {{ discount }}% - {{ discount_description }}
</small>
<div class="mt-2">
<button type="button" class="btn btn-danger" id="reset2fa" v-if="has2FA" @click="reset2FADialog">
{{ $t('user.reset-2fa') }}
</button>
<button type="button" class="btn btn-secondary" id="addbetasku" v-if="!hasBeta" @click="addBetaSku">
{{ $t('user.add-beta') }}
</button>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="user-domains" role="tabpanel" aria-labelledby="tab-domains">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('domain.namespace') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="domain in domains" :id="'domain' + domain.id" :key="domain.id" @click="$root.clickRecord">
<td>
- <svg-icon icon="globe" :class="$root.domainStatusClass(domain)" :title="$root.domainStatusText(domain)"></svg-icon>
+ <svg-icon icon="globe" :class="$root.statusClass(domain)" :title="$root.statusText(domain)"></svg-icon>
<router-link :to="{ path: '/domain/' + domain.id }">{{ domain.namespace }}</router-link>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td>{{ $t('user.domains-none') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user-users" role="tabpanel" aria-labelledby="tab-users">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('form.primary-email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="item in users" :id="'user' + item.id" :key="item.id" @click="$root.clickRecord">
<td>
- <svg-icon icon="user" :class="$root.userStatusClass(item)" :title="$root.userStatusText(item)"></svg-icon>
+ <svg-icon icon="user" :class="$root.statusClass(item)" :title="$root.statusText(item)"></svg-icon>
<router-link v-if="item.id != user.id" :to="{ path: '/user/' + item.id }">{{ item.email }}</router-link>
<span v-else>{{ item.email }}</span>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td>{{ $t('user.users-none') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user-distlists" role="tabpanel" aria-labelledby="tab-distlists">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('distlist.name') }}</th>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="list in distlists" :key="list.id" @click="$root.clickRecord">
<td>
- <svg-icon icon="users" :class="$root.distlistStatusClass(list)" :title="$root.distlistStatusText(list)"></svg-icon>
+ <svg-icon icon="users" :class="$root.statusClass(list)" :title="$root.statusText(list)"></svg-icon>
<router-link :to="{ path: '/distlist/' + list.id }">{{ list.name }}</router-link>
</td>
<td>
<router-link :to="{ path: '/distlist/' + list.id }">{{ list.email }}</router-link>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('distlist.list-empty') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user-resources" role="tabpanel" aria-labelledby="tab-resources">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('form.name') }}</th>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="resource in resources" :key="resource.id" @click="$root.clickRecord">
<td>
- <svg-icon icon="cog" :class="$root.resourceStatusClass(resource)" :title="$root.resourceStatusText(resource)"></svg-icon>
+ <svg-icon icon="cog" :class="$root.statusClass(resource)" :title="$root.statusText(resource)"></svg-icon>
<router-link :to="{ path: '/resource/' + resource.id }">{{ resource.name }}</router-link>
</td>
<td>
<router-link :to="{ path: '/resource/' + resource.id }">{{ resource.email }}</router-link>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('resource.list-empty') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user-shared-folders" role="tabpanel" aria-labelledby="tab-shared-folders">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('form.name') }}</th>
<th scope="col">{{ $t('form.type') }}</th>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="folder in folders" :key="folder.id" @click="$root.clickRecord">
<td>
- <svg-icon icon="folder-open" :class="$root.folderStatusClass(folder)" :title="$root.folderStatusText(folder)"></svg-icon>
+ <svg-icon icon="folder-open" :class="$root.statusClass(folder)" :title="$root.statusText(folder)"></svg-icon>
<router-link :to="{ path: '/shared-folder/' + folder.id }">{{ folder.name }}</router-link>
</td>
<td>{{ $t('shf.type-' + folder.type) }}</td>
<td><router-link :to="{ path: '/shared-folder/' + folder.id }">{{ folder.email }}</router-link></td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="3">{{ $t('shf.list-empty') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user-settings" role="tabpanel" aria-labelledby="tab-settings">
<div class="card-body">
<div class="card-text">
<form class="read-only short">
<div class="row plaintext">
<label for="greylist_enabled" class="col-sm-4 col-form-label">{{ $t('user.greylisting') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="greylist_enabled">
<span v-if="user.config.greylist_enabled" class="text-success">{{ $t('form.enabled') }}</span>
<span v-else class="text-danger">{{ $t('form.disabled') }}</span>
</span>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="discount-dialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('user.discount-title') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<p>
<select v-model="wallet.discount_id" class="form-select">
<option value="">- {{ $t('form.none') }} -</option>
<option v-for="item in discounts" :value="item.id" :key="item.id">{{ item.label }}</option>
</select>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-primary modal-action" @click="submitDiscount()">
<svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}
</button>
</div>
</div>
</div>
</div>
<div id="email-dialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('user.ext-email') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<p>
<input v-model="external_email" name="external_email" class="form-control">
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-primary modal-action" @click="submitEmail()">
<svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}
</button>
</div>
</div>
</div>
</div>
<div id="oneoff-dialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t(oneoff_negative ? 'user.add-penalty-title' : 'user.add-bonus-title') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<form data-validation-prefix="oneoff_">
<div class="row mb-3">
<label for="oneoff_amount" class="col-form-label">{{ $t('form.amount') }}</label>
<div class="input-group">
<input type="text" class="form-control" id="oneoff_amount" v-model="oneoff_amount" required>
<span class="input-group-text">{{ wallet.currency }}</span>
</div>
</div>
<div class="row">
<label for="oneoff_description" class="col-form-label">{{ $t('form.description') }}</label>
<input class="form-control" id="oneoff_description" v-model="oneoff_description" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-primary modal-action" @click="submitOneOff()">
<svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}
</button>
</div>
</div>
</div>
</div>
<div id="reset-2fa-dialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('user.reset-2fa-title') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<p>{{ $t('user.2fa-hint1') }}</p>
<p>{{ $t('user.2fa-hint2') }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-danger modal-action" @click="reset2FA()">{{ $t('btn.reset') }}</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from 'bootstrap'
import TransactionLog from '../Widgets/TransactionLog'
export default {
components: {
TransactionLog
},
beforeRouteUpdate (to, from, next) {
// An event called when the route that renders this component has changed,
// but this component is reused in the new route.
// Required to handle links from /user/XXX to /user/YYY
next()
this.$parent.routerReload()
},
data() {
return {
oneoff_amount: '',
oneoff_description: '',
oneoff_negative: false,
discount: 0,
discount_description: '',
discounts: [],
external_email: '',
folders: [],
has2FA: false,
hasBeta: false,
wallet: {},
walletReload: false,
distlists: [],
domains: [],
resources: [],
skus: [],
sku2FA: null,
users: [],
user: {
aliases: [],
config: {},
wallet: {},
skus: {},
}
}
},
created() {
const user_id = this.$route.params.user
this.$root.startLoading()
axios.get('/api/v4/users/' + user_id)
.then(response => {
this.$root.stopLoading()
this.user = response.data
const financesTab = '#user-finances'
const keys = ['first_name', 'last_name', 'external_email', 'billing_address', 'phone', 'organization']
let country = this.user.settings.country
if (country && country in window.config.countries) {
country = window.config.countries[country][1]
}
this.user.country = country
keys.forEach(key => { this.user[key] = this.user.settings[key] })
this.discount = this.user.wallet.discount
this.discount_description = this.user.wallet.discount_description
// TODO: currencies, multi-wallets, accounts
// Get more info about the wallet (e.g. payment provider related)
this.$root.addLoader(financesTab)
axios.get('/api/v4/wallets/' + this.user.wallets[0].id)
.then(response => {
this.$root.removeLoader(financesTab)
this.wallet = response.data
this.setMandateState()
})
.catch(error => {
this.$root.removeLoader(financesTab)
})
// Create subscriptions list
axios.get('/api/v4/users/' + user_id + '/skus')
.then(response => {
// "merge" SKUs with user entitlement-SKUs
response.data.forEach(sku => {
const userSku = this.user.skus[sku.id]
if (userSku) {
let cost = userSku.costs.reduce((sum, current) => sum + current)
let item = {
id: sku.id,
name: sku.name,
cost: cost,
price: this.$root.priceLabel(cost, this.discount)
}
if (sku.range) {
item.name += ' ' + userSku.count + ' ' + sku.range.unit
}
this.skus.push(item)
if (sku.handler == 'Auth2F') {
this.has2FA = true
this.sku2FA = sku.id
} else if (sku.handler == 'Beta') {
this.hasBeta = true
}
}
})
})
// Fetch users
// TODO: Multiple wallets
axios.get('/api/v4/users?owner=' + user_id)
.then(response => {
this.users = response.data.list;
})
// Fetch domains
axios.get('/api/v4/domains?owner=' + user_id)
.then(response => {
this.domains = response.data.list
})
// Fetch distribution lists
axios.get('/api/v4/groups?owner=' + user_id)
.then(response => {
this.distlists = response.data.list
})
// Fetch resources lists
axios.get('/api/v4/resources?owner=' + user_id)
.then(response => {
this.resources = response.data.list
})
// Fetch shared folders lists
axios.get('/api/v4/shared-folders?owner=' + user_id)
.then(response => {
this.folders = response.data.list
})
})
.catch(this.$root.errorHandler)
},
mounted() {
$(this.$el).find('ul.nav-tabs a').on('click', this.$root.tab)
},
methods: {
addBetaSku() {
axios.post('/api/v4/users/' + this.user.id + '/skus/beta')
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.hasBeta = true
const sku = response.data.sku
this.skus.push({
id: sku.id,
name: sku.name,
cost: sku.cost,
price: this.$root.priceLabel(sku.cost, this.discount)
})
}
})
},
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
awardDialog() {
this.oneOffDialog(false)
},
discountEdit() {
if (!this.discount_dialog) {
const dialog = $('#discount-dialog')[0]
dialog.addEventListener('shown.bs.modal', e => {
$(dialog).find('select').focus()
// Note: Vue v-model is strict, convert null to a string
this.wallet.discount_id = this.wallet_discount_id || ''
})
this.discount_dialog = new Modal(dialog)
}
this.discount_dialog.show()
if (!this.discounts.length) {
// Fetch discounts
axios.get('/api/v4/users/' + this.user.id + '/discounts')
.then(response => {
this.discounts = response.data.list
})
}
},
emailEdit() {
this.external_email = this.user.external_email
this.$root.clearFormValidation($('#email-dialog'))
if (!this.email_dialog) {
const dialog = $('#email-dialog')[0]
dialog.addEventListener('shown.bs.modal', e => {
$(dialog).find('input').focus()
})
this.email_dialog = new Modal(dialog)
}
this.email_dialog.show()
},
setMandateState() {
let mandate = this.wallet.mandate
if (mandate && mandate.id) {
if (!mandate.isValid) {
this.wallet.mandateState = mandate.isPending ? 'pending' : 'invalid'
} else if (mandate.isDisabled) {
this.wallet.mandateState = 'disabled'
}
}
},
oneOffDialog(negative) {
this.oneoff_negative = negative
if (!this.oneoff_dialog) {
const dialog = $('#oneoff-dialog')[0]
dialog.addEventListener('shown.bs.modal', () => {
this.$root.clearFormValidation(dialog)
$(dialog).find('#oneoff_amount').focus()
})
this.oneoff_dialog = new Modal(dialog)
}
this.oneoff_dialog.show()
},
penalizeDialog() {
this.oneOffDialog(true)
},
reload() {
// this is to reload transaction log
this.walletReload = true
this.$nextTick(() => { this.walletReload = false })
},
reset2FA() {
new Modal('#reset-2fa-dialog').hide()
axios.post('/api/v4/users/' + this.user.id + '/reset2FA')
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.skus = this.skus.filter(sku => sku.id != this.sku2FA)
this.has2FA = false
}
})
},
reset2FADialog() {
new Modal('#reset-2fa-dialog').show()
},
submitDiscount() {
this.discount_dialog.hide()
axios.put('/api/v4/wallets/' + this.user.wallets[0].id, { discount: this.wallet.discount_id })
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.wallet = Object.assign({}, this.wallet, response.data)
// Update prices in Subscriptions tab
if (this.user.wallet.id == response.data.id) {
this.discount = this.wallet.discount
this.discount_description = this.wallet.discount_description
this.skus.forEach(sku => {
sku.price = this.$root.priceLabel(sku.cost, this.discount)
})
}
}
})
},
submitEmail() {
axios.put('/api/v4/users/' + this.user.id, { external_email: this.external_email })
.then(response => {
if (response.data.status == 'success') {
this.email_dialog.hide()
this.$toast.success(response.data.message)
this.user.external_email = this.external_email
this.external_email = null // required because of Vue
}
})
},
submitOneOff() {
let wallet_id = this.user.wallets[0].id
let post = {
amount: this.oneoff_amount,
description: this.oneoff_description
}
if (this.oneoff_negative && /^\d+(\.?\d+)?$/.test(post.amount)) {
post.amount *= -1
}
this.$root.clearFormValidation('#oneoff-dialog')
axios.post('/api/v4/wallets/' + wallet_id + '/one-off', post)
.then(response => {
if (response.data.status == 'success') {
this.oneoff_dialog.hide()
this.$toast.success(response.data.message)
this.wallet = Object.assign({}, this.wallet, {balance: response.data.balance})
this.oneoff_amount = ''
this.oneoff_description = ''
this.reload()
}
})
},
suspendUser() {
axios.post('/api/v4/users/' + this.user.id + '/suspend', {})
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.user = Object.assign({}, this.user, { isSuspended: true })
}
})
},
unsuspendUser() {
axios.post('/api/v4/users/' + this.user.id + '/unsuspend', {})
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.user = Object.assign({}, this.user, { isSuspended: false })
}
})
}
}
}
</script>
diff --git a/src/resources/vue/Distlist/Info.vue b/src/resources/vue/Distlist/Info.vue
index f65f9413..93694c69 100644
--- a/src/resources/vue/Distlist/Info.vue
+++ b/src/resources/vue/Distlist/Info.vue
@@ -1,153 +1,153 @@
<template>
<div class="container">
<status-component v-if="list_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
<div class="card" id="distlist-info">
<div class="card-body">
<div class="card-title" v-if="list_id !== 'new'">
{{ $tc('distlist.list-title', 1) }}
<button class="btn btn-outline-danger button-delete float-end" @click="deleteList()" tag="button">
<svg-icon icon="trash-alt"></svg-icon> {{ $t('distlist.delete') }}
</button>
</div>
<div class="card-title" v-if="list_id === 'new'">{{ $t('distlist.new') }}</div>
<div class="card-text">
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
{{ $t('form.general') }}
</a>
</li>
<li v-if="list_id !== 'new'" class="nav-item">
<a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
<form @submit.prevent="submit" class="card-body">
<div v-if="list_id !== 'new'" class="row plaintext mb-3">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
- <span :class="$root.distlistStatusClass(list) + ' form-control-plaintext'" id="status">{{ $root.distlistStatusText(list) }}</span>
+ <span :class="$root.statusClass(list) + ' form-control-plaintext'" id="status">{{ $root.statusText(list) }}</span>
</div>
</div>
<div class="row mb-3">
<label for="name" class="col-sm-4 col-form-label">{{ $t('distlist.name') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="name" required v-model="list.name">
</div>
</div>
<div class="row mb-3">
<label for="email" class="col-sm-4 col-form-label">{{ $t('form.email') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="email" :disabled="list_id !== 'new'" required v-model="list.email">
</div>
</div>
<div class="row mb-3">
<label for="members-input" class="col-sm-4 col-form-label">{{ $t('distlist.recipients') }}</label>
<div class="col-sm-8">
<list-input id="members" :list="list.members"></list-input>
</div>
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</form>
</div>
<div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
<form @submit.prevent="submitSettings" class="card-body">
<div class="row mb-3">
<label for="sender-policy-input" class="col-sm-4 col-form-label">{{ $t('distlist.sender-policy') }}</label>
<div class="col-sm-8 pt-2">
<list-input id="sender-policy" :list="list.config.sender_policy" class="mb-1"></list-input>
<small id="sender-policy-hint" class="text-muted">
{{ $t('distlist.sender-policy-text') }}
</small>
</div>
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import ListInput from '../Widgets/ListInput'
import StatusComponent from '../Widgets/Status'
export default {
components: {
ListInput,
StatusComponent
},
data() {
return {
list_id: null,
list: { members: [], config: {} },
status: {}
}
},
created() {
this.list_id = this.$route.params.list
if (this.list_id != 'new') {
this.$root.startLoading()
axios.get('/api/v4/groups/' + this.list_id)
.then(response => {
this.$root.stopLoading()
this.list = response.data
this.status = response.data.statusInfo
})
.catch(this.$root.errorHandler)
}
},
mounted() {
$('#name').focus()
},
methods: {
deleteList() {
axios.delete('/api/v4/groups/' + this.list_id)
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.$router.push({ name: 'distlists' })
}
})
},
statusUpdate(list) {
this.list = Object.assign({}, this.list, list)
},
submit() {
this.$root.clearFormValidation($('#list-info form'))
let method = 'post'
let location = '/api/v4/groups'
if (this.list_id !== 'new') {
method = 'put'
location += '/' + this.list_id
}
axios[method](location, this.list)
.then(response => {
this.$toast.success(response.data.message)
this.$router.push({ name: 'distlists' })
})
},
submitSettings() {
this.$root.clearFormValidation($('#settings form'))
let post = this.list.config
axios.post('/api/v4/groups/' + this.list_id + '/config', post)
.then(response => {
this.$toast.success(response.data.message)
})
}
}
}
</script>
diff --git a/src/resources/vue/Distlist/List.vue b/src/resources/vue/Distlist/List.vue
index 837edb6d..fa0d1576 100644
--- a/src/resources/vue/Distlist/List.vue
+++ b/src/resources/vue/Distlist/List.vue
@@ -1,60 +1,60 @@
<template>
<div class="container">
<div class="card" id="distlist-list">
<div class="card-body">
<div class="card-title">
{{ $tc('distlist.list-title', 2) }}
<router-link v-if="!$root.isDegraded()" class="btn btn-success float-end create-list" :to="{ path: 'distlist/new' }" tag="button">
<svg-icon icon="users"></svg-icon> {{ $t('distlist.create') }}
</router-link>
</div>
<div class="card-text">
<table class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">{{ $t('distlist.name') }}</th>
<th scope="col">{{ $t('distlist.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="list in lists" :key="list.id" @click="$root.clickRecord">
<td>
- <svg-icon icon="users" :class="$root.distlistStatusClass(list)" :title="$root.distlistStatusText(list)"></svg-icon>
+ <svg-icon icon="users" :class="$root.statusClass(list)" :title="$root.statusText(list)"></svg-icon>
<router-link :to="{ path: 'distlist/' + list.id }">{{ list.name }}</router-link>
</td>
<td>
<router-link :to="{ path: 'distlist/' + list.id }">{{ list.email }}</router-link>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('distlist.list-empty') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
lists: []
}
},
created() {
this.$root.startLoading()
axios.get('/api/v4/groups')
.then(response => {
this.$root.stopLoading()
this.lists = response.data
})
.catch(this.$root.errorHandler)
}
}
</script>
diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue
index 87127731..38e0d6d4 100644
--- a/src/resources/vue/Domain/Info.vue
+++ b/src/resources/vue/Domain/Info.vue
@@ -1,231 +1,231 @@
<template>
<div class="container">
<status-component v-if="domain_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
<div class="card">
<div class="card-body">
<div class="card-title" v-if="domain_id === 'new'">{{ $t('domain.new') }}</div>
<div class="card-title" v-else>{{ $t('form.domain') }}
<button
class="btn btn-outline-danger button-delete float-end"
@click="showDeleteConfirmation()" type="button"
>
<svg-icon icon="trash-alt"></svg-icon> {{ $t('domain.delete') }}
</button>
</div>
<div class="card-text">
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
{{ $t('form.general') }}
</a>
</li>
<li class="nav-item" v-if="domain.id">
<a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
<form @submit.prevent="submit" class="card-body">
<div v-if="domain.id" class="row plaintext mb-3">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
- <span :class="$root.domainStatusClass(domain) + ' form-control-plaintext'" id="status">{{ $root.domainStatusText(domain) }}</span>
+ <span :class="$root.statusClass(domain) + ' form-control-plaintext'" id="status">{{ $root.statusText(domain) }}</span>
</div>
</div>
<div class="row mb-3">
<label for="name" class="col-sm-4 col-form-label">{{ $t('domain.namespace') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="namespace" v-model="domain.namespace" :disabled="domain.id">
</div>
</div>
<div v-if="!domain.id" id="domain-packages" class="row">
<label class="col-sm-4 col-form-label">{{ $t('user.package') }}</label>
<package-select class="col-sm-8 pt-sm-1" type="domain"></package-select>
</div>
<div v-if="domain.id" id="domain-skus" class="row">
<label class="col-sm-4 col-form-label">{{ $t('user.subscriptions') }}</label>
<subscription-select v-if="domain.id" class="col-sm-8 pt-sm-1" type="domain" :object="domain" :readonly="true"></subscription-select>
</div>
<button v-if="!domain.id" class="btn btn-primary mt-3" type="submit">
<svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}
</button>
</form>
<hr class="m-0" v-if="domain.id">
<div v-if="domain.id && !domain.isConfirmed" class="card-body" id="domain-verify">
<h5 class="mb-3">{{ $t('domain.verify') }}</h5>
<div class="card-text">
<p>{{ $t('domain.verify-intro') }}</p>
<p>
<span v-html="$t('domain.verify-dns')"></span>
<ul>
<li>{{ $t('domain.verify-dns-txt') }} <code>{{ domain.hash_text }}</code></li>
<li>{{ $t('domain.verify-dns-cname') }} <code>{{ domain.hash_cname }}.{{ domain.namespace }}. IN CNAME {{ domain.hash_code }}.{{ domain.namespace }}.</code></li>
</ul>
<span>{{ $t('domain.verify-outro') }}</span>
</p>
<p>{{ $t('domain.verify-sample') }} <pre>{{ domain.dns.join("\n") }}</pre></p>
<button class="btn btn-primary" type="button" @click="confirm"><svg-icon icon="sync-alt"></svg-icon> {{ $t('btn.verify') }}</button>
</div>
</div>
<div v-if="domain.isConfirmed" class="card-body" id="domain-config">
<h5 class="mb-3">{{ $t('domain.config') }}</h5>
<div class="card-text">
<p>{{ $t('domain.config-intro', { app: $root.appName }) }}</p>
<p>{{ $t('domain.config-sample') }} <pre>{{ domain.mx.join("\n") }}</pre></p>
<p>{{ $t('domain.config-hint') }}</p>
</div>
</div>
</div>
<div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
<div class="card-body">
<form @submit.prevent="submitSettings">
<div class="row mb-3">
<label for="spf_whitelist" class="col-sm-4 col-form-label">{{ $t('domain.spf-whitelist') }}</label>
<div class="col-sm-8">
<list-input id="spf_whitelist" name="spf_whitelist" :list="spf_whitelist"></list-input>
<small id="spf-hint" class="text-muted d-block mt-2">
{{ $t('domain.spf-whitelist-text') }}
<span class="d-block" v-html="$t('domain.spf-whitelist-ex')"></span>
</small>
</div>
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> Submit</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="delete-warning" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('domain.delete-domain', { domain: domain.namespace }) }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<p>{{ $t('domain.delete-text') }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-danger modal-action" @click="deleteDomain()">
<svg-icon icon="trash-alt"></svg-icon> {{ $t('btn.delete') }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from 'bootstrap'
import ListInput from '../Widgets/ListInput'
import PackageSelect from '../Widgets/PackageSelect'
import StatusComponent from '../Widgets/Status'
import SubscriptionSelect from '../Widgets/SubscriptionSelect'
export default {
components: {
ListInput,
PackageSelect,
StatusComponent,
SubscriptionSelect
},
data() {
return {
domain_id: null,
domain: {},
spf_whitelist: [],
status: {}
}
},
created() {
this.domain_id = this.$route.params.domain
if (this.domain_id !== 'new') {
this.$root.startLoading()
axios.get('/api/v4/domains/' + this.domain_id)
.then(response => {
this.$root.stopLoading()
this.domain = response.data
this.spf_whitelist = this.domain.config.spf_whitelist || []
if (!this.domain.isConfirmed) {
$('#domain-verify button').focus()
}
this.status = response.data.statusInfo
})
.catch(this.$root.errorHandler)
}
},
mounted() {
$('#namespace').focus()
$('#delete-warning')[0].addEventListener('shown.bs.modal', event => {
$(event.target).find('button.modal-cancel').focus()
})
},
methods: {
confirm() {
axios.get('/api/v4/domains/' + this.domain_id + '/confirm')
.then(response => {
if (response.data.status == 'success') {
this.domain.isConfirmed = true
this.status = response.data.statusInfo
}
if (response.data.message) {
this.$toast[response.data.status](response.data.message)
}
})
},
deleteDomain() {
// Delete the domain from the confirm dialog
axios.delete('/api/v4/domains/' + this.domain_id)
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.$router.push({ name: 'domains' })
}
})
},
showDeleteConfirmation() {
// Display the warning
new Modal('#delete-warning').show()
},
statusUpdate(domain) {
this.domain = Object.assign({}, this.domain, domain)
},
submit() {
this.$root.clearFormValidation($('#general form'))
let method = 'post'
let location = '/api/v4/domains'
this.domain.package = $('#domain-packages input:checked').val()
axios[method](location, this.domain)
.then(response => {
this.$toast.success(response.data.message)
this.$router.push({ name: 'domains' })
})
},
submitSettings() {
this.$root.clearFormValidation($('#settings form'))
let post = { spf_whitelist: this.spf_whitelist }
axios.post('/api/v4/domains/' + this.domain_id + '/config', post)
.then(response => {
this.$toast.success(response.data.message)
})
}
}
}
</script>
diff --git a/src/resources/vue/Domain/List.vue b/src/resources/vue/Domain/List.vue
index 3443669f..9187b86b 100644
--- a/src/resources/vue/Domain/List.vue
+++ b/src/resources/vue/Domain/List.vue
@@ -1,58 +1,58 @@
<template>
<div class="container">
<div class="card" id="domain-list">
<div class="card-body">
<div class="card-title">
{{ $t('user.domains') }}
<router-link v-if="!$root.isDegraded()" class="btn btn-success float-end create-domain" :to="{ path: 'domain/new' }" tag="button">
<svg-icon icon="globe"></svg-icon> {{ $t('domain.create') }}
</router-link>
</div>
<div class="card-text">
<table class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">{{ $t('domain.namespace') }}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="domain in domains" :key="domain.id" @click="$root.clickRecord">
<td>
- <svg-icon icon="globe" :class="$root.domainStatusClass(domain)" :title="$root.domainStatusText(domain)"></svg-icon>
+ <svg-icon icon="globe" :class="$root.statusClass(domain)" :title="$root.statusText(domain)"></svg-icon>
<router-link :to="{ path: 'domain/' + domain.id }">{{ domain.namespace }}</router-link>
</td>
<td class="buttons"></td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('user.domains-none') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
domains: []
}
},
created() {
this.$root.startLoading()
axios.get('/api/v4/domains')
.then(response => {
this.$root.stopLoading()
this.domains = response.data
})
.catch(this.$root.errorHandler)
}
}
</script>
diff --git a/src/resources/vue/Resource/Info.vue b/src/resources/vue/Resource/Info.vue
index e2d28eeb..a9f1ad10 100644
--- a/src/resources/vue/Resource/Info.vue
+++ b/src/resources/vue/Resource/Info.vue
@@ -1,189 +1,189 @@
<template>
<div class="container">
<status-component v-if="resource_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
<div class="card" id="resource-info">
<div class="card-body">
<div class="card-title" v-if="resource_id !== 'new'">
{{ $tc('resource.list-title', 1) }}
<button class="btn btn-outline-danger button-delete float-end" @click="deleteResource()" tag="button">
<svg-icon icon="trash-alt"></svg-icon> {{ $t('resource.delete') }}
</button>
</div>
<div class="card-title" v-if="resource_id === 'new'">{{ $t('resource.new') }}</div>
<div class="card-text">
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
{{ $t('form.general') }}
</a>
</li>
<li v-if="resource_id !== 'new'" class="nav-item">
<a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
<form @submit.prevent="submit" class="card-body">
<div v-if="resource_id !== 'new'" class="row plaintext mb-3">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
- <span :class="$root.resourceStatusClass(resource) + ' form-control-plaintext'" id="status">{{ $root.resourceStatusText(resource) }}</span>
+ <span :class="$root.statusClass(resource) + ' form-control-plaintext'" id="status">{{ $root.statusText(resource) }}</span>
</div>
</div>
<div class="row mb-3">
<label for="name" class="col-sm-4 col-form-label">{{ $t('form.name') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="name" v-model="resource.name">
</div>
</div>
<div v-if="domains.length" class="row mb-3">
<label for="domain" class="col-sm-4 col-form-label">{{ $t('form.domain') }}</label>
<div class="col-sm-8">
<select class="form-select" v-model="resource.domain">
<option v-for="_domain in domains" :key="_domain.id" :value="_domain.namespace">{{ _domain.namespace }}</option>
</select>
</div>
</div>
<div v-if="resource.email" class="row mb-3">
<label for="email" class="col-sm-4 col-form-label">{{ $t('form.email') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="email" disabled v-model="resource.email">
</div>
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</form>
</div>
<div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
<form @submit.prevent="submitSettings" class="card-body">
<div class="row mb-3">
<label for="invitation_policy" class="col-sm-4 col-form-label">{{ $t('resource.invitation-policy') }}</label>
<div class="col-sm-8">
<div class="input-group input-group-select mb-1">
<select class="form-select" id="invitation_policy" v-model="resource.config.invitation_policy" @change="policyChange">
<option value="accept">{{ $t('resource.ipolicy-accept') }}</option>
<option value="manual">{{ $t('resource.ipolicy-manual') }}</option>
<option value="reject">{{ $t('resource.ipolicy-reject') }}</option>
</select>
<input type="text" class="form-control" id="owner" v-model="resource.config.owner" :placeholder="$t('form.email')">
</div>
<small id="invitation-policy-hint" class="text-muted">
{{ $t('resource.invitation-policy-text') }}
</small>
</div>
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusComponent from '../Widgets/Status'
export default {
components: {
StatusComponent
},
data() {
return {
domains: [],
resource_id: null,
resource: { config: {} },
status: {}
}
},
created() {
this.resource_id = this.$route.params.resource
if (this.resource_id != 'new') {
this.$root.startLoading()
axios.get('/api/v4/resources/' + this.resource_id)
.then(response => {
this.$root.stopLoading()
this.resource = response.data
this.status = response.data.statusInfo
if (this.resource.config.invitation_policy.match(/^manual:(.+)$/)) {
this.resource.config.owner = RegExp.$1
this.resource.config.invitation_policy = 'manual'
}
this.$nextTick().then(() => { this.policyChange() })
})
.catch(this.$root.errorHandler)
} else {
this.$root.startLoading()
axios.get('/api/v4/domains')
.then(response => {
this.$root.stopLoading()
this.domains = response.data
this.resource.domain = this.domains[0].namespace
})
.catch(this.$root.errorHandler)
}
},
mounted() {
$('#name').focus()
},
methods: {
deleteResource() {
axios.delete('/api/v4/resources/' + this.resource_id)
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.$router.push({ name: 'resources' })
}
})
},
policyChange() {
let select = $('#invitation_policy')
select.parent()[select.val() == 'manual' ? 'addClass' : 'removeClass']('selected')
},
statusUpdate(resource) {
this.resource = Object.assign({}, this.resource, resource)
},
submit() {
this.$root.clearFormValidation($('#resource-info form'))
let method = 'post'
let location = '/api/v4/resources'
if (this.resource_id !== 'new') {
method = 'put'
location += '/' + this.resource_id
}
const post = this.$root.pick(this.resource, ['id', 'name', 'domain'])
axios[method](location, post)
.then(response => {
this.$toast.success(response.data.message)
this.$router.push({ name: 'resources' })
})
},
submitSettings() {
this.$root.clearFormValidation($('#settings form'))
let post = {...this.resource.config}
if (post.invitation_policy == 'manual') {
post.invitation_policy += ':' + post.owner
}
delete post.owner
axios.post('/api/v4/resources/' + this.resource_id + '/config', post)
.then(response => {
this.$toast.success(response.data.message)
})
}
}
}
</script>
diff --git a/src/resources/vue/Resource/List.vue b/src/resources/vue/Resource/List.vue
index 3b1a19eb..8607cd16 100644
--- a/src/resources/vue/Resource/List.vue
+++ b/src/resources/vue/Resource/List.vue
@@ -1,60 +1,60 @@
<template>
<div class="container">
<div class="card" id="resource-list">
<div class="card-body">
<div class="card-title">
{{ $tc('resource.list-title', 2) }}
<router-link v-if="!$root.isDegraded()" class="btn btn-success float-end create-resource" :to="{ path: 'resource/new' }" tag="button">
<svg-icon icon="cog"></svg-icon> {{ $t('resource.create') }}
</router-link>
</div>
<div class="card-text">
<table class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">{{ $t('form.name') }}</th>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="resource in resources" :key="resource.id" @click="$root.clickRecord">
<td>
- <svg-icon icon="cog" :class="$root.resourceStatusClass(resource)" :title="$root.resourceStatusText(resource)"></svg-icon>
+ <svg-icon icon="cog" :class="$root.statusClass(resource)" :title="$root.statusText(resource)"></svg-icon>
<router-link :to="{ path: 'resource/' + resource.id }">{{ resource.name }}</router-link>
</td>
<td>
<router-link :to="{ path: 'resource/' + resource.id }">{{ resource.email }}</router-link>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('resource.list-empty') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
resources: []
}
},
created() {
this.$root.startLoading()
axios.get('/api/v4/resources')
.then(response => {
this.$root.stopLoading()
this.resources = response.data
})
.catch(this.$root.errorHandler)
}
}
</script>
diff --git a/src/resources/vue/SharedFolder/Info.vue b/src/resources/vue/SharedFolder/Info.vue
index 706c6467..06152c1a 100644
--- a/src/resources/vue/SharedFolder/Info.vue
+++ b/src/resources/vue/SharedFolder/Info.vue
@@ -1,177 +1,177 @@
<template>
<div class="container">
<status-component v-if="folder_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
<div class="card" id="folder-info">
<div class="card-body">
<div class="card-title" v-if="folder_id !== 'new'">
{{ $tc('shf.list-title', 1) }}
<button class="btn btn-outline-danger button-delete float-end" @click="deleteFolder()" tag="button">
<svg-icon icon="trash-alt"></svg-icon> {{ $t('shf.delete') }}
</button>
</div>
<div class="card-title" v-if="folder_id === 'new'">{{ $t('shf.new') }}</div>
<div class="card-text">
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
{{ $t('form.general') }}
</a>
</li>
<li v-if="folder_id !== 'new'" class="nav-item">
<a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
<form @submit.prevent="submit" class="card-body">
<div v-if="folder_id !== 'new'" class="row plaintext mb-3">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
- <span :class="$root.folderStatusClass(folder) + ' form-control-plaintext'" id="status">{{ $root.folderStatusText(folder) }}</span>
+ <span :class="$root.statusClass(folder) + ' form-control-plaintext'" id="status">{{ $root.statusText(folder) }}</span>
</div>
</div>
<div class="row mb-3">
<label for="name" class="col-sm-4 col-form-label">{{ $t('form.name') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="name" v-model="folder.name">
</div>
</div>
<div class="row mb-3">
<label for="type" class="col-sm-4 col-form-label">{{ $t('form.type') }}</label>
<div class="col-sm-8">
<select id="type" class="form-select" v-model="folder.type" :disabled="folder_id !== 'new'">
<option v-for="type in types" :key="type" :value="type">{{ $t('shf.type-' + type) }}</option>
</select>
</div>
</div>
<div v-if="domains.length" class="row mb-3">
<label for="domain" class="col-sm-4 col-form-label">{{ $t('form.domain') }}</label>
<div v-if="domains.length" class="col-sm-8">
<select class="form-select" v-model="folder.domain">
<option v-for="_domain in domains" :key="_domain.id" :value="_domain.namespace">{{ _domain.namespace }}</option>
</select>
</div>
</div>
<div v-if="folder.email" class="row mb-3">
<label for="email" class="col-sm-4 col-form-label">{{ $t('form.email') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="email" disabled v-model="folder.email">
</div>
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</form>
</div>
<div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
<form @submit.prevent="submitSettings" class="card-body">
<div class="row mb-3">
<label for="acl-input" class="col-sm-4 col-form-label">{{ $t('form.acl') }}</label>
<div class="col-sm-8">
<acl-input id="acl" v-model="folder.config.acl" :list="folder.config.acl" class="mb-1"></acl-input>
<small id="acl-hint" class="text-muted">
{{ $t('shf.acl-text') }}
</small>
</div>
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import AclInput from '../Widgets/AclInput'
import StatusComponent from '../Widgets/Status'
export default {
components: {
AclInput,
StatusComponent
},
data() {
return {
domains: [],
folder_id: null,
folder: { type: 'mail', config: {} },
status: {},
types: [ 'mail', 'event', 'task', 'contact', 'note', 'file' ]
}
},
created() {
this.folder_id = this.$route.params.folder
if (this.folder_id != 'new') {
this.$root.startLoading()
axios.get('/api/v4/shared-folders/' + this.folder_id)
.then(response => {
this.$root.stopLoading()
this.folder = response.data
this.status = response.data.statusInfo
})
.catch(this.$root.errorHandler)
} else {
this.$root.startLoading()
axios.get('/api/v4/domains')
.then(response => {
this.$root.stopLoading()
this.domains = response.data
this.folder.domain = this.domains[0].namespace
})
.catch(this.$root.errorHandler)
}
},
mounted() {
$('#name').focus()
},
methods: {
deleteFolder() {
axios.delete('/api/v4/shared-folders/' + this.folder_id)
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.$router.push({ name: 'shared-folders' })
}
})
},
statusUpdate(folder) {
this.folder = Object.assign({}, this.folder, folder)
},
submit() {
this.$root.clearFormValidation($('#folder-info form'))
let method = 'post'
let location = '/api/v4/shared-folders'
if (this.folder_id !== 'new') {
method = 'put'
location += '/' + this.folder_id
}
const post = this.$root.pick(this.folder, ['id', 'name', 'domain', 'type'])
axios[method](location, post)
.then(response => {
this.$toast.success(response.data.message)
this.$router.push({ name: 'shared-folders' })
})
},
submitSettings() {
this.$root.clearFormValidation($('#settings form'))
let post = {...this.folder.config}
axios.post('/api/v4/shared-folders/' + this.folder_id + '/config', post)
.then(response => {
this.$toast.success(response.data.message)
})
}
}
}
</script>
diff --git a/src/resources/vue/SharedFolder/List.vue b/src/resources/vue/SharedFolder/List.vue
index 484696f3..41165321 100644
--- a/src/resources/vue/SharedFolder/List.vue
+++ b/src/resources/vue/SharedFolder/List.vue
@@ -1,60 +1,60 @@
<template>
<div class="container">
<div class="card" id="folder-list">
<div class="card-body">
<div class="card-title">
{{ $tc('shf.list-title', 2) }}
<router-link v-if="!$root.isDegraded()" class="btn btn-success float-end create-folder" :to="{ path: 'shared-folder/new' }" tag="button">
<svg-icon icon="cog"></svg-icon> {{ $t('shf.create') }}
</router-link>
</div>
<div class="card-text">
<table class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">{{ $t('form.name') }}</th>
<th scope="col">{{ $t('form.type') }}</th>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="folder in folders" :key="folder.id" @click="$root.clickRecord">
<td>
- <svg-icon icon="folder-open" :class="$root.folderStatusClass(folder)" :title="$root.folderStatusText(folder)"></svg-icon>
+ <svg-icon icon="folder-open" :class="$root.statusClass(folder)" :title="$root.statusText(folder)"></svg-icon>
<router-link :to="{ path: 'shared-folder/' + folder.id }">{{ folder.name }}</router-link>
</td>
<td>{{ $t('shf.type-' + folder.type) }}</td>
<td><router-link :to="{ path: 'shared-folder/' + folder.id }">{{ folder.email }}</router-link></td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="3">{{ $t('shf.list-empty') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
folders: []
}
},
created() {
this.$root.startLoading()
axios.get('/api/v4/shared-folders')
.then(response => {
this.$root.stopLoading()
this.folders = response.data
})
.catch(this.$root.errorHandler)
}
}
</script>
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
index bc014335..0ed16981 100644
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -1,243 +1,243 @@
<template>
<div class="container">
<status-component v-if="user_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
<div class="card" id="user-info">
<div class="card-body">
<div class="card-title" v-if="user_id !== 'new'">{{ $t('user.title') }}
<button
class="btn btn-outline-danger button-delete float-end"
@click="showDeleteConfirmation()" type="button"
>
<svg-icon icon="trash-alt"></svg-icon> {{ $t('user.delete') }}
</button>
</div>
<div class="card-title" v-if="user_id === 'new'">{{ $t('user.new') }}</div>
<div class="card-text">
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
{{ $t('form.general') }}
</a>
</li>
<li v-if="user_id !== 'new'" class="nav-item">
<a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
<form @submit.prevent="submit" class="card-body">
<div v-if="user_id !== 'new'" class="row plaintext mb-3">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
- <span :class="$root.userStatusClass(user) + ' form-control-plaintext'" id="status">{{ $root.userStatusText(user) }}</span>
+ <span :class="$root.statusClass(user) + ' form-control-plaintext'" id="status">{{ $root.statusText(user) }}</span>
</div>
</div>
<div class="row mb-3">
<label for="first_name" class="col-sm-4 col-form-label">{{ $t('form.firstname') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="first_name" v-model="user.first_name">
</div>
</div>
<div class="row mb-3">
<label for="last_name" class="col-sm-4 col-form-label">{{ $t('form.lastname') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="last_name" v-model="user.last_name">
</div>
</div>
<div class="row mb-3">
<label for="organization" class="col-sm-4 col-form-label">{{ $t('user.org') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="organization" v-model="user.organization">
</div>
</div>
<div class="row mb-3">
<label for="email" class="col-sm-4 col-form-label">{{ $t('form.email') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="email" :disabled="user_id !== 'new'" required v-model="user.email">
</div>
</div>
<div class="row mb-3">
<label for="aliases-input" class="col-sm-4 col-form-label">{{ $t('user.aliases-email') }}</label>
<div class="col-sm-8">
<list-input id="aliases" :list="user.aliases"></list-input>
</div>
</div>
<div class="row mb-3">
<label for="password" class="col-sm-4 col-form-label">{{ $t('form.password') }}</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="password" v-model="user.password" :required="user_id === 'new'">
</div>
</div>
<div class="row mb-3">
<label for="password_confirmaton" class="col-sm-4 col-form-label">{{ $t('form.password-confirm') }}</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="password_confirmation" v-model="user.password_confirmation" :required="user_id === 'new'">
</div>
</div>
<div v-if="user_id === 'new'" id="user-packages" class="row mb-3">
<label class="col-sm-4 col-form-label">{{ $t('user.package') }}</label>
<package-select class="col-sm-8 pt-sm-1"></package-select>
</div>
<div v-if="user_id !== 'new'" id="user-skus" class="row mb-3">
<label class="col-sm-4 col-form-label">{{ $t('user.subscriptions') }}</label>
<subscription-select v-if="user.id" class="col-sm-8 pt-sm-1" :object="user"></subscription-select>
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</form>
</div>
<div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
<form @submit.prevent="submitSettings" class="card-body">
<div class="row checkbox mb-3">
<label for="greylist_enabled" class="col-sm-4 col-form-label">{{ $t('user.greylisting') }}</label>
<div class="col-sm-8 pt-2">
<input type="checkbox" id="greylist_enabled" name="greylist_enabled" value="1" class="form-check-input d-block mb-2" :checked="user.config.greylist_enabled">
<small id="greylisting-hint" class="text-muted">
{{ $t('user.greylisting-text') }}
</small>
</div>
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
</form>
</div>
</div>
</div>
</div>
</div>
<div id="delete-warning" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('user.delete-email', { email: user.email }) }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<p>{{ $t('user.delete-text') }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-danger modal-action" @click="deleteUser()">
<svg-icon icon="trash-alt"></svg-icon> {{ $t('btn.delete') }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from 'bootstrap'
import ListInput from '../Widgets/ListInput'
import PackageSelect from '../Widgets/PackageSelect'
import StatusComponent from '../Widgets/Status'
import SubscriptionSelect from '../Widgets/SubscriptionSelect'
export default {
components: {
ListInput,
PackageSelect,
StatusComponent,
SubscriptionSelect
},
data() {
return {
user_id: null,
user: { aliases: [], config: [] },
status: {}
}
},
created() {
this.user_id = this.$route.params.user
if (this.user_id !== 'new') {
this.$root.startLoading()
axios.get('/api/v4/users/' + this.user_id)
.then(response => {
this.$root.stopLoading()
this.user = response.data
this.user.first_name = response.data.settings.first_name
this.user.last_name = response.data.settings.last_name
this.user.organization = response.data.settings.organization
this.status = response.data.statusInfo
})
.catch(this.$root.errorHandler)
}
},
mounted() {
$('#first_name').focus()
$('#delete-warning')[0].addEventListener('shown.bs.modal', event => {
$(event.target).find('button.modal-cancel').focus()
})
},
methods: {
submit() {
this.$root.clearFormValidation($('#general form'))
let method = 'post'
let location = '/api/v4/users'
if (this.user_id !== 'new') {
method = 'put'
location += '/' + this.user_id
let skus = {}
$('#user-skus input[type=checkbox]:checked').each((idx, input) => {
let id = $(input).val()
let range = $(input).parents('tr').first().find('input[type=range]').val()
skus[id] = range || 1
})
this.user.skus = skus
} else {
this.user.package = $('#user-packages input:checked').val()
}
axios[method](location, this.user)
.then(response => {
if (response.data.statusInfo) {
this.$store.state.authInfo.statusInfo = response.data.statusInfo
}
this.$toast.success(response.data.message)
this.$router.push({ name: 'users' })
})
},
submitSettings() {
this.$root.clearFormValidation($('#settings form'))
let post = { greylist_enabled: $('#greylist_enabled').prop('checked') ? 1 : 0 }
axios.post('/api/v4/users/' + this.user_id + '/config', post)
.then(response => {
this.$toast.success(response.data.message)
})
},
statusUpdate(user) {
this.user = Object.assign({}, this.user, user)
},
deleteUser() {
// Delete the user from the confirm dialog
axios.delete('/api/v4/users/' + this.user_id)
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.$router.push({ name: 'users' })
}
})
},
showDeleteConfirmation() {
if (this.user_id == this.$store.state.authInfo.id) {
// Deleting self, redirect to /profile/delete page
this.$router.push({ name: 'profile-delete' })
} else {
// Display the warning
new Modal('#delete-warning').show()
}
}
}
}
</script>
diff --git a/src/resources/vue/User/List.vue b/src/resources/vue/User/List.vue
index d067928b..7e1030e4 100644
--- a/src/resources/vue/User/List.vue
+++ b/src/resources/vue/User/List.vue
@@ -1,62 +1,62 @@
<template>
<div class="container">
<div class="card" id="user-list">
<div class="card-body">
<div class="card-title">
{{ $t('user.list-title') }}
</div>
<div class="card-text">
<div class="mb-2 d-flex">
<list-search :placeholder="$t('user.search')" :on-search="searchUsers"></list-search>
<div v-if="!$root.isDegraded()">
<router-link class="btn btn-success ms-1 create-user" :to="{ path: 'user/new' }" tag="button">
<svg-icon icon="user"></svg-icon> {{ $t('user.create') }}
</router-link>
</div>
</div>
<table id="users-list" class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">{{ $t('form.primary-email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :id="'user' + user.id" :key="user.id" @click="$root.clickRecord">
<td>
- <svg-icon icon="user" :class="$root.userStatusClass(user)" :title="$root.userStatusText(user)"></svg-icon>
+ <svg-icon icon="user" :class="$root.statusClass(user)" :title="$root.statusText(user)"></svg-icon>
<router-link :to="{ path: 'user/' + user.id }">{{ user.email }}</router-link>
</td>
</tr>
</tbody>
<list-foot :text="$t('user.users-none')"></list-foot>
</table>
<list-more v-if="hasMore" :on-click="loadUsers"></list-more>
</div>
</div>
</div>
</div>
</template>
<script>
import ListTools from '../Widgets/ListTools'
export default {
mixins: [ ListTools ],
data() {
return {
users: []
}
},
mounted() {
this.loadUsers({ init: true })
},
methods: {
loadUsers(params) {
this.listSearch('users', '/api/v4/users', params)
},
searchUsers(search) {
this.loadUsers({ reset: true, search })
}
}
}
</script>
diff --git a/src/resources/vue/Widgets/ListInput.vue b/src/resources/vue/Widgets/ListInput.vue
index e7b4d54a..0c15618f 100644
--- a/src/resources/vue/Widgets/ListInput.vue
+++ b/src/resources/vue/Widgets/ListInput.vue
@@ -1,74 +1,75 @@
<template>
<div class="list-input" :id="id">
<div class="input-group">
<input :id="id + '-input'" type="text" class="form-control main-input" @keydown="keyDown">
<a href="#" class="btn btn-outline-secondary" @click.prevent="addItem">
<svg-icon icon="plus"></svg-icon>
<span class="visually-hidden">{{ $t('btn.add') }}</span>
</a>
</div>
<div class="input-group" v-for="(item, index) in list" :key="index">
<input type="text" class="form-control" v-model="list[index]">
<a href="#" class="btn btn-outline-secondary" @click.prevent="deleteItem(index)">
<svg-icon icon="trash-alt"></svg-icon>
<span class="visually-hidden">{{ $t('btn.delete') }}</span>
</a>
</div>
</div>
</template>
<script>
export default {
props: {
list: { type: Array, default: () => [] },
id: { type: String, default: '' }
},
mounted() {
this.input = $(this.$el).find('.main-input')[0]
// On form submit add the text from main input to the list
// Users tend to forget about pressing the "plus" button
// Note: We can't use form.onsubmit (too late)
// Note: Use of input.onblur has been proven to be problematic
// TODO: What with forms that have no submit button?
$(this.$el).closest('form').find('button[type=submit]').on('click', () => {
this.addItem(false)
})
},
methods: {
addItem(focus) {
let value = this.input.value
if (value) {
- this.list.push(value)
+ this.$set(this.list, this.list.length, value)
+
this.input.value = ''
this.input.classList.remove('is-invalid')
if (focus !== false) {
this.input.focus()
}
if (this.list.length == 1) {
this.$el.classList.remove('is-invalid')
}
this.$emit('change', this.$el)
}
},
deleteItem(index) {
this.$delete(this.list, index)
this.$emit('change', this.$el)
if (!this.list.length) {
this.$el.classList.remove('is-invalid')
}
},
keyDown(e) {
if (e.which == 13 && e.target.value) {
this.addItem()
e.preventDefault()
}
}
}
}
</script>
diff --git a/src/resources/vue/Widgets/UserSearch.vue b/src/resources/vue/Widgets/UserSearch.vue
index ce76e2b5..74c53c08 100644
--- a/src/resources/vue/Widgets/UserSearch.vue
+++ b/src/resources/vue/Widgets/UserSearch.vue
@@ -1,76 +1,76 @@
<template>
<div id="search-box" class="card">
<div class="card-body">
<form @submit.prevent="searchUser" class="row justify-content-center">
<div class="input-group col-sm-8">
<input class="form-control" type="text" :placeholder="$t('user.search-pl')" v-model="search">
<button type="submit" class="btn btn-primary"><svg-icon icon="search"></svg-icon> {{ $t('btn.search') }}</button>
</div>
</form>
<table v-if="users.length" class="table table-sm table-hover mt-4">
<thead>
<tr>
<th scope="col">{{ $t('form.primary-email') }}</th>
<th scope="col">{{ $t('form.id') }}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $t('form.created') }}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $t('form.deleted') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :id="'user' + user.id" :key="user.id" :class="user.isDeleted ? 'text-secondary' : ''">
<td class="text-nowrap">
- <svg-icon icon="user" :class="$root.userStatusClass(user)" :title="$root.userStatusText(user)"></svg-icon>
+ <svg-icon icon="user" :class="$root.statusClass(user)" :title="$root.statusText(user)"></svg-icon>
<router-link v-if="!user.isDeleted" :to="{ path: 'user/' + user.id }">{{ user.email }}</router-link>
<span v-if="user.isDeleted">{{ user.email }}</span>
</td>
<td>
<router-link v-if="!user.isDeleted" :to="{ path: 'user/' + user.id }">{{ user.id }}</router-link>
<span v-if="user.isDeleted">{{ user.id }}</span>
</td>
<td class="d-none d-md-table-cell">{{ toDate(user.created_at) }}</td>
<td class="d-none d-md-table-cell">{{ toDate(user.deleted_at) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
data() {
return {
search: '',
users: []
}
},
mounted() {
$('#search-box input', this.$el).focus()
},
methods: {
searchUser() {
this.users = []
axios.get('/api/v4/users', { params: { search: this.search } })
.then(response => {
if (response.data.count == 1 && !response.data.list[0].isDeleted) {
this.$router.push({ name: 'user', params: { user: response.data.list[0].id } })
return
}
if (response.data.message) {
this.$toast.info(response.data.message)
}
this.users = response.data.list
})
.catch(this.$root.errorHandler)
},
toDate(datetime) {
if (datetime) {
return datetime.split(' ')[0]
}
}
}
}
</script>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Feb 1, 5:19 AM (1 d, 6 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426638
Default Alt Text
(159 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment