Pertemuan 05: Sistem Autentikasi, Registrasi Anggota, dan Hak Akses Pengguna (Spatie RBAC)
1. Pengantar: Membedakan Siapa yang Masuk dan Apa yang Boleh Dilakukan
Deskripsi Singkat:
Dalam sistem informasi perpustakaan, kita memiliki berbagai jenis pengguna dengan kepentingan berbeda: pengunjung umum, anggota terdaftar (mahasiswa/dosen), petugas perpustakaan (pustakawan), dan administrator sistem. Untuk mengelolanya, kita membedakan dua konsep fundamental:
Autentikasi (Authentication): Menjawab pertanyaan "Siapakah Anda?" (proses verifikasi identitas lewat email dan password).
Otorisasi (Authorization): Menjawab pertanyaan "Apa saja yang boleh Anda lakukan?" (hak akses berbasis peran / Role-Based Access Control).
2. Keamanan Autentikasi: Password Hashing & Proteksi CSRF
Deskripsi Singkat:
Laravel menyediakan standar keamanan tinggi secara otomatis:
A. Password Hashing (Bcrypt)
Password tidak boleh disimpan dalam bentuk teks biasa (plain text). Di Laravel, password di-hash secara satu arah menggunakan algoritma Bcrypt dengan salt acak melalui Hash::make($password). Hash satu arah artinya password tidak dapat di-dekripsi kembali; verifikasi login dilakukan dengan mencocokkan hash input dengan hash di database menggunakan Hash::check().
B. Proteksi Cross-Site Request Forgery (CSRF)
Setiap kali kita membuat form HTML dengan method POST, PUT, atau DELETE, kita wajib menyematkan direktif @csrf di dalam tag form. Direktif ini menyisipkan token acak rahasia. Saat form dikirim, middleware VerifyCsrfToken di Laravel akan memvalidasi token tersebut untuk memastikan request benar-benar berasal dari website kita sendiri, bukan dari website jahat pihak ketiga.
3. Menyiapkan Paket Spatie Laravel-Permission
Deskripsi Singkat:
Untuk mengelola role dan izin akses secara dinamis dan aman, kita memanfaatkan paket terpercaya spatie/laravel-permission.
A. Jalankan Migrasi Spatie:
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
Perintah ini akan membuat tabel sistem: roles, permissions, model_has_roles, model_has_permissions, dan role_has_permissions.
B. Menambahkan Trait HasRoles pada Model User (app/Models/User.php):
Buka file app/Models/User.php dan pastikan menggunakan trait HasRoles:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasFactory, Notifiable, HasRoles;
protected $fillable = [
'name',
'email',
'password',
'phone',
'address',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
4. Seeder Role dan Hak Akses (database/seeders/RoleAndPermissionSeeder.php)
Deskripsi Singkat:
Kita membuat tiga role utama: admin, librarian (pustakawan), dan user (anggota), lengkap dengan izin akses spesifik masing-masing.
Salin kode berikut ke dalam file database/seeders/RoleAndPermissionSeeder.php:
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
class RoleAndPermissionSeeder extends Seeder
{
public function run(): void
{
// Reset cache permission Spatie
app()[PermissionRegistrar::class]->forgetCachedPermissions();
// 1. Definisikan daftar izin akses operasional perpustakaan
$permissions = [
'dashboard.view',
'dashboard-user.view',
'books.view',
'books.manage',
'ebooks.view',
'ebooks.read',
'ebooks.manage',
'loans.view-own',
'loans.manage',
'inventory.stock-opname',
'memberships.manage',
'reports.view',
'settings.manage',
'roles.manage',
];
foreach ($permissions as $permissionName) {
Permission::firstOrCreate(['name' => $permissionName, 'guard_name' => 'web']);
}
// 2. Buat Role dan kaitkan permission
$adminRole = Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
$adminRole->syncPermissions(Permission::all());
$librarianRole = Role::firstOrCreate(['name' => 'librarian', 'guard_name' => 'web']);
$librarianRole->syncPermissions([
'dashboard.view',
'books.view',
'books.manage',
'ebooks.view',
'ebooks.read',
'ebooks.manage',
'loans.manage',
'inventory.stock-opname',
'memberships.manage',
'reports.view',
]);
$userRole = Role::firstOrCreate(['name' => 'user', 'guard_name' => 'web']);
$userRole->syncPermissions([
'dashboard-user.view',
'books.view',
'ebooks.view',
'ebooks.read',
'loans.view-own',
]);
}
}
Jalankan seeder dengan perintah:
php artisan db:seed --class=RoleAndPermissionSeeder
5. Rute Autentikasi dan Dashboard pada routes/web.php
Deskripsi Singkat:
Buka file routes/web.php dan pastikan rute autentikasi publik dan rute dashboard terproteksi didaftarkan:
use App\Http\Controllers\AuthController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\RoleController;
// Rute Publik untuk Login & Register
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
Route::get('/register', [AuthController::class, 'showRegister'])->name('register');
Route::post('/register', [AuthController::class, 'register']);
// Rute Terproteksi Sesi Login
Route::middleware(['auth'])->group(function () {
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
// Dashboard Pengelola / Pustakawan
Route::get('/dashboard', [DashboardController::class, 'index'])
->middleware('permission:dashboard.view')
->name('dashboard');
// Dashboard Anggota
Route::get('/dashboard-user', [DashboardController::class, 'userDashboard'])
->middleware('permission:dashboard-user.view')
->name('dashboard-user');
// Matriks Hak Akses & Role (Khusus Administrator)
Route::get('/roles', [RoleController::class, 'index'])
->middleware('permission:roles.manage')
->name('roles.manage');
Route::match(['put', 'patch'], '/roles/{role}', [RoleController::class, 'update'])
->middleware('permission:roles.manage')
->name('roles.update');
});
6. Controller Autentikasi (app/Http/Controllers/AuthController.php)
Deskripsi Singkat:
Controller ini menangani logika form login, proses pengecekan kredensial email & password, pendaftaran akun anggota baru, pengalihan dashboard berbasis role, dan proses logout aman.
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\View\View;
class AuthController extends Controller
{
public function showLogin(): View
{
return view('auth.login');
}
public function login(Request $request): RedirectResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
if (Auth::attempt($credentials, $request->boolean('remember'))) {
$request->session()->regenerate();
/** @var User $user */
$user = Auth::user();
if ($user->hasRole('admin') || $user->hasRole('librarian')) {
return redirect()->intended(route('dashboard'));
}
return redirect()->intended(route('dashboard-user'));
}
return back()->withErrors([
'email' => 'Email atau password yang Anda masukkan salah.',
])->onlyInput('email');
}
public function showRegister(): View
{
return view('auth.register');
}
public function register(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'phone' => ['nullable', 'string', 'max:20'],
'address' => ['nullable', 'string'],
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
'phone' => $validated['phone'] ?? null,
'address' => $validated['address'] ?? null,
]);
// Otomatis berikan role user (anggota) kepada pendaftar baru
$user->assignRole('user');
Auth::login($user);
$request->session()->regenerate();
return redirect()->route('dashboard-user')->with('success', 'Pendaftaran akun berhasil!');
}
public function logout(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login')->with('success', 'Anda telah berhasil keluar.');
}
}
7. Controller Matriks Role & Izin Akses (app/Http/Controllers/RoleController.php)
Deskripsi Singkat:
Controller ini memungkinkan administrator sistem mengatur hak akses izin (permission) untuk setiap peran (role) secara visual melalui form centang matriks.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
class RoleController extends Controller
{
public function index(): View
{
$roles = Role::with('permissions')->get();
$permissions = Permission::all();
$groupedPermissions = [
'Halaman Utama & Dashboard' => [
'dashboard.view' => 'Dashboard Operasional Perpustakaan',
'dashboard-user.view' => 'Dashboard Anggota',
],
'Koleksi Buku Fisik' => [
'books.view' => 'Melihat Katalog & Stok Buku',
'books.manage' => 'Kelola Data Buku Fisik (Tambah, Ubah, Hapus)',
],
'Koleksi Buku Digital (E-Book)' => [
'ebooks.view' => 'Melihat Koleksi E-Book',
'ebooks.read' => 'Membaca Buku Digital di Browser',
'ebooks.manage' => 'Unggah & Kelola E-Book',
],
'Layanan Peminjaman & Sirkulasi' => [
'loans.view-own' => 'Melihat Riwayat Pinjaman Pribadi',
'loans.manage' => 'Proses Pinjam, Pengembalian, & Denda',
],
'Pemeriksaan Fisik & Pengaturan' => [
'inventory.stock-opname' => 'Pemeriksaan Fisik Buku (Hitung Stok)',
'memberships.manage' => 'Kelola Jenis & Status Anggota',
'reports.view' => 'Melihat Laporan Perpustakaan',
'settings.manage' => 'Pengaturan Informasi Perpustakaan',
'roles.manage' => 'Pengaturan Hak Akses Pengguna',
],
];
return view('roles.index', compact('roles', 'permissions', 'groupedPermissions'));
}
public function update(Request $request, Role $role): RedirectResponse
{
$permissions = $request->input('permissions', []);
$role->syncPermissions($permissions);
app()[PermissionRegistrar::class]->forgetCachedPermissions();
return redirect()->route('roles.manage')->with('success', "Hak akses role '{$role->name}' berhasil diperbarui.");
}
}
8. Controller Dashboard Berbasis Peran (app/Http/Controllers/DashboardController.php)
Deskripsi Singkat:
Controller ini menyajikan metrik statistik untuk pengelola perpustakaan (pustakawan & admin) dan ringkasan aktivitas peminjaman untuk anggota umum:
<?php
namespace App\Http\Controllers;
use App\Models\Book;
use App\Models\Ebook;
use App\Models\Loan;
use App\Models\SiteSetting;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;
use Illuminate\View\View;
class DashboardController extends Controller
{
/**
* Dashboard Utama Operasional Perpustakaan (Admin & Pustakawan).
*/
public function index(): View
{
$hasBooks = Schema::hasTable('books');
$hasLoans = Schema::hasTable('loans');
$hasEbooks = Schema::hasTable('ebooks');
$hasSettings = Schema::hasTable('site_settings');
$totalBooks = $hasBooks ? Book::count() : 0;
$availableStock = 0;
if ($hasBooks) {
$availableStock = Schema::hasColumn('books', 'available_stock')
? (int) Book::sum('available_stock')
: (int) Book::sum('stock');
}
$activeLoansCount = $hasLoans ? Loan::where('status', 'BORROWED')->count() : 0;
$overdueLoansCount = $hasLoans
? Loan::where('status', 'BORROWED')->where('due_date', '<', Carbon::today())->count()
: 0;
$totalEbooks = $hasEbooks ? Ebook::where('status', 'ACTIVE')->count() : 0;
$unpaidFines = $hasLoans ? (float) Loan::where('fine_status', 'UNPAID')->sum('fine_amount') : 0.0;
$recentLoans = $hasLoans ? Loan::with(['user', 'book'])->latest()->take(6)->get() : collect();
$finePerDay = ($hasSettings && class_exists(SiteSetting::class))
? (SiteSetting::first()?->fine_per_day ?? 1000)
: 1000;
return view('dashboard.index', compact(
'totalBooks',
'availableStock',
'activeLoansCount',
'overdueLoansCount',
'totalEbooks',
'unpaidFines',
'recentLoans',
'finePerDay'
));
}
/**
* Dashboard Khusus Anggota / Mahasiswa.
*/
public function userDashboard(): View
{
$user = Auth::user();
$hasLoans = Schema::hasTable('loans');
$hasEbooks = Schema::hasTable('ebooks');
$activeLoans = $hasLoans
? Loan::with('book')->where('user_id', $user->id)->where('status', 'BORROWED')->latest()->get()
: collect();
$loanHistory = $hasLoans
? Loan::with('book')->where('user_id', $user->id)->where('status', '!=', 'BORROWED')->latest()->take(5)->get()
: collect();
$unpaidFines = $hasLoans
? (float) Loan::where('user_id', $user->id)->where('fine_status', 'UNPAID')->sum('fine_amount')
: 0.0;
$ebooks = $hasEbooks
? Ebook::with('category')->where('status', 'ACTIVE')->latest()->take(4)->get()
: collect();
return view('dashboard.user', compact(
'user',
'activeLoans',
'loanHistory',
'unpaidFines',
'ebooks'
));
}
}
9. Pengujian Otomatis
A. Pengujian Backend (Pest)
Verifikasi logika autentikasi, registrasi, pembatasan akses, dan password hashing:
php artisan test --filter=AuthTest
B. Pengujian End-to-End (Playwright)
Validasi interaksi pengguna di peramban (registrasi anggota baru, login admin, proteksi route 403 untuk anggota):
npx playwright test tests/e2e/pertemuan-05.spec.ts
10. Catatan Penting untuk Evaluasi Akhir dan Tanya Jawab Teknis
Pertanyaan: Apa perbedaan mendasar antara Authentication dan Authorization?
Jawaban Ideal: Authentication adalah proses membuktikan identitas pengguna ("Siapakah Anda?", diverifikasi via email & password), sedangkan Authorization adalah proses menentukan hak dan batas kewenangan tindakan yang boleh dilakukan oleh pengguna tersebut setelah berhasil masuk ("Apa saja yang boleh Anda akses?", dikelola via Spatie RBAC Role & Permission).
Pertanyaan: Mengapa kita tidak boleh menyimpan password dalam bentuk MD5 atau teks biasa?
Jawaban Ideal: MD5 rentan terhadap serangan kamus (dictionary attack) dan rainbow table karena tidak memiliki salt acak dan komputasinya terlalu cepat. Laravel menggunakan algoritma Bcrypt (
Hash::make()) yang menerapkan work factor (pemberat komputasi) dan salt unik sehingga sangat aman terhadap brute force.
Pertanyaan: Bagaimana cara Spatie Permission mencegah query database berulang setiap kali route diakses?
Jawaban Ideal: Spatie menyimpan seluruh peta role dan permission di dalam cache memori aplikasi (
PermissionRegistrar). Ketika ada perubahan hak akses (seperti diRoleController@update), kita memanggilapp()[PermissionRegistrar::class]->forgetCachedPermissions()untuk menyinkronkan kembali cache secara instan.