Membangun dashboard admin tidak cukup hanya membuat halaman CRUD. Begitu aplikasi memiliki banyak peran seperti admin, editor, atau staff, kebutuhan utamanya bergeser ke kontrol akses: siapa yang boleh melihat halaman tertentu, siapa yang boleh membuat data, siapa yang boleh mengedit, dan siapa yang tidak boleh menghapus.
Pada stack Laravel + Inertia.js, kontrol akses sebaiknya diterapkan di dua sisi sekaligus. Di backend, kita wajib memverifikasi otorisasi agar keamanan tidak bergantung pada UI. Di frontend, kita perlu menyembunyikan menu, tombol, dan aksi yang tidak relevan agar antarmuka lebih jelas dan tidak membingungkan pengguna. Artikel ini membahas cara menggabungkan Laravel Policies, Gates, dan Inertia.js untuk membangun dashboard admin yang aman dan tetap nyaman digunakan.
Contoh yang dipakai adalah modul Article Management dan sedikit pola untuk User Management, sehingga penerapan role dan permission terlihat nyata, bukan sekadar contoh abstrak.
Mengapa Policies dan Gates penting di dashboard admin
Dalam Laravel, otorisasi umumnya dibagi menjadi dua pendekatan:
- Gates cocok untuk izin yang tidak selalu terikat pada model tertentu, misalnya akses ke halaman dashboard, melihat laporan, atau membuka menu administrasi.
- Policies cocok untuk aksi yang berhubungan dengan model, misalnya apakah user boleh melihat, membuat, memperbarui, atau menghapus artikel tertentu.
Kombinasi keduanya sangat pas untuk dashboard admin. Sebagai contoh:
- Gunakan Gate untuk menentukan apakah user boleh membuka halaman User Management.
- Gunakan Policy untuk menentukan apakah user boleh update atau delete artikel tertentu.
Prinsip pentingnya: frontend hanya membantu presentasi, backend tetap menjadi sumber kebenaran. Artinya, meskipun tombol hapus disembunyikan di Vue atau React, controller tetap harus memanggil otorisasi. Jika tidak, user masih bisa mengakses endpoint langsung lewat HTTP request.
Struktur kasus: role, permission, dan modul artikel
Misalkan aplikasi memiliki aturan berikut:
- Admin: boleh melihat, membuat, mengedit, dan menghapus semua artikel; boleh mengelola user.
- Editor: boleh melihat semua artikel, membuat artikel, dan mengedit artikel miliknya sendiri.
- Staff: hanya boleh melihat artikel yang dipublikasikan.
Anda bisa menyimpan role dan permission dengan beberapa pendekatan. Untuk artikel ini, kita fokus pada mekanisme Laravel bawaan. Asumsikan model User memiliki kolom role sederhana. Pada aplikasi yang lebih kompleks, Anda bisa menambahkan tabel permission sendiri atau memakai paket seperti spatie/laravel-permission, tetapi prinsip integrasinya dengan Policies dan Inertia tetap sama.
Contoh helper sederhana pada model User:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function isEditor(): bool
{
return $this->role === 'editor';
}
public function canManageUsers(): bool
{
return $this->isAdmin();
}
}
Pendekatan ini cukup untuk menjelaskan alur. Jika kebutuhan izin bertambah banyak, pindahkan logika ke policy, gate, atau service khusus agar model tidak menjadi terlalu gemuk.
Mendefinisikan Gates untuk akses halaman dan menu admin
Gate berguna saat Anda ingin memproteksi area umum yang tidak terikat satu record tertentu. Misalnya, halaman user management hanya boleh diakses admin.
Di App\Providers\AuthServiceProvider atau provider otorisasi yang Anda gunakan, definisikan gate:
<?php
namespace App\Providers;
use App\Models\Article;
use App\Models\User;
use App\Policies\ArticlePolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
Article::class => ArticlePolicy::class,
];
public function boot(): void
{
$this->registerPolicies();
Gate::define('view-admin-dashboard', function (User $user) {
return in_array($user->role, ['admin', 'editor']);
});
Gate::define('manage-users', function (User $user) {
return $user->canManageUsers();
});
}
}
Dengan gate ini, Anda bisa melindungi route atau controller untuk halaman dashboard dan user management.
Contoh route:
use App\Http\Controllers\Admin\UserController;
use App\Http\Controllers\Admin\DashboardController;
Route::middleware(['auth'])->prefix('admin')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])
->middleware('can:view-admin-dashboard');
Route::resource('users', UserController::class)
->middleware('can:manage-users');
});
Proteksi di level route cocok untuk kasus yang sederhana. Tetapi pada banyak aplikasi, pengecekan juga tetap perlu dilakukan di controller agar lebih eksplisit dan mudah dibaca saat tim berkembang.
Membuat Policy untuk Article Management
Untuk resource seperti artikel, policy adalah tempat yang tepat karena keputusan izin sering bergantung pada pemilik artikel, status publikasi, atau kondisi lain.
Buat policy:
php artisan make:policy ArticlePolicy --model=ArticleContoh implementasi:
<?php
namespace App\Policies;
use App\Models\Article;
use App\Models\User;
class ArticlePolicy
{
public function viewAny(User $user): bool
{
return in_array($user->role, ['admin', 'editor', 'staff']);
}
public function view(User $user, Article $article): bool
{
if ($user->isAdmin() || $user->isEditor()) {
return true;
}
return $article->status === 'published';
}
public function create(User $user): bool
{
return in_array($user->role, ['admin', 'editor']);
}
public function update(User $user, Article $article): bool
{
if ($user->isAdmin()) {
return true;
}
return $user->isEditor() && $article->author_id === $user->id;
}
public function delete(User $user, Article $article): bool
{
return $user->isAdmin();
}
}
Beberapa hal penting dari policy di atas:
viewAnydipakai untuk daftar artikel.createtidak butuh objek artikel karena aksinya terjadi sebelum model dibuat.updatememeriksa kepemilikan artikel untuk editor.deletedibatasi hanya untuk admin.
Ini lebih aman dan rapi dibanding menulis if-else otorisasi berulang di banyak controller.
Pengecekan authorization di controller Inertia
Pada aplikasi Inertia, controller tetap merupakan lapisan penting karena di sinilah data dikirim ke frontend. Jangan hanya memfilter tombol di sisi client; lakukan pengecekan otorisasi sebelum render halaman atau sebelum menyimpan perubahan.
Proteksi halaman index dan form
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Article;
use Illuminate\Http\Request;
use Inertia\Inertia;
class ArticleController extends Controller
{
public function index(Request $request)
{
$this->authorize('viewAny', Article::class);
$articles = Article::query()
->latest()
->select('id', 'title', 'status', 'author_id', 'created_at')
->with('author:id,name')
->paginate(10)
->through(fn ($article) => [
'id' => $article->id,
'title' => $article->title,
'status' => $article->status,
'author' => $article->author?->name,
'can' => [
'update' => $request->user()->can('update', $article),
'delete' => $request->user()->can('delete', $article),
],
]);
return Inertia::render('Admin/Articles/Index', [
'articles' => $articles,
'can' => [
'create' => $request->user()->can('create', Article::class),
],
]);
}
public function create()
{
$this->authorize('create', Article::class);
return Inertia::render('Admin/Articles/Create');
}
}
Pola ini penting karena frontend menerima dua jenis informasi:
- Otorisasi halaman, misalnya user boleh membuka form create atau tidak.
- Otorisasi per-item, misalnya artikel A bisa diedit tetapi artikel B tidak.
Perhatikan bahwa kita tidak mengirim seluruh daftar role atau permission mentah ke frontend. Kita hanya mengirim kemampuan yang relevan untuk tampilan saat itu, misalnya can.create, can.update, dan can.delete. Ini lebih aman, lebih kecil, dan lebih mudah digunakan di komponen Inertia.
Proteksi aksi simpan, update, dan hapus
public function store(Request $request)
{
$this->authorize('create', Article::class);
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
'status' => ['required', 'in:draft,published'],
]);
Article::create([
...$validated,
'author_id' => $request->user()->id,
]);
return redirect()->route('articles.index')
->with('success', 'Artikel berhasil dibuat.');
}
public function edit(Article $article)
{
$this->authorize('update', $article);
return Inertia::render('Admin/Articles/Edit', [
'article' => $article,
]);
}
public function update(Request $request, Article $article)
{
$this->authorize('update', $article);
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
'status' => ['required', 'in:draft,published'],
]);
$article->update($validated);
return redirect()->route('articles.index')
->with('success', 'Artikel berhasil diperbarui.');
}
public function destroy(Article $article)
{
$this->authorize('delete', $article);
$article->delete();
return redirect()->route('articles.index')
->with('success', 'Artikel berhasil dihapus.');
}
Ini adalah lapisan keamanan utama. Walaupun user memanipulasi request dari browser, Laravel akan mengembalikan respons 403 Forbidden jika otorisasi gagal.
Mengirim kemampuan user ke frontend secara aman
Pada aplikasi Inertia, sering ada kebutuhan global seperti menampilkan atau menyembunyikan menu sidebar berdasarkan izin. Cara yang umum adalah membagikan data otorisasi melalui middleware Inertia, misalnya HandleInertiaRequests.
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user()
? [
'id' => $request->user()->id,
'name' => $request->user()->name,
'role' => $request->user()->role,
]
: null,
'can' => $request->user()
? [
'viewAdminDashboard' => $request->user()->can('view-admin-dashboard'),
'manageUsers' => $request->user()->can('manage-users'),
]
: [],
],
]);
}
}
Di frontend, data ini bisa dipakai untuk menampilkan menu hanya jika user berhak.
<script setup>
import { usePage, Link } from '@inertiajs/vue3'
const page = usePage()
const auth = page.props.auth
</script>
<template>
<nav>
<Link v-if="auth.can.viewAdminDashboard" href="/admin/dashboard">Dashboard</Link>
<Link v-if="auth.can.manageUsers" href="/admin/users">User Management</Link>
</nav>
</template>
Catatan penting: jangan mengirim seluruh matriks permission mentah jika tidak dibutuhkan. Selain menambah ukuran payload, itu bisa membuka struktur internal otorisasi aplikasi yang sebenarnya tidak perlu diketahui semua komponen frontend. Kirim hanya kemampuan yang memang diperlukan oleh layout atau halaman terkait.
Menampilkan tombol aksi sesuai izin di halaman Inertia
Pada halaman daftar artikel, Anda biasanya ingin tombol Create, Edit, dan Delete hanya muncul jika user punya izin. Cara paling aman adalah mengirim kemampuan tersebut dari controller, bukan menghitungnya sepenuhnya di client.
<script setup>
import { Link, router, usePage } from '@inertiajs/vue3'
const props = defineProps({
articles: Object,
can: Object,
})
function destroyArticle(id) {
if (confirm('Hapus artikel ini?')) {
router.delete(`/admin/articles/${id}`)
}
}
</script>
<template>
<div>
<Link v-if="can.create" href="/admin/articles/create">Buat Artikel</Link>
<table>
<tr v-for="article in articles.data" :key="article.id">
<td>{{ article.title }}</td>
<td>{{ article.status }}</td>
<td>{{ article.author }}</td>
<td>
<Link v-if="article.can.update" :href="`/admin/articles/${article.id}/edit`">Edit</Link>
<button v-if="article.can.delete" @click="destroyArticle(article.id)">Hapus</button>
</td>
</tr>
</table>
</div>
</template>
Pola ini memberi dua manfaat:
- UI lebih bersih, karena user hanya melihat aksi yang relevan.
- Frontend tetap sederhana, karena komponen tidak perlu mengetahui seluruh aturan bisnis otorisasi.
Namun ingat sekali lagi: penyembunyian tombol bukan pengganti otorisasi backend.
Contoh tambahan: User Management dengan Gate
Untuk modul user management, biasanya izin akses bersifat global. Misalnya hanya admin yang boleh membuka daftar user, membuat user baru, atau menonaktifkan akun. Karena sifatnya lebih ke area administrasi daripada kepemilikan model tertentu, Gate bisa cukup.
public function index(Request $request)
{
abort_unless($request->user()->can('manage-users'), 403);
return Inertia::render('Admin/Users/Index', [
'users' => User::query()
->select('id', 'name', 'email', 'role')
->paginate(10),
]);
}
Jika aturan user management semakin kompleks, misalnya admin biasa tidak boleh menghapus super admin, maka lebih baik pindahkan ke UserPolicy agar aturan per-record menjadi lebih jelas dan teruji.
Kesalahan umum dan tips debugging
1. Hanya mengandalkan frontend
Kesalahan paling sering adalah menyembunyikan tombol di UI lalu merasa fitur sudah aman. Ini tidak cukup. Endpoint tetap harus memanggil $this->authorize(), middleware can:, atau pengecekan gate/policy yang setara.
2. Mengirim data kemampuan terlalu banyak
Mengirim seluruh permission ke semua halaman membuat payload membesar dan coupling frontend-backend meningkat. Lebih baik kirim kemampuan global yang benar-benar dibutuhkan layout, lalu kirim kemampuan spesifik dari controller masing-masing halaman.
3. Aturan otorisasi tersebar di banyak tempat
Jika sebagian aturan ditulis di controller, sebagian di komponen frontend, dan sebagian di helper acak, sistem cepat menjadi sulit dipelihara. Letakkan keputusan inti di policy atau gate agar konsisten.
4. Lupa memetakan policy ke model
Jika policy tidak terdaftar dengan benar, pemanggilan authorize() bisa gagal atau tidak bekerja sesuai harapan. Pastikan policy diregistrasikan dan cache aplikasi dibersihkan jika perlu.
5. Sulit melacak kenapa akses ditolak
Gunakan Tinker, log sementara, atau unit test untuk memverifikasi hasil policy. Anda juga bisa memeriksa langsung:
$user->can('update', $article);
Gate::forUser($user)->allows('manage-users');Jika hasilnya tidak sesuai ekspektasi, periksa data role user, ownership model, policy registration, dan apakah route model binding mengembalikan record yang benar.
Penutup
Membangun dashboard admin dengan Inertia.js dan Laravel akan jauh lebih solid jika otorisasi dirancang sejak awal. Gunakan Gates untuk akses halaman atau area administratif yang bersifat umum, dan gunakan Policies untuk aksi terhadap model seperti artikel atau user tertentu.
Untuk hasil yang aman dan nyaman dipakai, terapkan tiga lapisan berikut secara konsisten:
- Proteksi route atau controller agar akses ilegal ditolak oleh backend.
- Pengecekan authorization pada aksi CRUD dengan
authorize(), gate, atau middlewarecan:. - Pengiriman kemampuan ke frontend secara selektif agar menu dan tombol aksi tampil sesuai izin tanpa membocorkan detail yang tidak perlu.
Dengan pola ini, dashboard admin Anda tidak hanya lebih rapi di sisi UI, tetapi juga benar secara arsitektur dan keamanan. Saat aplikasi berkembang dan role bertambah kompleks, struktur berbasis Gates dan Policies akan jauh lebih mudah dipelihara dibanding aturan akses yang tersebar acak di berbagai komponen.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!