Skip to content

Commit

Permalink
Impersonation action log implementation (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jampire authored May 25, 2023
1 parent 6d665bf commit c5d7095
Show file tree
Hide file tree
Showing 20 changed files with 383 additions and 4 deletions.
34 changes: 34 additions & 0 deletions database/factories/NonLogUserFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Jampire\MoonshineImpersonate\Database\Factories;

use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Jampire\MoonshineImpersonate\Tests\Stubs\Models\NonLogUser;
use Orchestra\Testbench\Factories\UserFactory as TestbenchUserFactory;

/**
* Class NonLogUserFactory
*
* @author Dzianis Kotau <[email protected]>
*/
class NonLogUserFactory extends TestbenchUserFactory
{
protected $model = NonLogUser::class;

/**
* @return array{name: string, email: string, email_verified_at: \Illuminate\Support\Carbon, password: string, remember_token: string}
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => Hash::make('secret'),
'remember_token' => Str::random(10),
];
}
}
27 changes: 27 additions & 0 deletions database/migrations/create_moonshine_change_logs_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateMoonShineChangeLogsTable extends Migration
{
public function up(): void
{
Schema::create('moonshine_change_logs', function (Blueprint $table): void {
$table->id();
$table->foreignId('moonshine_user_id');
$table->morphs('changelogable');
$table->longText('states_before');
$table->longText('states_after');
$table->timestamps();
});
}

public function down(): void
{
Schema::dropIfExists('moonshine_change_logs');
}
};
3 changes: 0 additions & 3 deletions database/migrations/create_moonshine_users_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ public function up(): void
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('moonshine_users');
Expand Down
10 changes: 10 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,13 @@ parameters:
message: "#^Parameter \\#2 \\$provider of class Jampire\\\\MoonshineImpersonate\\\\Guards\\\\SessionGuard constructor expects Illuminate\\\\Contracts\\\\Auth\\\\UserProvider, Illuminate\\\\Contracts\\\\Auth\\\\UserProvider\\|null given\\.$#"
count: 1
path: src/ImpersonateServiceProvider.php

-
message: "#^Call to an undefined method Illuminate\\\\Contracts\\\\Auth\\\\Authenticatable\\:\\:changeLogs\\(\\)\\.$#"
count: 1
path: src/Listeners/LogImpersonationEnter.php

-
message: "#^Call to an undefined method Illuminate\\\\Contracts\\\\Auth\\\\Authenticatable\\:\\:changeLogs\\(\\)\\.$#"
count: 1
path: src/Listeners/LogImpersonationStopped.php
3 changes: 3 additions & 0 deletions src/Enums/Permission.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace Jampire\MoonshineImpersonate\Enums;

/**
* @author Dzianis Kotau <[email protected]>
*/
enum Permission: string
{
case IMPERSONATE = 'impersonate';
Expand Down
12 changes: 12 additions & 0 deletions src/Enums/State.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Jampire\MoonshineImpersonate\Enums;

/**
* @author Dzianis Kotau <[email protected]>
*/
enum State: string
{
case IMPERSONATION_ENTERED = 'impersonation_entered';
case IMPERSONATION_STOPPED = 'impersonation_stopped';
}
3 changes: 3 additions & 0 deletions src/ImpersonateServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Jampire\MoonshineImpersonate\Actions\StopAction;
use Jampire\MoonshineImpersonate\Guards\SessionGuard;
use Jampire\MoonshineImpersonate\Http\Middleware\ImpersonateMiddleware;
use Jampire\MoonshineImpersonate\Providers\EventServiceProvider;
use Jampire\MoonshineImpersonate\Services\ImpersonateManager;
use Jampire\MoonshineImpersonate\Support\Settings;
use Jampire\MoonshineImpersonate\UI\View\Components\StopImpersonation;
Expand Down Expand Up @@ -39,6 +40,8 @@ public function register(): void
fn (): StopAction => new StopAction(app(ImpersonateManager::class))
);

$this->app->register(EventServiceProvider::class);

$this->mergeConfigFrom(__DIR__.'/../config/'.Settings::ALIAS.'.php', Settings::ALIAS);

$this->registerAuthDriver();
Expand Down
31 changes: 31 additions & 0 deletions src/Listeners/LogImpersonationEnter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Jampire\MoonshineImpersonate\Listeners;

use Illuminate\Contracts\Queue\ShouldQueue;
use Jampire\MoonshineImpersonate\Enums\State;
use Jampire\MoonshineImpersonate\Events\ImpersonationEntered;

/**
* Class LogImpersonationEnter
*
* @author Dzianis Kotau <[email protected]>
*/
class LogImpersonationEnter implements ShouldQueue
{
public function handle(ImpersonationEntered $event): void
{
$event->impersonated->changeLogs()->create([
'moonshine_user_id' => $event->impersonator->getAuthIdentifier(),
'states_before' => State::IMPERSONATION_STOPPED->value,
'states_after' => State::IMPERSONATION_ENTERED->value,
]);
}

public function shouldQueue(ImpersonationEntered $event): bool
{
return method_exists($event->impersonated, 'changeLogs');
}
}
31 changes: 31 additions & 0 deletions src/Listeners/LogImpersonationStopped.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Jampire\MoonshineImpersonate\Listeners;

use Illuminate\Contracts\Queue\ShouldQueue;
use Jampire\MoonshineImpersonate\Enums\State;
use Jampire\MoonshineImpersonate\Events\ImpersonationStopped;

/**
* Class LogImpersonationStopped
*
* @author Dzianis Kotau <[email protected]>
*/
class LogImpersonationStopped implements ShouldQueue
{
public function handle(ImpersonationStopped $event): void
{
$event->impersonated->changeLogs()->create([
'moonshine_user_id' => $event->impersonator->getAuthIdentifier(),
'states_before' => State::IMPERSONATION_ENTERED->value,
'states_after' => State::IMPERSONATION_STOPPED->value,
]);
}

public function shouldQueue(ImpersonationStopped $event): bool
{
return method_exists($event->impersonated, 'changeLogs');
}
}
26 changes: 26 additions & 0 deletions src/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Jampire\MoonshineImpersonate\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Jampire\MoonshineImpersonate\Events\ImpersonationEntered;
use Jampire\MoonshineImpersonate\Events\ImpersonationStopped;
use Jampire\MoonshineImpersonate\Listeners\LogImpersonationEnter;
use Jampire\MoonshineImpersonate\Listeners\LogImpersonationStopped;

/**
* Class EventServiceProvider
*
* @author Dzianis Kotau <[email protected]>
*/
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
ImpersonationEntered::class => [
LogImpersonationEnter::class,
],
ImpersonationStopped::class => [
LogImpersonationStopped::class,
],
];
}
3 changes: 3 additions & 0 deletions tests/Feature/Actions/EnterActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Event;
use Jampire\MoonshineImpersonate\Actions\EnterAction;
use Jampire\MoonshineImpersonate\Support\Settings;
use Jampire\MoonshineImpersonate\Tests\Stubs\Models\MoonshineUser;
Expand All @@ -28,6 +29,8 @@
});

test('enter action validation works correctly', function (): void {
Event::fake();

$user = User::factory()->create();
$moonShineUser = MoonshineUser::factory()->create();
actingAs($moonShineUser, Settings::moonShineGuard());
Expand Down
1 change: 1 addition & 0 deletions tests/Feature/Http/ImpersonateEnterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

test('privileged user can impersonate another user', function (): void {
Event::fake();

$user = User::factory()->create([
'name' => 'user',
]);
Expand Down
66 changes: 66 additions & 0 deletions tests/Feature/Listeners/LogImpersonationEnterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

use Illuminate\Support\Facades\Event;
use Jampire\MoonshineImpersonate\Enums\State;
use Jampire\MoonshineImpersonate\Events\ImpersonationEntered;
use Jampire\MoonshineImpersonate\Listeners\LogImpersonationEnter;
use Jampire\MoonshineImpersonate\Support\Settings;
use Jampire\MoonshineImpersonate\Tests\Stubs\Models\MoonshineUser;
use Jampire\MoonshineImpersonate\Tests\Stubs\Models\NonLogUser;
use Jampire\MoonshineImpersonate\Tests\Stubs\Models\User;

use function Pest\Laravel\actingAs;
use function Pest\Laravel\assertDatabaseEmpty;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\post;

it('handles enter impersonation mode', function (): void {
$user = User::factory()->create();
$moonShineUser = MoonshineUser::factory()->create();

assertDatabaseEmpty('moonshine_change_logs');

$event = new ImpersonationEntered($moonShineUser, $user);
$listener = new LogImpersonationEnter();
$listener->handle($event);

assertDatabaseHas('moonshine_change_logs', [
'moonshine_user_id' => $moonShineUser->getAuthIdentifier(),
'changelogable_type' => $user::class,
'changelogable_id' => $user->getKey(),
'states_before' => '"'.State::IMPERSONATION_STOPPED->value.'"',
'states_after' => '"'.State::IMPERSONATION_ENTERED->value.'"',
]);
});

it('listens enter impersonation event', function (): void {
Event::fake();

$user = User::factory()->create();
$moonShineUser = MoonshineUser::factory()->create();

actingAs($moonShineUser, Settings::moonShineGuard());
post(route_impersonate('enter'), [
'id' => $user->id,
])->assertSessionHasNoErrors();

Event::assertListening(
ImpersonationEntered::class,
LogImpersonationEnter::class
);
});

it('cannot log enter impersonation mode without changeLogs relation', function (): void {
config(['auth.providers.users.model' => NonLogUser::class]);

$user = NonLogUser::factory()->create();
$moonShineUser = MoonshineUser::factory()->create();

$event = new ImpersonationEntered($moonShineUser, $user);
$listener = new LogImpersonationEnter();

expect($listener->shouldQueue($event))
->toBeFalse();
});
67 changes: 67 additions & 0 deletions tests/Feature/Listeners/LogImpersonationStoppedTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

use Illuminate\Support\Facades\Event;
use Jampire\MoonshineImpersonate\Enums\State;
use Jampire\MoonshineImpersonate\Events\ImpersonationStopped;
use Jampire\MoonshineImpersonate\Listeners\LogImpersonationStopped;
use Jampire\MoonshineImpersonate\Support\Settings;
use Jampire\MoonshineImpersonate\Tests\Stubs\Models\MoonshineUser;
use Jampire\MoonshineImpersonate\Tests\Stubs\Models\NonLogUser;
use Jampire\MoonshineImpersonate\Tests\Stubs\Models\User;

use function Pest\Laravel\actingAs;
use function Pest\Laravel\assertDatabaseEmpty;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\get;

it('handles stopped impersonation mode', function (): void {
$user = User::factory()->create();
$moonShineUser = MoonshineUser::factory()->create();

assertDatabaseEmpty('moonshine_change_logs');

$event = new ImpersonationStopped($moonShineUser, $user);
$listener = new LogImpersonationStopped();
$listener->handle($event);

assertDatabaseHas('moonshine_change_logs', [
'moonshine_user_id' => $moonShineUser->getAuthIdentifier(),
'changelogable_type' => $user::class,
'changelogable_id' => $user->getKey(),
'states_before' => '"'.State::IMPERSONATION_ENTERED->value.'"',
'states_after' => '"'.State::IMPERSONATION_STOPPED->value.'"',
]);
});

it('listens stopped impersonation event', function (): void {
Event::fake();

$user = User::factory()->create();
$moonShineUser = MoonshineUser::factory()->create();

actingAs($moonShineUser, Settings::moonShineGuard())
->withSession([Settings::key() => $user->getKey()]);

get(route_impersonate('stop'))
->assertSessionHasNoErrors();

Event::assertListening(
ImpersonationStopped::class,
LogImpersonationStopped::class
);
});

it('cannot log stopped impersonation mode without changeLogs relation', function (): void {
config(['auth.providers.users.model' => NonLogUser::class]);

$user = NonLogUser::factory()->create();
$moonShineUser = MoonshineUser::factory()->create();

$event = new ImpersonationStopped($moonShineUser, $user);
$listener = new LogImpersonationStopped();

expect($listener->shouldQueue($event))
->toBeFalse();
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

declare(strict_types=1);

use Illuminate\Foundation\Auth\User as AuthUser;
use Jampire\MoonshineImpersonate\Support\Settings;
use Jampire\MoonshineImpersonate\Tests\Stubs\Models\MoonshineUser;
use Jampire\MoonshineImpersonate\Tests\Stubs\Models\User;
Expand All @@ -12,6 +13,9 @@
use function Pest\Laravel\actingAs;

it('resolves correct item action class', function (): void {
// don't need to use User model here
config(['auth.providers.users.model' => AuthUser::class]);

$user = User::factory()->create();
$moonShineUser = MoonshineUser::factory()->create();
actingAs($moonShineUser, Settings::moonShineGuard());
Expand Down
Loading

0 comments on commit c5d7095

Please sign in to comment.