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();
// ========== 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
if ($request->filled('status')) {
$query->where('status', $request->status);
@ -30,7 +42,15 @@ public function index(Request $request)
}
$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'));
}
@ -40,7 +60,15 @@ public function index(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;
if ($request->filled('kontrakan_id')) {
@ -79,6 +107,14 @@ public function store(Request $request)
// Lock kontrakan row untuk mencegah double booking
$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
$hasConflict = Booking::hasConflict(
$request->kontrakan_id,
@ -196,6 +232,15 @@ public function userStore(Request $request)
*/
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']);
return view('admin.bookings.show', compact('booking'));
}
@ -205,11 +250,26 @@ public function show(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])) {
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'));
}
@ -218,6 +278,14 @@ public function edit(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])) {
return back()->with('error', 'Booking tidak bisa diedit.');
}
@ -274,6 +342,14 @@ public function update(Request $request, 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()) {
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)
{
// ========== 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()) {
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)
{
// ========== 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()) {
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)
{
// ========== 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');
if ($booking->cancel($reason)) {
@ -324,6 +424,14 @@ public function cancel(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');
if ($booking->markAsPaid($method)) {
@ -338,6 +446,14 @@ public function markPaid(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') {
// Set ke belum lunas
$booking->update([
@ -362,6 +478,14 @@ public function togglePaymentStatus(Request $request, 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
$booking->delete();
@ -383,6 +507,15 @@ public function bulkDestroy(Request $request)
$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') {
$ids = $request->input('booking_ids', []);

View File

@ -16,6 +16,16 @@ public function index(Request $request)
{
// Query builder
$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) ==========
if ($request->filled('search')) {
@ -212,8 +222,9 @@ public function store(Request $request)
$file->move(public_path('uploads/kontrakan'), $filename);
}
// Simpan data ke database (DENGAN WHATSAPP)
// Simpan data ke database (DENGAN WHATSAPP & ADMIN_ID)
$kontrakan = Kontrakan::create([
'admin_id' => auth()->guard('admin')->id(),
'nama' => $request->nama,
'alamat' => $request->alamat,
'no_whatsapp' => $request->no_whatsapp,
@ -268,6 +279,14 @@ public function edit($id)
->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'));
}
@ -283,6 +302,14 @@ public function update(Request $request, $id)
->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)
$request->validate([
'nama' => 'required|string|max:255',
@ -350,12 +377,6 @@ public function update(Request $request, $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
$kontrakan = Kontrakan::find($id);
@ -363,6 +384,14 @@ public function destroy($id)
return redirect()->route('kontrakan.index')
->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
if ($kontrakan->foto && File::exists(public_path('uploads/kontrakan/' . $kontrakan->foto))) {
@ -387,12 +416,6 @@ public function destroy($id)
*/
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
$ids = $request->ids;
if (is_string($ids)) {
@ -409,12 +432,19 @@ public function bulkDestroy(Request $request)
$deletedCount = 0;
$deletedNames = [];
$admin = auth()->guard('admin')->user();
// Loop dan hapus satu per satu
foreach ($request->ids as $id) {
$kontrakan = Kontrakan::find($id);
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;
// Hapus foto jika ada

View File

@ -13,13 +13,13 @@ class EnsureRole
*/
public function handle(Request $request, Closure $next, string ...$roles)
{
if (!Auth::check()) {
if (!Auth::guard('admin')->check()) {
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.');
}

View File

@ -44,4 +44,32 @@ protected function casts(): array
'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;
protected $fillable = [
'admin_id',
'nama',
'alamat',
'no_whatsapp',
@ -31,6 +32,18 @@ class Kontrakan extends Model
'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
*/

View File

@ -12,10 +12,14 @@
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// Add role column with default 'user'
$table->string('role')->default('user')->after('password');
// Add phone column (nullable)
$table->string('phone')->nullable()->after('role');
// Add role column with default 'user' (if not already exists)
if (!Schema::hasColumn('users', 'role')) {
$table->string('role')->default('user')->after('password');
}
// 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 --}}
<div class="row g-3 mb-4">
@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 = [
['status' => 'pending', 'label' => 'Menunggu', 'color' => 'warning', 'icon' => 'hourglass-split'],
['status' => 'confirmed', 'label' => 'Dikonfirmasi', 'color' => 'info', 'icon' => 'check-circle'],
@ -89,7 +101,7 @@
<div class="card-body text-center">
<i class="bi bi-{{ $stat['icon'] }} text-{{ $stat['color'] }} fs-3"></i>
<h4 class="fw-bold mb-0 mt-2">
{{ \App\Models\Booking::where('status', $stat['status'])->count() }}
{{ (clone $statsQuery)->where('status', $stat['status'])->count() }}
</h4>
<small class="text-muted">{{ $stat['label'] }}</small>
</div>