Add RBAC management (roles, permissions, users) with UI and tests

This commit is contained in:
Arya Dwi Putra
2025-12-01 08:24:59 +07:00
parent d97f359035
commit 0fb556e700
17 changed files with 852 additions and 2 deletions

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\PermissionRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use App\Models\Permission;
class PermissionController extends Controller
{
public function index(): View
{
$permissions = Permission::orderBy('name')->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.');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\RoleRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use App\Models\Permission;
use App\Models\Role;
class RoleController extends Controller
{
public function index(): View
{
$roles = Role::with('permissions')->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.');
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UserRequest;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use App\Models\Role;
class UserManagementController extends Controller
{
public function index(): View
{
$users = User::with('roles')->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.');
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PermissionRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$permissionId = $this->route('permission')?->getKey();
return [
'name' => [
'required',
'string',
'max:100',
Rule::unique('permissions', 'name')->ignore($permissionId, 'uuid'),
],
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class RoleRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$roleId = $this->route('role')?->getKey();
return [
'name' => [
'required',
'string',
'max:100',
Rule::unique('roles', 'name')->ignore($roleId, 'uuid'),
],
'permissions' => ['array'],
'permissions.*' => ['uuid', 'exists:permissions,uuid'],
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$userId = $this->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'],
];
}
}

15
app/Models/Permission.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Spatie\Permission\Models\Permission as SpatiePermission;
class Permission extends SpatiePermission
{
use HasUuids;
protected $primaryKey = 'uuid';
public $incrementing = false;
protected $keyType = 'string';
}

15
app/Models/Role.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Spatie\Permission\Models\Role as SpatieRole;
class Role extends SpatieRole
{
use HasUuids;
protected $primaryKey = 'uuid';
public $incrementing = false;
protected $keyType = 'string';
}

View File

@@ -1,5 +1,8 @@
<?php
use App\Models\Permission;
use App\Models\Role;
return [
'models' => [
@@ -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,
],

View File

@@ -122,6 +122,56 @@
System
</a>
</li>
<li class="slide {{ request()->routeIs('users.index') ? 'active' : '' }}">
<a href="{{ route('users.index') }}" class="side-menu__item">
<svg xmlns="http://www.w3.org/2000/svg" class="side-menu-doublemenu__icon"
viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<path d="M160,112a40,40,0,1,1-40-40A40,40,0,0,1,160,112Z" opacity="0.2" />
<path d="M24,200a72,72,0,0,1,144,0" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="16" />
<circle cx="120" cy="104" r="40" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="16" />
</svg>
Users
</a>
</li>
<li class="slide {{ request()->routeIs('roles.index') ? 'active' : '' }}">
<a href="{{ route('roles.index') }}" class="side-menu__item">
<svg xmlns="http://www.w3.org/2000/svg" class="side-menu-doublemenu__icon"
viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<circle cx="84" cy="108" r="52" opacity="0.2" />
<circle cx="172" cy="68" r="28" opacity="0.2" />
<path d="M84,160a76,76,0,0,0-76,76H160a76,76,0,0,0-76-76Z" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="16" />
<circle cx="84" cy="108" r="52" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="16" />
<circle cx="172" cy="68" r="28" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="16" />
<path d="M200,96a51.93,51.93,0,0,1,48,52H192a48,48,0,0,0-31.55,11.91" fill="none"
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" />
</svg>
Roles
</a>
</li>
<li class="slide {{ request()->routeIs('permissions.index') ? 'active' : '' }}">
<a href="{{ route('permissions.index') }}" class="side-menu__item">
<svg xmlns="http://www.w3.org/2000/svg" class="side-menu-doublemenu__icon"
viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<circle cx="128" cy="96" r="64" opacity="0.2" />
<circle cx="128" cy="96" r="64" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="16" />
<path d="M55.43,200a96.08,96.08,0,0,1,145.14,0" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="16" />
<line x1="128" y1="160" x2="128" y2="232" fill="none"
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="16" />
</svg>
Permissions
</a>
</li>
</ul>
</li>
<!-- End::slide -->

View File

@@ -0,0 +1,141 @@
@extends('layouts.master')
@section('content')
<div class="d-flex align-items-center justify-content-between mb-3 page-header-breadcrumb flex-wrap gap-2">
<div>
<h1 class="page-title fw-medium fs-20 mb-0">Permission Management</h1>
<p class="text-muted mb-0">Kelola daftar izin yang akan di-attach ke role.</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card custom-card">
<div class="card-header d-flex align-items-center justify-content-between">
<div>
<div class="card-title">Permissions</div>
<div class="card-subtitle">Gunakan nama yang konsisten, mis: asset.create, user.manage.</div>
</div>
<button class="btn btn-primary btn-wave" data-bs-toggle="modal" data-bs-target="#createPermissionModal">
<i class="ri-add-line me-1"></i>Tambah Permission
</button>
</div>
<div class="card-body">
@if(session('success'))
<div class="alert alert-success alert-dismissible" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Permission</th>
<th class="text-center" style="width: 10%;">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($permissions as $permission)
<tr>
<td class="fw-semibold">{{ $permission->name }}</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#editPermissionModal"
data-id="{{ $permission->getKey() }}"
data-name="{{ $permission->name }}">
Edit
</button>
</td>
</tr>
@empty
<tr>
<td colspan="2" class="text-center text-muted">Belum ada permission.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{{-- Modal create --}}
<div class="modal fade" id="createPermissionModal" tabindex="-1" aria-labelledby="createPermissionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Tambah Permission</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ route('permissions.store') }}" method="POST">
@csrf
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Nama Permission</label>
<input type="text" name="name" class="form-control" placeholder="mis: asset.view" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Simpan</button>
</div>
</form>
</div>
</div>
</div>
{{-- Modal edit --}}
<div class="modal fade" id="editPermissionModal" tabindex="-1" aria-labelledby="editPermissionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Permission</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="editPermissionForm" method="POST">
@csrf
@method('PUT')
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Nama Permission</label>
<input type="text" name="name" id="editPermissionName" class="form-control" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
const editPermissionModal = document.getElementById('editPermissionModal');
if (editPermissionModal) {
editPermissionModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
const form = document.getElementById('editPermissionForm');
form.action = "{{ url('permissions') }}/" + button.getAttribute('data-id');
document.getElementById('editPermissionName').value = button.getAttribute('data-name');
});
}
</script>
@endsection

View File

@@ -0,0 +1,152 @@
@extends('layouts.master')
@section('content')
<div class="d-flex align-items-center justify-content-between mb-3 page-header-breadcrumb flex-wrap gap-2">
<div>
<h1 class="page-title fw-medium fs-20 mb-0">Role Management</h1>
<p class="text-muted mb-0">Kelola role dan izin yang melekat pada role.</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card custom-card">
<div class="card-header d-flex align-items-center justify-content-between">
<div>
<div class="card-title">Daftar Role</div>
<div class="card-subtitle">Atur mapping role ke permission agar konsisten.</div>
</div>
<button class="btn btn-primary btn-wave" data-bs-toggle="modal" data-bs-target="#createRoleModal">
<i class="ri-add-line me-1"></i>Tambah Role
</button>
</div>
<div class="card-body">
@if(session('success'))
<div class="alert alert-success alert-dismissible" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th style="width: 20%;">Role</th>
<th>Permissions</th>
<th style="width: 10%;" class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($roles as $role)
<tr>
<td class="fw-semibold">{{ $role->name }}</td>
<td>
@if($role->permissions->isEmpty())
<span class="text-muted">Belum ada permission</span>
@else
@foreach($role->permissions as $permission)
<span class="badge bg-primary-transparent text-primary mb-1">{{ $permission->name }}</span>
@endforeach
@endif
</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#editRoleModal"
data-id="{{ $role->getKey() }}"
data-name="{{ $role->name }}"
data-permissions='@json($role->permissions->pluck("uuid"))'>
Edit
</button>
</td>
</tr>
@empty
<tr>
<td colspan="3" class="text-center text-muted">Belum ada role.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{{-- Modal create --}}
<div class="modal fade" id="createRoleModal" tabindex="-1" aria-labelledby="createRoleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Tambah Role</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ route('roles.store') }}" method="POST">
@csrf
<div class="modal-body">
@include('rbac.roles.partials.form')
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Simpan</button>
</div>
</form>
</div>
</div>
</div>
{{-- Modal edit --}}
<div class="modal fade" id="editRoleModal" tabindex="-1" aria-labelledby="editRoleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Role</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="editRoleForm" method="POST">
@csrf
@method('PUT')
<div class="modal-body">
@include('rbac.roles.partials.form')
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
const editRoleModal = document.getElementById('editRoleModal');
if (editRoleModal) {
editRoleModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
const form = document.getElementById('editRoleForm');
const permissions = JSON.parse(button.getAttribute('data-permissions') || '[]');
form.action = "{{ url('roles') }}/" + button.getAttribute('data-id');
form.querySelector('[name="name"]').value = button.getAttribute('data-name');
// reset checkboxes
form.querySelectorAll('input[name="permissions[]"]').forEach((checkbox) => {
checkbox.checked = permissions.includes(checkbox.value);
});
});
}
</script>
@endsection

View File

@@ -0,0 +1,18 @@
<div class="mb-3">
<label class="form-label">Nama Role</label>
<input type="text" name="name" class="form-control" placeholder="mis: admin" required>
</div>
<div class="mb-3">
<label class="form-label">Permissions</label>
<div class="row">
@foreach($permissions as $permission)
<div class="col-md-4 mb-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="{{ $permission->uuid }}"
id="perm-{{ $permission->uuid }}">
<label class="form-check-label" for="perm-{{ $permission->uuid }}">{{ $permission->name }}</label>
</div>
</div>
@endforeach
</div>
</div>

View File

@@ -0,0 +1,160 @@
@extends('layouts.master')
@section('content')
<div class="d-flex align-items-center justify-content-between mb-3 page-header-breadcrumb flex-wrap gap-2">
<div>
<h1 class="page-title fw-medium fs-20 mb-0">User Management</h1>
<p class="text-muted mb-0">Kelola user dan role yang diberikan.</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card custom-card">
<div class="card-header d-flex align-items-center justify-content-between">
<div>
<div class="card-title">Daftar User</div>
<div class="card-subtitle">Gunakan role untuk menentukan akses, bukan hardcode permission per user.</div>
</div>
<button class="btn btn-primary btn-wave" data-bs-toggle="modal" data-bs-target="#createUserModal">
<i class="ri-add-line me-1"></i>Tambah User
</button>
</div>
<div class="card-body">
@if(session('success'))
<div class="alert alert-success alert-dismissible" role="alert">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Nama</th>
<th>Email</th>
<th>Roles</th>
<th class="text-center" style="width: 10%;">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr>
<td class="fw-semibold">{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td>
@if($user->roles->isEmpty())
<span class="text-muted">Belum ada role</span>
@else
@foreach($user->roles as $role)
<span class="badge bg-primary-transparent text-primary mb-1">{{ $role->name }}</span>
@endforeach
@endif
</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#editUserModal"
data-id="{{ $user->getKey() }}"
data-name="{{ $user->name }}"
data-email="{{ $user->email }}"
data-roles='@json($user->roles->pluck("uuid"))'>
Edit
</button>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="text-center text-muted">Belum ada user.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{{-- Modal create --}}
<div class="modal fade" id="createUserModal" tabindex="-1" aria-labelledby="createUserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Tambah User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ route('users.store') }}" method="POST">
@csrf
<div class="modal-body">
@include('rbac.users.partials.form')
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Simpan</button>
</div>
</form>
</div>
</div>
</div>
{{-- Modal edit --}}
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="editUserForm" method="POST">
@csrf
@method('PUT')
<div class="modal-body">
@include('rbac.users.partials.form')
<div class="alert alert-info mt-2">
Password dikosongkan berarti tidak diubah.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
const editUserModal = document.getElementById('editUserModal');
if (editUserModal) {
editUserModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
const form = document.getElementById('editUserForm');
const roles = JSON.parse(button.getAttribute('data-roles') || '[]');
form.action = "{{ url('users') }}/" + button.getAttribute('data-id');
form.querySelector('[name="name"]').value = button.getAttribute('data-name');
form.querySelector('[name="email"]').value = button.getAttribute('data-email');
form.querySelector('[name="password"]').value = '';
form.querySelector('[name="password_confirmation"]').value = '';
form.querySelectorAll('input[name="roles[]"]').forEach((checkbox) => {
checkbox.checked = roles.includes(checkbox.value);
});
});
}
</script>
@endsection

View File

@@ -0,0 +1,35 @@
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Nama</label>
<input type="text" name="name" class="form-control" placeholder="Nama lengkap" required>
</div>
<div class="col-md-6">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" placeholder="email@example.com" required>
</div>
<div class="col-md-6">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-control" autocomplete="new-password" {{ request()->routeIs('users.store') ? 'required' : '' }}>
</div>
<div class="col-md-6">
<label class="form-label">Konfirmasi Password</label>
<input type="password" name="password_confirmation" class="form-control" autocomplete="new-password" {{ request()->routeIs('users.store') ? 'required' : '' }}>
</div>
</div>
<div class="mt-3">
<label class="form-label">Roles</label>
<div class="row">
@forelse($roles as $role)
<div class="col-md-4 mb-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="roles[]" value="{{ $role->uuid }}"
id="role-{{ $role->uuid }}">
<label class="form-check-label" for="role-{{ $role->uuid }}">{{ $role->name }}</label>
</div>
</div>
@empty
<div class="col-12 text-muted">Belum ada role, buat dulu di menu Roles.</div>
@endforelse
</div>
</div>

View File

@@ -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']);

View File

@@ -0,0 +1,46 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\Permission;
use App\Models\Role;
use Tests\TestCase;
class RbacTest extends TestCase
{
use RefreshDatabase;
public function test_can_create_role_and_assign_permissions(): void
{
$perm = Permission::create(['name' => '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'));
}
}