mirror of
https://github.com/Kovah/LinkAce.git
synced 2025-04-21 23:42:10 +02:00
WIP: Add blocking and deleting of users
This commit is contained in:
parent
3668524b0c
commit
78ee6eabda
@ -4,6 +4,9 @@ namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use OwenIt\Auditing\Events\AuditCustom;
|
||||
|
||||
class UserManagementController extends Controller
|
||||
{
|
||||
@ -23,13 +26,71 @@ class UserManagementController extends Controller
|
||||
//
|
||||
}
|
||||
|
||||
public function deleteUser()
|
||||
public function blockUser(User $user): RedirectResponse
|
||||
{
|
||||
//
|
||||
$this->checkIfActionAllowed($user);
|
||||
|
||||
$user->blocked_at = now();
|
||||
$user->save();
|
||||
|
||||
$this->addBlockingAuditEvent($user, User::AUDIT_BLOCK_EVENT, null, $user->blocked_at);
|
||||
|
||||
flash()->warning(trans('user.block_successful', ['username' => $user->name]));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function restoreUser()
|
||||
public function unblockUser(User $user): RedirectResponse
|
||||
{
|
||||
//
|
||||
$this->checkIfActionAllowed($user);
|
||||
|
||||
$blockedAt = $user->blocked_at;
|
||||
$user->blocked_at = null;
|
||||
$user->save();
|
||||
|
||||
$this->addBlockingAuditEvent($user, User::AUDIT_UNBLOCK_EVENT, $blockedAt, null);
|
||||
|
||||
flash()->warning(trans('user.unblock_successful', ['username' => $user->name]));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function deleteUser(User $user): RedirectResponse
|
||||
{
|
||||
$this->checkIfActionAllowed($user);
|
||||
|
||||
$user->delete();
|
||||
|
||||
flash()->warning(trans('user.delete_successful', ['username' => $user->name]));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function restoreUser(User $user): RedirectResponse
|
||||
{
|
||||
$this->checkIfActionAllowed($user);
|
||||
|
||||
$user->restore();
|
||||
|
||||
flash()->warning(trans('user.restore_successful', ['username' => $user->name]));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
protected function checkIfActionAllowed(User $user): void
|
||||
{
|
||||
if ($user->id === auth()->id()) {
|
||||
abort(403, trans('user.action_not_allowed_on_user'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function addBlockingAuditEvent(User $user, string $event, $oldValue, $newValue): void
|
||||
{
|
||||
$user->auditEvent = $event;
|
||||
$user->isCustomEvent = true;
|
||||
$user->auditCustomOld = [
|
||||
'blocked_at' => $oldValue,
|
||||
];
|
||||
$user->auditCustomNew = [
|
||||
'blocked_at' => $newValue,
|
||||
];
|
||||
|
||||
Event::dispatch(AuditCustom::class, [$user]);
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ class Kernel extends HttpKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
|
||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
|
20
app/Http/Middleware/Authenticate.php
Normal file
20
app/Http/Middleware/Authenticate.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Auth\Middleware\Authenticate as IlluminateAuthenticate;
|
||||
|
||||
class Authenticate extends IlluminateAuthenticate
|
||||
{
|
||||
public function handle($request, Closure $next, ...$guards)
|
||||
{
|
||||
$this->authenticate($request, $guards);
|
||||
|
||||
if ($request->user()->isBlocked()) {
|
||||
abort(403, trans('user.block_warning'));
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
@ -2,13 +2,12 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Audits\Modifiers\BlockedAtModifier;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use OwenIt\Auditing\Auditable as AuditableTrait;
|
||||
use OwenIt\Auditing\Contracts\Auditable;
|
||||
@ -43,6 +42,7 @@ class User extends Authenticatable implements Auditable
|
||||
'email',
|
||||
'password',
|
||||
'api_token',
|
||||
'blocked_at',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
@ -51,6 +51,10 @@ class User extends Authenticatable implements Auditable
|
||||
'api_token',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'blocked_at' => 'datetime',
|
||||
];
|
||||
|
||||
public string $langBase = 'user';
|
||||
|
||||
/*
|
||||
@ -58,6 +62,9 @@ class User extends Authenticatable implements Auditable
|
||||
* AUDIT SETTINGS
|
||||
*/
|
||||
|
||||
public const AUDIT_BLOCK_EVENT = 'blocked';
|
||||
public const AUDIT_UNBLOCK_EVENT = 'unblocked';
|
||||
|
||||
protected array $auditEvents = [
|
||||
'created',
|
||||
'updated',
|
||||
@ -71,4 +78,14 @@ class User extends Authenticatable implements Auditable
|
||||
];
|
||||
|
||||
public array $auditModifiers = [];
|
||||
|
||||
/*
|
||||
* ========================================================================
|
||||
* METHODS
|
||||
*/
|
||||
|
||||
public function isBlocked(): bool
|
||||
{
|
||||
return $this->blocked_at !== null;
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,10 @@ class UserEntry extends Component
|
||||
$this->changes[] = trans('user.history_restored', ['name' => $this->entry->getModified()['name']['new']]);
|
||||
} elseif ($this->entry->event === 'created') {
|
||||
$this->changes[] = trans('user.history_created', ['name' => $this->entry->getModified()['name']['new']]);
|
||||
} elseif ($this->entry->event === 'blocked') {
|
||||
$this->changes[] = trans('user.history_blocked', ['name' => $this->entry->auditable->name]);
|
||||
} elseif ($this->entry->event === 'unblocked') {
|
||||
$this->changes[] = trans('user.history_unblocked', ['name' => $this->entry->auditable->name]);
|
||||
} else {
|
||||
foreach ($this->entry->getModified() as $field => $change) {
|
||||
$this->processChange($field, $change);
|
||||
|
@ -113,6 +113,7 @@ class MigrateUserData extends Migration
|
||||
\Illuminate\Support\Facades\Artisan::call('db:seed', ['--class' => 'RolesAndPermissionsSeeder']);
|
||||
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->timestamp('blocked_at')->nullable()->after('api_token');
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
|
@ -21,6 +21,9 @@ return [
|
||||
'added_at' => 'Added at',
|
||||
'updated_at' => 'Updated at',
|
||||
'last_update' => 'Last Update',
|
||||
'blocked' => 'Blocked',
|
||||
'blocked_at' => 'Blocked at',
|
||||
'deleted' => 'Deleted',
|
||||
'deleted_at' => 'Deleted at',
|
||||
|
||||
'add' => 'Add',
|
||||
@ -28,6 +31,10 @@ return [
|
||||
'edit' => 'Edit',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'restore' => 'Restore',
|
||||
'block' => 'Block',
|
||||
'unblock' => 'Unblock',
|
||||
'unblocked' => 'Unblocked',
|
||||
|
||||
'menu' => 'Menu',
|
||||
'entries' => 'Entries',
|
||||
|
@ -6,10 +6,29 @@ return [
|
||||
'username' => 'Username',
|
||||
'name' => 'Username',
|
||||
'email' => 'Email',
|
||||
'blocked_at' => 'Blocked at',
|
||||
|
||||
'block' => 'Block User',
|
||||
'block_confirmation' => 'Do you really want to block this User?',
|
||||
'block_warning' => 'Your user account is currently blocked. Please contact your administrator for help.',
|
||||
'block_successful' => 'The user :username was blocked successfully.',
|
||||
'unblock' => 'Unblock User',
|
||||
'unblock_confirmation' => 'Do you really want to unblock this User?',
|
||||
'unblock_successful' => 'The user :username was unblocked successfully.',
|
||||
'delete' => 'Delete User',
|
||||
'delete_confirmation' => 'Do you really want to delete this User?',
|
||||
'delete_successful' => 'The user :username was deleted successfully.',
|
||||
'restore' => 'Restore User',
|
||||
'restore_confirmation' => 'Do you really want to restore this User?',
|
||||
'restore_successful' => 'The user :username was restored successfully.',
|
||||
|
||||
'action_not_allowed_on_user' => 'This action cannot be performed on the selected user.',
|
||||
|
||||
'history_deleted' => 'User <code>:name</code> was deleted',
|
||||
'history_restored' => 'User <code>:name</code> was restored',
|
||||
'history_created' => 'User <code>:name</code> was created',
|
||||
'history_blocked' => 'User <code>:name</code> was blocked',
|
||||
'history_unblocked' => 'User <code>:name</code> was unblocked',
|
||||
|
||||
'hello' => 'Hello :user!',
|
||||
'for_user' => 'for User',
|
||||
|
@ -7,8 +7,12 @@ export default class LoadingButton {
|
||||
this.$btn.addEventListener('click', this.onClick.bind(this));
|
||||
}
|
||||
|
||||
onClick () {
|
||||
onClick (event) {
|
||||
if (this.$form.checkValidity()) {
|
||||
if (typeof this.$form.dataset.confirmation !== 'undefined' && confirm(this.$form.dataset.confirmation) === false) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
this.$btn.disabled = true;
|
||||
this.$form.submit();
|
||||
}
|
||||
|
@ -6,12 +6,68 @@
|
||||
<div class="card-header">
|
||||
@lang('admin.user_management.title')
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-body p-0">
|
||||
|
||||
<div class="d-grid row-cols-1 gap-2">
|
||||
<div class="list-group list-group-flush">
|
||||
@foreach($users as $user)
|
||||
<div @class(['text-danger' => $user->trashed()])>
|
||||
{{ $user->name }} <span class="ms-2 text-muted">{{ $user->email }}</span>
|
||||
<div class="list-group-item d-md-flex justify-content-between">
|
||||
<div>
|
||||
{{ $user->name }} <span class="ms-2 text-muted">{{ $user->email }}</span>
|
||||
@if($user->isBlocked())
|
||||
<span class="badge bg-warning">@lang('linkace.blocked')</span>
|
||||
@endif
|
||||
@if($user->trashed())
|
||||
<span class="badge bg-danger">@lang('linkace.deleted')</span>
|
||||
@endif
|
||||
</div>
|
||||
<div @class(['d-none' => $user->id === auth()->id()])>
|
||||
@if($user->isBlocked())
|
||||
<button type="submit" form="unblock-user-{{ $user->id }}"
|
||||
class="btn btn-sm btn-outline-warning">
|
||||
@lang('linkace.unblock')
|
||||
</button>
|
||||
@else
|
||||
<button type="submit" form="block-user-{{ $user->id }}"
|
||||
class="btn btn-sm btn-outline-warning">
|
||||
@lang('linkace.block')
|
||||
</button>
|
||||
@endif
|
||||
@if($user->trashed())
|
||||
<button type="submit" form="restore-user-{{ $user->id }}"
|
||||
class="btn btn-sm btn-outline-danger">
|
||||
@lang('linkace.restore')
|
||||
</button>
|
||||
@else
|
||||
<button type="submit" form="delete-user-{{ $user->id }}"
|
||||
class="btn btn-sm btn-outline-danger">
|
||||
@lang('linkace.delete')
|
||||
</button>
|
||||
@endif
|
||||
<form action="{{ route('user-management-block', ['user' => $user]) }}"
|
||||
id="block-user-{{ $user->id }}"
|
||||
method="post" class="d-none" data-confirmation="@lang('user.block_confirmation')">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
</form>
|
||||
<form action="{{ route('user-management-unblock', ['user' => $user]) }}"
|
||||
id="unblock-user-{{ $user->id }}"
|
||||
method="post" class="d-none" data-confirmation="@lang('user.unblock_confirmation')">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
</form>
|
||||
<form action="{{ route('user-management-delete', ['user' => $user]) }}"
|
||||
id="delete-user-{{ $user->id }}"
|
||||
method="post" class="d-none" data-confirmation="@lang('user.delete_confirmation')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
</form>
|
||||
<form action="{{ route('user-management-restore', ['user' => $user]) }}"
|
||||
id="restore-user-{{ $user->id }}"
|
||||
method="post" class="d-none" data-confirmation="@lang('user.restore_confirmation')">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
@ -59,6 +59,9 @@ Route::prefix('bookmarklet')->group(function () {
|
||||
|
||||
Route::get('cron/{token}', CronController::class)->name('cron');
|
||||
|
||||
Route::post('system/users/accept-invite', [UserManagementController::class, 'acceptInvitation'])
|
||||
->name('user-management-accept-invite');
|
||||
|
||||
Route::group(['middleware' => 'auth:api'], function () {
|
||||
Route::get('links/feed', [FeedController::class, 'links'])->name('links.feed');
|
||||
Route::get('lists/feed', [FeedController::class, 'lists'])->name('lists.feed');
|
||||
@ -139,6 +142,15 @@ Route::group(['middleware' => ['auth', 'role:admin']], function () {
|
||||
->name('generate-cron-token');
|
||||
|
||||
Route::get('system/users', [UserManagementController::class, 'index'])->name('user-management');
|
||||
Route::post('system/users/invite', [UserManagementController::class, 'index'])->name('user-management-invite');
|
||||
Route::patch('system/users/{user}/block', [UserManagementController::class, 'blockUser'])
|
||||
->name('user-management-block')->withTrashed();
|
||||
Route::patch('system/users/{user}/unblock', [UserManagementController::class, 'unblockUser'])
|
||||
->name('user-management-unblock')->withTrashed();
|
||||
Route::delete('system/users/{user}/delete', [UserManagementController::class, 'deleteUser'])
|
||||
->name('user-management-delete')->withTrashed();
|
||||
Route::patch('system/users/{user}/restore', [UserManagementController::class, 'restoreUser'])
|
||||
->name('user-management-restore')->withTrashed();
|
||||
|
||||
Route::get('system/logs', [LogViewerController::class, 'index'])->name('system-logs');
|
||||
Route::get('system/audit', AuditController::class)->name('system-audit');
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Tests\Components\History;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\User;
|
||||
use App\View\Components\History\UserEntry;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@ -39,5 +40,35 @@ class UserEntryTest extends TestCase
|
||||
|
||||
$output = (new UserEntry($historyEntries[1]))->render();
|
||||
$this->assertStringContainsString('User <code>TestUser</code> was deleted', $output);
|
||||
|
||||
$user->restore();
|
||||
|
||||
$historyEntries = $user->audits()->latest()->get();
|
||||
|
||||
$output = (new UserEntry($historyEntries[2]))->render();
|
||||
$this->assertStringContainsString('User <code>TestUser</code> was restored', $output);
|
||||
}
|
||||
|
||||
public function testModelBlocking(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole(Role::ADMIN);
|
||||
$this->actingAs($admin);
|
||||
|
||||
$user = User::factory()->create(['name' => 'TestUser']);
|
||||
|
||||
$this->patch('system/users/2/block');
|
||||
|
||||
$historyEntries = $user->audits()->latest()->get();
|
||||
|
||||
$output = (new UserEntry($historyEntries[1]))->render();
|
||||
$this->assertStringContainsString('User <code>TestUser</code> was blocked', $output);
|
||||
|
||||
$this->patch('system/users/2/unblock');
|
||||
|
||||
$historyEntries = $user->audits()->latest()->get();
|
||||
|
||||
$output = (new UserEntry($historyEntries[2]))->render();
|
||||
$this->assertStringContainsString('User <code>TestUser</code> was unblocked', $output);
|
||||
}
|
||||
}
|
||||
|
@ -45,4 +45,41 @@ class UserManagementControllerTest extends TestCase
|
||||
$response->assertOk()->assertSee('User Management')
|
||||
->assertSee($this->user->name);
|
||||
}
|
||||
|
||||
public function testUserBlocking(): void
|
||||
{
|
||||
$otherUser = User::factory()->create();
|
||||
|
||||
$response = $this->patch('system/users/2/block');
|
||||
$response->assertRedirect();
|
||||
|
||||
$this->assertTrue($otherUser->refresh()->isBlocked());
|
||||
|
||||
$this->actingAs($otherUser);
|
||||
|
||||
$response = $this->get('dashboard');
|
||||
$response->assertForbidden();
|
||||
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$response = $this->patch('system/users/2/unblock');
|
||||
$response->assertRedirect();
|
||||
|
||||
$this->assertFalse($otherUser->refresh()->isBlocked());
|
||||
}
|
||||
|
||||
public function testUserDeletion(): void
|
||||
{
|
||||
$otherUser = User::factory()->create();
|
||||
|
||||
$response = $this->delete('system/users/2/delete');
|
||||
$response->assertRedirect();
|
||||
|
||||
$this->assertTrue($otherUser->refresh()->trashed());
|
||||
|
||||
$response = $this->patch('system/users/2/restore');
|
||||
$response->assertRedirect();
|
||||
|
||||
$this->assertFalse($otherUser->refresh()->trashed());
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user