From e404aee576408d7020a3550599540229a2126b95 Mon Sep 17 00:00:00 2001 From: Saufi Date: Mon, 11 May 2026 14:13:25 +0800 Subject: [PATCH] tambah relation user-role --- .claude/settings.local.json | 3 - app/Http/Controllers/UserController.php | 19 +++++ app/Http/Requests/UpdateUserRolesRequest.php | 28 ++++++ app/Models/Role.php | 6 ++ app/Models/User.php | 6 ++ ...26_05_11_044620_create_role_user_table.php | 28 ++++++ resources/views/layouts/navigation.blade.php | 10 ++- resources/views/users/_table.blade.php | 8 +- resources/views/users/edit.blade.php | 65 ++++++++++++++ resources/views/users/index.blade.php | 5 +- routes/web.php | 4 +- tests/Feature/EditUserTest.php | 85 +++++++++++++++++++ 12 files changed, 256 insertions(+), 11 deletions(-) create mode 100644 app/Http/Requests/UpdateUserRolesRequest.php create mode 100644 database/migrations/2026_05_11_044620_create_role_user_table.php create mode 100644 resources/views/users/edit.blade.php create mode 100644 tests/Feature/EditUserTest.php diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4dc6383..59f1246 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,10 +1,7 @@ { "permissions": { "allow": [ -<<<<<<< HEAD -======= "mcp__laravel-boost__database-schema", ->>>>>>> role-module "Bash(php artisan *)", "Bash(vendor/bin/pint --dirty --format agent)", "mcp__laravel-boost__search-docs" diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 6675d8a..2163c02 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -2,8 +2,12 @@ namespace App\Http\Controllers; +use App\Http\Requests\UpdateUserRolesRequest; +use App\Models\Role; use App\Models\User; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\View\View; class UserController extends Controller { @@ -17,4 +21,19 @@ class UserController extends Controller return view('users.index', compact('users')); } + + public function edit(User $user): View + { + $roles = Role::orderBy('name')->get(); + + return view('users.edit', compact('user', 'roles')); + } + + public function update(UpdateUserRolesRequest $request, User $user): RedirectResponse + { + $user->roles()->sync($request->validated()['roles'] ?? []); + + return redirect()->route('users.index') + ->with('status', 'user-updated'); + } } diff --git a/app/Http/Requests/UpdateUserRolesRequest.php b/app/Http/Requests/UpdateUserRolesRequest.php new file mode 100644 index 0000000..03b8b09 --- /dev/null +++ b/app/Http/Requests/UpdateUserRolesRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + 'roles' => ['nullable', 'array'], + 'roles.*' => ['integer', 'exists:roles,id'], + ]; + } +} diff --git a/app/Models/Role.php b/app/Models/Role.php index a68d904..c8829b4 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -5,6 +5,7 @@ namespace App\Models; use Database\Factories\RoleFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Role extends Model { @@ -12,4 +13,9 @@ class Role extends Model use HasFactory; protected $fillable = ['name', 'description']; + + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class); + } } diff --git a/app/Models/User.php b/app/Models/User.php index f6ba1d2..ee6a6d4 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,9 @@ class User extends Authenticatable 'password' => 'hashed', ]; } + + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class); + } } diff --git a/database/migrations/2026_05_11_044620_create_role_user_table.php b/database/migrations/2026_05_11_044620_create_role_user_table.php new file mode 100644 index 0000000..bc623bd --- /dev/null +++ b/database/migrations/2026_05_11_044620_create_role_user_table.php @@ -0,0 +1,28 @@ +foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('role_id')->constrained()->cascadeOnDelete(); + $table->primary(['user_id', 'role_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('role_user'); + } +}; diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 63fc66a..9b508b8 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -15,9 +15,10 @@ {{ __('Dashboard') }} - + {{ __('Users') }} - + + {{ __('Roles') }} @@ -75,9 +76,10 @@ {{ __('Dashboard') }} - + {{ __('Users') }} - + + {{ __('Roles') }} diff --git a/resources/views/users/_table.blade.php b/resources/views/users/_table.blade.php index 93b983a..67667f6 100644 --- a/resources/views/users/_table.blade.php +++ b/resources/views/users/_table.blade.php @@ -5,6 +5,7 @@ {{ __('Name') }} {{ __('Email') }} {{ __('Joined') }} + @@ -14,12 +15,17 @@ {{ $user->name }} {{ $user->email }} {{ $user->created_at->format('d M Y') }} + + + {{ __('Edit') }} + + @endforeach @if ($users->isEmpty()) - + {{ __('No users found.') }} diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php new file mode 100644 index 0000000..5341c6c --- /dev/null +++ b/resources/views/users/edit.blade.php @@ -0,0 +1,65 @@ + + +

+ {{ __('Edit User') }}: {{ $user->name }} +

+
+ +
+
+
+
+
+
+

+ {{ __('Assign Roles') }} +

+ +

+ {{ __('Select the roles to assign to this user.') }} +

+
+ +
+ @csrf + @method('put') + +
+ @forelse ($roles as $role) + + @empty +

{{ __('No roles available. Create one first.') }}

+ @endforelse +
+ + + + +
+ {{ __('Save Roles') }} + + + {{ __('Cancel') }} + +
+ +
+
+
+
+
+
diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php index 9e63aaa..90168b0 100644 --- a/resources/views/users/index.blade.php +++ b/resources/views/users/index.blade.php @@ -21,9 +21,10 @@ this.loading = false; } }" - @click.prevent=" + @click=" const link = $event.target.closest('a[href]'); - if (link && $refs.tableContainer.contains(link)) { + if (link && link.closest('nav[role=navigation]') && $refs.tableContainer.contains(link)) { + $event.preventDefault(); paginate(link.href); } " diff --git a/routes/web.php b/routes/web.php index fb24986..3e6a2b5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,8 +1,8 @@ group(function () { Route::get('/users', [UserController::class, 'index'])->name('users.index'); + Route::get('/users/{user}/edit', [UserController::class, 'edit'])->name('users.edit'); + Route::put('/users/{user}', [UserController::class, 'update'])->name('users.update'); Route::get('/roles', [RoleController::class, 'index'])->name('roles.index'); Route::get('/roles/create', [RoleController::class, 'create'])->name('roles.create'); Route::post('/roles', [RoleController::class, 'store'])->name('roles.store'); diff --git a/tests/Feature/EditUserTest.php b/tests/Feature/EditUserTest.php new file mode 100644 index 0000000..eeb9c82 --- /dev/null +++ b/tests/Feature/EditUserTest.php @@ -0,0 +1,85 @@ +create(); + + $this->get(route('users.edit', $user))->assertRedirect('/login'); +}); + +test('authenticated users can access the edit user page', function () { + $user = User::factory()->create(); + + $this->actingAs($user) + ->get(route('users.edit', $user)) + ->assertOk() + ->assertViewIs('users.edit') + ->assertViewHas('user', $user) + ->assertViewHas('roles'); +}); + +test('edit page shows all available roles', function () { + $roles = Role::factory()->count(3)->create(); + $user = User::factory()->create(); + + $this->actingAs($user) + ->get(route('users.edit', $user)) + ->assertOk() + ->assertViewHas('roles', fn ($viewRoles) => $viewRoles->count() === $roles->count()); +}); + +test('can assign roles to a user', function () { + $user = User::factory()->create(); + $roles = Role::factory()->count(2)->create(); + + $this->actingAs($user) + ->put(route('users.update', $user), ['roles' => $roles->pluck('id')->all()]) + ->assertRedirect(route('users.index')) + ->assertSessionHas('status', 'user-updated'); + + expect($user->roles()->count())->toBe(2); +}); + +test('can remove all roles from a user', function () { + $user = User::factory()->create(); + $role = Role::factory()->create(); + $user->roles()->attach($role); + + $this->actingAs($user) + ->put(route('users.update', $user), []) + ->assertRedirect(route('users.index')); + + expect($user->roles()->count())->toBe(0); +}); + +test('syncs roles replacing previous assignments', function () { + $user = User::factory()->create(); + $oldRole = Role::factory()->create(); + $newRole = Role::factory()->create(); + $user->roles()->attach($oldRole); + + $this->actingAs($user) + ->put(route('users.update', $user), ['roles' => [$newRole->id]]); + + expect($user->roles()->pluck('id')->all())->toBe([$newRole->id]); +}); + +test('role ids must exist in the database', function () { + $user = User::factory()->create(); + + $this->actingAs($user) + ->put(route('users.update', $user), ['roles' => [999]]) + ->assertSessionHasErrors('roles.*'); +}); + +test('guests cannot update user roles', function () { + $user = User::factory()->create(); + $role = Role::factory()->create(); + + $this->put(route('users.update', $user), ['roles' => [$role->id]]) + ->assertRedirect('/login'); + + expect($user->roles()->count())->toBe(0); +});