Laravel Inertia Vue 3: Autentikasi, RBAC & Upload File
Lanjutan Laravel Inertia Vue 3 berfokus pada pengamanan dan penskalaan aplikasi Single Page Application (SPA) menuju standar produksi melalui implementasi autentikasi terpusat, otorisasi berbasis peran (Role-Based Access Control / RBAC), paginasi sisi server, serta unggah berkas asinkron via FormData. Menguasai empat pilar arsitektur ini mengubah prototipe CRUD dasar menjadi sistem perangkat lunak SaaS dan Enterprise Architecture yang aman, cepat, serta siap menampung puluhan ribu baris data.
Pada bagian pertama seri ini, kita telah membangun fondasi operasi Create, Read, Update, dan Delete tanpa perlu menulis titik akhir REST API terpisah. Namun, saat saya memimpin migrasi sistem manajemen inventaris klien ke infrastruktur Cloud VPS tahun lalu, fitur CRUD dasar tersebut langsung menghadapi tiga kendala serius begitu data menembus 50.000 baris dan mulai diakses oleh berbagai divisi.
Tanpa pembatasan hak akses di sisi server, staf operasional biasa dapat memicu penghapusan data sensitif hanya dengan memanipulasi komponen Vue di peramban. Dalam panduan lanjutan ini, saya akan membedah cara menyuntikkan izin akses secara global melalui Inertia Middleware, membangun paginasi berkinerja tinggi, serta menangani unggah berkas dengan indikator progres langsung.
1. Prasyarat Teknis & Komparasi Arsitektur State Produksi
Sebelum melanjutkan penulisan kode, pastikan proyek dari artikel sebelumnya sudah berjalan stabil dengan tumpukan teknologi berikut: PHP 8.3+, Laravel 11 atau 12, Inertia.js v2.0, Vue 3.5+ (Composition API dengan <script setup>), serta Vite sebagai bundler aset.
Dalam arsitektur SPA modern, kesalahan paling fatal yang sering dilakukan pengembang adalah mengandalkan penyembunyian tombol di antarmuka Vue sebagai satu-satunya mekanisme keamanan. Tabel berikut membandingkan cara mengelola autentikasi, paginasi, dan pengiriman berkas pada arsitektur SPA konvensional versus pendekatan Inertia.js skala produksi:
Komponen Fitur | SPA Tradisional (Axios + JWT) | Laravel Inertia Vue 3 (Produksi) | Dampak Keamanan & Performa |
|---|---|---|---|
Autentikasi & Sesi | Menyimpan token di | Sesi berbasis HttpOnly Cookie terenkripsi bawaan Laravel. | Perlindungan penuh terhadap pencurian sesi dan serangan CSRF. |
Otorisasi (RBAC) | Memanggil endpoint | Injeksi daftar | Nol permintaan jaringan tambahan & validasi tetap terkunci di Policy backend. |
Paginasi Data Besar | Memuat seluruh JSON ke memori browser lalu memfilter di klien. |
| Konsumsi RAM peramban stabil dan halaman dapat di-bookmark. |
Unggah Berkas | Konversi Base64 dalam JSON (membengkakkan ukuran 33%). | Konversi otomatis ke | Beban CPU server ringan dengan pelacakan persentase progres asli. |
2. Langkah 1: Berbagi Data Autentikasi & Izin Akses (RBAC) Global
Agar setiap komponen halaman di Vue 3 mengetahui siapa pengguna yang sedang masuk dan tindakan apa saja yang diizinkan, kita memanfaatkan mekanisme Shared Data pada middleware app/Http/Middleware/HandleInertiaRequests.php. Pendekatan ini menjaga lapisan API Security tetap konsisten antara tampilan antarmuka dan aturan otorisasi di backend.
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
protected $rootView = 'app';
public function share(Request $request): array
{
$user = $request->user();
return [
...parent::share($request),
'auth' => [
'user' => $user ? [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'role' => $user->role,
] : null,
// Peta izin eksplisit untuk kontrol elemen UI di Vue 3
'can' => [
'create_product' => $user ? $user->can('create', \App\Models\Product::class) : false,
'delete_product' => $user ? $user->role === 'admin' : false,
],
],
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
],
];
}
}Bedah Penjelasan Kode Middleware: Menggunakan closure fn () => $request->session()->get('success') memastikan pesan kilat (flash message) hanya dievaluasi saat benar-benar dibutuhkan pada siklus respons. Sementara itu, objek auth.can menerjemahkan aturan Laravel Policy langsung menjadi nilai boolean ringan yang siap dibaca oleh komponen Vue tanpa mengekspos struktur tabel basis data internal.
Tips Praktis & Optimasi Performa dari Pengalaman Saya: Jangan pernah mengirimkan seluruh objek model
$request->user()secara mentah ke dalam arrayshare(). Dalam sebuah audit keamanan aplikasi SaaS, saya pernah menemukan atribut sensitif sepertitwo_factor_secretdan token internal ikut bocor ke dalam atribut HTMLdata-pagehanya karena pengembang tidak menyaring kolom pengguna secara eksplisit seperti pada contoh array di atas.
3. Langkah 2: Controller dengan Otorisasi Policy, Paginasi & Upload File
Selanjutnya, kita memperbarui ProductController.php untuk menangani tiga tugas berat sekaligus: memvalidasi hak akses melalui Gate::authorize(), menyaring pencarian serta paginasi data di tingkat SQL, dan menyimpan berkas gambar produk ke disk penyimpanan publik maupun Cloud Storage (S3).
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Inertia\Inertia;
class ProductController extends Controller
{
public function index(Request $request)
{
$products = Product::query()
->when($request->input('search'), function ($query, $search) {
$query->where('name', 'like', "%{$search}%")
->orWhere('sku', 'like', "%{$search}%");
})
->latest()
->paginate(10)
->withQueryString();
return Inertia::render('Products/Index', [
'products' => $products,
'filters' => $request->only(['search']),
]);
}
public function update(Request $request, Product $product)
{
// Kunci keamanan utama tetap berada di sisi server
Gate::authorize('update', $product);
$validated = $request->validate([
'name' => ['required', 'string', 'max:150'],
'price' => ['required', 'numeric', 'min:0'],
'thumbnail' => ['nullable', 'image', 'mimes:webp,jpg,png', 'max:2048'],
]);
if ($request->hasFile('thumbnail')) {
if ($product->thumbnail_path) {
Storage::disk('public')->delete($product->thumbnail_path);
}
$validated['thumbnail_path'] = $request->file('thumbnail')->store('products', 'public');
}
$product->update($validated);
return redirect()->route('products.index')
->with('success', 'Data produk dan aset gambar berhasil diperbarui.');
}
}Bedah Penjelasan Kode Controller: Pemanggilan metode ->withQueryString() setelah paginate(10) sangat krusial agar parameter pencarian ?search=... tetap menempel ketika pengguna berpindah ke halaman kedua atau ketiga. Selain itu, blok Storage::disk('public')->delete() mencegah penumpukan berkas yatim (orphaned files) yang sering menghabiskan kapasitas SSD pada server produksi.

4. Langkah 3: Komponen Vue 3 untuk Pencarian Debounce, Paginasi & Upload FormData
Di sisi frontend Vue 3, kita akan memadukan helper router.get() dengan fitur partial reload dan useForm(). Perhatikan cara khusus menangani metode HTTP PUT/PATCH saat mengirimkan berkas menggunakan kunci _method: 'PUT' agar PHP dapat memproses muatan multipart/form-data dengan benar.
<script setup>
import { ref, watch } from 'vue';
import { router, useForm, usePage, Link } from '@inertiajs/vue3';
const props = defineProps({
products: Object,
filters: Object,
});
// Mengakses shared props auth dan izin akses secara reaktif
const page = usePage();
const canCreate = page.props.auth.can.create_product;
// 1. Pencarian Server-Side dengan Debounce Manual Tanpa Library Eksternal
const search = ref(props.filters.search || '');
let searchTimeout = null;
watch(search, (newValue) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
router.get(
'/products',
{ search: newValue },
{
preserveState: true,
preserveScroll: true,
replace: true,
only: ['products', 'filters'],
}
);
}, 350);
});
// 2. Pengelolaan Form Edit & Upload File dengan Indikator Progres
const editForm = useForm({
_method: 'PUT',
name: '',
price: 0,
thumbnail: null,
});
const submitUpdate = (productId) => {
// Wajib gunakan POST dengan spoof _method: 'PUT' saat membawa objek File
editForm.post(`/products/${productId}`, {
forceFormData: true,
preserveScroll: true,
onSuccess: () => editForm.reset('thumbnail'),
});
};
</script>
<template>
<div class="p-6 max-w-5xl mx-auto">
<div class="flex justify-between items-center mb-6">
<input
v-model="search"
type="search"
placeholder="Cari nama produk atau SKU..."
class="border rounded-lg px-4 py-2 w-72"
/>
<Link
v-if="canCreate"
href="/products/create"
class="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium"
>
+ Tambah Produk
</Link>
</div>
<!-- Indikator Progres Upload Berkas Real-time -->
<div v-if="editForm.progress" class="w-full bg-slate-200 rounded-full h-2.5 mb-4">
<div
class="bg-blue-600 h-2.5 rounded-full transition-all"
:style="{ width: editForm.progress.percentage + '%' }"
></div>
</div>
<!-- Navigasi Paginasi Bawaan Laravel -->
<nav class="flex gap-1 mt-6" aria-label="Pagination">
<template v-for="(link, index) in products.links" :key="index">
<Link
v-if="link.url"
:href="link.url"
v-html="link.label"
preserve-scroll
preserve-state
class="px-3 py-1.5 border rounded text-sm"
:class="{ 'bg-slate-900 text-white font-bold': link.active }"
/>
<span v-else v-html="link.label" class="px-3 py-1.5 text-slate-400 text-sm" />
</template>
</nav>
</div>
</template>Bedah Penjelasan Kode Vue 3: Opsi only: ['products', 'filters'] pada panggilan router.get() menginstruksikan server Laravel untuk tidak mengeksekusi ulang query shared props lain yang tidak relevan, sehingga beban CPU basis data turun drastis. Sedangkan penggunaan replace: true mencegah riwayat tombol Back peramban dipenuhi oleh setiap ketikan huruf pada kotak pencarian.

Troubleshooting & Skenario Error Handling di Lapangan: Salah satu jebakan paling sering dialami pengembang saat memperbarui data dengan unggahan file adalah memanggil
editForm.put('/products/1')secara langsung. Secara bawaan, engine PHP tidak dapat mem-parsing payloadmultipart/form-datapada metode HTTPPUTmurni, sehingga$request->all()dan$request->file()akan terbaca kosong (null) dan memicu kegagalan validasiThe name field is required. Selalu gunakaneditForm.post()yang dipasangkan dengan properti_method: 'PUT'seperti pada kode di atas agar method spoofing Laravel bekerja sempurna.
5. FAQ Teknis: Pertanyaan Seputar Laravel Inertia Vue 3 Skala Produksi
1. Apakah menyembunyikan tombol dengan v-if="canCreate" di Vue sudah cukup mengamankan aplikasi?
Tidak. Pengaturan v-if di Vue hanya berfungsi untuk pengalaman pengguna (UX) agar antarmuka terlihat bersih. Anda wajib selalu menyertakan Gate::authorize() atau FormRequest di dalam Controller Laravel karena penyerang dapat memodifikasi state JavaScript di peramban dan mengirim permintaan POST secara manual.
2. Mengapa gambar yang baru diunggah tidak muncul dan menghasilkan error 404 Not Found?
Hal ini terjadi karena direktori storage/app/public belum terhubung ke folder publik web server. Jalankan perintah php artisan storage:link pada terminal server produksi Anda untuk membuat tautan simbolis (symlink) ke public/storage.
3. Bagaimana cara mencegah komponen layout utama ikut ter-render ulang saat berpindah halaman paginasi?
Gunakan fitur Persistent Layouts pada Inertia Vue 3 dengan mendefinisikan properti defineOptions({ layout: AuthenticatedLayout }) di dalam komponen halaman. Dengan begitu, bilah navigasi samping dan status menu tidak akan kehilangan posisi gulir saat tabel data berpindah halaman.
4. Kapan sebaiknya beralih dari Inertia.js ke arsitektur REST/GraphQL API terpisah?
Inertia.js sangat ideal apabila backend Laravel Anda melayani antarmuka web SPA tunggal. Namun, jika Anda berencana membangun aplikasi seluler asli (iOS/Android) atau membuka API publik bagi mitra pihak ketiga dalam waktu dekat, gunakan Laravel Sanctum dengan REST API agar lapisan backend dapat digunakan ulang lintas platform.
Diskusi & Komentar
0Sampaikan pertanyaan teknis, masukan, atau feedback Anda seputar artikel ini.
Belum Ada Komentar
Jadilah pembaca pertama yang memulai diskusi menarik atau bertanya seputar materi artikel ini!