Page MenuHomePhorge

D1045.1775298149.diff
No OneTemporary

Authored By
Unknown
Size
51 KB
Referenced Files
None
Subscribers
None

D1045.1775298149.diff

diff --git a/src/app/Console/Development/DomainStatus.php b/src/app/Console/Development/DomainStatus.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Development/DomainStatus.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace App\Console\Development;
+
+use App\Domain;
+use Illuminate\Console\Command;
+
+class DomainStatus extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'domain:status {domain} {--add=} {--del=}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Set/get a domain's status.";
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $domain = Domain::where('namespace', $this->argument('domain'))->firstOrFail();
+
+ $this->info("Found domain: {$domain->id}");
+
+ $statuses = [
+ 'active' => Domain::STATUS_ACTIVE,
+ 'suspended' => Domain::STATUS_SUSPENDED,
+ 'deleted' => Domain::STATUS_DELETED,
+ 'ldapReady' => Domain::STATUS_LDAP_READY,
+ 'verified' => Domain::STATUS_VERIFIED,
+ 'confirmed' => Domain::STATUS_CONFIRMED,
+ ];
+
+ // I'd prefer "-state" and "+state" syntax, but it's not possible
+ $delete = false;
+ if ($update = $this->option('del')) {
+ $delete = true;
+ } elseif ($update = $this->option('add')) {
+ // do nothing
+ }
+
+ if (!empty($update)) {
+ $map = \array_change_key_case($statuses);
+ $update = \strtolower($update);
+
+ if (isset($map[$update])) {
+ if ($delete && $domain->status & $map[$update]) {
+ $domain->status ^= $map[$update];
+ $domain->save();
+ } elseif (!$delete && !($domain->status & $map[$update])) {
+ $domain->status |= $map[$update];
+ $domain->save();
+ }
+ }
+ }
+
+ $domain_state = [];
+ foreach (\array_keys($statuses) as $state) {
+ $func = 'is' . \ucfirst($state);
+ if ($domain->$func()) {
+ $domain_state[] = $state;
+ }
+ }
+
+ $this->info("Status: " . \implode(',', $domain_state));
+ }
+}
diff --git a/src/app/Console/Development/UserStatus.php b/src/app/Console/Development/UserStatus.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Development/UserStatus.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace App\Console\Development;
+
+use App\User;
+use Illuminate\Console\Command;
+
+class UserStatus extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'user:status {userid} {--add=} {--del=}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Set/get a user's status.";
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $user = User::where('email', $this->argument('userid'))->firstOrFail();
+
+ $this->info("Found user: {$user->id}");
+
+ $statuses = [
+ 'active' => User::STATUS_ACTIVE,
+ 'suspended' => User::STATUS_SUSPENDED,
+ 'deleted' => User::STATUS_DELETED,
+ 'ldapReady' => User::STATUS_LDAP_READY,
+ 'imapReady' => User::STATUS_IMAP_READY,
+ ];
+
+ // I'd prefer "-state" and "+state" syntax, but it's not possible
+ $delete = false;
+ if ($update = $this->option('del')) {
+ $delete = true;
+ } elseif ($update = $this->option('add')) {
+ // do nothing
+ }
+
+ if (!empty($update)) {
+ $map = \array_change_key_case($statuses);
+ $update = \strtolower($update);
+
+ if (isset($map[$update])) {
+ if ($delete && $user->status & $map[$update]) {
+ $user->status ^= $map[$update];
+ $user->save();
+ } elseif (!$delete && !($user->status & $map[$update])) {
+ $user->status |= $map[$update];
+ $user->save();
+ }
+ }
+ }
+
+ $user_state = [];
+ foreach (\array_keys($statuses) as $state) {
+ $func = 'is' . \ucfirst($state);
+ if ($user->$func()) {
+ $user_state[] = $state;
+ }
+ }
+
+ $this->info("Status: " . \implode(',', $user_state));
+ }
+}
diff --git a/src/app/Console/Kernel.php b/src/app/Console/Kernel.php
--- a/src/app/Console/Kernel.php
+++ b/src/app/Console/Kernel.php
@@ -38,6 +38,10 @@
{
$this->load(__DIR__ . '/Commands');
+ if (\app('env') != 'production') {
+ $this->load(__DIR__ . '/Development');
+ }
+
include base_path('routes/console.php');
}
}
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;
}
}
@@ -55,11 +57,13 @@
}
if (!$domain->confirm()) {
+ // TODO: This should include an error message to display to the user
return response()->json(['status' => 'error']);
}
return response()->json([
'status' => 'success',
+ 'statusInfo' => self::statusInfo($domain),
'message' => __('app.domain-verify-success'),
]);
}
@@ -127,7 +131,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 +212,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();
@@ -489,6 +484,24 @@
}
/**
+ * 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
*
* @param \Illuminate\Http\Request $request The API request.
diff --git a/src/app/Http/Middleware/RequestLogger.php b/src/app/Http/Middleware/RequestLogger.php
--- a/src/app/Http/Middleware/RequestLogger.php
+++ b/src/app/Http/Middleware/RequestLogger.php
@@ -13,7 +13,7 @@
public function terminate($request, $response)
{
- if (\config('env') != 'production') {
+ if (\app('env') != 'production') {
$url = $request->fullUrl();
$method = $request->getMethod();
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,9 +1,13 @@
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,
faInfoCircle,
@@ -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 @@
<template>
- <div class="container" dusk="dashboard-component">
- <div v-if="!$root.isLoading" id="status-box" class="card">
+ <div v-if="!$root.isLoading" class="container" dusk="dashboard-component">
+ <div v-if="!isReady" id="status-box" class="card">
<div class="card-body">
- <div class="card-title">Status</div>
+ <div class="card-title">Account status: <span class="text-danger">Not ready</span></div>
<div class="card-text">
- <ul style="list-style: none; padding: 0;">
+ <p>The process to create your account has not been completed yet.
+ Some features may be disabled or readonly.</p>
+ <ul class="status-list">
<li v-for="item in statusProcess" :key="item.label">
- <span v-if="item.state">&check;</span><span v-else>&cir;</span>
+ <svg-icon :icon="['far', item.state ? 'check-square' : 'square']"
+ :class="item.state ? 'text-success' : 'text-muted'"
+ ></svg-icon>
<router-link v-if="item.link" :to="{ path: item.link }">{{ item.title }}</router-link>
<span v-if="!item.link">{{ item.title }}</span>
</li>
@@ -36,6 +40,7 @@
export default {
data() {
return {
+ isReady: true,
statusProcess: [],
request: null,
balance: 0
@@ -44,8 +49,6 @@
mounted() {
const authInfo = this.$store.state.isLoggedIn ? this.$store.state.authInfo : null
- clearTimeout(window.infoRequest)
-
if (authInfo) {
this.parseStatusInfo(authInfo.statusInfo)
this.getBalance(authInfo)
@@ -65,12 +68,14 @@
// Displays account status information
parseStatusInfo(info) {
this.statusProcess = info.process
+ this.isReady = info.isReady
// Update status process info every 10 seconds
// FIXME: This probably should have some limit, or the interval
// should grow (well, until it could be done with websocket notifications)
- if (info.status != 'active') {
+ if (!info.isReady && !window.infoRequest) {
window.infoRequest = setTimeout(() => {
+ delete window.infoRequest
// Stop updates after user logged out
if (!this.$store.state.isLoggedIn) {
return;
diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue
--- a/src/resources/vue/Domain/Info.vue
+++ b/src/resources/vue/Domain/Info.vue
@@ -1,6 +1,23 @@
<template>
<div class="container">
- <div v-if="domain && !is_confirmed" class="card" id="domain-verify">
+ <div v-if="!isReady" id="domain-status-box" class="card">
+ <div class="card-body">
+ <div class="card-title">Domain status: <span class="text-danger">Not ready</span></div>
+ <div class="card-text">
+ <p>The process to create the domain has not been completed yet.
+ Some features may be disabled or readonly.</p>
+ <ul class="status-list">
+ <li v-for="item in statusProcess" :key="item.label">
+ <svg-icon :icon="['far', item.state ? 'check-square' : 'square']"
+ :class="item.state ? 'text-success' : 'text-muted'"
+ ></svg-icon>
+ <span>{{ item.title }}</span>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div v-if="domain && !domain.isConfirmed" class="card" id="domain-verify">
<div class="card-body">
<div class="card-title">Domain verification</div>
<div class="card-text">
@@ -17,7 +34,7 @@
</div>
</div>
</div>
- <div v-if="domain && is_confirmed" class="card" id="domain-config">
+ <div v-if="domain && domain.isConfirmed" class="card" id="domain-config">
<div class="card-body">
<div class="card-title">Domain configuration</div>
<div class="card-text">
@@ -40,35 +57,61 @@
return {
domain_id: null,
domain: null,
- is_confirmed: false,
- app_name: window.config['app.name']
+ app_name: window.config['app.name'],
+ isReady: true,
+ statusProcess: []
}
},
created() {
if (this.domain_id = this.$route.params.domain) {
axios.get('/api/v4/domains/' + this.domain_id)
.then(response => {
- this.is_confirmed = response.data.confirmed
this.domain = response.data
- if (!this.is_confirmed) {
+ if (!this.domain.isConfirmed) {
$('#domain-verify button').focus()
}
+ this.parseStatusInfo(response.data.statusInfo)
})
.catch(this.$root.errorHandler)
} else {
this.$root.errorPage(404)
}
},
+ destroyed() {
+ clearTimeout(window.domainRequest)
+ },
methods: {
confirm() {
axios.get('/api/v4/domains/' + this.domain_id + '/confirm')
.then(response => {
if (response.data.status == 'success') {
- this.is_confirmed = true
+ this.domain.isConfirmed = true
+ this.parseStatusInfo(response.data.statusInfo)
this.$toastr('success', response.data.message)
}
})
- }
+ },
+ // Displays domain status information
+ parseStatusInfo(info) {
+ this.statusProcess = info.process
+ this.isReady = info.isReady
+
+ // Update status process info every 10 seconds
+ // FIXME: This probably should have some limit, or the interval
+ // should grow (well, until it could be done with websocket notifications)
+ if (!info.isReady) {
+ window.domainRequest = setTimeout(() => {
+ axios.get('/api/v4/domains/' + this.domain_id)
+ .then(response => {
+ this.domain = response.data
+ this.parseStatusInfo(this.domain.statusInfo)
+ })
+ .catch(error => {
+ this.parseStatusInfo(info)
+ })
+ }, 10000);
+ }
+ },
}
}
</script>
diff --git a/src/resources/vue/Domain/List.vue b/src/resources/vue/Domain/List.vue
--- a/src/resources/vue/Domain/List.vue
+++ b/src/resources/vue/Domain/List.vue
@@ -13,7 +13,10 @@
</thead>
<tbody>
<tr v-for="domain in domains" :key="domain.id">
- <td><router-link :to="{ path: 'domain/' + domain.id }">{{ domain.namespace }}</router-link></td>
+ <td>
+ <svg-icon icon="globe" :class="$root.domainStatusClass(domain)" :title="$root.domainStatusText(domain)"></svg-icon>
+ <router-link :to="{ path: 'domain/' + domain.id }">{{ domain.namespace }}</router-link>
+ </td>
<td class="buttons"></td>
</tr>
</tbody>
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -6,6 +6,12 @@
<div class="card-title" v-if="user_id === 'new'">New user account</div>
<div class="card-text">
<form @submit.prevent="submit">
+ <div v-if="user_id !== 'new'" class="form-group row">
+ <label for="first_name" class="col-sm-4 col-form-label">Status</label>
+ <div class="col-sm-8">
+ <span :class="$root.userStatusClass(user) + ' form-control-plaintext'" id="status">{{ $root.userStatusText(user) }}</span>
+ </div>
+ </div>
<div class="form-group row">
<label for="first_name" class="col-sm-4 col-form-label">First name</label>
<div class="col-sm-8">
diff --git a/src/resources/vue/User/List.vue b/src/resources/vue/User/List.vue
--- a/src/resources/vue/User/List.vue
+++ b/src/resources/vue/User/List.vue
@@ -4,7 +4,7 @@
<div class="card-body">
<div class="card-title">
User Accounts
- <router-link class="btn btn-success float-right create-user" :to="{ path: 'user/new' }" tag="button">
+ <router-link class="btn btn-primary float-right create-user" :to="{ path: 'user/new' }" tag="button">
<svg-icon icon="user"></svg-icon> Create user
</router-link>
</div>
@@ -19,6 +19,7 @@
<tbody>
<tr v-for="user in users" :id="'user' + user.id" :key="user.id">
<td>
+ <svg-icon icon="user" :class="$root.userStatusClass(user)" :title="$root.userStatusText(user)"></svg-icon>
<router-link :to="{ path: 'user/' + user.id }">{{ user.email }}</router-link>
</td>
<td class="buttons">
diff --git a/src/tests/Browser.php b/src/tests/Browser.php
--- a/src/tests/Browser.php
+++ b/src/tests/Browser.php
@@ -27,7 +27,7 @@
}
}
- Assert::assertEquals($expected_count, $count);
+ Assert::assertEquals($expected_count, $count, "Count of [$selector] elements is not $count");
return $this;
}
@@ -64,7 +64,20 @@
$element = $this->resolver->findOrFail($selector);
$classes = explode(' ', (string) $element->getAttribute('class'));
- Assert::assertContains($class_name, $classes);
+ Assert::assertContains($class_name, $classes, "[$selector] has no class '{$class_name}'");
+
+ return $this;
+ }
+
+ /**
+ * Assert that the given element contains specified text,
+ * no matter it's displayed or not.
+ */
+ public function assertText($selector, $text)
+ {
+ $element = $this->resolver->findOrFail($selector);
+
+ Assert::assertTrue(strpos($element->getText(), $text) !== false, "No expected text in [$selector]");
return $this;
}
diff --git a/src/tests/Browser/DomainTest.php b/src/tests/Browser/DomainTest.php
--- a/src/tests/Browser/DomainTest.php
+++ b/src/tests/Browser/DomainTest.php
@@ -59,6 +59,7 @@
$browser->visit('/domain/' . $domain->id)
->on(new DomainInfo())
+ ->assertVisible('@status')
->whenAvailable('@verify', function ($browser) use ($domain) {
// Make sure the domain is confirmed now
// TODO: Test verification process failure
@@ -118,6 +119,8 @@
->click('@links a.link-domains')
// On Domains List page click the domain entry
->on(new DomainList())
+ ->assertVisible('@table tbody tr:first-child td:first-child svg.fa-globe.text-success')
+ ->assertText('@table tbody tr:first-child td:first-child svg title', 'Active')
->assertSeeIn('@table tbody tr:first-child td:first-child', 'kolab.org')
->click('@table tbody tr:first-child td:first-child a')
// On Domain Info page verify that's the clicked domain
diff --git a/src/tests/Browser/LogonTest.php b/src/tests/Browser/LogonTest.php
--- a/src/tests/Browser/LogonTest.php
+++ b/src/tests/Browser/LogonTest.php
@@ -77,7 +77,8 @@
})
->assertUser('john@kolab.org');
- // TODO: Verify dashboard content
+ // Assert no "Account status" for this account
+ $browser->assertMissing('@status');
// Goto /domains and assert that the link on logo element
// leads to the dashboard
diff --git a/src/tests/Browser/Pages/Dashboard.php b/src/tests/Browser/Pages/Dashboard.php
--- a/src/tests/Browser/Pages/Dashboard.php
+++ b/src/tests/Browser/Pages/Dashboard.php
@@ -51,6 +51,7 @@
return [
'@app' => '#app',
'@links' => '#dashboard-nav',
+ '@status' => '#status-box',
];
}
}
diff --git a/src/tests/Browser/Pages/DomainInfo.php b/src/tests/Browser/Pages/DomainInfo.php
--- a/src/tests/Browser/Pages/DomainInfo.php
+++ b/src/tests/Browser/Pages/DomainInfo.php
@@ -40,6 +40,7 @@
'@app' => '#app',
'@config' => '#domain-config',
'@verify' => '#domain-verify',
+ '@status' => '#domain-status-box',
];
}
}
diff --git a/src/tests/Browser/StatusTest.php b/src/tests/Browser/StatusTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/StatusTest.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace Tests\Browser;
+
+use App\Domain;
+use App\User;
+use Tests\Browser;
+use Tests\Browser\Pages\Dashboard;
+use Tests\Browser\Pages\DomainInfo;
+use Tests\Browser\Pages\DomainList;
+use Tests\Browser\Pages\Home;
+use Tests\Browser\Pages\UserInfo;
+use Tests\Browser\Pages\UserList;
+use Tests\TestCaseDusk;
+use Illuminate\Support\Facades\DB;
+
+class StatusTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ DB::statement("UPDATE domains SET status = (status | " . Domain::STATUS_CONFIRMED . ")"
+ . " WHERE namespace = 'kolab.org'");
+ DB::statement("UPDATE users SET status = (status | " . User::STATUS_IMAP_READY . ")"
+ . " WHERE email = 'john@kolab.org'");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ DB::statement("UPDATE domains SET status = (status | " . Domain::STATUS_CONFIRMED . ")"
+ . " WHERE namespace = 'kolab.org'");
+ DB::statement("UPDATE users SET status = (status | " . User::STATUS_IMAP_READY . ")"
+ . " WHERE email = 'john@kolab.org'");
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test account status in the Dashboard
+ */
+ public function testDashboard(): void
+ {
+ // Unconfirmed domain
+ $domain = Domain::where('namespace', 'kolab.org')->first();
+ $domain->status ^= Domain::STATUS_CONFIRMED;
+ $domain->save();
+
+ $this->browse(function ($browser) use ($domain) {
+ $browser->visit(new Home())
+ ->submitLogon('john@kolab.org', 'simple123', true)
+ ->on(new Dashboard())
+ ->whenAvailable('@status', function ($browser) {
+ $browser->assertSeeIn('.card-title', 'Account status:')
+ ->assertSeeIn('.card-title span.text-danger', 'Not ready')
+ ->with('ul.status-list', function ($browser) {
+ $browser->assertElementsCount('li', 7)
+ ->assertVisible('li:nth-child(1) svg.fa-check-square')
+ ->assertSeeIn('li:nth-child(1) span', 'User registered')
+ ->assertVisible('li:nth-child(2) svg.fa-check-square')
+ ->assertSeeIn('li:nth-child(2) span', 'User created')
+ ->assertVisible('li:nth-child(3) svg.fa-check-square')
+ ->assertSeeIn('li:nth-child(3) span', 'User mailbox created')
+ ->assertVisible('li:nth-child(4) svg.fa-check-square')
+ ->assertSeeIn('li:nth-child(4) span', 'Custom domain registered')
+ ->assertVisible('li:nth-child(5) svg.fa-check-square')
+ ->assertSeeIn('li:nth-child(5) span', 'Custom domain created')
+ ->assertVisible('li:nth-child(6) svg.fa-check-square')
+ ->assertSeeIn('li:nth-child(6) span', 'Custom domain verified')
+ ->assertVisible('li:nth-child(7) svg.fa-square')
+ ->assertSeeIn('li:nth-child(7) a', 'Custom domain ownership verified');
+ });
+ });
+
+ // Confirm the domain and wait until the whole status box disappears
+ $domain->status |= Domain::STATUS_CONFIRMED;
+ $domain->save();
+
+ // At the moment, this may take about 10 seconds
+ $browser->waitUntilMissing('@status', 15);
+ });
+ }
+
+ /**
+ * Test domain status on domains list and domain info page
+ *
+ * @depends testDashboard
+ */
+ public function testDomainStatus(): void
+ {
+ $domain = Domain::where('namespace', 'kolab.org')->first();
+ $domain->status ^= Domain::STATUS_CONFIRMED;
+ $domain->save();
+
+ $this->browse(function ($browser) use ($domain) {
+ $browser->on(new Dashboard())
+ ->click('@links a.link-domains')
+ ->on(new DomainList())
+ // Assert domain status icon
+ ->assertVisible('@table tbody tr:first-child td:first-child svg.fa-globe.text-danger')
+ ->assertText('@table tbody tr:first-child td:first-child svg title', 'Not Ready')
+ ->click('@table tbody tr:first-child td:first-child a')
+ ->on(new DomainInfo())
+ ->whenAvailable('@status', function ($browser) {
+ $browser->assertSeeIn('.card-title', 'Domain status:')
+ ->assertSeeIn('.card-title span.text-danger', 'Not ready')
+ ->with('ul.status-list', function ($browser) {
+ $browser->assertElementsCount('li', 4)
+ ->assertVisible('li:nth-child(1) svg.fa-check-square')
+ ->assertSeeIn('li:nth-child(1) span', 'Custom domain registered')
+ ->assertVisible('li:nth-child(2) svg.fa-check-square')
+ ->assertSeeIn('li:nth-child(2) span', 'Custom domain created')
+ ->assertVisible('li:nth-child(3) svg.fa-check-square')
+ ->assertSeeIn('li:nth-child(3) span', 'Custom domain verified')
+ ->assertVisible('li:nth-child(4) svg.fa-square')
+ ->assertSeeIn('li:nth-child(4) span', 'Custom domain ownership verified');
+ });
+ });
+
+ // Confirm the domain and wait until the whole status box disappears
+ $domain->status |= Domain::STATUS_CONFIRMED;
+ $domain->save();
+
+ // At the moment, this may take about 10 seconds
+ $browser->waitUntilMissing('@status', 15);
+ });
+ }
+
+ /**
+ * Test user status on users list
+ *
+ * @depends testDashboard
+ */
+ public function testUserStatus(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $john->status ^= User::STATUS_IMAP_READY;
+ $john->save();
+
+ $this->browse(function ($browser) {
+ $browser->visit(new Dashboard())
+ ->click('@links a.link-users')
+ ->on(new UserList())
+ // Assert user status icons
+ ->assertVisible('@table tbody tr:first-child td:first-child svg.fa-user.text-success')
+ ->assertText('@table tbody tr:first-child td:first-child svg title', 'Active')
+ ->assertVisible('@table tbody tr:nth-child(2) td:first-child svg.fa-user.text-danger')
+ ->assertText('@table tbody tr:nth-child(2) td:first-child svg title', 'Not Ready')
+ ->click('@table tbody tr:nth-child(2) td:first-child a')
+ ->on(new UserInfo())
+ ->with('@form', function (Browser $browser) {
+ // Assert stet in the user edit form
+ $browser->assertSeeIn('div.row:nth-child(1) label', 'Status')
+ ->assertSeeIn('div.row:nth-child(1) #status', 'Not Ready');
+ });
+
+ // TODO: The status should also be live-updated here
+ // Maybe when we have proper websocket communication
+ });
+ }
+}
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -145,24 +145,26 @@
->assertSeeIn('#user-info .card-title', 'User account')
->with('@form', function (Browser $browser) {
// Assert form content
- $browser->assertFocused('div.row:nth-child(1) input')
- ->assertSeeIn('div.row:nth-child(1) label', 'First name')
- ->assertValue('div.row:nth-child(1) input[type=text]', $this->profile['first_name'])
- ->assertSeeIn('div.row:nth-child(2) label', 'Last name')
- ->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['last_name'])
- ->assertSeeIn('div.row:nth-child(3) label', 'Email')
- ->assertValue('div.row:nth-child(3) input[type=text]', 'john@kolab.org')
- ->assertDisabled('div.row:nth-child(3) input[type=text]')
- ->assertSeeIn('div.row:nth-child(4) label', 'Email aliases')
- ->assertVisible('div.row:nth-child(4) .listinput-widget')
+ $browser->assertSeeIn('div.row:nth-child(1) label', 'Status')
+ ->assertSeeIn('div.row:nth-child(1) #status', 'Active')
+ ->assertFocused('div.row:nth-child(2) input')
+ ->assertSeeIn('div.row:nth-child(2) label', 'First name')
+ ->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['first_name'])
+ ->assertSeeIn('div.row:nth-child(3) label', 'Last name')
+ ->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['last_name'])
+ ->assertSeeIn('div.row:nth-child(4) label', 'Email')
+ ->assertValue('div.row:nth-child(4) input[type=text]', 'john@kolab.org')
+ ->assertDisabled('div.row:nth-child(4) input[type=text]')
+ ->assertSeeIn('div.row:nth-child(5) label', 'Email aliases')
+ ->assertVisible('div.row:nth-child(5) .listinput-widget')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertListInputValue(['john.doe@kolab.org'])
->assertValue('@input', '');
})
- ->assertSeeIn('div.row:nth-child(5) label', 'Password')
- ->assertValue('div.row:nth-child(5) input[type=password]', '')
- ->assertSeeIn('div.row:nth-child(6) label', 'Confirm password')
+ ->assertSeeIn('div.row:nth-child(6) label', 'Password')
->assertValue('div.row:nth-child(6) input[type=password]', '')
+ ->assertSeeIn('div.row:nth-child(7) label', 'Confirm password')
+ ->assertValue('div.row:nth-child(7) input[type=password]', '')
->assertSeeIn('button[type=submit]', 'Submit');
// Clear some fields and submit
@@ -236,8 +238,8 @@
// Test subscriptions
$browser->with('@form', function (Browser $browser) {
- $browser->assertSeeIn('div.row:nth-child(7) label', 'Subscriptions')
- ->assertVisible('@skus.row:nth-child(7)')
+ $browser->assertSeeIn('div.row:nth-child(8) label', 'Subscriptions')
+ ->assertVisible('@skus.row:nth-child(8)')
->with('@skus', function ($browser) {
$browser->assertElementsCount('tbody tr', 4)
// groupware SKU
diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php
--- a/src/tests/Feature/Controller/DomainsTest.php
+++ b/src/tests/Feature/Controller/DomainsTest.php
@@ -70,6 +70,7 @@
$this->assertEquals('success', $json['status']);
$this->assertEquals('Domain verified successfully.', $json['message']);
+ $this->assertTrue(is_array($json['statusInfo']));
// Not authorized access
$response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}/confirm");
@@ -103,9 +104,15 @@
$response->assertStatus(200);
$json = $response->json();
-
$this->assertCount(1, $json);
$this->assertSame('kolab.org', $json[0]['namespace']);
+ // Values below are tested by Unit tests
+ $this->assertArrayHasKey('isConfirmed', $json[0]);
+ $this->assertArrayHasKey('isDeleted', $json[0]);
+ $this->assertArrayHasKey('isVerified', $json[0]);
+ $this->assertArrayHasKey('isSuspended', $json[0]);
+ $this->assertArrayHasKey('isActive', $json[0]);
+ $this->assertArrayHasKey('isLdapReady', $json[0]);
$response = $this->actingAs($ned)->get("api/v4/domains");
$response->assertStatus(200);
@@ -144,7 +151,6 @@
$this->assertEquals($domain->namespace, $json['namespace']);
$this->assertEquals($domain->status, $json['status']);
$this->assertEquals($domain->type, $json['type']);
- $this->assertTrue($json['confirmed'] === false);
$this->assertSame($domain->hash(Domain::HASH_TEXT), $json['hash_text']);
$this->assertSame($domain->hash(Domain::HASH_CNAME), $json['hash_cname']);
$this->assertSame($domain->hash(Domain::HASH_CODE), $json['hash_code']);
@@ -153,6 +159,14 @@
$this->assertCount(8, $json['dns']);
$this->assertTrue(strpos(implode("\n", $json['dns']), $domain->namespace) !== false);
$this->assertTrue(strpos(implode("\n", $json['dns']), $domain->hash()) !== false);
+ $this->assertTrue(is_array($json['statusInfo']));
+ // Values below are tested by Unit tests
+ $this->assertArrayHasKey('isConfirmed', $json);
+ $this->assertArrayHasKey('isDeleted', $json);
+ $this->assertArrayHasKey('isVerified', $json);
+ $this->assertArrayHasKey('isSuspended', $json);
+ $this->assertArrayHasKey('isActive', $json);
+ $this->assertArrayHasKey('isLdapReady', $json);
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -61,7 +61,7 @@
$this->assertEquals($user->id, $json['id']);
$this->assertEquals($user->email, $json['email']);
- $this->assertEquals(User::STATUS_NEW, $json['status']);
+ $this->assertEquals(User::STATUS_NEW | User::STATUS_ACTIVE, $json['status']);
$this->assertTrue(is_array($json['statusInfo']));
$this->assertTrue(is_array($json['settings']));
$this->assertTrue(is_array($json['aliases']));
@@ -202,6 +202,12 @@
$this->assertSame($jack->email, $json[0]['email']);
$this->assertSame($john->email, $json[1]['email']);
$this->assertSame($ned->email, $json[2]['email']);
+ // Values below are tested by Unit tests
+ $this->assertArrayHasKey('isDeleted', $json[0]);
+ $this->assertArrayHasKey('isSuspended', $json[0]);
+ $this->assertArrayHasKey('isActive', $json[0]);
+ $this->assertArrayHasKey('isLdapReady', $json[0]);
+ $this->assertArrayHasKey('isImapReady', $json[0]);
$response = $this->actingAs($ned)->get("/api/v4/users");
$response->assertStatus(200);
@@ -300,7 +306,7 @@
$result = UsersController::statusInfo($user);
- $this->assertSame('new', $result['status']);
+ $this->assertFalse($result['isReady']);
$this->assertCount(3, $result['process']);
$this->assertSame('user-new', $result['process'][0]['label']);
$this->assertSame(true, $result['process'][0]['state']);
@@ -314,7 +320,7 @@
$result = UsersController::statusInfo($user);
- $this->assertSame('new', $result['status']);
+ $this->assertTrue($result['isReady']);
$this->assertCount(3, $result['process']);
$this->assertSame('user-new', $result['process'][0]['label']);
$this->assertSame(true, $result['process'][0]['state']);
@@ -323,15 +329,13 @@
$this->assertSame('user-imap-ready', $result['process'][2]['label']);
$this->assertSame(true, $result['process'][2]['state']);
- $user->status |= User::STATUS_ACTIVE;
- $user->save();
$domain->status |= Domain::STATUS_VERIFIED;
$domain->type = Domain::TYPE_EXTERNAL;
$domain->save();
$result = UsersController::statusInfo($user);
- $this->assertSame('active', $result['status']);
+ $this->assertFalse($result['isReady']);
$this->assertCount(7, $result['process']);
$this->assertSame('user-new', $result['process'][0]['label']);
$this->assertSame(true, $result['process'][0]['state']);
@@ -347,13 +351,6 @@
$this->assertSame(true, $result['process'][5]['state']);
$this->assertSame('domain-confirmed', $result['process'][6]['label']);
$this->assertSame(false, $result['process'][6]['state']);
-
- $user->status |= User::STATUS_DELETED;
- $user->save();
-
- $result = UsersController::statusInfo($user);
-
- $this->assertSame('deleted', $result['status']);
}
/**
@@ -418,6 +415,12 @@
$this->assertTrue(is_array($json['settings']));
$this->assertTrue(is_array($json['aliases']));
$this->assertSame([], $json['skus']);
+ // Values below are tested by Unit tests
+ $this->assertArrayHasKey('isDeleted', $json);
+ $this->assertArrayHasKey('isSuspended', $json);
+ $this->assertArrayHasKey('isActive', $json);
+ $this->assertArrayHasKey('isLdapReady', $json);
+ $this->assertArrayHasKey('isImapReady', $json);
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php
--- a/src/tests/Feature/DomainTest.php
+++ b/src/tests/Feature/DomainTest.php
@@ -86,18 +86,17 @@
$domain = Domain::create([
'namespace' => 'public-active.com',
'status' => Domain::STATUS_NEW,
- 'type' => Domain::TYPE_PUBLIC,
+ 'type' => Domain::TYPE_EXTERNAL,
]);
- // Public but non-active domain should not be returned
+ // External domains should not be returned
$public_domains = Domain::getPublicDomains();
$this->assertNotContains('public-active.com', $public_domains);
$domain = Domain::where('namespace', 'public-active.com')->first();
- $domain->status = Domain::STATUS_ACTIVE;
+ $domain->type = Domain::TYPE_PUBLIC;
$domain->save();
- // Public and active domain should be returned
$public_domains = Domain::getPublicDomains();
$this->assertContains('public-active.com', $public_domains);
}
diff --git a/src/tests/Unit/Controller/DomainsTest.php b/src/tests/Unit/Controller/DomainsTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Unit/Controller/DomainsTest.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Tests\Unit\Controller;
+
+use App\Domain;
+use Tests\TestCase;
+
+class DomainsTest extends TestCase
+{
+ /**
+ * Test DomainsController::domainStatuses()
+ */
+ public function testDomainStatuses(): void
+ {
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Test DomainsController::statusInfo()
+ */
+ public function testStatusInfo(): void
+ {
+ $this->markTestIncomplete();
+ }
+}
diff --git a/src/tests/Unit/Controller/UsersTest.php b/src/tests/Unit/Controller/UsersTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Unit/Controller/UsersTest.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Tests\Unit\Controller;
+
+use App\User;
+use Tests\TestCase;
+
+class UsersTest extends TestCase
+{
+ /**
+ * Test UsersController::userStatuses()
+ */
+ public function testUserStatuses(): void
+ {
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Test UsersController::statusInfo()
+ */
+ public function testStatusInfo(): void
+ {
+ $this->markTestIncomplete();
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 10:22 AM (22 h, 28 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18827315
Default Alt Text
D1045.1775298149.diff (51 KB)

Event Timeline