diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -91,11 +91,9 @@ */ public static function getPublicDomains(): array { - $where = sprintf('(type & %s) AND (status & %s)', Domain::TYPE_PUBLIC, Domain::STATUS_ACTIVE); + $where = sprintf('(type & %s)', Domain::TYPE_PUBLIC); - return self::whereRaw($where)->get(['namespace'])->map(function ($domain) { - return $domain->namespace; - })->toArray(); + return self::whereRaw($where)->get(['namespace'])->pluck('namespace')->toArray(); } /** diff --git a/src/app/Http/Controllers/API/DomainsController.php b/src/app/Http/Controllers/API/DomainsController.php --- a/src/app/Http/Controllers/API/DomainsController.php +++ b/src/app/Http/Controllers/API/DomainsController.php @@ -21,7 +21,9 @@ foreach ($user->domains() as $domain) { if (!$domain->isPublic()) { - $list[] = $domain->toArray(); + $data = $domain->toArray(); + $data = array_merge($data, self::domainStatuses($domain)); + $list[] = $data; } } @@ -60,6 +62,7 @@ return response()->json([ 'status' => 'success', + 'statusInfo' => self::statusInfo($domain), 'message' => __('app.domain-verify-success'), ]); } @@ -127,7 +130,10 @@ $response['dns'] = self::getDNSConfig($domain); $response['config'] = self::getMXConfig($domain->namespace); - $response['confirmed'] = $domain->isConfirmed(); + // Status info + $response['statusInfo'] = self::statusInfo($domain); + + $response = array_merge($response, self::domainStatuses($domain)); return response()->json($response); } @@ -205,4 +211,69 @@ "@ 3600 TXT \"{$hash_txt}\"", ]; } + + /** + * Prepare domain statuses for the UI + * + * @param \App\Domain $domain Domain object + * + * @return array Statuses array + */ + protected static function domainStatuses(Domain $domain): array + { + return [ + 'isLdapReady' => $domain->isLdapReady(), + 'isConfirmed' => $domain->isConfirmed(), + 'isVerified' => $domain->isVerified(), + 'isSuspended' => $domain->isSuspended(), + 'isActive' => $domain->isActive(), + 'isDeleted' => $domain->isDeleted() || $domain->trashed(), + ]; + } + + /** + * Domain status (extended) information. + * + * @param \App\Domain $domain Domain object + * + * @return array Status information + */ + public static function statusInfo(Domain $domain): array + { + $process = []; + + // If that is not a public domain, add domain specific steps + $steps = [ + 'domain-new' => true, + 'domain-ldap-ready' => $domain->isLdapReady(), + 'domain-verified' => $domain->isVerified(), + 'domain-confirmed' => $domain->isConfirmed(), + ]; + + $count = count($steps); + + // Create a process check list + foreach ($steps as $step_name => $state) { + $step = [ + 'label' => $step_name, + 'title' => \trans("app.process-{$step_name}"), + 'state' => $state, + ]; + + if ($step_name == 'domain-confirmed' && !$state) { + $step['link'] = "/domain/{$domain->id}"; + } + + $process[] = $step; + + if ($state) { + $count--; + } + } + + return [ + 'process' => $process, + 'isReady' => $count === 0, + ]; + } } diff --git a/src/app/Http/Controllers/API/UsersController.php b/src/app/Http/Controllers/API/UsersController.php --- a/src/app/Http/Controllers/API/UsersController.php +++ b/src/app/Http/Controllers/API/UsersController.php @@ -85,7 +85,11 @@ { $user = $this->guard()->user(); - $result = $user->users()->orderBy('email')->get(); + $result = $user->users()->orderBy('email')->get()->map(function ($user) { + $data = $user->toArray(); + $data = array_merge($data, self::userStatuses($user)); + return $data; + }); return response()->json($result); } @@ -220,53 +224,42 @@ */ public static function statusInfo(User $user): array { - $status = 'new'; $process = []; $steps = [ 'user-new' => true, - 'user-ldap-ready' => 'isLdapReady', - 'user-imap-ready' => 'isImapReady', + 'user-ldap-ready' => $user->isLdapReady(), + 'user-imap-ready' => $user->isImapReady(), ]; - if ($user->isDeleted()) { - $status = 'deleted'; - } elseif ($user->isSuspended()) { - $status = 'suspended'; - } elseif ($user->isActive()) { - $status = 'active'; + // Create a process check list + foreach ($steps as $step_name => $state) { + $step = [ + 'label' => $step_name, + 'title' => \trans("app.process-{$step_name}"), + 'state' => $state, + ]; + + $process[] = $step; } + list ($local, $domain) = explode('@', $user->email); $domain = Domain::where('namespace', $domain)->first(); // If that is not a public domain, add domain specific steps if ($domain && !$domain->isPublic()) { - $steps['domain-new'] = true; - $steps['domain-ldap-ready'] = 'isLdapReady'; - $steps['domain-verified'] = 'isVerified'; - $steps['domain-confirmed'] = 'isConfirmed'; + $domain_status = DomainsController::statusInfo($domain); + $process = array_merge($process, $domain_status['process']); } - // Create a process check list - foreach ($steps as $step_name => $func) { - $object = strpos($step_name, 'user-') === 0 ? $user : $domain; - - $step = [ - 'label' => $step_name, - 'title' => __("app.process-{$step_name}"), - 'state' => is_bool($func) ? $func : $object->{$func}(), - ]; - - if ($step_name == 'domain-confirmed' && !$step['state']) { - $step['link'] = "/domain/{$domain->id}"; - } - - $process[] = $step; - } + $all = count($process); + $checked = count(array_filter($process, function ($v) { + return $v['state']; + })); return [ 'process' => $process, - 'status' => $status, + 'isReady' => $all === $checked, ]; } @@ -480,6 +473,8 @@ // Status info $response['statusInfo'] = self::statusInfo($user); + $response = array_merge($response, self::userStatuses($user)); + // Information about wallets and accounts for access checks $response['wallets'] = $user->wallets->toArray(); $response['accounts'] = $user->accounts->toArray(); @@ -488,6 +483,24 @@ return $response; } + /** + * Prepare user statuses for the UI + * + * @param \App\User $user User object + * + * @return array Statuses array + */ + protected static function userStatuses(User $user): array + { + return [ + 'isImapReady' => $user->isImapReady(), + 'isLdapReady' => $user->isLdapReady(), + 'isSuspended' => $user->isSuspended(), + 'isActive' => $user->isActive(), + 'isDeleted' => $user->isDeleted() || $user->trashed(), + ]; + } + /** * Validate user input * diff --git a/src/app/Observers/DomainObserver.php b/src/app/Observers/DomainObserver.php --- a/src/app/Observers/DomainObserver.php +++ b/src/app/Observers/DomainObserver.php @@ -24,7 +24,7 @@ } } - $domain->status |= Domain::STATUS_NEW; + $domain->status |= Domain::STATUS_NEW | Domain::STATUS_ACTIVE; } /** diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php --- a/src/app/Observers/UserObserver.php +++ b/src/app/Observers/UserObserver.php @@ -28,7 +28,7 @@ } } - $user->status |= User::STATUS_NEW; + $user->status |= User::STATUS_NEW | User::STATUS_ACTIVE; // can't dispatch job here because it'll fail serialization } diff --git a/src/resources/js/app.js b/src/resources/js/app.js --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -213,7 +213,9 @@ errorHandler(error) { this.stopLoading() - if (error.response.status === 401) { + 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) @@ -221,6 +223,66 @@ }, price(price) { return (price/100).toLocaleString('de-DE', { style: 'currency', currency: 'CHF' }) + }, + 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' } } }) diff --git a/src/resources/js/fontawesome.js b/src/resources/js/fontawesome.js --- a/src/resources/js/fontawesome.js +++ b/src/resources/js/fontawesome.js @@ -1,8 +1,12 @@ import { library } from '@fortawesome/fontawesome-svg-core' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' -//import { } from '@fortawesome/free-regular-svg-icons' //import { } from '@fortawesome/free-brands-svg-icons' +import { + faCheckSquare, + faSquare, +} from '@fortawesome/free-regular-svg-icons' + import { faCheck, faGlobe, @@ -17,6 +21,8 @@ // Register only these icons we need library.add( + faCheckSquare, + faSquare, faCheck, faGlobe, faInfoCircle, diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss --- a/src/resources/sass/app.scss +++ b/src/resources/sass/app.scss @@ -139,6 +139,21 @@ } } +ul.status-list { + list-style: none; + padding: 0; + margin: 0; + + svg { + width: 1.25rem !important; + height: 1.25rem; + } + + span { + vertical-align: top; + } +} + #dashboard-nav { display: flex; flex-wrap: wrap; diff --git a/src/resources/sass/menu.scss b/src/resources/sass/menu.scss --- a/src/resources/sass/menu.scss +++ b/src/resources/sass/menu.scss @@ -5,6 +5,7 @@ .navbar-brand { padding: 0; + outline: 0; > img { display: inline; diff --git a/src/resources/vue/Dashboard.vue b/src/resources/vue/Dashboard.vue --- a/src/resources/vue/Dashboard.vue +++ b/src/resources/vue/Dashboard.vue @@ -1,12 +1,16 @@