Add RBAC management (roles, permissions, users) with UI and tests
This commit is contained in:
35
app/Http/Controllers/PermissionController.php
Normal file
35
app/Http/Controllers/PermissionController.php
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Http/Controllers/RoleController.php
Normal file
40
app/Http/Controllers/RoleController.php
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Http/Controllers/UserManagementController.php
Normal file
43
app/Http/Controllers/UserManagementController.php
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Http/Requests/PermissionRequest.php
Normal file
28
app/Http/Requests/PermissionRequest.php
Normal 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'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Http/Requests/RoleRequest.php
Normal file
30
app/Http/Requests/RoleRequest.php
Normal 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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Http/Requests/UserRequest.php
Normal file
32
app/Http/Requests/UserRequest.php
Normal 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
15
app/Models/Permission.php
Normal 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
15
app/Models/Role.php
Normal 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';
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Permission;
|
||||||
|
use App\Models\Role;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'models' => [
|
'models' => [
|
||||||
@@ -13,7 +16,7 @@ return [
|
|||||||
* `Spatie\Permission\Contracts\Permission` contract.
|
* `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
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
@@ -24,7 +27,7 @@ return [
|
|||||||
* `Spatie\Permission\Contracts\Role` contract.
|
* `Spatie\Permission\Contracts\Role` contract.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'role' => Spatie\Permission\Models\Role::class,
|
'role' => Role::class,
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,56 @@
|
|||||||
System
|
System
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<!-- End::slide -->
|
<!-- End::slide -->
|
||||||
|
|||||||
141
resources/views/rbac/permissions/index.blade.php
Normal file
141
resources/views/rbac/permissions/index.blade.php
Normal 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
|
||||||
152
resources/views/rbac/roles/index.blade.php
Normal file
152
resources/views/rbac/roles/index.blade.php
Normal 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
|
||||||
18
resources/views/rbac/roles/partials/form.blade.php
Normal file
18
resources/views/rbac/roles/partials/form.blade.php
Normal 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>
|
||||||
160
resources/views/rbac/users/index.blade.php
Normal file
160
resources/views/rbac/users/index.blade.php
Normal 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
|
||||||
35
resources/views/rbac/users/partials/form.blade.php
Normal file
35
resources/views/rbac/users/partials/form.blade.php
Normal 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>
|
||||||
@@ -4,6 +4,9 @@ use Illuminate\Support\Facades\Route;
|
|||||||
|
|
||||||
use App\Http\Controllers\DashboardsController;
|
use App\Http\Controllers\DashboardsController;
|
||||||
use App\Http\Controllers\SystemSettingController;
|
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;
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
@@ -19,3 +22,7 @@ Route::get('/', function () {
|
|||||||
Route::get('index', [DashboardsController::class, 'index']);
|
Route::get('index', [DashboardsController::class, 'index']);
|
||||||
|
|
||||||
Route::resource('settings', SystemSettingController::class)->only(['index', 'store', 'update', 'destroy']);
|
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']);
|
||||||
|
|||||||
46
tests/Feature/RbacTest.php
Normal file
46
tests/Feature/RbacTest.php
Normal 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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user