From 0fb556e700b0c0353bf3b825876fe8bc00e67085 Mon Sep 17 00:00:00 2001 From: Arya Dwi Putra Date: Mon, 1 Dec 2025 08:24:59 +0700 Subject: [PATCH] Add RBAC management (roles, permissions, users) with UI and tests --- app/Http/Controllers/PermissionController.php | 35 ++++ app/Http/Controllers/RoleController.php | 40 +++++ .../Controllers/UserManagementController.php | 43 +++++ app/Http/Requests/PermissionRequest.php | 28 +++ app/Http/Requests/RoleRequest.php | 30 ++++ app/Http/Requests/UserRequest.php | 32 ++++ app/Models/Permission.php | 15 ++ app/Models/Role.php | 15 ++ config/permission.php | 7 +- .../layouts/components/main-sidebar.blade.php | 50 ++++++ .../views/rbac/permissions/index.blade.php | 141 +++++++++++++++ resources/views/rbac/roles/index.blade.php | 152 +++++++++++++++++ .../views/rbac/roles/partials/form.blade.php | 18 ++ resources/views/rbac/users/index.blade.php | 160 ++++++++++++++++++ .../views/rbac/users/partials/form.blade.php | 35 ++++ routes/web.php | 7 + tests/Feature/RbacTest.php | 46 +++++ 17 files changed, 852 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/PermissionController.php create mode 100644 app/Http/Controllers/RoleController.php create mode 100644 app/Http/Controllers/UserManagementController.php create mode 100644 app/Http/Requests/PermissionRequest.php create mode 100644 app/Http/Requests/RoleRequest.php create mode 100644 app/Http/Requests/UserRequest.php create mode 100644 app/Models/Permission.php create mode 100644 app/Models/Role.php create mode 100644 resources/views/rbac/permissions/index.blade.php create mode 100644 resources/views/rbac/roles/index.blade.php create mode 100644 resources/views/rbac/roles/partials/form.blade.php create mode 100644 resources/views/rbac/users/index.blade.php create mode 100644 resources/views/rbac/users/partials/form.blade.php create mode 100644 tests/Feature/RbacTest.php diff --git a/app/Http/Controllers/PermissionController.php b/app/Http/Controllers/PermissionController.php new file mode 100644 index 0000000..bcdd231 --- /dev/null +++ b/app/Http/Controllers/PermissionController.php @@ -0,0 +1,35 @@ +get(); + + return view('rbac.permissions.index', compact('permissions')); + } + + public function store(PermissionRequest $request): RedirectResponse + { + Permission::create([ + 'name' => $request->input('name'), + 'guard_name' => 'web', + ]); + + return back()->with('success', 'Permission berhasil ditambahkan.'); + } + + public function update(PermissionRequest $request, Permission $permission): RedirectResponse + { + $permission->update(['name' => $request->input('name')]); + + return back()->with('success', 'Permission berhasil diperbarui.'); + } +} diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php new file mode 100644 index 0000000..e5781ec --- /dev/null +++ b/app/Http/Controllers/RoleController.php @@ -0,0 +1,40 @@ +orderBy('name')->get(); + $permissions = Permission::orderBy('name')->get(); + + return view('rbac.roles.index', compact('roles', 'permissions')); + } + + public function store(RoleRequest $request): RedirectResponse + { + $role = Role::create([ + 'name' => $request->input('name'), + 'guard_name' => 'web', + ]); + + $role->syncPermissions($request->input('permissions', [])); + + return back()->with('success', 'Role berhasil ditambahkan.'); + } + + public function update(RoleRequest $request, Role $role): RedirectResponse + { + $role->update(['name' => $request->input('name')]); + $role->syncPermissions($request->input('permissions', [])); + + return back()->with('success', 'Role berhasil diperbarui.'); + } +} diff --git a/app/Http/Controllers/UserManagementController.php b/app/Http/Controllers/UserManagementController.php new file mode 100644 index 0000000..d2b4d12 --- /dev/null +++ b/app/Http/Controllers/UserManagementController.php @@ -0,0 +1,43 @@ +orderBy('name')->get(); + $roles = Role::orderBy('name')->get(); + + return view('rbac.users.index', compact('users', 'roles')); + } + + public function store(UserRequest $request): RedirectResponse + { + $user = User::create($request->validated()); + + $user->syncRoles($request->input('roles', [])); + + return back()->with('success', 'User berhasil ditambahkan.'); + } + + public function update(UserRequest $request, User $user): RedirectResponse + { + $data = $request->validated(); + + if (empty($data['password'])) { + unset($data['password']); + } + + $user->update($data); + $user->syncRoles($request->input('roles', [])); + + return back()->with('success', 'User berhasil diperbarui.'); + } +} diff --git a/app/Http/Requests/PermissionRequest.php b/app/Http/Requests/PermissionRequest.php new file mode 100644 index 0000000..3ed1bba --- /dev/null +++ b/app/Http/Requests/PermissionRequest.php @@ -0,0 +1,28 @@ +route('permission')?->getKey(); + + return [ + 'name' => [ + 'required', + 'string', + 'max:100', + Rule::unique('permissions', 'name')->ignore($permissionId, 'uuid'), + ], + ]; + } +} diff --git a/app/Http/Requests/RoleRequest.php b/app/Http/Requests/RoleRequest.php new file mode 100644 index 0000000..3ebbbc1 --- /dev/null +++ b/app/Http/Requests/RoleRequest.php @@ -0,0 +1,30 @@ +route('role')?->getKey(); + + return [ + 'name' => [ + 'required', + 'string', + 'max:100', + Rule::unique('roles', 'name')->ignore($roleId, 'uuid'), + ], + 'permissions' => ['array'], + 'permissions.*' => ['uuid', 'exists:permissions,uuid'], + ]; + } +} diff --git a/app/Http/Requests/UserRequest.php b/app/Http/Requests/UserRequest.php new file mode 100644 index 0000000..accc1f7 --- /dev/null +++ b/app/Http/Requests/UserRequest.php @@ -0,0 +1,32 @@ +route('user')?->getKey(); + + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'email', + 'max:255', + Rule::unique('users', 'email')->ignore($userId), + ], + 'password' => [$this->isMethod('post') ? 'required' : 'nullable', 'string', 'min:8', 'confirmed'], + 'roles' => ['array'], + 'roles.*' => ['uuid', 'exists:roles,uuid'], + ]; + } +} diff --git a/app/Models/Permission.php b/app/Models/Permission.php new file mode 100644 index 0000000..af8365b --- /dev/null +++ b/app/Models/Permission.php @@ -0,0 +1,15 @@ + [ @@ -13,7 +16,7 @@ return [ * `Spatie\Permission\Contracts\Permission` contract. */ - 'permission' => Spatie\Permission\Models\Permission::class, + 'permission' => Permission::class, /* * When using the "HasRoles" trait from this package, we need to know which @@ -24,7 +27,7 @@ return [ * `Spatie\Permission\Contracts\Role` contract. */ - 'role' => Spatie\Permission\Models\Role::class, + 'role' => Role::class, ], diff --git a/resources/views/layouts/components/main-sidebar.blade.php b/resources/views/layouts/components/main-sidebar.blade.php index 733bd0f..fc01f70 100644 --- a/resources/views/layouts/components/main-sidebar.blade.php +++ b/resources/views/layouts/components/main-sidebar.blade.php @@ -122,6 +122,56 @@ System +
  • + + + + + + + + Users + +
  • +
  • + + + + + + + + + + + Roles + +
  • +
  • + + + + + + + + + Permissions + +
  • diff --git a/resources/views/rbac/permissions/index.blade.php b/resources/views/rbac/permissions/index.blade.php new file mode 100644 index 0000000..36fc421 --- /dev/null +++ b/resources/views/rbac/permissions/index.blade.php @@ -0,0 +1,141 @@ +@extends('layouts.master') + +@section('content') +
    +
    +

    Permission Management

    +

    Kelola daftar izin yang akan di-attach ke role.

    +
    +
    + +
    +
    +
    +
    +
    +
    Permissions
    +
    Gunakan nama yang konsisten, mis: asset.create, user.manage.
    +
    + +
    +
    + @if(session('success')) + + @endif + + @if($errors->any()) +
    +
      + @foreach($errors->all() as $error) +
    • {{ $error }}
    • + @endforeach +
    +
    + @endif + +
    + + + + + + + + + @forelse($permissions as $permission) + + + + + @empty + + + + @endforelse + +
    PermissionAksi
    {{ $permission->name }} + +
    Belum ada permission.
    +
    +
    +
    +
    +
    + + {{-- Modal create --}} + + + {{-- Modal edit --}} + +@endsection + +@section('scripts') + +@endsection diff --git a/resources/views/rbac/roles/index.blade.php b/resources/views/rbac/roles/index.blade.php new file mode 100644 index 0000000..a67a87d --- /dev/null +++ b/resources/views/rbac/roles/index.blade.php @@ -0,0 +1,152 @@ +@extends('layouts.master') + +@section('content') +
    +
    +

    Role Management

    +

    Kelola role dan izin yang melekat pada role.

    +
    +
    + +
    +
    +
    +
    +
    +
    Daftar Role
    +
    Atur mapping role ke permission agar konsisten.
    +
    + +
    +
    + @if(session('success')) + + @endif + + @if($errors->any()) +
    +
      + @foreach($errors->all() as $error) +
    • {{ $error }}
    • + @endforeach +
    +
    + @endif + +
    + + + + + + + + + + @forelse($roles as $role) + + + + + + @empty + + + + @endforelse + +
    RolePermissionsAksi
    {{ $role->name }} + @if($role->permissions->isEmpty()) + Belum ada permission + @else + @foreach($role->permissions as $permission) + {{ $permission->name }} + @endforeach + @endif + + +
    Belum ada role.
    +
    +
    +
    +
    +
    + + {{-- Modal create --}} + + + {{-- Modal edit --}} + +@endsection + +@section('scripts') + +@endsection diff --git a/resources/views/rbac/roles/partials/form.blade.php b/resources/views/rbac/roles/partials/form.blade.php new file mode 100644 index 0000000..7d416c7 --- /dev/null +++ b/resources/views/rbac/roles/partials/form.blade.php @@ -0,0 +1,18 @@ +
    + + +
    +
    + +
    + @foreach($permissions as $permission) +
    +
    + + +
    +
    + @endforeach +
    +
    diff --git a/resources/views/rbac/users/index.blade.php b/resources/views/rbac/users/index.blade.php new file mode 100644 index 0000000..fb2c54c --- /dev/null +++ b/resources/views/rbac/users/index.blade.php @@ -0,0 +1,160 @@ +@extends('layouts.master') + +@section('content') +
    +
    +

    User Management

    +

    Kelola user dan role yang diberikan.

    +
    +
    + +
    +
    +
    +
    +
    +
    Daftar User
    +
    Gunakan role untuk menentukan akses, bukan hardcode permission per user.
    +
    + +
    +
    + @if(session('success')) + + @endif + + @if($errors->any()) +
    +
      + @foreach($errors->all() as $error) +
    • {{ $error }}
    • + @endforeach +
    +
    + @endif + +
    + + + + + + + + + + + @forelse($users as $user) + + + + + + + @empty + + + + @endforelse + +
    NamaEmailRolesAksi
    {{ $user->name }}{{ $user->email }} + @if($user->roles->isEmpty()) + Belum ada role + @else + @foreach($user->roles as $role) + {{ $role->name }} + @endforeach + @endif + + +
    Belum ada user.
    +
    +
    +
    +
    +
    + + {{-- Modal create --}} + + + {{-- Modal edit --}} + +@endsection + +@section('scripts') + +@endsection diff --git a/resources/views/rbac/users/partials/form.blade.php b/resources/views/rbac/users/partials/form.blade.php new file mode 100644 index 0000000..00cfdbc --- /dev/null +++ b/resources/views/rbac/users/partials/form.blade.php @@ -0,0 +1,35 @@ +
    +
    + + +
    +
    + + +
    +
    + + routeIs('users.store') ? 'required' : '' }}> +
    +
    + + routeIs('users.store') ? 'required' : '' }}> +
    +
    + +
    + +
    + @forelse($roles as $role) +
    +
    + + +
    +
    + @empty +
    Belum ada role, buat dulu di menu Roles.
    + @endforelse +
    +
    diff --git a/routes/web.php b/routes/web.php index 55cddbe..9f64ff4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,6 +4,9 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\DashboardsController; use App\Http\Controllers\SystemSettingController; +use App\Http\Controllers\RoleController; +use App\Http\Controllers\PermissionController; +use App\Http\Controllers\UserManagementController; use App\Http\Controllers\Controller; Route::get('/', function () { @@ -19,3 +22,7 @@ Route::get('/', function () { Route::get('index', [DashboardsController::class, 'index']); Route::resource('settings', SystemSettingController::class)->only(['index', 'store', 'update', 'destroy']); + +Route::resource('roles', RoleController::class)->only(['index', 'store', 'update']); +Route::resource('permissions', PermissionController::class)->only(['index', 'store', 'update']); +Route::resource('users', UserManagementController::class)->only(['index', 'store', 'update']); diff --git a/tests/Feature/RbacTest.php b/tests/Feature/RbacTest.php new file mode 100644 index 0000000..ea30c05 --- /dev/null +++ b/tests/Feature/RbacTest.php @@ -0,0 +1,46 @@ + 'asset.create', 'guard_name' => 'web']); + + $response = $this->post(route('roles.store'), [ + 'name' => 'admin', + 'permissions' => [$perm->getKey()], + ]); + + $response->assertRedirect(); + $this->assertDatabaseHas('roles', ['name' => 'admin']); + $role = Role::whereName('admin')->first(); + $this->assertTrue($role->hasPermissionTo('asset.create')); + } + + public function test_can_create_user_and_attach_role(): void + { + $role = Role::create(['name' => 'viewer', 'guard_name' => 'web']); + + $response = $this->post(route('users.store'), [ + 'name' => 'Jane', + 'email' => 'jane@example.com', + 'password' => 'password123', + 'password_confirmation' => 'password123', + 'roles' => [$role->getKey()], + ]); + + $response->assertRedirect(); + $user = User::whereEmail('jane@example.com')->first(); + $this->assertTrue($user->hasRole('viewer')); + } +}