diff --git a/src/app/Utils.php b/src/app/Utils.php
index 50fa6341..3eb059f2 100644
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -1,164 +1,164 @@
toString();
}
private static function combine($input, $r, $index, $data, $i, &$output): void
{
$n = count($input);
// Current cobination is ready
if ($index == $r) {
$output[] = array_slice($data, 0, $r);
return;
}
// When no more elements are there to put in data[]
if ($i >= $n) {
return;
}
// current is included, put next at next location
$data[$index] = $input[$i];
self::combine($input, $r, $index + 1, $data, $i + 1, $output);
// current is excluded, replace it with next (Note that i+1
// is passed, but index is not changed)
self::combine($input, $r, $index, $data, $i + 1, $output);
}
/**
* Create a configuration/environment data to be passed to
* the UI
*
* @todo For a lack of better place this is put here for now
*
* @return array Configuration data
*/
public static function uiEnv(): array
{
$opts = ['app.name', 'app.url', 'app.domain'];
$env = \app('config')->getMany($opts);
$countries = include resource_path('countries.php');
$env['countries'] = $countries ?: [];
- $env['isAdmin'] = strpos(request()->getHttpHost(), 'admin.') === 0;
- $env['jsapp'] = $env['isAdmin'] ? 'admin.js' : 'user.js';
+ $isAdmin = strpos(request()->getHttpHost(), 'admin.') === 0;
+ $env['jsapp'] = $isAdmin ? 'admin.js' : 'user.js';
return $env;
}
/**
* Email address (login or alias) validation
*
* @param string $email Email address
* @param \App\User $user The account owner
* @param bool $is_alias The email is an alias
*
* @return string Error message on validation error
*/
public static function validateEmail(
string $email,
\App\User $user,
bool $is_alias = false
): ?string {
$attribute = $is_alias ? 'alias' : 'email';
if (strpos($email, '@') === false) {
return \trans('validation.entryinvalid', ['attribute' => $attribute]);
}
list($login, $domain) = explode('@', $email);
// Check if domain exists
$domain = Domain::where('namespace', Str::lower($domain))->first();
if (empty($domain)) {
return \trans('validation.domaininvalid');
}
// Validate login part alone
$v = Validator::make(
[$attribute => $login],
[$attribute => ['required', new UserEmailLocal(!$domain->isPublic())]]
);
if ($v->fails()) {
return $v->errors()->toArray()[$attribute][0];
}
// Check if it is one of domains available to the user
// TODO: We should have a helper that returns "flat" array with domain names
// I guess we could use pluck() somehow
$domains = array_map(
function ($domain) {
return $domain->namespace;
},
$user->domains()
);
if (!in_array($domain->namespace, $domains)) {
return \trans('validation.entryexists', ['attribute' => 'domain']);
}
// Check if user with specified address already exists
if (User::findByEmail($email)) {
return \trans('validation.entryexists', ['attribute' => $attribute]);
}
return null;
}
}
diff --git a/src/resources/js/admin.js b/src/resources/js/admin.js
index caf680dc..2ef6047d 100644
--- a/src/resources/js/admin.js
+++ b/src/resources/js/admin.js
@@ -1,9 +1,10 @@
/**
* Application code for the admin UI
*/
-import router from './routes-admin'
+import routes from './routes-admin.js'
-window.router = router
+window.routes = routes
+window.isAdmin = true
require('./app')
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
index 951cc640..e03c518b 100644
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -1,289 +1,266 @@
/**
* 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/Menu'
import store from './store'
-import FontAwesomeIcon from './fontawesome'
-import VueToastr from '@deveodk/vue-toastr'
-
-window.Vue = require('vue')
-
-Vue.component('svg-icon', FontAwesomeIcon)
-
-Vue.use(VueToastr, {
- defaultPosition: 'toast-bottom-right',
- defaultTimeout: 5000
-})
-
-const vTooltip = (el, binding) => {
- const t = []
-
- if (binding.modifiers.focus) t.push('focus')
- if (binding.modifiers.hover) t.push('hover')
- if (binding.modifiers.click) t.push('click')
- if (!t.length) t.push('hover')
-
- $(el).tooltip({
- title: binding.value,
- placement: binding.arg || 'top',
- trigger: t.join(' '),
- html: !!binding.modifiers.html,
- });
-}
-
-Vue.directive('tooltip', {
- bind: vTooltip,
- update: vTooltip,
- unbind (el) {
- $(el).tooltip('dispose')
- }
-})
-
-// Add a response interceptor for general/validation error handler
-// This have to be before Vue and Router setup. Otherwise we would
-// not be able to handle axios responses initiated from inside
-// components created/mounted handlers (e.g. signup code verification link)
-window.axios.interceptors.response.use(
- response => {
- // Do nothing
- return response
- },
- error => {
- let error_msg
- let status = error.response ? error.response.status : 200
-
- if (error.response && status == 422) {
- error_msg = "Form validation error"
-
- $.each(error.response.data.errors || {}, (idx, msg) => {
- $('form').each((i, form) => {
- const input_name = ($(form).data('validation-prefix') || '') + idx
- const input = $('#' + input_name)
-
- if (input.length) {
- // Create an error message\
- // API responses can use a string, array or object
- let msg_text = ''
- if ($.type(msg) !== 'string') {
- $.each(msg, (index, str) => {
- msg_text += str + ' '
- })
- }
- else {
- msg_text = msg
- }
-
- let feedback = $('
').text(msg_text)
-
- if (input.is('.list-input')) {
- // List input widget
- input.children(':not(:first-child)').each((index, element) => {
- if (msg[index]) {
- $(element).find('input').addClass('is-invalid')
- }
- })
-
- input.addClass('is-invalid').next('.invalid-feedback').remove()
- input.after(feedback)
- }
- else {
- // Standard form element
- input.addClass('is-invalid')
- input.parent().find('.invalid-feedback').remove()
- input.parent().append(feedback)
- }
-
- return false
- }
- });
- })
-
- $('form .is-invalid:not(.listinput-widget)').first().focus()
- }
- else if (error.response && error.response.data) {
- error_msg = error.response.data.message
- }
- else {
- error_msg = error.request ? error.request.statusText : error.message
- }
-
- app.$toastr('error', error_msg || "Server Error", 'Error')
-
- // Pass the error as-is
- return Promise.reject(error)
- }
-)
const app = new Vue({
el: '#app',
components: {
'app-component': AppComponent,
'menu-component': MenuComponent
},
store,
router: window.router,
data() {
return {
- isLoading: true
+ isLoading: true,
+ isAdmin: window.isAdmin
}
},
methods: {
// Clear (bootstrap) form validation state
clearFormValidation(form) {
$(form).find('.is-invalid').removeClass('is-invalid')
$(form).find('.invalid-feedback').remove()
},
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(token, dashboard) {
store.commit('logoutUser') // destroy old state data
store.commit('loginUser')
localStorage.setItem('token', token)
axios.defaults.headers.common.Authorization = 'Bearer ' + token
if (dashboard !== false) {
this.$router.push(store.state.afterLogin || { name: 'dashboard' })
}
store.state.afterLogin = null
},
// Set user state to "not logged in"
logoutUser() {
store.commit('logoutUser')
localStorage.setItem('token', '')
delete axios.defaults.headers.common.Authorization
this.$router.push({ name: 'login' })
},
// Display "loading" overlay (to be used by route components)
startLoading() {
this.isLoading = true
// Lock the UI with the 'loading...' element
let loading = $('#app > .app-loader').show()
if (!loading.length) {
$('#app').append($('
Loading
'))
}
},
// Hide "loading" overlay
stopLoading() {
$('#app > .app-loader').fadeOut()
this.isLoading = false
},
errorPage(code, msg) {
// 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".
const map = {
400: "Bad request",
401: "Unauthorized",
403: "Access denied",
404: "Not found",
405: "Method not allowed",
500: "Internal server error"
}
if (!msg) msg = map[code] || "Unknown Error"
const error_page = `
${code}
${msg}
`
$('#app').children(':not(nav)').remove()
$('#app').append(error_page)
},
errorHandler(error) {
this.stopLoading()
if (!error.response) {
// TODO: probably network connection error
} else if (error.response.status === 401) {
this.logoutUser()
} else {
this.errorPage(error.response.status, error.response.statusText)
}
},
price(price) {
return (price/100).toLocaleString('de-DE', { style: 'currency', currency: 'CHF' })
},
+ priceLabel(cost, units = 1, discount) {
+ let index = ''
+
+ if (units < 0) {
+ units = 1
+ }
+
+ if (discount) {
+ cost = Math.floor(cost * ((100 - discount) / 100))
+ index = '\u00B9'
+ }
+
+ return this.price(cost * units) + '/month' + index
+ },
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 'Deleted'
}
if (domain.isSuspended) {
return 'Suspended'
}
if (!domain.isVerified || !domain.isLdapReady || !domain.isConfirmed) {
return 'Not Ready'
}
return 'Active'
},
userStatusClass(user) {
if (user.isDeleted) {
return 'text-muted'
}
if (user.isSuspended) {
return 'text-warning'
}
if (!user.isImapReady || !user.isLdapReady) {
return 'text-danger'
}
return 'text-success'
},
userStatusText(user) {
if (user.isDeleted) {
return 'Deleted'
}
if (user.isSuspended) {
return 'Suspended'
}
if (!user.isImapReady || !user.isLdapReady) {
return 'Not Ready'
}
return 'Active'
}
}
})
+
+// Add a axios response interceptor for general/validation error handler
+window.axios.interceptors.response.use(
+ response => {
+ // Do nothing
+ return response
+ },
+ error => {
+ let error_msg
+ let status = error.response ? error.response.status : 200
+
+ if (error.response && status == 422) {
+ error_msg = "Form validation error"
+
+ $.each(error.response.data.errors || {}, (idx, msg) => {
+ $('form').each((i, form) => {
+ const input_name = ($(form).data('validation-prefix') || '') + idx
+ const input = $('#' + input_name)
+
+ if (input.length) {
+ // Create an error message\
+ // API responses can use a string, array or object
+ let msg_text = ''
+ if ($.type(msg) !== 'string') {
+ $.each(msg, (index, str) => {
+ msg_text += str + ' '
+ })
+ }
+ else {
+ msg_text = msg
+ }
+
+ let feedback = $('
').text(msg_text)
+
+ if (input.is('.list-input')) {
+ // List input widget
+ input.children(':not(:first-child)').each((index, element) => {
+ if (msg[index]) {
+ $(element).find('input').addClass('is-invalid')
+ }
+ })
+
+ input.addClass('is-invalid').next('.invalid-feedback').remove()
+ input.after(feedback)
+ }
+ else {
+ // Standard form element
+ input.addClass('is-invalid')
+ input.parent().find('.invalid-feedback').remove()
+ input.parent().append(feedback)
+ }
+
+ return false
+ }
+ });
+ })
+
+ $('form .is-invalid:not(.listinput-widget)').first().focus()
+ }
+ else if (error.response && error.response.data) {
+ error_msg = error.response.data.message
+ }
+ else {
+ error_msg = error.request ? error.request.statusText : error.message
+ }
+
+ app.$toastr('error', error_msg || "Server Error", 'Error')
+
+ // Pass the error as-is
+ return Promise.reject(error)
+ }
+)
diff --git a/src/resources/js/bootstrap.js b/src/resources/js/bootstrap.js
index a1b0631d..f5648f25 100644
--- a/src/resources/js/bootstrap.js
+++ b/src/resources/js/bootstrap.js
@@ -1,41 +1,89 @@
window._ = require('lodash')
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
try {
window.Popper = require('popper.js').default
window.$ = window.jQuery = require('jquery')
require('bootstrap')
} catch (e) {}
/**
- * We'll load the axios HTTP library which allows us to easily issue requests
- * to our Laravel back-end. This library automatically handles sending the
- * CSRF token as a header based on the value of the "XSRF" token cookie.
+ * We'll load Vue, VueRouter and global components
*/
-window.axios = require('axios')
+import FontAwesomeIcon from './fontawesome'
+import VueRouter from 'vue-router'
+import VueToastr from '@deveodk/vue-toastr'
+import store from './store'
+
+window.Vue = require('vue')
+
+Vue.component('svg-icon', FontAwesomeIcon)
+
+Vue.use(VueToastr, {
+ defaultPosition: 'toast-bottom-right',
+ defaultTimeout: 5000
+})
+
+const vTooltip = (el, binding) => {
+ const t = []
+
+ if (binding.modifiers.focus) t.push('focus')
+ if (binding.modifiers.hover) t.push('hover')
+ if (binding.modifiers.click) t.push('click')
+ if (!t.length) t.push('hover')
+
+ $(el).tooltip({
+ title: binding.value,
+ placement: binding.arg || 'top',
+ trigger: t.join(' '),
+ html: !!binding.modifiers.html,
+ });
+}
+
+Vue.directive('tooltip', {
+ bind: vTooltip,
+ update: vTooltip,
+ unbind (el) {
+ $(el).tooltip('dispose')
+ }
+})
-window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
+
+Vue.use(VueRouter)
+
+window.router = new VueRouter({
+ mode: 'history',
+ routes: window.routes
+})
+
+router.beforeEach((to, from, next) => {
+ // check if the route requires authentication and user is not logged in
+ if (to.matched.some(route => route.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
+ }
+
+ next()
+})
/**
- * Echo exposes an expressive API for subscribing to channels and listening
- * for events that are broadcast by Laravel. Echo and event broadcasting
- * allows your team to easily build robust real-time web applications.
+ * We'll load the axios HTTP library which allows us to easily issue requests
+ * to our Laravel back-end. This library automatically handles sending the
+ * CSRF token as a header based on the value of the "XSRF" token cookie.
*/
-// import Echo from 'laravel-echo';
-
-// window.Pusher = require('pusher-js');
+window.axios = require('axios')
-// window.Echo = new Echo({
-// broadcaster: 'pusher',
-// key: process.env.MIX_PUSHER_APP_KEY,
-// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
-// encrypted: true
-// });
+axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
diff --git a/src/resources/js/routes-admin.js b/src/resources/js/routes-admin.js
index 6ee3ce4b..27665db2 100644
--- a/src/resources/js/routes-admin.js
+++ b/src/resources/js/routes-admin.js
@@ -1,75 +1,48 @@
-import Vue from 'vue'
-import VueRouter from 'vue-router'
-
-Vue.use(VueRouter)
-
import DashboardComponent from '../vue/Admin/Dashboard'
import DomainComponent from '../vue/Admin/Domain'
import Error404Component from '../vue/404'
import LoginComponent from '../vue/Login'
import LogoutComponent from '../vue/Logout'
import UserComponent from '../vue/Admin/User'
-import store from './store'
-
const routes = [
{
path: '/',
redirect: { name: 'dashboard' }
},
{
path: '/dashboard',
name: 'dashboard',
component: DashboardComponent,
meta: { requiresAuth: true }
},
{
path: '/domain/:domain',
name: 'domain',
component: DomainComponent,
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'login',
component: LoginComponent
},
{
path: '/logout',
name: 'logout',
component: LogoutComponent
},
{
path: '/user/:user',
name: 'user',
component: UserComponent,
meta: { requiresAuth: true }
},
{
name: '404',
path: '*',
component: Error404Component
}
]
-const router = new VueRouter({
- mode: 'history',
- routes
-})
-
-router.beforeEach((to, from, next) => {
- // check if the route requires authentication and user is not logged in
- if (to.matched.some(route => route.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
- }
-
- next()
-})
-
-export default router
+export default routes
diff --git a/src/resources/js/routes-user.js b/src/resources/js/routes-user.js
index a9935b11..1612aaac 100644
--- a/src/resources/js/routes-user.js
+++ b/src/resources/js/routes-user.js
@@ -1,123 +1,96 @@
-import Vue from 'vue'
-import VueRouter from 'vue-router'
-
-Vue.use(VueRouter)
-
import DashboardComponent from '../vue/Dashboard'
import DomainInfoComponent from '../vue/Domain/Info'
import DomainListComponent from '../vue/Domain/List'
import Error404Component from '../vue/404'
import LoginComponent from '../vue/Login'
import LogoutComponent from '../vue/Logout'
import PasswordResetComponent from '../vue/PasswordReset'
import SignupComponent from '../vue/Signup'
import UserInfoComponent from '../vue/User/Info'
import UserListComponent from '../vue/User/List'
import UserProfileComponent from '../vue/User/Profile'
import UserProfileDeleteComponent from '../vue/User/ProfileDelete'
import WalletComponent from '../vue/Wallet'
-import store from './store'
-
const routes = [
{
path: '/',
redirect: { name: 'dashboard' }
},
{
path: '/dashboard',
name: 'dashboard',
component: DashboardComponent,
meta: { requiresAuth: true }
},
{
path: '/domain/:domain',
name: 'domain',
component: DomainInfoComponent,
meta: { requiresAuth: true }
},
{
path: '/domains',
name: 'domains',
component: DomainListComponent,
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'login',
component: LoginComponent
},
{
path: '/logout',
name: 'logout',
component: LogoutComponent
},
{
path: '/password-reset/:code?',
name: 'password-reset',
component: PasswordResetComponent
},
{
path: '/profile',
name: 'profile',
component: UserProfileComponent,
meta: { requiresAuth: true }
},
{
path: '/profile/delete',
name: 'profile-delete',
component: UserProfileDeleteComponent,
meta: { requiresAuth: true }
},
{
path: '/signup/:param?',
alias: '/signup/voucher/:param',
name: 'signup',
component: SignupComponent
},
{
path: '/user/:user',
name: 'user',
component: UserInfoComponent,
meta: { requiresAuth: true }
},
{
path: '/users',
name: 'users',
component: UserListComponent,
meta: { requiresAuth: true }
},
{
path: '/wallet',
name: 'wallet',
component: WalletComponent,
meta: { requiresAuth: true }
},
{
name: '404',
path: '*',
component: Error404Component
}
]
-const router = new VueRouter({
- mode: 'history',
- routes
-})
-
-router.beforeEach((to, from, next) => {
- // check if the route requires authentication and user is not logged in
- if (to.matched.some(route => route.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
- }
-
- next()
-})
-
-export default router
+export default routes
diff --git a/src/resources/js/user.js b/src/resources/js/user.js
index d99e318d..99bf191a 100644
--- a/src/resources/js/user.js
+++ b/src/resources/js/user.js
@@ -1,9 +1,10 @@
/**
* Application code for the user UI
*/
-import router from './routes-user'
+import routes from './routes-user.js'
-window.router = router
+window.routes = routes
+window.isAdmin = false
require('./app')
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
index 2a3ae14b..b6fd1423 100644
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -1,418 +1,404 @@