Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117690189
D4400.1774878016.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
93 KB
Referenced Files
None
Subscribers
None
D4400.1774878016.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D4400: Event Log (History)
Attached
Detach File
Event Timeline