Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117824468
D1045.1775298149.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
51 KB
Referenced Files
None
Subscribers
None
D1045.1775298149.diff
View Options
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">✓</span><span v-else>○</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
Details
Attached
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)
Attached To
Mode
D1045: Display user/domain status in UI, add Active status
Attached
Detach File
Event Timeline