diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php
new file mode 100644
index 0000000..b9c1291
--- /dev/null
+++ b/app/Http/Controllers/Admin/UserController.php
@@ -0,0 +1,38 @@
+ $user
+ ]);
+ }
+
+ /**
+ * Delete user.
+ */
+ public function destroy(User $user)
+ {
+ $user->delete();
+
+ return redirect()->route('admin.users.index')
+ ->with(['success' => "{$user->username} was deleted!"]);
+ }
+}
diff --git a/app/Http/Livewire/AdminUserTable.php b/app/Http/Livewire/AdminUserTable.php
new file mode 100644
index 0000000..fc22762
--- /dev/null
+++ b/app/Http/Livewire/AdminUserTable.php
@@ -0,0 +1,44 @@
+ ['except' => ''],
+ 'role' => ['except' => '']
+ ];
+
+ /**
+ * Filter by username
+ */
+ public $username;
+
+ /**
+ * Filter by role
+ */
+ public $role;
+
+ public function render()
+ {
+ $query = User::query()->orderBy('username');
+
+ if (strlen($this->username) >= 3) {
+ $query->where('username', 'LIKE', '%' . $this->username . '%');
+ }
+
+ if (in_array($this->role, ['user','admin'])) {
+ $query->where('role', $this->role);
+ }
+
+ return view('livewire.admin-user-table', [
+ 'users' => $query->paginate($this->perPage)
+ ]);
+ }
+}
diff --git a/app/Http/Livewire/Form/Admin/UserForm.php b/app/Http/Livewire/Form/Admin/UserForm.php
new file mode 100644
index 0000000..50f3f6d
--- /dev/null
+++ b/app/Http/Livewire/Form/Admin/UserForm.php
@@ -0,0 +1,83 @@
+user = $user;
+ }
+
+ /**
+ * Get the validation rules that apply to the request.
+ *
+ * @return array
+ */
+ public function rules()
+ {
+ return [
+ 'user.username' => ['required', 'min:4', Rule::unique('users', 'username')->ignore($this->user) ],
+ 'user.role' => 'required|in:user,admin',
+ 'password' => ($this->user->exists ? 'nullable' : 'required') . '|min:8|confirmed',
+ ];
+ }
+
+ public function updated($property)
+ {
+ $this->validateOnly($property);
+ }
+
+ /**
+ * Save the user
+ */
+ public function save()
+ {
+ $this->authorize('administrate');
+
+ $this->validate();
+
+ if ($this->password) {
+ $this->user->password = Hash::make($this->password);
+ }
+ $this->user->save();
+
+ // Livewire redirect() does not have "with" method.
+ // so we call session()->flash() directly instead.
+ session()->flash('success', "{$this->user->username} was "
+ . ($this->user->exists ? "updated!" : "created!"));
+ return redirect()->route('admin.users.index');
+ }
+
+ public function render()
+ {
+ return view('livewire.form.admin.user');
+ }
+}
diff --git a/resources/views/admin/user/form.blade.php b/resources/views/admin/user/form.blade.php
new file mode 100644
index 0000000..a0ffe42
--- /dev/null
+++ b/resources/views/admin/user/form.blade.php
@@ -0,0 +1,11 @@
+
+
+{{ __('Admin') }} - {{ __('Users') }} - {{ __(isset($model) ? 'Edit' : 'New') }}
+
+@if (isset($model))
+
+@else
+
+@endif
+
+
diff --git a/resources/views/admin/user/index.blade.php b/resources/views/admin/user/index.blade.php
new file mode 100644
index 0000000..3fd04e7
--- /dev/null
+++ b/resources/views/admin/user/index.blade.php
@@ -0,0 +1,13 @@
+
+
+{{ __('Admin') }} - {{ __('Users') }}
+
+
+
+ {{ __('New') }}
+
+
+
+
+
+
diff --git a/resources/views/livewire/admin-user-table.blade.php b/resources/views/livewire/admin-user-table.blade.php
new file mode 100644
index 0000000..592738a
--- /dev/null
+++ b/resources/views/livewire/admin-user-table.blade.php
@@ -0,0 +1,42 @@
+
+
+
+
+
+ |
+ |
+
+
+
+ | Username |
+ Role |
+ Created |
+ Updated |
+ |
+
+
+ @foreach($users as $user)
+
+ |
+
+ {{ $user->username }}
+
+ |
+ {{ $user->role }} |
+ {{ $user->created_at }} |
+ {{ $user->updated_at }} |
+
+
+
+
+
+
+ |
+
+ @endforeach
+
+
+
+ {{ $users->links() }}
+
+
diff --git a/resources/views/livewire/form/admin/user.blade.php b/resources/views/livewire/form/admin/user.blade.php
new file mode 100644
index 0000000..f52d936
--- /dev/null
+++ b/resources/views/livewire/form/admin/user.blade.php
@@ -0,0 +1,27 @@
+
+
+
+ {{ __('Username') }}
+
+
+
+
+ {{ __('Role') }}
+
+
+
+
+
+ {{ __('Password') }}
+
+
+
+
+ {{ __('Confirm Password') }}
+
+
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php
index bdc532c..c53ac4d 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -10,6 +10,8 @@ use App\Http\Controllers\RecipeController;
use App\Http\Controllers\Auth\SessionController;
+use App\Http\Controllers\Admin\UserController as AdminUserController;
+
require "oauth.php";
/*
@@ -70,4 +72,10 @@ Route::middleware(['auth'])->group(function() {
Route::get('/edit', [UserController::class, 'edit'])->name('edit');
Route::post('/', [UserController::class, 'update'])->name('update');
});
+
+ // Admin
+ Route::middleware(['can:administrate'])->prefix('admin')->name('admin.')->group(function () {
+
+ Route::resource('users', AdminUserController::class)->except(['show', 'store', 'update']);
+ });
});
diff --git a/tests/Feature/Admin/UserTest.php b/tests/Feature/Admin/UserTest.php
new file mode 100644
index 0000000..2b1bd52
--- /dev/null
+++ b/tests/Feature/Admin/UserTest.php
@@ -0,0 +1,219 @@
+create(['role' => 'admin']);
+
+ $response = $this->actingAs($user)
+ ->get(route('admin.users.index'));
+
+ $response->assertStatus(200);
+ }
+
+ public function test_non_admin_cannot_view_users_list()
+ {
+ $user = User::factory()->create(['role' => 'user']);
+
+ // Standard user
+ $response = $this->actingAs($user)
+ ->get(route('admin.users.index'));
+
+ $response->assertForbidden("Standard user");
+
+ // Guest
+ $response = $this->get(route('admin.users.index'));
+
+ $response->assertForbidden("Guest");
+ }
+
+ public function test_admin_can_render_create_page()
+ {
+ $user = User::factory()->create(['role' => 'admin']);
+
+ $response = $this->actingAs($user)
+ ->get(route('admin.users.create'));
+
+ $response->assertStatus(200);
+ }
+
+ public function test_non_admin_cannot_render_create_page()
+ {
+ $user = User::factory()->create(['role' => 'user']);
+
+ // Standard user
+ $response = $this->actingAs($user)
+ ->get(route('admin.users.create'));
+
+ $response->assertForbidden("Standard user");
+
+ // Guest
+ $response = $this->get(route('admin.users.create'));
+
+ $response->assertForbidden("Guest");
+ }
+
+ public function test_admin_can_create_user()
+ {
+ $user = User::factory()->create(['role' => 'admin']);
+
+ $this->actingAs($user);
+
+ \Livewire::test(UserForm::class)
+ ->set('user.username', 'scammer123')
+ ->set('user.role', 'user')
+ ->set('password', 'password1234')
+ ->set('password_confirmation', 'password1234')
+ ->call('save')
+ ->assertRedirect(route('admin.users.index'));
+
+ $this->assertDatabaseHas('users', [
+ 'username' => 'scammer123',
+ 'role' => 'user'
+ ]);
+ }
+
+ public function test_non_admin_cannot_create_users()
+ {
+ // Guest
+ \Livewire::test(UserForm::class)
+ ->set('user.username', 'nonadmin')
+ ->set('user.role', 'user')
+ ->set('password', 'password1234')
+ ->set('password_confirmation', 'password1234')
+ ->call('save')
+ ->assertForbidden();
+
+ $this->assertDatabaseMissing('users', ['username' => 'nonadmin']);
+
+ // Standard user
+ $this->actingAs(User::factory()->create(['role' => 'user']));
+
+ \Livewire::test(UserForm::class)
+ ->set('user.username', 'nonadmin')
+ ->set('user.role', 'user')
+ ->set('password', 'password1234')
+ ->set('password_confirmation', 'password1234')
+ ->call('save')
+ ->assertForbidden();
+
+ $this->assertDatabaseMissing('users', ['username' => 'nonadmin']);
+ }
+
+ public function test_admin_can_render_edit_page()
+ {
+ $user = User::factory()->create(['role' => 'admin']);
+ $edit = User::factory()->create();
+
+ $response = $this->actingAs($user)
+ ->get(route('admin.users.edit', ['user' => $edit]));
+
+ $response->assertStatus(200);
+ }
+
+ public function test_non_admin_cannot_render_edit_page()
+ {
+ $user = User::factory()->create(['role' => 'user']);
+ $edit = User::factory()->create();
+
+ // Standard user
+ $response = $this->actingAs($user)
+ ->get(route('admin.users.edit', ['user' => $edit]));
+
+ $response->assertForbidden("Standard user");
+
+ // Guest
+ $response = $this->get(route('admin.users.edit', ['user' => $edit]));
+
+ $response->assertForbidden("Guest");
+ }
+
+ public function test_admin_can_edit_user()
+ {
+ $user = User::factory()->create(['role' => 'admin']);
+ $edit = User::factory()->create(['role' => 'user']);
+
+ $this->actingAs($user);
+
+ \Livewire::test(UserForm::class, [ 'user' => $edit ])
+ ->set('user.username', 'Edited')
+ ->call('save')
+ ->assertRedirect(route('admin.users.index'));
+
+ $this->assertDatabaseHas('users', [
+ 'username' => 'Edited',
+ 'role' => 'user',
+ ]);
+ }
+
+ public function test_non_admin_cannot_edit_user()
+ {
+ $edit = User::factory()->create(['username' => 'Untouched', 'role' => 'user']);
+
+ // Guest
+ \Livewire::test(UserForm::class, [ 'user' => $edit ])
+ ->set('user.username', 'Cantedit')
+ ->call('save')
+ ->assertForbidden();
+
+ $this->assertDatabaseHas('users', [
+ 'username' => 'Untouched',
+ 'role' => 'user',
+ ]);
+
+ // Standard user
+ $this->actingAs(User::factory()->create(['role' => 'user']));
+
+ \Livewire::test(UserForm::class, [ 'user' => $edit ])
+ ->set('user.username', 'Cantedit')
+ ->call('save')
+ ->assertForbidden();
+
+ $this->assertDatabaseHas('users', [
+ 'username' => 'Untouched',
+ 'role' => 'user',
+ ]);
+ }
+
+ public function test_admin_can_delete_user()
+ {
+ $user = User::factory()->create(['role' => 'admin']);
+ $delete = User::factory()->create();
+
+ $response = $this->actingAs($user)
+ ->delete(route('admin.users.destroy', [ 'user' => $delete ]));
+
+ $this->assertDatabaseMissing('users', [ 'id' => $delete->id, 'deleted_at' => NULL ]);
+ }
+
+ public function test_non_admin_cannot_delete_user()
+ {
+ $user = User::factory()->create(['role' => 'user']);
+ $delete = User::factory()->create();
+
+ // Standard user
+ $response = $this->actingAs($user)
+ ->delete(route('admin.users.destroy', [ 'user' => $delete ]));
+
+ $response->assertForbidden("Standard user");
+ $this->assertDatabaseHas('users', [ 'id' => $delete->id, 'deleted_at' => NULL ]);
+
+ // Guest
+ $response = $this->delete(route('admin.users.destroy', [ 'user' => $delete ]));
+
+ $response->assertForbidden("Guest");
+ $this->assertDatabaseHas('users', [ 'id' => $delete->id, 'deleted_at' => NULL ]);
+ }
+}