Page MenuHomePhorge

D4400.1774878016.diff
No OneTemporary

Authored By
Unknown
Size
93 KB
Referenced Files
None
Subscribers
None

D4400.1774878016.diff

diff --git a/src/app/Console/Commands/Domain/SuspendCommand.php b/src/app/Console/Commands/Domain/SuspendCommand.php
--- a/src/app/Console/Commands/Domain/SuspendCommand.php
+++ b/src/app/Console/Commands/Domain/SuspendCommand.php
@@ -11,7 +11,7 @@
*
* @var string
*/
- protected $signature = 'domain:suspend {domain}';
+ protected $signature = 'domain:suspend {domain} {--comment=}';
/**
* The console command description.
@@ -35,5 +35,7 @@
}
$domain->suspend();
+
+ \App\EventLog::createFor($domain, \App\EventLog::TYPE_SUSPENDED, $this->option('comment'));
}
}
diff --git a/src/app/Console/Commands/Domain/UnsuspendCommand.php b/src/app/Console/Commands/Domain/UnsuspendCommand.php
--- a/src/app/Console/Commands/Domain/UnsuspendCommand.php
+++ b/src/app/Console/Commands/Domain/UnsuspendCommand.php
@@ -11,7 +11,7 @@
*
* @var string
*/
- protected $signature = 'domain:unsuspend {domain}';
+ protected $signature = 'domain:unsuspend {domain} {--comment=}';
/**
* The console command description.
@@ -35,5 +35,7 @@
}
$domain->unsuspend();
+
+ \App\EventLog::createFor($domain, \App\EventLog::TYPE_UNSUSPENDED, $this->option('comment'));
}
}
diff --git a/src/app/Console/Commands/Group/SuspendCommand.php b/src/app/Console/Commands/Group/SuspendCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/SuspendCommand.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class SuspendCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:suspend {group} {--comment=}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Suspend a distribution list (group)';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $group = $this->getGroup($this->argument('group'));
+
+ if (!$group) {
+ $this->error("Group not found.");
+ return 1;
+ }
+
+ $group->suspend();
+
+ \App\EventLog::createFor($group, \App\EventLog::TYPE_SUSPENDED, $this->option('comment'));
+ }
+}
diff --git a/src/app/Console/Commands/Group/UnsuspendCommand.php b/src/app/Console/Commands/Group/UnsuspendCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/UnsuspendCommand.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class UnsuspendCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:unsuspend {group} {--comment=}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Remove a group suspension';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $group = $this->getGroup($this->argument('group'));
+
+ if (!$group) {
+ $this->error("Group not found.");
+ return 1;
+ }
+
+ $group->unsuspend();
+
+ \App\EventLog::createFor($group, \App\EventLog::TYPE_UNSUSPENDED, $this->option('comment'));
+ }
+}
diff --git a/src/app/Console/Commands/User/SuspendCommand.php b/src/app/Console/Commands/User/SuspendCommand.php
--- a/src/app/Console/Commands/User/SuspendCommand.php
+++ b/src/app/Console/Commands/User/SuspendCommand.php
@@ -11,7 +11,7 @@
*
* @var string
*/
- protected $signature = 'user:suspend {user}';
+ protected $signature = 'user:suspend {user} {--comment=}';
/**
* The console command description.
@@ -35,5 +35,7 @@
}
$user->suspend();
+
+ \App\EventLog::createFor($user, \App\EventLog::TYPE_SUSPENDED, $this->option('comment'));
}
}
diff --git a/src/app/Console/Commands/User/UnsuspendCommand.php b/src/app/Console/Commands/User/UnsuspendCommand.php
--- a/src/app/Console/Commands/User/UnsuspendCommand.php
+++ b/src/app/Console/Commands/User/UnsuspendCommand.php
@@ -11,7 +11,7 @@
*
* @var string
*/
- protected $signature = 'user:unsuspend {user}';
+ protected $signature = 'user:unsuspend {user} {--comment=}';
/**
* The console command description.
@@ -35,5 +35,7 @@
}
$user->unsuspend();
+
+ \App\EventLog::createFor($user, \App\EventLog::TYPE_UNSUSPENDED, $this->option('comment'));
}
}
diff --git a/src/app/EventLog.php b/src/app/EventLog.php
new file mode 100644
--- /dev/null
+++ b/src/app/EventLog.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace App;
+
+use App\Traits\UuidStrKeyTrait;
+use Dyrynda\Database\Support\NullableFields;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * The eloquent definition of an EventLog record.
+ *
+ * @property ?string $comment Optional event description
+ * @property ?array $data Optional event data
+ * @property string $id Log record identifier
+ * @property string $object_id Object identifier
+ * @property string $object_type Object type (class)
+ * @property int $type Event type (0-255)
+ * @property ?string $user_email Acting user email
+ */
+class EventLog extends Model
+{
+ use NullableFields;
+ use UuidStrKeyTrait;
+
+ public const TYPE_SUSPENDED = 1;
+ public const TYPE_UNSUSPENDED = 2;
+ public const TYPE_COMMENT = 3;
+
+ /** @var array<int, string> The attributes that are mass assignable */
+ protected $fillable = [
+ 'comment',
+ // extra event info (json)
+ 'data',
+ 'type',
+ // user, domain, etc.
+ 'object_id',
+ 'object_type',
+ // actor, if any
+ 'user_email',
+ ];
+
+ /** @var array<string, string> Casts properties as type */
+ protected $casts = [
+ 'created_at' => 'datetime:Y-m-d H:i:s',
+ 'data' => 'array',
+ 'type' => 'integer',
+ ];
+
+ /** @var array<int, string> The attributes that can be not set */
+ protected $nullable = ['comment', 'data', 'user_email'];
+
+ /** @var string Database table name */
+ protected $table = 'eventlog';
+
+ /** @var bool Indicates if the model should be timestamped. */
+ public $timestamps = false;
+
+
+ /**
+ * Create an eventlog object for a specified object.
+ *
+ * @param object $object Object (User, Domain, etc.)
+ * @param int $type Event type (use one of EventLog::TYPE_* consts)
+ * @param ?string $comment Event description
+ * @param ?array $data Extra information
+ *
+ * @return EventLog
+ */
+ public static function createFor($object, int $type, string $comment = null, array $data = null): EventLog
+ {
+ $event = self::create([
+ 'object_id' => $object->id,
+ 'object_type' => get_class($object),
+ 'type' => $type,
+ 'comment' => $comment,
+ 'data' => $data,
+ ]);
+
+ return $event;
+ }
+
+ /**
+ * Principally an object such as Domain, User, Group.
+ * Note that it may be trashed (soft-deleted).
+ *
+ * @return mixed
+ */
+ public function object()
+ {
+ return $this->morphTo()->withTrashed(); // @phpstan-ignore-line
+ }
+
+ /**
+ * Get an event type name.
+ *
+ * @return ?string Event type name
+ */
+ public function eventName(): ?string
+ {
+ switch ($this->type) {
+ case self::TYPE_SUSPENDED:
+ return \trans('app.event-suspended');
+ case self::TYPE_UNSUSPENDED:
+ return \trans('app.event-unsuspended');
+ case self::TYPE_COMMENT:
+ return \trans('app.event-comment');
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Event type mutator
+ *
+ * @throws \Exception
+ */
+ public function setTypeAttribute($type)
+ {
+ $type = (int) $type;
+
+ if ($type < 0 || $type > 255) {
+ throw new \Exception("Expecting an event type between 0 and 255");
+ }
+
+ $this->attributes['type'] = $type;
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
@@ -3,8 +3,10 @@
namespace App\Http\Controllers\API\V4\Admin;
use App\Domain;
+use App\EventLog;
use App\User;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Validator;
class DomainsController extends \App\Http\Controllers\API\V4\DomainsController
{
@@ -94,8 +96,16 @@
return $this->errorResponse(404);
}
+ $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
$domain->suspend();
+ EventLog::createFor($domain, EventLog::TYPE_SUSPENDED, $request->comment);
+
return response()->json([
'status' => 'success',
'message' => self::trans('app.domain-suspend-success'),
@@ -118,8 +128,16 @@
return $this->errorResponse(404);
}
+ $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
$domain->unsuspend();
+ EventLog::createFor($domain, EventLog::TYPE_UNSUSPENDED, $request->comment);
+
return response()->json([
'status' => 'success',
'message' => self::trans('app.domain-unsuspend-success'),
diff --git a/src/app/Http/Controllers/API/V4/Admin/EventLogController.php b/src/app/Http/Controllers/API/V4/Admin/EventLogController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Admin/EventLogController.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace App\Http\Controllers\API\V4\Admin;
+
+use App\EventLog;
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+
+class EventLogController extends Controller
+{
+ /**
+ * Listing of eventlog entries.
+ *
+ * @param \Illuminate\Http\Request $request HTTP Request
+ * @param string $object_type Object type
+ * @param string $object_id Object id
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index(Request $request, string $object_type, string $object_id)
+ {
+ $object_type = "App\\" . ucfirst($object_type);
+
+ if (!class_exists($object_type)) {
+ return $this->errorResponse(404);
+ }
+
+ $object = (new $object_type)->find($object_id);
+
+ if (!$this->checkTenant($object)) {
+ return $this->errorResponse(404);
+ }
+
+ $page = intval($request->input('page')) ?: 1;
+ $pageSize = 20;
+ $hasMore = false;
+
+ $result = EventLog::where('object_id', $object_id)
+ ->where('object_type', $object_type)
+ ->orderBy('created_at', 'desc')
+ ->limit($pageSize + 1)
+ ->offset($pageSize * ($page - 1))
+ ->get();
+
+ if (count($result) > $pageSize) {
+ $result->pop();
+ $hasMore = true;
+ }
+
+ $result = $result->map(function ($event) {
+ return [
+ 'id' => $event->id,
+ 'comment' => $event->comment,
+ 'createdAt' => $event->created_at->toDateTimeString(),
+ 'event' => $event->eventName(),
+ 'data' => $event->data,
+ 'user' => $event->user_email,
+ ];
+ });
+
+ return response()->json([
+ 'list' => $result,
+ 'count' => count($result),
+ 'hasMore' => $hasMore,
+ ]);
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php
@@ -2,9 +2,11 @@
namespace App\Http\Controllers\API\V4\Admin;
+use App\EventLog;
use App\Group;
use App\User;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Validator;
class GroupsController extends \App\Http\Controllers\API\V4\GroupsController
{
@@ -73,8 +75,16 @@
return $this->errorResponse(404);
}
+ $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
$group->suspend();
+ EventLog::createFor($group, EventLog::TYPE_SUSPENDED, $request->comment);
+
return response()->json([
'status' => 'success',
'message' => self::trans('app.distlist-suspend-success'),
@@ -97,8 +107,16 @@
return $this->errorResponse(404);
}
+ $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
$group->unsuspend();
+ EventLog::createFor($group, EventLog::TYPE_UNSUSPENDED, $request->comment);
+
return response()->json([
'status' => 'success',
'message' => self::trans('app.distlist-unsuspend-success'),
diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
--- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\API\V4\Admin;
use App\Domain;
+use App\EventLog;
use App\Sku;
use App\User;
use App\Wallet;
@@ -321,8 +322,16 @@
return $this->errorResponse(403);
}
+ $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
$user->suspend();
+ EventLog::createFor($user, EventLog::TYPE_SUSPENDED, $request->comment);
+
return response()->json([
'status' => 'success',
'message' => self::trans('app.user-suspend-success'),
@@ -349,8 +358,16 @@
return $this->errorResponse(403);
}
+ $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
$user->unsuspend();
+ EventLog::createFor($user, EventLog::TYPE_UNSUSPENDED, $request->comment);
+
return response()->json([
'status' => 'success',
'message' => self::trans('app.user-unsuspend-success'),
diff --git a/src/app/Http/Controllers/API/V4/Reseller/EventLogController.php b/src/app/Http/Controllers/API/V4/Reseller/EventLogController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Reseller/EventLogController.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Http\Controllers\API\V4\Reseller;
+
+class EventLogController extends \App\Http\Controllers\API\V4\Admin\EventLogController
+{
+}
diff --git a/src/app/Jobs/User/CreateJob.php b/src/app/Jobs/User/CreateJob.php
--- a/src/app/Jobs/User/CreateJob.php
+++ b/src/app/Jobs/User/CreateJob.php
@@ -76,6 +76,8 @@
$code = \Artisan::call("user:abuse-check {$this->userId}");
if ($code == 2) {
\Log::info("Suspending user due to suspected abuse: {$this->userId} {$user->email}");
+ \App\EventLog::createFor($user, \App\EventLog::TYPE_SUSPENDED, "Suspected spammer");
+
$user->status |= \App\User::STATUS_SUSPENDED;
}
}
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
@@ -45,6 +45,9 @@
public function deleted(Domain $domain)
{
if ($domain->isForceDeleting()) {
+ // Remove EventLog records
+ \App\EventLog::where('object_id', $domain->id)->where('object_type', Domain::class)->delete();
+
return;
}
diff --git a/src/app/Observers/EventLogObserver.php b/src/app/Observers/EventLogObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/EventLogObserver.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Observers;
+
+use App\EventLog;
+
+class EventLogObserver
+{
+ /**
+ * Ensure the event entry ID is a custom ID (uuid).
+ *
+ * @param \App\EventLog $eventlog The EventLog object
+ */
+ public function creating(EventLog $eventlog): void
+ {
+ if (!isset($eventlog->user_email)) {
+ $eventlog->user_email = \App\Utils::userEmailOrNull();
+ }
+
+ if (!isset($eventlog->type)) {
+ throw new \Exception("Unset event type");
+ }
+ }
+}
diff --git a/src/app/Observers/GroupObserver.php b/src/app/Observers/GroupObserver.php
--- a/src/app/Observers/GroupObserver.php
+++ b/src/app/Observers/GroupObserver.php
@@ -45,6 +45,9 @@
public function deleted(Group $group)
{
if ($group->isForceDeleting()) {
+ // Remove EventLog records
+ \App\EventLog::where('object_id', $group->id)->where('object_type', Group::class)->delete();
+
return;
}
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
@@ -224,7 +224,7 @@
}
/**
- * Remove entitleables/transactions related to the user (in user's wallets)
+ * Remove entities related to the user (in user's wallets), entitlements, transactions, etc.
*
* @param \App\User $user The user
* @param bool $force Force-delete mode
@@ -261,6 +261,9 @@
\App\Transaction::where('object_type', Wallet::class)
->whereIn('object_id', $wallets)
->delete();
+
+ // Remove EventLog records
+ \App\EventLog::where('object_id', $user->id)->where('object_type', User::class)->delete();
}
// regardless of force delete, we're always purging whitelists... just in case
diff --git a/src/app/PackageSku.php b/src/app/PackageSku.php
--- a/src/app/PackageSku.php
+++ b/src/app/PackageSku.php
@@ -37,9 +37,6 @@
'cost',
];
- /** @var string Database table name */
- protected $table = 'package_skus';
-
/** @var bool Indicates if the model should be timestamped. */
public $timestamps = false;
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -69,6 +69,7 @@
{
\App\Domain::observe(\App\Observers\DomainObserver::class);
\App\Entitlement::observe(\App\Observers\EntitlementObserver::class);
+ \App\EventLog::observe(\App\Observers\EventLogObserver::class);
\App\Group::observe(\App\Observers\GroupObserver::class);
\App\GroupSetting::observe(\App\Observers\GroupSettingObserver::class);
\App\Meet\Room::observe(\App\Observers\Meet\RoomObserver::class);
diff --git a/src/database/migrations/2023_06_06_100000_create_eventlog_table.php b/src/database/migrations/2023_06_06_100000_create_eventlog_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2023_06_06_100000_create_eventlog_table.php
@@ -0,0 +1,43 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create(
+ 'eventlog',
+ function (Blueprint $table) {
+ $table->string('id', 36)->primary();
+ $table->string('object_id', 36);
+ $table->string('object_type', 36);
+ $table->tinyInteger('type')->unsigned();
+ $table->string('user_email')->nullable();
+ $table->string('comment', 1024)->nullable();
+ $table->text('data')->nullable(); // json
+ $table->timestamp('created_at')->useCurrent();
+
+ $table->index(['object_id', 'object_type', 'type']);
+ $table->index('created_at');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('eventlog');
+ }
+};
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -23,6 +23,10 @@
'companion-create-success' => 'Companion app has been created.',
'companion-delete-success' => 'Companion app has been removed.',
+ 'event-suspended' => 'Suspended',
+ 'event-unsuspended' => 'Unsuspended',
+ 'event-comment' => 'Commented',
+
'mandate-delete-success' => 'The auto-payment has been removed.',
'mandate-update-success' => 'The auto-payment has been updated.',
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -42,6 +42,12 @@
'verify' => "Verify",
],
+ 'collection' => [
+ 'create' => "Create collection",
+ 'new' => "New Collection",
+ 'name' => "Name",
+ ],
+
'companion' => [
'title' => "Companion Apps",
'companion' => "Companion App",
@@ -167,12 +173,6 @@
. "to the file via a unique link.",
],
- 'collection' => [
- 'create' => "Create collection",
- 'new' => "New Collection",
- 'name' => "Name",
- ],
-
'form' => [
'acl' => "Access rights",
'acl-full' => "All",
@@ -182,6 +182,7 @@
'anyone' => "Anyone",
'code' => "Confirmation Code",
'config' => "Configuration",
+ 'comment' => "Comment",
'companion' => "Companion App",
'date' => "Date",
'description' => "Description",
@@ -195,8 +196,10 @@
'general' => "General",
'geolocation' => "Your current location: {location}",
'lastname' => "Last Name",
+ 'less' => "Less",
'name' => "Name",
'months' => "months",
+ 'more' => "More",
'none' => "none",
'norestrictions' => "No restrictions",
'or' => "or",
@@ -242,6 +245,12 @@
'it' => "Italian",
],
+ 'log' => [
+ 'event' => "Event",
+ 'list-none' => "There's no events in the log",
+ 'history' => "History",
+ ],
+
'login' => [
'2fa' => "Second factor code",
'2fa_desc' => "Second factor code is optional for users with no 2-Factor Authentication setup.",
diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss
--- a/src/resources/themes/app.scss
+++ b/src/resources/themes/app.scss
@@ -205,6 +205,31 @@
width: 50px;
}
}
+
+ &.eventlog {
+ .details,
+ .btn-less {
+ display: none;
+ }
+
+ tr.open {
+ .btn-more {
+ display: none;
+ }
+
+ .details {
+ display: block;
+ }
+
+ .btn-less {
+ display: initial;
+ }
+ }
+
+ td.description {
+ width: 98%;
+ }
+ }
}
.table > :not(:first-child) {
diff --git a/src/resources/vue/Admin/Distlist.vue b/src/resources/vue/Admin/Distlist.vue
--- a/src/resources/vue/Admin/Distlist.vue
+++ b/src/resources/vue/Admin/Distlist.vue
@@ -1,6 +1,6 @@
<template>
- <div v-if="list.id" class="container">
- <div class="card" id="distlist-info">
+ <div class="container">
+ <div v-if="list.id" class="card" id="distlist-info">
<div class="card-body">
<div class="card-title">{{ list.email }}</div>
<div class="card-text">
@@ -37,18 +37,15 @@
</div>
</form>
<div class="mt-2 buttons">
- <button v-if="!list.isSuspended" id="button-suspend" class="btn btn-warning" type="button" @click="suspendList">
- {{ $t('btn.suspend') }}
- </button>
- <button v-if="list.isSuspended" id="button-unsuspend" class="btn btn-warning" type="button" @click="unsuspendList">
- {{ $t('btn.unsuspend') }}
- </button>
+ <btn :id="`button-${suspendAction}`" class="btn-outline-primary" @click="setSuspendState">
+ {{ $t(`btn.${suspendAction}`) }}
+ </btn>
</div>
</div>
</div>
</div>
- <tabs class="mt-3" :tabs="['form.settings']"></tabs>
- <div class="tab-content">
+ <tabs class="mt-3" :tabs="['form.settings', 'log.history']" ref="tabs"></tabs>
+ <div v-if="list.id" class="tab-content">
<div class="tab-pane show active" id="settings" role="tabpanel" aria-labelledby="tab-settings">
<div class="card-body">
<div class="card-text">
@@ -65,15 +62,38 @@
</div>
</div>
</div>
+ <div class="tab-pane" id="history" role="tabpanel" aria-labelledby="tab-history">
+ <div class="card-body">
+ <event-log v-if="loadEventLog" :object-id="list.id" object-type="group" ref="eventLog" class="card-text mb-0"></event-log>
+ </div>
+ </div>
</div>
+
+ <modal-dialog id="suspend-dialog" ref="suspendDialog" :title="$t(`btn.${suspendAction}`)" @click="submitSuspend()" :buttons="['submit']">
+ <textarea v-model="comment" name="comment" class="form-control" :placeholder="$t('form.comment')" rows="3"></textarea>
+ </modal-dialog>
</div>
</template>
<script>
+ import EventLog from '../Widgets/EventLog'
+ import ModalDialog from '../Widgets/ModalDialog'
+
export default {
+ components: {
+ EventLog,
+ ModalDialog
+ },
data() {
return {
- list: { members: [], config: {} }
+ comment: '',
+ list: { members: [], config: {} },
+ loadEventLog: false
+ }
+ },
+ computed: {
+ suspendAction() {
+ return this.list.isSuspended ? 'unsuspend' : 'suspend'
}
},
created() {
@@ -83,22 +103,29 @@
})
.catch(this.$root.errorHandler)
},
+ mounted() {
+ this.$refs.tabs.clickHandler('history', () => { this.loadEventLog = true })
+ },
methods: {
- suspendList() {
- axios.post('/api/v4/groups/' + this.list.id + '/suspend')
- .then(response => {
- if (response.data.status == 'success') {
- this.$toast.success(response.data.message)
- this.list = Object.assign({}, this.list, { isSuspended: true })
- }
- })
+ setSuspendState() {
+ this.$root.clearFormValidation($('#suspend-dialog'))
+ this.$refs.suspendDialog.show()
},
- unsuspendList() {
- axios.post('/api/v4/groups/' + this.list.id + '/unsuspend')
+ submitSuspend() {
+ const post = { comment: this.comment }
+
+ axios.post(`/api/v4/groups/${this.list.id}/${this.suspendAction}`, post)
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
- this.list = Object.assign({}, this.list, { isSuspended: false })
+ this.list = Object.assign({}, this.list, { isSuspended: !this.list.isSuspended })
+
+ this.$refs.suspendDialog.hide()
+ this.comment = ''
+
+ if (this.loadEventLog) {
+ this.$refs.eventLog.loadLog({ reset: true })
+ }
}
})
}
diff --git a/src/resources/vue/Admin/Domain.vue b/src/resources/vue/Admin/Domain.vue
--- a/src/resources/vue/Admin/Domain.vue
+++ b/src/resources/vue/Admin/Domain.vue
@@ -1,5 +1,5 @@
<template>
- <div v-if="domain" class="container">
+ <div class="container">
<div class="card" id="domain-info">
<div class="card-body">
<div class="card-title">{{ domain.namespace }}</div>
@@ -25,19 +25,16 @@
</div>
</form>
<div class="mt-2 buttons">
- <btn v-if="!domain.isSuspended" id="button-suspend" class="btn-warning" @click="suspendDomain">
- {{ $t('btn.suspend') }}
- </btn>
- <btn v-if="domain.isSuspended" id="button-unsuspend" class="btn-warning" @click="unsuspendDomain">
- {{ $t('btn.unsuspend') }}
+ <btn :id="`button-${suspendAction}`" class="btn-outline-primary" @click="setSuspendState">
+ {{ $t(`btn.${suspendAction}`) }}
</btn>
</div>
</div>
</div>
</div>
- <tabs class="mt-3" :tabs="['form.config', 'form.settings']"></tabs>
+ <tabs class="mt-3" :tabs="['form.config', 'form.settings', 'log.history']" ref="tabs"></tabs>
<div class="tab-content">
- <div class="tab-pane show active" id="config" role="tabpanel" aria-labelledby="tab-config">
+ <div v-if="domain.id" class="tab-pane show active" id="config" role="tabpanel" aria-labelledby="tab-config">
<div class="card-body">
<div class="card-text">
<p>{{ $t('domain.dns-verify') }}</p>
@@ -47,7 +44,7 @@
</div>
</div>
</div>
- <div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
+ <div v-if="domain.id" class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
<div class="card-body">
<div class="card-text">
<form class="read-only short">
@@ -63,15 +60,38 @@
</div>
</div>
</div>
+ <div class="tab-pane" id="history" role="tabpanel" aria-labelledby="tab-history">
+ <div class="card-body">
+ <event-log v-if="loadEventLog" :object-id="domain.id" object-type="domain" ref="eventLog" class="card-text mb-0"></event-log>
+ </div>
+ </div>
</div>
+
+ <modal-dialog id="suspend-dialog" ref="suspendDialog" :title="$t(`btn.${suspendAction}`)" @click="submitSuspend()" :buttons="['submit']">
+ <textarea v-model="comment" name="comment" class="form-control" :placeholder="$t('form.comment')" rows="3"></textarea>
+ </modal-dialog>
</div>
</template>
<script>
+ import EventLog from '../Widgets/EventLog'
+ import ModalDialog from '../Widgets/ModalDialog'
+
export default {
+ components: {
+ EventLog,
+ ModalDialog
+ },
data() {
return {
- domain: null
+ comment: '',
+ domain: {},
+ loadEventLog: false
+ }
+ },
+ computed: {
+ suspendAction() {
+ return this.domain.isSuspended ? 'unsuspend' : 'suspend'
}
},
created() {
@@ -83,22 +103,29 @@
})
.catch(this.$root.errorHandler)
},
+ mounted() {
+ this.$refs.tabs.clickHandler('history', () => { this.loadEventLog = true })
+ },
methods: {
- suspendDomain() {
- axios.post('/api/v4/domains/' + this.domain.id + '/suspend')
- .then(response => {
- if (response.data.status == 'success') {
- this.$toast.success(response.data.message)
- this.domain = Object.assign({}, this.domain, { isSuspended: true })
- }
- })
+ setSuspendState() {
+ this.$root.clearFormValidation($('#suspend-dialog'))
+ this.$refs.suspendDialog.show()
},
- unsuspendDomain() {
- axios.post('/api/v4/domains/' + this.domain.id + '/unsuspend')
+ submitSuspend() {
+ const post = { comment: this.comment }
+
+ axios.post(`/api/v4/domains/${this.domain.id}/${this.suspendAction}`, post)
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
- this.domain = Object.assign({}, this.domain, { isSuspended: false })
+ this.domain = Object.assign({}, this.domain, { isSuspended: !this.domain.isSuspended })
+
+ this.$refs.suspendDialog.hide()
+ this.comment = ''
+
+ if (this.loadEventLog) {
+ this.$refs.eventLog.loadLog({ reset: true })
+ }
}
})
}
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -77,8 +77,8 @@
</div>
</form>
<div class="mt-2 buttons">
- <btn :id="'button-' + (user.isSuspended ? 'unsuspend' : 'suspend')" class="btn-outline-primary" @click="setSuspendState">
- {{ $t(user.isSuspended ? 'btn.unsuspend' : 'btn.suspend') }}
+ <btn :id="`button-${suspendAction}`" class="btn-outline-primary" @click="setSuspendState">
+ {{ $t(`btn.${suspendAction}`) }}
</btn>
<btn id="button-resync" class="btn-outline-primary" @click="resyncUser">
{{ $t('btn.resync') }}
@@ -228,21 +228,26 @@
</div>
</div>
</div>
+ <div class="tab-pane" id="history" role="tabpanel" aria-labelledby="tab-history">
+ <div class="card-body">
+ <event-log v-if="loadEventLog" :object-id="user.id" object-type="user" ref="eventLog" class="card-text mb-0"></event-log>
+ </div>
+ </div>
</div>
<modal-dialog id="discount-dialog" ref="discountDialog" :title="$t('user.discount-title')" @click="submitDiscount()" :buttons="['submit']">
- <div>
- <select v-model="wallet.discount_id" class="form-select">
- <option value="">- {{ $t('form.none') }} -</option>
- <option v-for="item in discounts" :value="item.id" :key="item.id">{{ item.label }}</option>
- </select>
- </div>
+ <select v-model="wallet.discount_id" class="form-select">
+ <option value="">- {{ $t('form.none') }} -</option>
+ <option v-for="item in discounts" :value="item.id" :key="item.id">{{ item.label }}</option>
+ </select>
</modal-dialog>
<modal-dialog id="email-dialog" ref="emailDialog" :title="$t('user.ext-email')" @click="submitEmail()" :buttons="['submit']">
- <div>
- <input v-model="external_email" name="external_email" class="form-control">
- </div>
+ <input v-model="external_email" name="external_email" class="form-control">
+ </modal-dialog>
+
+ <modal-dialog id="suspend-dialog" ref="suspendDialog" :title="$t(`btn.${suspendAction}`)" @click="submitSuspend()" :buttons="['submit']">
+ <textarea v-model="comment" name="comment" class="form-control" :placeholder="$t('form.comment')" rows="3"></textarea>
</modal-dialog>
<modal-dialog id="oneoff-dialog" ref="oneoffDialog" @click="submitOneOff()" :buttons="['submit']"
@@ -273,6 +278,7 @@
</template>
<script>
+ import EventLog from '../Widgets/EventLog'
import ModalDialog from '../Widgets/ModalDialog'
import TransactionLog from '../Widgets/TransactionLog'
import { ListTable } from '../Widgets/ListTools'
@@ -295,6 +301,7 @@
components: {
DistlistList,
DomainList,
+ EventLog,
ListTable,
ModalDialog,
ResourceList,
@@ -320,9 +327,7 @@
],
footLabel: 'user.aliases-none'
},
- oneoff_amount: '',
- oneoff_description: '',
- oneoff_negative: false,
+ comment: '',
discount: 0,
discount_description: '',
discounts: [],
@@ -330,6 +335,10 @@
folders: [],
has2FA: false,
hasBeta: false,
+ loadEventLog: false,
+ oneoff_amount: '',
+ oneoff_description: '',
+ oneoff_negative: false,
wallet: {},
walletReload: false,
distlists: [],
@@ -361,7 +370,8 @@
{ label: 'user.distlists', count: 0 },
{ label: 'user.resources', count: 0 },
{ label: 'dashboard.shared-folders', count: 0 },
- { label: 'form.settings' }
+ { label: 'form.settings' },
+ { label: 'log.history' }
],
users: [],
user: {
@@ -372,6 +382,11 @@
}
}
},
+ computed: {
+ suspendAction() {
+ return this.user.isSuspended ? 'unsuspend' : 'suspend'
+ }
+ },
created() {
const user_id = this.$route.params.user
@@ -476,6 +491,7 @@
.catch(this.$root.errorHandler)
},
mounted() {
+ this.$refs.tabs.clickHandler('history', () => { this.loadEventLog = true })
this.$refs.discountDialog.events({
shown: () => {
// Note: Vue v-model is strict, convert null to a string
@@ -633,11 +649,23 @@
})
},
setSuspendState() {
- axios.post('/api/v4/users/' + this.user.id + '/' + (this.user.isSuspended ? 'unsuspend' : 'suspend'))
+ this.$root.clearFormValidation($('#suspend-dialog'))
+ this.$refs.suspendDialog.show()
+ },
+ submitSuspend() {
+ const post = { comment: this.comment }
+
+ axios.post(`/api/v4/users/${this.user.id}/${this.suspendAction}`, post)
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.user = Object.assign({}, this.user, { isSuspended: !this.user.isSuspended })
+ this.$refs.suspendDialog.hide()
+ this.comment = ''
+
+ if (this.loadEventLog) {
+ this.$refs.eventLog.loadLog({ reset: true })
+ }
}
})
}
diff --git a/src/resources/vue/Widgets/EventLog.vue b/src/resources/vue/Widgets/EventLog.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/EventLog.vue
@@ -0,0 +1,69 @@
+<template>
+ <div>
+ <table class="table table-sm m-0 eventlog">
+ <thead>
+ <tr>
+ <th scope="col">{{ $t('form.date') }}</th>
+ <th scope="col">{{ $t('log.event') }}</th>
+ <th scope="col">{{ $t('form.comment') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="event in log" :id="'log' + event.id" :key="event.id">
+ <td class="datetime">{{ event.createdAt }}</td>
+ <td>{{ event.event }}</td>
+ <td class="description">
+ <btn v-if="event.data || event.user" class="btn-link btn-action btn-more" icon="angle-right" :title="$t('form.more')" @click="loadDetails"></btn>
+ <btn v-if="event.data || event.user" class="btn-link btn-action btn-less" icon="angle-down" :title="$t('form.less')" @click="hideDetails"></btn>
+ {{ event.comment }}
+ <pre v-if="event.data" class="details text-monospace p-1 m-1 ms-3">{{ JSON.stringify(event.data, null, 2) }}</pre>
+ <div v-if="event.user" class="details email text-nowrap text-secondary ms-3">
+ <svg-icon icon="user" class="me-1"></svg-icon>{{ event.user }}
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ <list-foot :text="$t('log.list-none')" :colspan="3"></list-foot>
+ </table>
+ <list-more v-if="hasMore" :on-click="loadLog"></list-more>
+ </div>
+</template>
+
+<script>
+ import ListTools from './ListTools'
+ import { library } from '@fortawesome/fontawesome-svg-core'
+
+ library.add(
+ require('@fortawesome/free-solid-svg-icons/faAngleDown').definition,
+ require('@fortawesome/free-solid-svg-icons/faAngleRight').definition
+ )
+
+ export default {
+ mixins: [ ListTools ],
+ props: {
+ objectId: { type: [ String, Number ], default: null },
+ objectType: { type: String, default: null },
+ },
+ data() {
+ return {
+ log: []
+ }
+ },
+ mounted() {
+ this.loadLog({ reset: true })
+ },
+ methods: {
+ loadDetails(event) {
+ $(event.target).closest('tr').addClass('open')
+ },
+ hideDetails(event) {
+ $(event.target).closest('tr').removeClass('open')
+ },
+ loadLog(params) {
+ if (this.objectId && this.objectType) {
+ this.listSearch('log', `/api/v4/eventlog/${this.objectType}/${this.objectId}`, params)
+ }
+ }
+ }
+ }
+</script>
diff --git a/src/resources/vue/Widgets/ListTools.vue b/src/resources/vue/Widgets/ListTools.vue
--- a/src/resources/vue/Widgets/ListTools.vue
+++ b/src/resources/vue/Widgets/ListTools.vue
@@ -125,7 +125,6 @@
get.search = this.currentSearch
}
-
if ('parent' in params) {
get.parent = params.parent
this.currentParent = params.parent
diff --git a/src/resources/vue/Widgets/ModalDialog.vue b/src/resources/vue/Widgets/ModalDialog.vue
--- a/src/resources/vue/Widgets/ModalDialog.vue
+++ b/src/resources/vue/Widgets/ModalDialog.vue
@@ -62,7 +62,7 @@
if (this.cancelFocus) {
$(event.target).find('button.modal-cancel').focus()
} else {
- $(event.target).find('input,select').first().focus()
+ $(event.target).find('input,select,textarea').first().focus()
}
})
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -235,6 +235,8 @@
Route::post('domains/{id}/suspend', [API\V4\Admin\DomainsController::class, 'suspend']);
Route::post('domains/{id}/unsuspend', [API\V4\Admin\DomainsController::class, 'unsuspend']);
+ Route::get('eventlog/{type}/{id}', [API\V4\Admin\EventLogController::class, 'index']);
+
Route::apiResource('groups', API\V4\Admin\GroupsController::class);
Route::post('groups/{id}/suspend', [API\V4\Admin\GroupsController::class, 'suspend']);
Route::post('groups/{id}/unsuspend', [API\V4\Admin\GroupsController::class, 'unsuspend']);
@@ -275,6 +277,8 @@
Route::post('domains/{id}/suspend', [API\V4\Reseller\DomainsController::class, 'suspend']);
Route::post('domains/{id}/unsuspend', [API\V4\Reseller\DomainsController::class, 'unsuspend']);
+ Route::get('eventlog/{type}/{id}', [API\V4\Reseller\EventLogController::class, 'index']);
+
Route::apiResource('groups', API\V4\Reseller\GroupsController::class);
Route::post('groups/{id}/suspend', [API\V4\Reseller\GroupsController::class, 'suspend']);
Route::post('groups/{id}/unsuspend', [API\V4\Reseller\GroupsController::class, 'unsuspend']);
diff --git a/src/tests/Browser/Admin/DistlistTest.php b/src/tests/Browser/Admin/DistlistTest.php
--- a/src/tests/Browser/Admin/DistlistTest.php
+++ b/src/tests/Browser/Admin/DistlistTest.php
@@ -2,9 +2,11 @@
namespace Tests\Browser\Admin;
+use App\EventLog;
use App\Group;
use Illuminate\Support\Facades\Queue;
use Tests\Browser;
+use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Admin\Distlist as DistlistPage;
use Tests\Browser\Pages\Admin\User as UserPage;
@@ -23,6 +25,7 @@
self::useAdminUrl();
$this->deleteTestGroup('group-test@kolab.org');
+ Eventlog::query()->delete();
}
/**
@@ -31,6 +34,7 @@
public function tearDown(): void
{
$this->deleteTestGroup('group-test@kolab.org');
+ Eventlog::query()->delete();
parent::tearDown();
}
@@ -65,6 +69,12 @@
$group->save();
$group->setConfig(['sender_policy' => ['test1.com', 'test2.com']]);
+ $event1 = EventLog::createFor($group, EventLog::TYPE_SUSPENDED, 'Event 1');
+ $event2 = EventLog::createFor($group, EventLog::TYPE_UNSUSPENDED, 'Event 2', ['test' => 'test-data']);
+ $event2->refresh();
+ $event1->created_at = (clone $event2->created_at)->subDay();
+ $event1->save();
+
$distlist_page = new DistlistPage($group->id);
$user_page = new UserPage($user->id);
@@ -75,7 +85,7 @@
->visit($user_page)
->on($user_page)
->click('@nav #tab-distlists')
- ->pause(1000)
+ ->waitFor('@user-distlists table tbody')
->click('@user-distlists table tbody tr:first-child td a')
->on($distlist_page)
->assertSeeIn('@distlist-info .card-title', $group->email)
@@ -91,14 +101,42 @@
->assertSeeIn('.row:nth-child(4) #members', $group->members[0])
->assertSeeIn('.row:nth-child(4) #members', $group->members[1]);
})
- ->assertElementsCount('ul.nav-tabs', 1)
- ->assertSeeIn('ul.nav-tabs .nav-link', 'Settings')
+ ->assertElementsCount('ul.nav-tabs li', 2)
+ ->assertSeeIn('ul.nav-tabs #tab-settings', 'Settings')
->with('@distlist-settings form', function (Browser $browser) {
$browser->assertElementsCount('.row', 1)
->assertSeeIn('.row:nth-child(1) label', 'Sender Access List')
->assertSeeIn('.row:nth-child(1) #sender_policy', 'test1.com, test2.com');
});
+ // Assert History tab
+ $browser->assertSeeIn('ul.nav-tabs #tab-history', 'History')
+ ->click('ul.nav-tabs #tab-history')
+ ->whenAvailable('@distlist-history table', function (Browser $browser) use ($event1, $event2) {
+ $browser->waitFor('tbody tr')->assertElementsCount('tbody tr', 2)
+ // row 1
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(1)', $event2->created_at->toDateTimeString())
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(2)', 'Unsuspended')
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(3)', $event2->comment)
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+ ->click('tr:nth-child(1) td:nth-child(3) .btn-more')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) div.email')
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(3) pre', 'test-data')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-more')
+ ->click('tr:nth-child(1) td:nth-child(3) .btn-less')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+ // row 2
+ ->assertSeeIn('tr:nth-child(2) td:nth-child(1)', $event1->created_at->toDateTimeString())
+ ->assertSeeIn('tr:nth-child(2) td:nth-child(2)', 'Suspended')
+ ->assertSeeIn('tr:nth-child(2) td:nth-child(3)', $event1->comment)
+ ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-more')
+ ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-less');
+ });
+
// Test invalid group identifier
$browser->visit('/distlist/abc')->assertErrorPage(404);
});
@@ -112,6 +150,7 @@
public function testSuspendAndUnsuspend(): void
{
Queue::fake();
+ Eventlog::query()->delete();
$this->browse(function (Browser $browser) {
$user = $this->getTestUser('john@kolab.org');
@@ -125,14 +164,36 @@
->assertMissing('@distlist-info #button-unsuspend')
->assertSeeIn('@distlist-info #status.text-success', 'Active')
->click('@distlist-info #button-suspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Suspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->type('textarea', 'test suspend')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'Distribution list suspended successfully.')
->assertSeeIn('@distlist-info #status.text-warning', 'Suspended')
- ->assertMissing('@distlist-info #button-suspend')
- ->click('@distlist-info #button-unsuspend')
+ ->assertMissing('@distlist-info #button-suspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+ $this->assertSame('test suspend', $event->comment);
+ $this->assertEquals($group->id, $event->object_id);
+
+ $browser->click('@distlist-info #button-unsuspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Unsuspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'Distribution list unsuspended successfully.')
->assertSeeIn('@distlist-info #status.text-success', 'Active')
->assertVisible('@distlist-info #button-suspend')
->assertMissing('@distlist-info #button-unsuspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+ $this->assertSame(null, $event->comment);
+ $this->assertEquals($group->id, $event->object_id);
});
}
}
diff --git a/src/tests/Browser/Admin/DomainTest.php b/src/tests/Browser/Admin/DomainTest.php
--- a/src/tests/Browser/Admin/DomainTest.php
+++ b/src/tests/Browser/Admin/DomainTest.php
@@ -3,7 +3,9 @@
namespace Tests\Browser\Admin;
use App\Domain;
+use App\EventLog;
use Tests\Browser;
+use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Admin\Domain as DomainPage;
use Tests\Browser\Pages\Admin\User as UserPage;
@@ -24,6 +26,8 @@
$this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
+ Eventlog::query()->delete();
+
self::useAdminUrl();
}
@@ -38,6 +42,8 @@
$this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
+ Eventlog::query()->delete();
+
parent::tearDown();
}
@@ -66,6 +72,12 @@
$domain->setSetting('spf_whitelist', null);
+ $event1 = EventLog::createFor($domain, EventLog::TYPE_SUSPENDED, 'Event 1');
+ $event2 = EventLog::createFor($domain, EventLog::TYPE_UNSUSPENDED, 'Event 2', ['test' => 'test-data']);
+ $event2->refresh();
+ $event1->created_at = (clone $event2->created_at)->subDay();
+ $event1->save();
+
// Goto the domain page
$browser->visit(new Home())
->submitLogon('jeroen@jeroen.jeroen', \App\Utils::generatePassphrase(), true)
@@ -73,7 +85,7 @@
->visit($user_page)
->on($user_page)
->click('@nav #tab-domains')
- ->pause(1000)
+ ->waitFor('@user-domains table tbody')
->click('@user-domains table tbody tr:first-child td a');
$browser->on($domain_page)
@@ -88,7 +100,7 @@
// Some tabs are loaded in background, wait a second
$browser->pause(500)
- ->assertElementsCount('@nav a', 2);
+ ->assertElementsCount('@nav a', 3);
// Assert Configuration tab
$browser->assertSeeIn('@nav #tab-config', 'Configuration')
@@ -110,11 +122,40 @@
$domain->setSetting('spf_whitelist', json_encode(['.test1.com', '.test2.com']));
$browser->refresh()
+ ->on($domain_page)
->waitFor('@nav #tab-settings')
->click('@nav #tab-settings')
- ->with('@domain-settings form', function (Browser $browser) {
+ ->whenAvailable('@domain-settings form', function (Browser $browser) {
$browser->assertSeeIn('.row:first-child .form-control-plaintext', '.test1.com, .test2.com');
});
+
+ // Assert History tab
+ $browser->assertSeeIn('@nav #tab-history', 'History')
+ ->click('@nav #tab-history')
+ ->whenAvailable('@domain-history table', function (Browser $browser) use ($event1, $event2) {
+ $browser->waitFor('tbody tr')->assertElementsCount('tbody tr', 2)
+ // row 1
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(1)', $event2->created_at->toDateTimeString())
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(2)', 'Unsuspended')
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(3)', $event2->comment)
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+ ->click('tr:nth-child(1) td:nth-child(3) .btn-more')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) div.email')
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(3) pre', 'test-data')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-more')
+ ->click('tr:nth-child(1) td:nth-child(3) .btn-less')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+ // row 2
+ ->assertSeeIn('tr:nth-child(2) td:nth-child(1)', $event1->created_at->toDateTimeString())
+ ->assertSeeIn('tr:nth-child(2) td:nth-child(2)', 'Suspended')
+ ->assertSeeIn('tr:nth-child(2) td:nth-child(3)', $event1->comment)
+ ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-more')
+ ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-less');
+ });
});
}
@@ -125,6 +166,8 @@
*/
public function testSuspendAndUnsuspend(): void
{
+ EventLog::query()->delete();
+
$this->browse(function (Browser $browser) {
$sku_domain = \App\Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
$user = $this->getTestUser('test1@domainscontroller.com');
@@ -146,14 +189,36 @@
->assertVisible('@domain-info #button-suspend')
->assertMissing('@domain-info #button-unsuspend')
->click('@domain-info #button-suspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Suspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->type('textarea', 'test suspend')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'Domain suspended successfully.')
->assertSeeIn('@domain-info #status span.text-warning', 'Suspended')
- ->assertMissing('@domain-info #button-suspend')
- ->click('@domain-info #button-unsuspend')
+ ->assertMissing('@domain-info #button-suspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+ $this->assertSame('test suspend', $event->comment);
+ $this->assertEquals($domain->id, $event->object_id);
+
+ $browser->click('@domain-info #button-unsuspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Unsuspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'Domain unsuspended successfully.')
->assertSeeIn('@domain-info #status span.text-success', 'Active')
->assertVisible('@domain-info #button-suspend')
->assertMissing('@domain-info #button-unsuspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+ $this->assertSame(null, $event->comment);
+ $this->assertEquals($domain->id, $event->object_id);
});
}
}
diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php
--- a/src/tests/Browser/Admin/UserTest.php
+++ b/src/tests/Browser/Admin/UserTest.php
@@ -5,6 +5,7 @@
use App\Auth\SecondFactor;
use App\Discount;
use App\Entitlement;
+use App\EventLog;
use App\Sku;
use App\User;
use Tests\Browser;
@@ -39,6 +40,8 @@
$wallet->save();
Entitlement::where('cost', '>=', 5000)->delete();
+ Eventlog::query()->delete();
+
$this->deleteTestGroup('group-test@kolab.org');
$this->deleteTestUser('userstest1@kolabnow.com');
}
@@ -61,6 +64,8 @@
$wallet->save();
Entitlement::where('cost', '>=', 5000)->delete();
+ Eventlog::query()->delete();
+
$this->deleteTestGroup('group-test@kolab.org');
$this->deleteTestUser('userstest1@kolabnow.com');
@@ -89,6 +94,15 @@
$jack->setSetting('limit_geo', null);
$jack->setSetting('guam_enabled', null);
+ $event1 = EventLog::createFor($jack, EventLog::TYPE_SUSPENDED, 'Event 1');
+ $event2 = EventLog::createFor($jack, EventLog::TYPE_UNSUSPENDED, 'Event 2', ['test' => 'test-data']);
+ $event2->refresh();
+ $event1->created_at = (clone $event2->created_at)->subDay();
+ $event1->user_email = 'jeroen@jeroen.jeroen';
+ $event1->save();
+ $event2->user_email = 'test@test.com';
+ $event2->save();
+
$page = new UserPage($jack->id);
$browser->visit(new Home())
@@ -120,7 +134,7 @@
// Some tabs are loaded in background, wait a second
$browser->pause(500)
- ->assertElementsCount('@nav a', 9);
+ ->assertElementsCount('@nav a', 10);
// Note: Finances tab is tested in UserFinancesTest.php
$browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -202,6 +216,35 @@
->assertSeeIn('.row:nth-child(3) #limit_geo', 'No restrictions')
->assertMissing('#limit_geo + button');
});
+
+ // Assert History tab
+ $browser->assertSeeIn('@nav #tab-history', 'History')
+ ->click('@nav #tab-history')
+ ->whenAvailable('@user-history table', function (Browser $browser) use ($event1, $event2) {
+ $browser->waitFor('tbody tr')->assertElementsCount('tbody tr', 2)
+ // row 1
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(1)', $event2->created_at->toDateTimeString())
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(2)', 'Unsuspended')
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(3)', $event2->comment)
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+ ->click('tr:nth-child(1) td:nth-child(3) .btn-more')
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(3) div.email', $event2->user_email)
+ ->assertSeeIn('tr:nth-child(1) td:nth-child(3) pre', 'test-data')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-more')
+ ->click('tr:nth-child(1) td:nth-child(3) .btn-less')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+ ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+ // row 2
+ ->assertSeeIn('tr:nth-child(2) td:nth-child(1)', $event1->created_at->toDateTimeString())
+ ->assertSeeIn('tr:nth-child(2) td:nth-child(2)', 'Suspended')
+ ->assertSeeIn('tr:nth-child(2) td:nth-child(3)', $event1->comment)
+ ->click('tr:nth-child(2) td:nth-child(3) .btn-more')
+ ->assertSeeIn('tr:nth-child(2) td:nth-child(3) div.email', $event1->user_email)
+ ->assertMissing('tr:nth-child(2) td:nth-child(3) pre');
+ });
});
}
@@ -257,7 +300,7 @@
// Some tabs are loaded in background, wait a second
$browser->pause(500)
- ->assertElementsCount('@nav a', 9);
+ ->assertElementsCount('@nav a', 10);
// Note: Finances tab is tested in UserFinancesTest.php
$browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -348,6 +391,14 @@
->assertSeeIn('table tbody tr:nth-child(2) td:last-child', 'folder-contact@kolab.org')
->assertMissing('table tfoot');
});
+
+ // Assert History tab
+ $browser->assertSeeIn('@nav #tab-history', 'History')
+ ->click('@nav #tab-history')
+ ->whenAvailable('@user-history table', function (Browser $browser) {
+ $browser->assertElementsCount('tbody tr', 0)
+ ->assertSeeIn('tfoot tr td', "There's no events in the log");
+ });
});
// Now we go to Ned's info page, he's a controller on John's wallet
@@ -389,7 +440,7 @@
// Some tabs are loaded in background, wait a second
$browser->pause(500)
- ->assertElementsCount('@nav a', 9);
+ ->assertElementsCount('@nav a', 10);
// Note: Finances tab is tested in UserFinancesTest.php
$browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -543,14 +594,34 @@
->assertVisible('@user-info #button-suspend')
->assertMissing('@user-info #button-unsuspend')
->click('@user-info #button-suspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Suspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->type('textarea', 'test suspend')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.')
->assertSeeIn('@user-info #status span.text-warning', 'Suspended')
- ->assertMissing('@user-info #button-suspend')
- ->click('@user-info #button-unsuspend')
+ ->assertMissing('@user-info #button-suspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+ $this->assertSame('test suspend', $event->comment);
+
+ $browser->click('@user-info #button-unsuspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Unsuspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.')
->assertSeeIn('@user-info #status span.text-success', 'Active')
->assertVisible('@user-info #button-suspend')
->assertMissing('@user-info #button-unsuspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+ $this->assertSame(null, $event->comment);
});
}
diff --git a/src/tests/Browser/Pages/Admin/Distlist.php b/src/tests/Browser/Pages/Admin/Distlist.php
--- a/src/tests/Browser/Pages/Admin/Distlist.php
+++ b/src/tests/Browser/Pages/Admin/Distlist.php
@@ -38,6 +38,7 @@
public function assert($browser): void
{
$browser->waitForLocation($this->url())
+ ->waitUntilMissing('@app .app-loader')
->waitFor('@distlist-info');
}
@@ -52,6 +53,7 @@
'@app' => '#app',
'@distlist-info' => '#distlist-info',
'@distlist-settings' => '#settings',
+ '@distlist-history' => '#history',
];
}
}
diff --git a/src/tests/Browser/Pages/Admin/Domain.php b/src/tests/Browser/Pages/Admin/Domain.php
--- a/src/tests/Browser/Pages/Admin/Domain.php
+++ b/src/tests/Browser/Pages/Admin/Domain.php
@@ -38,6 +38,7 @@
public function assert($browser): void
{
$browser->waitForLocation($this->url())
+ ->waitUntilMissing('@app .app-loader')
->waitFor('@domain-info');
}
@@ -54,6 +55,7 @@
'@nav' => 'ul.nav-tabs',
'@domain-config' => '#config',
'@domain-settings' => '#settings',
+ '@domain-history' => '#history',
];
}
}
diff --git a/src/tests/Browser/Pages/Admin/Resource.php b/src/tests/Browser/Pages/Admin/Resource.php
--- a/src/tests/Browser/Pages/Admin/Resource.php
+++ b/src/tests/Browser/Pages/Admin/Resource.php
@@ -38,6 +38,7 @@
public function assert($browser): void
{
$browser->waitForLocation($this->url())
+ ->waitUntilMissing('@app .app-loader')
->waitFor('@resource-info');
}
diff --git a/src/tests/Browser/Pages/Admin/SharedFolder.php b/src/tests/Browser/Pages/Admin/SharedFolder.php
--- a/src/tests/Browser/Pages/Admin/SharedFolder.php
+++ b/src/tests/Browser/Pages/Admin/SharedFolder.php
@@ -38,6 +38,7 @@
public function assert($browser): void
{
$browser->waitForLocation($this->url())
+ ->waitUntilMissing('@app .app-loader')
->waitFor('@folder-info');
}
diff --git a/src/tests/Browser/Pages/Admin/User.php b/src/tests/Browser/Pages/Admin/User.php
--- a/src/tests/Browser/Pages/Admin/User.php
+++ b/src/tests/Browser/Pages/Admin/User.php
@@ -58,6 +58,7 @@
'@user-subscriptions' => '#subscriptions',
'@user-distlists' => '#distlists',
'@user-domains' => '#domains',
+ '@user-history' => '#history',
'@user-resources' => '#resources',
'@user-folders' => '#folders',
'@user-users' => '#users',
diff --git a/src/tests/Browser/Reseller/DistlistTest.php b/src/tests/Browser/Reseller/DistlistTest.php
--- a/src/tests/Browser/Reseller/DistlistTest.php
+++ b/src/tests/Browser/Reseller/DistlistTest.php
@@ -2,9 +2,11 @@
namespace Tests\Browser\Reseller;
+use App\EventLog;
use App\Group;
use Illuminate\Support\Facades\Queue;
use Tests\Browser;
+use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Admin\Distlist as DistlistPage;
use Tests\Browser\Pages\Admin\User as UserPage;
@@ -23,6 +25,7 @@
self::useResellerUrl();
$this->deleteTestGroup('group-test@kolab.org');
+ Eventlog::query()->delete();
}
/**
@@ -31,6 +34,7 @@
public function tearDown(): void
{
$this->deleteTestGroup('group-test@kolab.org');
+ Eventlog::query()->delete();
parent::tearDown();
}
@@ -75,7 +79,7 @@
->visit($user_page)
->on($user_page)
->click('@nav #tab-distlists')
- ->pause(1000)
+ ->waitFor('@user-distlists table tbody')
->click('@user-distlists table tbody tr:first-child td a')
->on($distlist_page)
->assertSeeIn('@distlist-info .card-title', $group->email)
@@ -91,13 +95,14 @@
->assertSeeIn('.row:nth-child(4) #members', $group->members[0])
->assertSeeIn('.row:nth-child(4) #members', $group->members[1]);
})
- ->assertElementsCount('ul.nav-tabs', 1)
- ->assertSeeIn('ul.nav-tabs .nav-link', 'Settings')
+ ->assertElementsCount('ul.nav-tabs li', 2)
+ ->assertSeeIn('ul.nav-tabs #tab-settings', 'Settings')
->with('@distlist-settings form', function (Browser $browser) {
$browser->assertElementsCount('.row', 1)
->assertSeeIn('.row:nth-child(1) label', 'Sender Access List')
->assertSeeIn('.row:nth-child(1) #sender_policy', 'test1.com, test2.com');
- });
+ })
+ ->assertSeeIn('ul.nav-tabs #tab-history', 'History');
// Test invalid group identifier
$browser->visit('/distlist/abc')->assertErrorPage(404);
@@ -112,6 +117,7 @@
public function testSuspendAndUnsuspend(): void
{
Queue::fake();
+ Eventlog::query()->delete();
$this->browse(function (Browser $browser) {
$user = $this->getTestUser('john@kolab.org');
@@ -125,14 +131,36 @@
->assertMissing('@distlist-info #button-unsuspend')
->assertSeeIn('@distlist-info #status.text-success', 'Active')
->click('@distlist-info #button-suspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Suspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->type('textarea', 'test suspend')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'Distribution list suspended successfully.')
->assertSeeIn('@distlist-info #status.text-warning', 'Suspended')
- ->assertMissing('@distlist-info #button-suspend')
- ->click('@distlist-info #button-unsuspend')
+ ->assertMissing('@distlist-info #button-suspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+ $this->assertSame('test suspend', $event->comment);
+ $this->assertEquals($group->id, $event->object_id);
+
+ $browser->click('@distlist-info #button-unsuspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Unsuspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'Distribution list unsuspended successfully.')
->assertSeeIn('@distlist-info #status.text-success', 'Active')
->assertVisible('@distlist-info #button-suspend')
->assertMissing('@distlist-info #button-unsuspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+ $this->assertSame(null, $event->comment);
+ $this->assertEquals($group->id, $event->object_id);
});
}
}
diff --git a/src/tests/Browser/Reseller/DomainTest.php b/src/tests/Browser/Reseller/DomainTest.php
--- a/src/tests/Browser/Reseller/DomainTest.php
+++ b/src/tests/Browser/Reseller/DomainTest.php
@@ -3,7 +3,9 @@
namespace Tests\Browser\Reseller;
use App\Domain;
+use App\EventLog;
use Tests\Browser;
+use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Admin\Domain as DomainPage;
use Tests\Browser\Pages\Admin\User as UserPage;
@@ -24,6 +26,8 @@
$this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
+ Eventlog::query()->delete();
+
self::useResellerUrl();
}
@@ -35,6 +39,8 @@
$this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
+ Eventlog::query()->delete();
+
parent::tearDown();
}
@@ -84,7 +90,9 @@
// Some tabs are loaded in background, wait a second
$browser->pause(500)
- ->assertElementsCount('@nav a', 2);
+ ->assertElementsCount('@nav a', 3)
+ ->assertSeeIn('@nav #tab-settings', 'Settings')
+ ->assertSeeIn('@nav #tab-history', 'History');
// Assert Configuration tab
$browser->assertSeeIn('@nav #tab-config', 'Configuration')
@@ -102,6 +110,8 @@
*/
public function testSuspendAndUnsuspend(): void
{
+ EventLog::query()->delete();
+
$this->browse(function (Browser $browser) {
$sku_domain = \App\Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
$user = $this->getTestUser('test1@domainscontroller.com');
@@ -123,14 +133,36 @@
->assertVisible('@domain-info #button-suspend')
->assertMissing('@domain-info #button-unsuspend')
->click('@domain-info #button-suspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Suspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->type('textarea', 'test suspend')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'Domain suspended successfully.')
->assertSeeIn('@domain-info #status span.text-warning', 'Suspended')
- ->assertMissing('@domain-info #button-suspend')
- ->click('@domain-info #button-unsuspend')
+ ->assertMissing('@domain-info #button-suspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+ $this->assertSame('test suspend', $event->comment);
+ $this->assertEquals($domain->id, $event->object_id);
+
+ $browser->click('@domain-info #button-unsuspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Unsuspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'Domain unsuspended successfully.')
->assertSeeIn('@domain-info #status span.text-success', 'Active')
->assertVisible('@domain-info #button-suspend')
->assertMissing('@domain-info #button-unsuspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+ $this->assertSame(null, $event->comment);
+ $this->assertEquals($domain->id, $event->object_id);
});
}
}
diff --git a/src/tests/Browser/Reseller/UserTest.php b/src/tests/Browser/Reseller/UserTest.php
--- a/src/tests/Browser/Reseller/UserTest.php
+++ b/src/tests/Browser/Reseller/UserTest.php
@@ -4,6 +4,7 @@
use App\Auth\SecondFactor;
use App\Discount;
+use App\EventLog;
use App\Sku;
use App\User;
use Tests\Browser;
@@ -114,7 +115,7 @@
// Some tabs are loaded in background, wait a second
$browser->pause(500)
- ->assertElementsCount('@nav a', 9);
+ ->assertElementsCount('@nav a', 10);
// Note: Finances tab is tested in UserFinancesTest.php
$browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -250,7 +251,7 @@
// Some tabs are loaded in background, wait a second
$browser->pause(500)
- ->assertElementsCount('@nav a', 9);
+ ->assertElementsCount('@nav a', 10);
// Note: Finances tab is tested in UserFinancesTest.php
$browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -341,6 +342,9 @@
->assertSeeIn('table tbody tr:nth-child(2) td:last-child', 'folder-contact@kolab.org')
->assertMissing('table tfoot');
});
+
+ // Assert History tab
+ $browser->assertSeeIn('@nav #tab-history', 'History');
});
// Now we go to Ned's info page, he's a controller on John's wallet
@@ -362,7 +366,7 @@
// Some tabs are loaded in background, wait a second
$browser->pause(500)
- ->assertElementsCount('@nav a', 9);
+ ->assertElementsCount('@nav a', 10);
// Note: Finances tab is tested in UserFinancesTest.php
$browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -447,6 +451,9 @@
->assertSeeIn('.row:nth-child(3) label', 'Geo-lockin')
->assertSeeIn('.row:nth-child(3) #limit_geo', 'No restrictions');
});
+
+ // Assert History tab
+ $browser->assertSeeIn('@nav #tab-history', 'History');
});
}
@@ -510,6 +517,8 @@
*/
public function testSuspendAndUnsuspend(): void
{
+ EventLog::query()->delete();
+
$this->browse(function (Browser $browser) {
$john = $this->getTestUser('john@kolab.org');
@@ -517,14 +526,34 @@
->assertVisible('@user-info #button-suspend')
->assertMissing('@user-info #button-unsuspend')
->click('@user-info #button-suspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Suspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->type('textarea', 'test suspend')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.')
->assertSeeIn('@user-info #status span.text-warning', 'Suspended')
- ->assertMissing('@user-info #button-suspend')
- ->click('@user-info #button-unsuspend')
+ ->assertMissing('@user-info #button-suspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+ $this->assertSame('test suspend', $event->comment);
+
+ $browser->click('@user-info #button-unsuspend')
+ ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Unsuspend')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Submit')
+ ->click('@button-action');
+ })
->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.')
->assertSeeIn('@user-info #status span.text-success', 'Active')
->assertVisible('@user-info #button-suspend')
->assertMissing('@user-info #button-unsuspend');
+
+ $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+ $this->assertSame(null, $event->comment);
});
}
diff --git a/src/tests/Feature/Controller/Admin/EventLogTest.php b/src/tests/Feature/Controller/Admin/EventLogTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/Admin/EventLogTest.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Tests\Feature\Controller\Admin;
+
+use App\EventLog;
+use Tests\TestCase;
+
+class EventLogTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ self::useAdminUrl();
+
+ EventLog::query()->delete();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ EventLog::query()->delete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test listing events for a user (GET /api/v4/eventlog/user/{user})
+ */
+ public function testUserLog(): void
+ {
+ $user = $this->getTestUser('john@kolab.org');
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+
+ // Non-admin user
+ $response = $this->actingAs($user)->get("api/v4/eventlog/user/{$user->id}");
+ $response->assertStatus(403);
+
+ // Admin user (unknown object type)
+ $response = $this->actingAs($admin)->get("api/v4/eventlog/eeee/{$user->id}");
+ $response->assertStatus(404);
+
+ // Admin user (empty list)
+ $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(0, $json['count']);
+ $this->assertSame([], $json['list']);
+ $this->assertFalse($json['hasMore']);
+
+ // Non-empty list
+ $event1 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 1", ['test' => 'test1']);
+ $event1->created_at = now();
+ $event1->save();
+ $event2 = EventLog::createFor($user, EventLog::TYPE_UNSUSPENDED, "Event 2", ['test' => 'test2']);
+ $event2->created_at = (clone now())->subDay();
+ $event2->save();
+
+ $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(2, $json['count']);
+ $this->assertCount(2, $json['list']);
+ $this->assertFalse($json['hasMore']);
+ $this->assertSame($event1->id, $json['list'][0]['id']);
+ $this->assertSame($event1->comment, $json['list'][0]['comment']);
+ $this->assertSame($event1->data, $json['list'][0]['data']);
+ $this->assertSame($admin->email, $json['list'][0]['user']);
+ $this->assertSame('Suspended', $json['list'][0]['event']);
+ $this->assertSame($event2->id, $json['list'][1]['id']);
+ $this->assertSame($event2->comment, $json['list'][1]['comment']);
+ $this->assertSame($event2->data, $json['list'][1]['data']);
+ $this->assertSame($admin->email, $json['list'][1]['user']);
+ $this->assertSame('Unsuspended', $json['list'][1]['event']);
+
+ // A user in another tenant
+ $user = $this->getTestUser('user@sample-tenant.dev-local');
+ $event3 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 3");
+
+ $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($event3->id, $json['list'][0]['id']);
+ }
+}
diff --git a/src/tests/Feature/Controller/Reseller/EventLogTest.php b/src/tests/Feature/Controller/Reseller/EventLogTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/Reseller/EventLogTest.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Tests\Feature\Controller\Reseller;
+
+use App\EventLog;
+use Tests\TestCase;
+
+class EventLogTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ self::useResellerUrl();
+
+ EventLog::query()->delete();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ EventLog::query()->delete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test listing events for a user (GET /api/v4/eventlog/user/{user})
+ */
+ public function testUserLog(): void
+ {
+ $user = $this->getTestUser('john@kolab.org');
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+ $reseller1 = $this->getTestUser('reseller@' . \config('app.domain'));
+ $reseller2 = $this->getTestUser('reseller@sample-tenant.dev-local');
+
+ // Non-admin user
+ $response = $this->actingAs($user)->get("api/v4/eventlog/user/{$user->id}");
+ $response->assertStatus(403);
+
+ // Admin user
+ $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}");
+ $response->assertStatus(403);
+
+ // Reseller user (unknown object type)
+ $response = $this->actingAs($reseller1)->get("api/v4/eventlog/eeee/{$user->id}");
+ $response->assertStatus(404);
+
+ // Reseller user (empty list)
+ $response = $this->actingAs($reseller1)->get("api/v4/eventlog/user/{$user->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(0, $json['count']);
+ $this->assertSame([], $json['list']);
+ $this->assertFalse($json['hasMore']);
+
+ // Non-empty list
+ $event1 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 1", ['test' => 'test1']);
+ $event1->created_at = now();
+ $event1->save();
+ $event2 = EventLog::createFor($user, EventLog::TYPE_UNSUSPENDED, "Event 2", ['test' => 'test2']);
+ $event2->created_at = (clone now())->subDay();
+ $event2->save();
+
+ $response = $this->actingAs($reseller1)->get("api/v4/eventlog/user/{$user->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(2, $json['count']);
+ $this->assertCount(2, $json['list']);
+ $this->assertFalse($json['hasMore']);
+ $this->assertSame($event1->id, $json['list'][0]['id']);
+ $this->assertSame($event1->comment, $json['list'][0]['comment']);
+ $this->assertSame($event1->data, $json['list'][0]['data']);
+ $this->assertSame($reseller1->email, $json['list'][0]['user']);
+ $this->assertSame('Suspended', $json['list'][0]['event']);
+ $this->assertSame($event2->id, $json['list'][1]['id']);
+ $this->assertSame($event2->comment, $json['list'][1]['comment']);
+ $this->assertSame($event2->data, $json['list'][1]['data']);
+ $this->assertSame($reseller1->email, $json['list'][1]['user']);
+ $this->assertSame('Unsuspended', $json['list'][1]['event']);
+
+ // A user in another tenant
+ $user = $this->getTestUser('user@sample-tenant.dev-local');
+ $event3 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 3");
+
+ $response = $this->actingAs($reseller1)->get("api/v4/eventlog/user/{$user->id}");
+ $response->assertStatus(404);
+
+ $response = $this->actingAs($reseller2)->get("api/v4/eventlog/user/{$user->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($event3->id, $json['list'][0]['id']);
+ }
+}
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
@@ -4,6 +4,7 @@
use App\Domain;
use App\Entitlement;
+use App\EventLog;
use App\Sku;
use App\User;
use App\Tenant;
@@ -231,6 +232,29 @@
$this->assertCount(0, Domain::withTrashed()->where('id', $domain->id)->get());
}
+ /**
+ * Test eventlog on domain deletion
+ */
+ public function testDeleteAndEventLog(): void
+ {
+ Queue::fake();
+
+ $domain = $this->getTestDomain('gmail.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_PUBLIC,
+ ]);
+
+ EventLog::createFor($domain, EventLog::TYPE_SUSPENDED, 'test');
+
+ $domain->delete();
+
+ $this->assertCount(1, EventLog::where('object_id', $domain->id)->where('object_type', Domain::class)->get());
+
+ $domain->forceDelete();
+
+ $this->assertCount(0, EventLog::where('object_id', $domain->id)->where('object_type', Domain::class)->get());
+ }
+
/**
* Test isEmpty() method
*/
diff --git a/src/tests/Feature/GroupTest.php b/src/tests/Feature/GroupTest.php
--- a/src/tests/Feature/GroupTest.php
+++ b/src/tests/Feature/GroupTest.php
@@ -3,6 +3,7 @@
namespace Tests\Feature;
use App\Group;
+use App\EventLog;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
@@ -132,6 +133,26 @@
);
}
+ /**
+ * Test eventlog on group deletion
+ */
+ public function testDeleteAndEventLog(): void
+ {
+ Queue::fake();
+
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+
+ EventLog::createFor($group, EventLog::TYPE_SUSPENDED, 'test');
+
+ $group->delete();
+
+ $this->assertCount(1, EventLog::where('object_id', $group->id)->where('object_type', Group::class)->get());
+
+ $group->forceDelete();
+
+ $this->assertCount(0, EventLog::where('object_id', $group->id)->where('object_type', Group::class)->get());
+ }
+
/**
* Tests for Group::emailExists()
*/
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -3,6 +3,7 @@
namespace Tests\Feature;
use App\Domain;
+use App\EventLog;
use App\Group;
use App\Package;
use App\PackageSku;
@@ -838,6 +839,26 @@
$this->assertCount(0, \App\SharedFolder::withTrashed()->where('id', $folder->id)->get());
}
+ /**
+ * Test eventlog on user deletion
+ */
+ public function testDeleteAndEventLog(): void
+ {
+ Queue::fake();
+
+ $user = $this->getTestUser('user-test@' . \config('app.domain'));
+
+ EventLog::createFor($user, EventLog::TYPE_SUSPENDED, 'test');
+
+ $user->delete();
+
+ $this->assertCount(1, EventLog::where('object_id', $user->id)->where('object_type', User::class)->get());
+
+ $user->forceDelete();
+
+ $this->assertCount(0, EventLog::where('object_id', $user->id)->where('object_type', User::class)->get());
+ }
+
/**
* Test user deletion vs. group membership
*/

File Metadata

Mime Type
text/plain
Expires
Mon, Mar 30, 1:40 PM (5 d, 3 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18811812
Default Alt Text
D4400.1774878016.diff (93 KB)

Event Timeline