Home Paket Belajar Bootcamp Instruktur

Tutorial Laravel Authorization #7 - UI Manajemen Role & Permission dari Halaman Admin

Pelajari sistem Authorization di Laravel dari nol hingga implementasi dalam studi kasus nyata melalui 5 episode terstruktur. Ebook ini membahas konsep Authentication vs Authorization, Gates, Policies, fitur lanjutan, hingga menggabungkan seluruh konsep dalam sebuah aplikasi blog. Setiap materi dilengkapi penjelasan konsep, contoh kode siap pakai, tabel perbandingan, dan praktik terbaik agar mudah dipahami oleh developer Laravel pemula maupun menengah.

✅ Telah dilihat 34 kali

Rating: 5.00 ⭐

... 11 June 2026, 14:31

Setelah membaca episode ini, teman-teman diharapkan bisa:

  • Membuat halaman CRUD untuk Role dari panel admin
  • Membuat halaman assign role ke user
  • Melindungi semua route admin dengan middleware Spatie
  • Menjaga agar role admin tidak bisa dihapus atau diubah secara tidak sengaja (guard)

1. Gambaran Fitur yang Akan Dibangun

Admin Panel
├── /admin/roles
│   ├── index   — daftar semua role + permission yang dimiliki
│   ├── create  — form buat role baru + pilih permissions
│   ├── edit    — ubah nama role + permissions
│   └── destroy — hapus role (dengan guard)
│
└── /admin/users
    ├── index   — daftar user + role mereka
    └── edit    — assign/cabut role untuk user tertentu

2. Buat Controller

php artisan make:controller Admin/RoleController --resource
php artisan make:controller Admin/UserRoleController

3. Setup Routes

routes/web.php:

use App\Http\Controllers\Admin\RoleController;
use App\Http\Controllers\Admin\UserRoleController;

Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->group(function () {

    // Dashboard
    Route::get('/dashboard', [Admin\DashboardController::class, 'index'])->name('dashboard');

    // Manajemen Role
    Route::resource('roles', RoleController::class);

    // Assign Role ke User
    Route::get('/users', [UserRoleController::class, 'index'])->name('users.index');
    Route::get('/users/{user}/edit', [UserRoleController::class, 'edit'])->name('users.edit');
    Route::put('/users/{user}', [UserRoleController::class, 'update'])->name('users.update');
});

4. RoleController — CRUD Role

app/Http/Controllers/Admin/RoleController.php:

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

class RoleController extends Controller
{
    // Daftar semua role
    public function index()
    {
        $roles = Role::with('permissions')->get();
        return view('admin.roles.index', compact('roles'));
    }

    // Form buat role baru
    public function create()
    {
        $permissions = Permission::orderBy('name')->get();
        return view('admin.roles.create', compact('permissions'));
    }

    // Simpan role baru
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name'        => 'required|string|max:64|unique:roles,name',
            'permissions' => 'nullable|array',
            'permissions.*' => 'exists:permissions,name',
        ]);

        $role = Role::create(['name' => $validated['name']]);

        if (!empty($validated['permissions'])) {
            $role->syncPermissions($validated['permissions']);
        }

        return redirect()->route('admin.roles.index')
            ->with('success', "Role \"{$role->name}\" berhasil dibuat.");
    }

    // Form edit role
    public function edit(Role $role)
    {
        $permissions    = Permission::orderBy('name')->get();
        $rolePermissions = $role->permissions->pluck('name')->toArray();

        return view('admin.roles.edit', compact('role', 'permissions', 'rolePermissions'));
    }

    // Update role
    public function update(Request $request, Role $role)
    {
        // Jaga role admin dari perubahan nama
        if ($role->name === 'admin') {
            return back()->with('error', 'Role admin tidak bisa diubah.');
        }

        $validated = $request->validate([
            'name'          => "required|string|max:64|unique:roles,name,{$role->id}",
            'permissions'   => 'nullable|array',
            'permissions.*' => 'exists:permissions,name',
        ]);

        $role->update(['name' => $validated['name']]);
        $role->syncPermissions($validated['permissions'] ?? []);

        return redirect()->route('admin.roles.index')
            ->with('success', "Role \"{$role->name}\" berhasil diperbarui.");
    }

    // Hapus role
    public function destroy(Role $role)
    {
        // Jaga role admin dari penghapusan
        if ($role->name === 'admin') {
            return back()->with('error', 'Role admin tidak bisa dihapus.');
        }

        // Cegah hapus role yang masih dipakai user
        if ($role->users()->count() > 0) {
            return back()->with('error', "Role \"{$role->name}\" masih digunakan oleh {$role->users()->count()} user.");
        }

        $role->delete();

        return redirect()->route('admin.roles.index')
            ->with('success', "Role \"{$role->name}\" berhasil dihapus.");
    }
}

5. UserRoleController — Assign Role ke User

app/Http/Controllers/Admin/UserRoleController.php:

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Spatie\Permission\Models\Role;

class UserRoleController extends Controller
{
    // Daftar semua user + role mereka
    public function index()
    {
        $users = User::with('roles')->latest()->paginate(15);
        return view('admin.users.index', compact('users'));
    }

    // Form assign role untuk user tertentu
    public function edit(User $user)
    {
        $roles     = Role::orderBy('name')->get();
        $userRoles = $user->roles->pluck('name')->toArray();

        return view('admin.users.edit', compact('user', 'roles', 'userRoles'));
    }

    // Simpan perubahan role user
    public function update(Request $request, User $user)
    {
        // Cegah admin mencabut role admin dari dirinya sendiri
        if ($user->id === auth()->id() && !in_array('admin', $request->roles ?? [])) {
            return back()->with('error', 'teman-teman tidak bisa mencabut role admin dari akunmu sendiri.');
        }

        $request->validate([
            'roles'   => 'nullable|array',
            'roles.*' => 'exists:roles,name',
        ]);

        $user->syncRoles($request->roles ?? []);

        return redirect()->route('admin.users.index')
            ->with('success', "Role untuk {$user->name} berhasil diperbarui.");
    }
}

6. Blade Templates

resources/views/admin/roles/index.blade.php

<x-app-layout>
    <div class="max-w-4xl mx-auto py-8">
        <div class="flex justify-between items-center mb-6">
            <h1 class="text-2xl font-bold">Manajemen Role</h1>
            <a href="{{ route('admin.roles.create') }}" class="btn btn-primary">+ Tambah Role</a>
        </div>

        @if (session('success'))
            <div class="alert alert-success mb-4">{{ session('success') }}</div>
        @endif
        @if (session('error'))
            <div class="alert alert-danger mb-4">{{ session('error') }}</div>
        @endif

        <table class="w-full border-collapse">
            <thead>
                <tr class="bg-gray-100">
                    <th class="p-3 text-left">Role</th>
                    <th class="p-3 text-left">Permissions</th>
                    <th class="p-3 text-left">Jumlah User</th>
                    <th class="p-3 text-left">Aksi</th>
                </tr>
            </thead>
            <tbody>
                @foreach ($roles as $role)
                    <tr class="border-b">
                        <td class="p-3">
                            <span class="font-semibold">{{ $role->name }}</span>
                            @if ($role->name === 'admin')
                                <span class="text-xs text-orange-500 ml-1">(terlindungi)</span>
                            @endif
                        </td>
                        <td class="p-3">
                            <div class="flex flex-wrap gap-1">
                                @forelse ($role->permissions as $permission)
                                    <span class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded">
                                        {{ $permission->name }}
                                    </span>
                                @empty
                                    <span class="text-gray-400 text-sm">—</span>
                                @endforelse
                            </div>
                        </td>
                        <td class="p-3">{{ $role->users()->count() }} user</td>
                        <td class="p-3 flex gap-2">
                            @if ($role->name !== 'admin')
                                <a href="{{ route('admin.roles.edit', $role) }}"
                                   class="text-sm text-blue-600 hover:underline">Edit</a>

                                <form action="{{ route('admin.roles.destroy', $role) }}" method="POST"
                                      onsubmit="return confirm('Hapus role {{ $role->name }}?')">
                                    @csrf @method('DELETE')
                                    <button type="submit" class="text-sm text-red-600 hover:underline">
                                        Hapus
                                    </button>
                                </form>
                            @else
                                <span class="text-sm text-gray-400">—</span>
                            @endif
                        </td>
                    </tr>
                @endforeach
            </tbody>
        </table>
    </div>
</x-app-layout>

resources/views/admin/roles/create.blade.php

<x-app-layout>
    <div class="max-w-2xl mx-auto py-8">
        <h1 class="text-2xl font-bold mb-6">Tambah Role Baru</h1>

        <form action="{{ route('admin.roles.store') }}" method="POST">
            @csrf

            {{-- Nama Role --}}
            <div class="mb-4">
                <label class="block font-medium mb-1">Nama Role</label>
                <input type="text" name="name" value="{{ old('name') }}"
                       class="w-full border rounded px-3 py-2 @error('name') border-red-500 @enderror"
                       placeholder="contoh: moderator">
                @error('name')
                    <p class="text-red-500 text-sm mt-1">{{ $message }}</p>
                @enderror
            </div>

            {{-- Pilih Permissions --}}
            <div class="mb-6">
                <label class="block font-medium mb-2">Permissions</label>

                {{-- Kelompokkan permission berdasarkan prefix (post., user., dsb.) --}}
                @php
                    $grouped = $permissions->groupBy(fn($p) => explode('.', $p->name)[0]);
                @endphp

                @foreach ($grouped as $group => $perms)
                    <div class="mb-3">
                        <p class="text-sm font-semibold text-gray-600 uppercase mb-1">{{ $group }}</p>
                        <div class="flex flex-wrap gap-3">
                            @foreach ($perms as $permission)
                                <label class="flex items-center gap-2 cursor-pointer">
                                    <input type="checkbox"
                                           name="permissions[]"
                                           value="{{ $permission->name }}"
                                           {{ in_array($permission->name, old('permissions', [])) ? 'checked' : '' }}>
                                    <span class="text-sm">{{ $permission->name }}</span>
                                </label>
                            @endforeach
                        </div>
                    </div>
                @endforeach
            </div>

            <div class="flex gap-3">
                <button type="submit" class="btn btn-primary">Simpan Role</button>
                <a href="{{ route('admin.roles.index') }}" class="btn btn-secondary">Batal</a>
            </div>
        </form>
    </div>
</x-app-layout>

resources/views/admin/users/edit.blade.php

<x-app-layout>
    <div class="max-w-xl mx-auto py-8">
        <h1 class="text-2xl font-bold mb-2">Edit Role User</h1>
        <p class="text-gray-500 mb-6">{{ $user->name }} — {{ $user->email }}</p>

        <form action="{{ route('admin.users.update', $user) }}" method="POST">
            @csrf @method('PUT')

            <div class="mb-6">
                <label class="block font-medium mb-2">Assign Role</label>
                <div class="flex flex-col gap-2">
                    @foreach ($roles as $role)
                        <label class="flex items-center gap-3 cursor-pointer p-2 rounded hover:bg-gray-50">
                            <input type="checkbox"
                                   name="roles[]"
                                   value="{{ $role->name }}"
                                   {{ in_array($role->name, $userRoles) ? 'checked' : '' }}>
                            <div>
                                <span class="font-medium">{{ $role->name }}</span>
                                <span class="text-xs text-gray-400 ml-2">
                                    {{ $role->permissions->count() }} permissions
                                </span>
                            </div>
                        </label>
                    @endforeach
                </div>
            </div>

            <div class="flex gap-3">
                <button type="submit" class="btn btn-primary">Simpan Perubahan</button>
                <a href="{{ route('admin.users.index') }}" class="btn btn-secondary">Batal</a>
            </div>
        </form>
    </div>
</x-app-layout>

7. Tips: Cache Permission di Produksi

Spatie melakukan query ke database setiap request untuk memuat permissions. Di produksi, aktifkan cache:

config/permission.php:

'cache' => [
    'expiration_time'  => \DateInterval::createFromDateString('24 hours'),
    'key'              => 'spatie.permission.cache',
    'store'            => 'default', // ganti ke 'redis' di produksi
],

Dan selalu jalankan ini setiap deploy jika ada perubahan role/permission:

php artisan permission:cache-reset

Atau panggil dari seeder/command:

app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();

Ringkasan Episode

  • RoleController menangani CRUD role — dengan guard untuk melindungi role admin dari penghapusan/perubahan nama
  • UserRoleController menangani assign role ke user — dengan guard agar admin tidak bisa mencabut rolenya sendiri
  • Permissions dikelompokkan berdasarkan prefix (post.user.settings.) agar form lebih rapi
  • Gunakan syncPermissions() untuk update permissions role, dan syncRoles() untuk update roles user
  • Di produksi, aktifkan cache permission dan reset setiap deploy

Preview Episode Berikutnya

Di Episode 8, kita akan membahas penggunaan Spatie untuk API — mengamankan endpoint Laravel dengan Laravel Sanctum + Spatie Permission, termasuk cara mengembalikan data role/permission di response JSON dan proteksi route API berdasarkan permission.

Daftar eBook