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 @@ +
+ + + + + + + + + + + + + + + + + @foreach($users as $user) + + + + + + + + @endforeach +
UsernameRoleCreatedUpdated 
+ + {{ $user->username }} + + {{ $user->role }}{{ $user->created_at }}{{ $user->updated_at }} + + + + + +
+ +
+ {{ $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 ]); + } +}