feat: Implement admin-scoped data visibility and comprehensive authorization

- Add admin_id FK to kontrakans table
- Add authorization checks to KontrakanController (edit, update, destroy, bulkDestroy)
- Add authorization checks to BookingController (store, bulkDestroy)
- Fix EnsureRole middleware to use auth:admin guard
- Fix booking index stats to filter by admin ownership
- Add model relationships: Admin.kontrakans(), Admin.isSuperAdmin(), Admin.isAdmin()
- Ensure admin biasa sees only their data, super_admin sees all data
- Secure bulk delete operations with per-item admin validation
This commit is contained in:
micko samawa 2026-05-25 14:00:20 +07:00
parent e29c87645b
commit 6bad1aa7b2
8 changed files with 278 additions and 24 deletions

View File

@ -19,6 +19,18 @@ public function index(Request $request)
{ {
$query = Booking::with(['kontrakan', 'user'])->latest(); $query = Booking::with(['kontrakan', 'user'])->latest();
// ========== FILTER: ADMIN (Hanya booking dari kontrakan milik admin) ==========
$admin = auth()->guard('admin')->user();
if ($admin) {
if ($admin->role !== 'super_admin') {
// Admin biasa hanya bisa lihat booking dari kontrakan milik mereka
$query->whereHas('kontrakan', function($q) use ($admin) {
$q->where('admin_id', $admin->id);
});
}
// Super admin bisa lihat semua booking
}
// Filter by status // Filter by status
if ($request->filled('status')) { if ($request->filled('status')) {
$query->where('status', $request->status); $query->where('status', $request->status);
@ -30,7 +42,15 @@ public function index(Request $request)
} }
$bookings = $query->paginate(15); $bookings = $query->paginate(15);
$kontrakans = Kontrakan::orderBy('nama')->get();
// ========== GET KONTRAKANS (sesuai filter admin) ==========
$kontrakanQuery = Kontrakan::orderBy('nama');
if ($admin && $admin->role !== 'super_admin') {
$kontrakanQuery->where('admin_id', $admin->id);
}
$kontrakans = $kontrakanQuery->get();
return view('admin.bookings.index', compact('bookings', 'kontrakans')); return view('admin.bookings.index', compact('bookings', 'kontrakans'));
} }
@ -40,7 +60,15 @@ public function index(Request $request)
*/ */
public function create(Request $request) public function create(Request $request)
{ {
$kontrakans = Kontrakan::where('status', 'available')->orderBy('nama')->get(); $kontrakanQuery = Kontrakan::where('status', 'available')->orderBy('nama');
// ========== FILTER: ADMIN (Hanya kontrakan milik admin) ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
$kontrakanQuery->where('admin_id', $admin->id);
}
$kontrakans = $kontrakanQuery->get();
$selectedKontrakan = null; $selectedKontrakan = null;
if ($request->filled('kontrakan_id')) { if ($request->filled('kontrakan_id')) {
@ -79,6 +107,14 @@ public function store(Request $request)
// Lock kontrakan row untuk mencegah double booking // Lock kontrakan row untuk mencegah double booking
$kontrakan = Kontrakan::lockForUpdate()->findOrFail($request->kontrakan_id); $kontrakan = Kontrakan::lockForUpdate()->findOrFail($request->kontrakan_id);
// ========== AUTHORIZATION: Cek apakah admin punya akses ke kontrakan ini ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($kontrakan->admin_id !== $admin->id) {
throw new Exception('Anda tidak memiliki akses ke kontrakan ini.');
}
}
// Cek konflik dengan booking aktif lainnya // Cek konflik dengan booking aktif lainnya
$hasConflict = Booking::hasConflict( $hasConflict = Booking::hasConflict(
$request->kontrakan_id, $request->kontrakan_id,
@ -196,6 +232,15 @@ public function userStore(Request $request)
*/ */
public function show(Booking $booking) public function show(Booking $booking)
{ {
// ========== AUTHORIZATION: Cek apakah admin punya akses ke booking ini ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
// Admin biasa hanya bisa lihat booking dari kontrakan milik mereka
if ($booking->kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke booking ini.');
}
}
$booking->load(['kontrakan', 'user']); $booking->load(['kontrakan', 'user']);
return view('admin.bookings.show', compact('booking')); return view('admin.bookings.show', compact('booking'));
} }
@ -205,11 +250,26 @@ public function show(Booking $booking)
*/ */
public function edit(Booking $booking) public function edit(Booking $booking)
{ {
// ========== AUTHORIZATION: Cek apakah admin punya akses ke booking ini ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
// Admin biasa hanya bisa edit booking dari kontrakan milik mereka
if ($booking->kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke booking ini.');
}
}
if (!in_array($booking->status, [Booking::STATUS_PENDING, Booking::STATUS_CONFIRMED])) { if (!in_array($booking->status, [Booking::STATUS_PENDING, Booking::STATUS_CONFIRMED])) {
return back()->with('error', 'Booking tidak bisa diedit karena statusnya: ' . $booking->status_label); return back()->with('error', 'Booking tidak bisa diedit karena statusnya: ' . $booking->status_label);
} }
$kontrakans = Kontrakan::orderBy('nama')->get(); $kontrakanQuery = Kontrakan::orderBy('nama');
if ($admin && $admin->role !== 'super_admin') {
$kontrakanQuery->where('admin_id', $admin->id);
}
$kontrakans = $kontrakanQuery->get();
return view('admin.bookings.edit', compact('booking', 'kontrakans')); return view('admin.bookings.edit', compact('booking', 'kontrakans'));
} }
@ -218,6 +278,14 @@ public function edit(Booking $booking)
*/ */
public function update(Request $request, Booking $booking) public function update(Request $request, Booking $booking)
{ {
// ========== AUTHORIZATION: Cek apakah admin punya akses ke booking ini ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($booking->kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke booking ini.');
}
}
if (!in_array($booking->status, [Booking::STATUS_PENDING, Booking::STATUS_CONFIRMED])) { if (!in_array($booking->status, [Booking::STATUS_PENDING, Booking::STATUS_CONFIRMED])) {
return back()->with('error', 'Booking tidak bisa diedit.'); return back()->with('error', 'Booking tidak bisa diedit.');
} }
@ -274,6 +342,14 @@ public function update(Request $request, Booking $booking)
*/ */
public function confirm(Booking $booking) public function confirm(Booking $booking)
{ {
// ========== AUTHORIZATION ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($booking->kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke booking ini.');
}
}
if ($booking->confirm()) { if ($booking->confirm()) {
return back()->with('success', 'Booking berhasil dikonfirmasi! Status kontrakan diubah menjadi "Dipesan".'); return back()->with('success', 'Booking berhasil dikonfirmasi! Status kontrakan diubah menjadi "Dipesan".');
} }
@ -286,6 +362,14 @@ public function confirm(Booking $booking)
*/ */
public function checkIn(Booking $booking) public function checkIn(Booking $booking)
{ {
// ========== AUTHORIZATION ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($booking->kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke booking ini.');
}
}
if ($booking->checkIn()) { if ($booking->checkIn()) {
return back()->with('success', 'Check-in berhasil! Penyewa sudah masuk. Status kontrakan diubah menjadi "Terisi".'); return back()->with('success', 'Check-in berhasil! Penyewa sudah masuk. Status kontrakan diubah menjadi "Terisi".');
} }
@ -298,6 +382,14 @@ public function checkIn(Booking $booking)
*/ */
public function checkOut(Booking $booking) public function checkOut(Booking $booking)
{ {
// ========== AUTHORIZATION ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($booking->kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke booking ini.');
}
}
if ($booking->checkOut()) { if ($booking->checkOut()) {
return back()->with('success', 'Check-out berhasil! Penyewa sudah keluar. Status kontrakan kembali tersedia.'); return back()->with('success', 'Check-out berhasil! Penyewa sudah keluar. Status kontrakan kembali tersedia.');
} }
@ -310,6 +402,14 @@ public function checkOut(Booking $booking)
*/ */
public function cancel(Request $request, Booking $booking) public function cancel(Request $request, Booking $booking)
{ {
// ========== AUTHORIZATION ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($booking->kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke booking ini.');
}
}
$reason = $request->input('cancellation_reason'); $reason = $request->input('cancellation_reason');
if ($booking->cancel($reason)) { if ($booking->cancel($reason)) {
@ -324,6 +424,14 @@ public function cancel(Request $request, Booking $booking)
*/ */
public function markPaid(Request $request, Booking $booking) public function markPaid(Request $request, Booking $booking)
{ {
// ========== AUTHORIZATION ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($booking->kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke booking ini.');
}
}
$method = $request->input('payment_method', 'cash'); $method = $request->input('payment_method', 'cash');
if ($booking->markAsPaid($method)) { if ($booking->markAsPaid($method)) {
@ -338,6 +446,14 @@ public function markPaid(Request $request, Booking $booking)
*/ */
public function togglePaymentStatus(Request $request, Booking $booking) public function togglePaymentStatus(Request $request, Booking $booking)
{ {
// ========== AUTHORIZATION ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($booking->kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke booking ini.');
}
}
if ($booking->payment_status === 'paid') { if ($booking->payment_status === 'paid') {
// Set ke belum lunas // Set ke belum lunas
$booking->update([ $booking->update([
@ -362,6 +478,14 @@ public function togglePaymentStatus(Request $request, Booking $booking)
*/ */
public function destroy(Booking $booking) public function destroy(Booking $booking)
{ {
// ========== AUTHORIZATION ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($booking->kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke booking ini.');
}
}
// delete() akan trigger boot()->deleted() yang auto sync status kontrakan // delete() akan trigger boot()->deleted() yang auto sync status kontrakan
$booking->delete(); $booking->delete();
@ -383,6 +507,15 @@ public function bulkDestroy(Request $request)
$query = Booking::query(); $query = Booking::query();
// ========== AUTHORIZATION: Filter berdasarkan admin ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
// Admin biasa hanya bisa hapus booking dari kontrakan milik mereka
$query->whereHas('kontrakan', function($q) use ($admin) {
$q->where('admin_id', $admin->id);
});
}
if ($request->action === 'selected') { if ($request->action === 'selected') {
$ids = $request->input('booking_ids', []); $ids = $request->input('booking_ids', []);

View File

@ -17,6 +17,16 @@ public function index(Request $request)
// Query builder // Query builder
$query = Kontrakan::query(); $query = Kontrakan::query();
// ========== FILTER: ADMIN (Hanya kontrakan milik admin yang login) ==========
$admin = auth()->guard('admin')->user();
if ($admin) {
if ($admin->role !== 'super_admin') {
// Admin biasa hanya bisa lihat kontrakan milik mereka
$query->where('admin_id', $admin->id);
}
// Super admin bisa lihat semua kontrakan
}
// ========== FILTER: SEARCH (Nama, Alamat, Fasilitas) ========== // ========== FILTER: SEARCH (Nama, Alamat, Fasilitas) ==========
if ($request->filled('search')) { if ($request->filled('search')) {
$search = $request->search; $search = $request->search;
@ -212,8 +222,9 @@ public function store(Request $request)
$file->move(public_path('uploads/kontrakan'), $filename); $file->move(public_path('uploads/kontrakan'), $filename);
} }
// Simpan data ke database (DENGAN WHATSAPP) // Simpan data ke database (DENGAN WHATSAPP & ADMIN_ID)
$kontrakan = Kontrakan::create([ $kontrakan = Kontrakan::create([
'admin_id' => auth()->guard('admin')->id(),
'nama' => $request->nama, 'nama' => $request->nama,
'alamat' => $request->alamat, 'alamat' => $request->alamat,
'no_whatsapp' => $request->no_whatsapp, 'no_whatsapp' => $request->no_whatsapp,
@ -268,6 +279,14 @@ public function edit($id)
->with('error', 'Data kontrakan tidak ditemukan!'); ->with('error', 'Data kontrakan tidak ditemukan!');
} }
// ========== AUTHORIZATION: Cek apakah admin punya akses ke kontrakan ini ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke kontrakan ini.');
}
}
return view('kontrakan.edit', compact('kontrakan')); return view('kontrakan.edit', compact('kontrakan'));
} }
@ -283,6 +302,14 @@ public function update(Request $request, $id)
->with('error', 'Data kontrakan tidak ditemukan!'); ->with('error', 'Data kontrakan tidak ditemukan!');
} }
// ========== AUTHORIZATION: Cek apakah admin punya akses ke kontrakan ini ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses ke kontrakan ini.');
}
}
// Validasi input (DENGAN WHATSAPP) // Validasi input (DENGAN WHATSAPP)
$request->validate([ $request->validate([
'nama' => 'required|string|max:255', 'nama' => 'required|string|max:255',
@ -350,12 +377,6 @@ public function update(Request $request, $id)
*/ */
public function destroy($id) public function destroy($id)
{ {
// Proteksi Role - Hanya Admin dan Super Admin yang bisa hapus
if (!in_array(auth()->user()->role, ['admin', 'super_admin'])) {
return redirect()->route('kontrakan.index')
->with('error', 'Anda tidak memiliki akses untuk menghapus data!');
}
// Cari kontrakan berdasarkan ID // Cari kontrakan berdasarkan ID
$kontrakan = Kontrakan::find($id); $kontrakan = Kontrakan::find($id);
@ -364,6 +385,14 @@ public function destroy($id)
->with('error', 'Data kontrakan tidak ditemukan atau sudah dihapus sebelumnya!'); ->with('error', 'Data kontrakan tidak ditemukan atau sudah dihapus sebelumnya!');
} }
// ========== AUTHORIZATION: Cek apakah admin punya akses ke kontrakan ini ==========
$admin = auth()->guard('admin')->user();
if ($admin && $admin->role !== 'super_admin') {
if ($kontrakan->admin_id !== $admin->id) {
abort(403, 'Anda tidak memiliki akses untuk menghapus kontrakan ini.');
}
}
// Hapus foto dari folder sebelum hapus data // Hapus foto dari folder sebelum hapus data
if ($kontrakan->foto && File::exists(public_path('uploads/kontrakan/' . $kontrakan->foto))) { if ($kontrakan->foto && File::exists(public_path('uploads/kontrakan/' . $kontrakan->foto))) {
File::delete(public_path('uploads/kontrakan/' . $kontrakan->foto)); File::delete(public_path('uploads/kontrakan/' . $kontrakan->foto));
@ -387,12 +416,6 @@ public function destroy($id)
*/ */
public function bulkDestroy(Request $request) public function bulkDestroy(Request $request)
{ {
// Proteksi Role - Hanya Admin dan Super Admin yang bisa hapus
if (!in_array(auth()->user()->role, ['admin', 'super_admin'])) {
return redirect()->route('kontrakan.index')
->with('error', 'Anda tidak memiliki akses untuk menghapus data!');
}
// Handle both JSON string and array input // Handle both JSON string and array input
$ids = $request->ids; $ids = $request->ids;
if (is_string($ids)) { if (is_string($ids)) {
@ -409,12 +432,19 @@ public function bulkDestroy(Request $request)
$deletedCount = 0; $deletedCount = 0;
$deletedNames = []; $deletedNames = [];
$admin = auth()->guard('admin')->user();
// Loop dan hapus satu per satu // Loop dan hapus satu per satu
foreach ($request->ids as $id) { foreach ($request->ids as $id) {
$kontrakan = Kontrakan::find($id); $kontrakan = Kontrakan::find($id);
if ($kontrakan) { if ($kontrakan) {
// ========== AUTHORIZATION: Cek apakah admin punya akses ke kontrakan ini ==========
if ($admin && $admin->role !== 'super_admin') {
if ($kontrakan->admin_id !== $admin->id) {
continue; // Skip kontrakan yang bukan milik admin
}
}
$deletedNames[] = $kontrakan->nama; $deletedNames[] = $kontrakan->nama;
// Hapus foto jika ada // Hapus foto jika ada

View File

@ -13,13 +13,13 @@ class EnsureRole
*/ */
public function handle(Request $request, Closure $next, string ...$roles) public function handle(Request $request, Closure $next, string ...$roles)
{ {
if (!Auth::check()) { if (!Auth::guard('admin')->check()) {
return redirect()->route('admin.login'); return redirect()->route('admin.login');
} }
$user = Auth::user(); $admin = Auth::guard('admin')->user();
if (!in_array($user->role, $roles, true)) { if (!in_array($admin->role, $roles, true)) {
abort(403, 'Akses ditolak. Anda tidak memiliki izin untuk halaman ini.'); abort(403, 'Akses ditolak. Anda tidak memiliki izin untuk halaman ini.');
} }

View File

@ -44,4 +44,32 @@ protected function casts(): array
'password' => 'hashed', 'password' => 'hashed',
]; ];
} }
/**
* ========== RELASI KONTRAKAN ==========
*/
/**
* Relasi: 1 Admin punya banyak Kontrakan
*/
public function kontrakans()
{
return $this->hasMany(Kontrakan::class);
}
/**
* Helper: Cek apakah admin adalah super_admin
*/
public function isSuperAdmin()
{
return $this->role === 'super_admin';
}
/**
* Helper: Cek apakah admin adalah admin biasa
*/
public function isAdmin()
{
return $this->role === 'admin';
}
} }

View File

@ -10,6 +10,7 @@ class Kontrakan extends Model
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'admin_id',
'nama', 'nama',
'alamat', 'alamat',
'no_whatsapp', 'no_whatsapp',
@ -31,6 +32,18 @@ class Kontrakan extends Model
'availability_confirmed_at' => 'datetime', 'availability_confirmed_at' => 'datetime',
]; ];
/**
* ========== RELASI ADMIN ==========
*/
/**
* Relasi: 1 Kontrakan dimiliki oleh 1 Admin
*/
public function admin()
{
return $this->belongsTo(Admin::class);
}
/** /**
* Generate Google Maps URL * Generate Google Maps URL
*/ */

View File

@ -12,10 +12,14 @@
public function up(): void public function up(): void
{ {
Schema::table('users', function (Blueprint $table) { Schema::table('users', function (Blueprint $table) {
// Add role column with default 'user' // Add role column with default 'user' (if not already exists)
$table->string('role')->default('user')->after('password'); if (!Schema::hasColumn('users', 'role')) {
// Add phone column (nullable) $table->string('role')->default('user')->after('password');
$table->string('phone')->nullable()->after('role'); }
// Add phone column (nullable) (if not already exists)
if (!Schema::hasColumn('users', 'phone')) {
$table->string('phone')->nullable()->after('role');
}
}); });
} }

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('kontrakans', function (Blueprint $table) {
// Tambah kolom admin_id sebagai foreign key ke admins table
$table->foreignId('admin_id')
->nullable()
->after('id')
->constrained('admins')
->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('kontrakans', function (Blueprint $table) {
$table->dropForeignKey(['admin_id']);
$table->dropColumn('admin_id');
});
}
};

View File

@ -76,6 +76,18 @@
{{-- Stats Cards --}} {{-- Stats Cards --}}
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
@php @php
// Filter stats berdasarkan admin yang login
$admin = auth()->guard('admin')->user();
$statsQuery = \App\Models\Booking::query();
if ($admin && $admin->role !== 'super_admin') {
// Admin biasa: hanya lihat booking dari kontrakan milik mereka
$statsQuery->whereHas('kontrakan', function($q) use ($admin) {
$q->where('admin_id', $admin->id);
});
}
// Super admin: lihat semua
$stats = [ $stats = [
['status' => 'pending', 'label' => 'Menunggu', 'color' => 'warning', 'icon' => 'hourglass-split'], ['status' => 'pending', 'label' => 'Menunggu', 'color' => 'warning', 'icon' => 'hourglass-split'],
['status' => 'confirmed', 'label' => 'Dikonfirmasi', 'color' => 'info', 'icon' => 'check-circle'], ['status' => 'confirmed', 'label' => 'Dikonfirmasi', 'color' => 'info', 'icon' => 'check-circle'],
@ -89,7 +101,7 @@
<div class="card-body text-center"> <div class="card-body text-center">
<i class="bi bi-{{ $stat['icon'] }} text-{{ $stat['color'] }} fs-3"></i> <i class="bi bi-{{ $stat['icon'] }} text-{{ $stat['color'] }} fs-3"></i>
<h4 class="fw-bold mb-0 mt-2"> <h4 class="fw-bold mb-0 mt-2">
{{ \App\Models\Booking::where('status', $stat['status'])->count() }} {{ (clone $statsQuery)->where('status', $stat['status'])->count() }}
</h4> </h4>
<small class="text-muted">{{ $stat['label'] }}</small> <small class="text-muted">{{ $stat['label'] }}</small>
</div> </div>