diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 7796359..9a21442 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Http\Requests\UpdateUserRequest; +use App\Models\Role; use App\Models\User; use Illuminate\Http\RedirectResponse; use Illuminate\View\View; @@ -28,8 +29,14 @@ class UserController extends Controller */ public function edit(User $user): View { + $user->load('roles:id'); + $roles = Role::query() + ->orderBy('name') + ->get(['id', 'name']); + return view('user.edit', [ 'user' => $user, + 'roles' => $roles, ]); } @@ -38,13 +45,19 @@ class UserController extends Controller */ public function update(UpdateUserRequest $request, User $user): RedirectResponse { - $user->fill($request->validated()); + $validated = $request->validated(); + $roleIds = $validated['roles'] ?? []; + + unset($validated['roles']); + + $user->fill($validated); if ($user->isDirty('email')) { $user->email_verified_at = null; } $user->save(); + $user->roles()->sync($roleIds); return redirect() ->route('user.edit', $user) diff --git a/app/Http/Requests/UpdateUserRequest.php b/app/Http/Requests/UpdateUserRequest.php index 469ee8f..26a00b5 100644 --- a/app/Http/Requests/UpdateUserRequest.php +++ b/app/Http/Requests/UpdateUserRequest.php @@ -32,6 +32,8 @@ class UpdateUserRequest extends FormRequest 'max:255', Rule::unique('users', 'email')->ignore($this->route('user')), ], + 'roles' => ['nullable', 'array'], + 'roles.*' => ['integer', 'exists:roles,id'], ]; } } diff --git a/app/Models/Role.php b/app/Models/Role.php index c5fa26b..1ff0fd7 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -4,6 +4,16 @@ namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; #[Fillable(['name'])] -class Role extends Model {} +class Role extends Model +{ + /** + * The users that belong to the role. + */ + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index f6ba1d2..4ec3016 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,6 +7,7 @@ use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -29,4 +30,12 @@ class User extends Authenticatable 'password' => 'hashed', ]; } + + /** + * The roles that belong to the user. + */ + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class); + } } diff --git a/database/migrations/2026_05_11_040743_create_role_user_table.php b/database/migrations/2026_05_11_040743_create_role_user_table.php new file mode 100644 index 0000000..fce2c58 --- /dev/null +++ b/database/migrations/2026_05_11_040743_create_role_user_table.php @@ -0,0 +1,29 @@ +foreignId('role_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + + $table->primary(['role_id', 'user_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('role_user'); + } +}; diff --git a/resources/views/user/edit.blade.php b/resources/views/user/edit.blade.php index 399a4f1..9b93407 100644 --- a/resources/views/user/edit.blade.php +++ b/resources/views/user/edit.blade.php @@ -31,6 +31,30 @@ +
+ + +
+ @forelse ($roles as $role) + + @empty +

{{ __('No roles available.') }}

+ @endforelse +
+ + + +
+
{{ __('Save') }} diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index 7d4b744..93393de 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -1,5 +1,6 @@ create(); $user = User::factory()->create(); + $role = Role::query()->create(['name' => 'Admin']); + $user->roles()->attach($role->id); $response = $this ->actingAs($actingUser) @@ -20,18 +23,24 @@ test('authenticated users can view the user edit page', function () { ->assertSuccessful() ->assertSee('Edit User') ->assertSee($user->name) - ->assertSee($user->email); + ->assertSee($user->email) + ->assertSee('Roles') + ->assertSee('Admin') + ->assertSee('checked', false); }); test('authenticated users can update a user', function () { $actingUser = User::factory()->create(); $user = User::factory()->create(); + $adminRole = Role::query()->create(['name' => 'Admin']); + $editorRole = Role::query()->create(['name' => 'Editor']); $response = $this ->actingAs($actingUser) ->patch("/user/{$user->id}", [ 'name' => 'Updated User', 'email' => 'updated@example.com', + 'roles' => [$adminRole->id, $editorRole->id], ]); $response @@ -41,4 +50,25 @@ test('authenticated users can update a user', function () { $this->assertSame('Updated User', $user->name); $this->assertSame('updated@example.com', $user->email); + expect($user->roles()->pluck('roles.id')->all()) + ->toMatchArray([$adminRole->id, $editorRole->id]); +}); + +test('user roles can be cleared by submitting no roles', function () { + $actingUser = User::factory()->create(); + $user = User::factory()->create(); + $role = Role::query()->create(['name' => 'Admin']); + + $user->roles()->attach($role->id); + + $response = $this + ->actingAs($actingUser) + ->patch("/user/{$user->id}", [ + 'name' => $user->name, + 'email' => $user->email, + ]); + + $response->assertRedirect("/user/{$user->id}/edit"); + + expect($user->refresh()->roles()->count())->toBe(0); });