Skip to content

[Add] Menambah fitur Log Activity#1334

Open
hasanlq69 wants to merge 5 commits intoOpenSID:devfrom
hasanlq69:master
Open

[Add] Menambah fitur Log Activity#1334
hasanlq69 wants to merge 5 commits intoOpenSID:devfrom
hasanlq69:master

Conversation

@hasanlq69
Copy link
Copy Markdown

@hasanlq69 hasanlq69 commented Sep 22, 2025

Pull Request untuk Issue Penambahan fitur Logi Aktivitas #1324

Dokumentasi Fitur Log Aktivitas OpenDK

Overview

Fitur Log Aktivitas OpenDK menggunakan package Spatie Activity Log untuk mencatat semua aktivitas pengguna dalam sistem. Fitur ini memungkinkan administrator untuk memantau dan melacak semua perubahan yang terjadi dalam aplikasi.

Fitur Utama

  • Tabel Interaktif: Menampilkan semua log aktivitas dengan fitur pencarian, filter, dan pagination.
  • Pencatatan Otomatis: Model User secara otomatis mencatat perubahan (create, update, delete).
  • Pencatatan Manual Lengkap: Mencatat aktivitas penting seperti Login, Logout, perubahan pengaturan, dan update profil.
  • Detail Aktivitas: Modal detail menampilkan informasi lengkap termasuk IP Address, User Agent, dan detail perubahan data (jika ada).

Implementasi Teknis

Best Practice Pencatatan Log

Struktur ideal untuk mencatat log secara manual adalah sebagai berikut:

activity()
    ->causedBy(auth()->user())  // Siapa yang melakukan (Pengguna)
    ->performedOn($model)       // Apa yang diubah (Subject)
    ->event('updated')          // Aksi apa (Event)
    ->withProperties($data)   // Data tambahan (opsional)
    ->log('Deskripsi...');       // Deskripsi aktivitas

Contoh Implementasi di Controller

LoginController (authenticated & logout)

// Login Berhasil
activity()
    ->causedBy($user)
    ->performedOn($user)
    ->event('login')
    ->log('Pengguna berhasil masuk ke sistem');

// Logout
activity()
    ->causedBy(auth()->user())
    ->performedOn(auth()->user())
    ->event('logout')
    ->log('Pengguna keluar dari sistem');

UserController (store & update)

// Membuat Pengguna
activity()
    ->causedBy(auth()->user())
    ->performedOn($user)
    ->event('created')
    ->withProperties(['roles' => $roles])
    ->log("Membuat pengguna baru: {$user->name}");

// Mengubah Pengguna
activity()
    ->causedBy(auth()->user())
    ->performedOn($user)
    ->event('updated')
    ->withProperties(['roles' => $roles])
    ->log("Mengubah data pengguna: {$user->name}");

Frontend

Urutan Pemuatan Script

Untuk menghindari error JavaScript, semua script custom untuk halaman ini harus dimuat di dalam @push('scripts') agar dieksekusi setelah jQuery dan DataTables siap.

{{-- di activity-logs.blade.php --}}

@push('scripts')
<script>
    $(document).ready(function() {
        // Kode DataTables di sini
    });
</script>
@endpush

Modal Detail

Modal detail akan menampilkan data dari server, termasuk ip_address dan user_agent yang diambil dari kolom properties pada log.

Troubleshooting

Berikut adalah beberapa masalah umum yang mungkin terjadi dan solusinya.

  1. Masalah: Tabel log aktivitas kosong dan di console muncul error Uncaught TypeError: $ is not a function.

    • Penyebab: Script DataTables dieksekusi sebelum library jQuery selesai dimuat.
    • Solusi: Pindahkan semua kode JavaScript ke dalam @push('scripts') di file view Blade.
  2. Masalah: Kolom Event atau Subject menampilkan N/A.

    • Penyebab: Saat mencatat log, metode event() atau performedOn() tidak dipanggil.
    • Solusi: Pastikan semua panggilan activity() menyertakan ->event('nama_event') dan ->performedOn($model) jika relevan.
  3. Masalah: Saat klik tombol "Detail", muncul error Cannot read properties of undefined (reading 'ip_address').

    • Penyebab: Log lama tidak memiliki properties atau tidak berisi ip_address. Kode JavaScript mencoba membaca properti dari objek yang undefined.
    • Solusi: Buat kode JavaScript lebih defensif dengan menambahkan pengecekan: var properties = response.properties || {};.
  4. Masalah: Di console muncul error CORS saat mencoba memuat file Indonesian.json dari CDN.

    • Penyebab: Server CDN tidak mengizinkan request dari domain lokal Anda (opendk.test).
    • Solusi: Unduh file Indonesian.json dan simpan secara lokal di dalam direktori public/, lalu ubah URL di konfigurasi DataTables untuk menunjuk ke file lokal tersebut.

API Endpoints

Get Activity Logs Data

  • URL: GET /setting/info-sistem/activity-logs/data
  • Parameters:
    • action (optional) - Filter by event type
    • user_id (optional) - Filter by user
    • date_from (optional) - Start date filter
    • date_to (optional) - End date filter

Get Activity Log Detail

  • URL: GET /setting/info-sistem/activity-logs/detail/{id}
  • Response: JSON dengan detail lengkap log

Maintenance

Performance Considerations

  1. Indexing: Tabel sudah memiliki index pada kolom penting
  2. Cleanup: Rutin hapus log lama untuk menjaga performa
  3. Selective Logging: Hanya log field yang penting
  4. Batch Operations: Gunakan batch UUID untuk operasi massal

Security

Permission Control

  • Hanya user dengan role super-admin atau administrator-website yang bisa akses
  • Middleware protection pada semua route

Data Privacy

  • Password tidak pernah di-log
  • Sensitive data bisa dikecualikan dari logging
  • IP address dan user agent dicatat untuk audit trail

Troubleshooting

Common Issues

  1. Log tidak muncul

    • Pastikan model menggunakan LogsActivity trait
    • Check getActivitylogOptions() configuration
    • Verify database connection
  2. Performance lambat

    • Cleanup log lama secara berkala
    • Check database indexes
    • Optimize query filters
  3. Memory issues

    • Batasi jumlah data yang di-load
    • Gunakan pagination
    • Cleanup properties yang tidak perlu

Future Enhancements

  1. Export Functionality: Export log ke Excel/PDF
  2. Real-time Notifications: Notifikasi real-time untuk aktivitas penting
  3. Advanced Filtering: Filter berdasarkan IP, user agent, dll
  4. Dashboard Analytics: Grafik dan statistik aktivitas
  5. Automated Cleanup: Cron job untuk cleanup otomatis

Testing

Manual Testing

// Test basic logging
activity()->log('Test message');

// Test with user
activity()
    ->causedBy(auth()->user())
    ->log('Test with user');

// Test model changes
$user = User::first();
$user->name = 'Updated Name';
$user->save(); // Should automatically log

Verification

  1. Check database: SELECT * FROM activity_log ORDER BY created_at DESC LIMIT 10;
  2. Access web interface: http://localhost:8000/setting/info-sistem
  3. Test filters and search functionality
  4. Verify modal detail works correctly

@vickyrolanda
Copy link
Copy Markdown
Contributor

mas @hasanlq69 silahkan perbaiki PR dengan menghapus file-file yang tidak diperlukan untuk masuk ke repo.
ada banyak file yang tidak diperlukan termasuk git ignore.

@vickyrolanda
Copy link
Copy Markdown
Contributor

mas @hasanlq69 masih ditemukan error saat kondisi masuk dengan user & password salah :
sleekshot

@vickyrolanda vickyrolanda changed the base branch from master to dev October 13, 2025 19:10
@@ -41,6 +41,7 @@
use Spatie\Permission\Models\Role;
use App\Http\Controllers\Controller;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🔒 Security: Insecure Direct Object Reference (IDOR) - User Update

Masalah: Method update() tidak memiliki authorization check. Attacker bisa mengubah data user lain dengan mengganti parameter $id.

Kode:

public function update(UserUpdateRequest $request, $id)
{
    $user = User::findOrFail($id);
    $user->update([...]);
}

Risiko:

  • Attacker bisa mengubah nama, email, phone, address user lain
  • Bisa mengubah role user lain menjadi admin
  • Privilege escalation attack
  • Data integrity compromise

Cara Reproduksi:

# Login sebagai user biasa (ID: 5)
# Kemudian kirim request untuk update user admin (ID: 1)

curl -X PUT 'https://opendk.local/user/1' \
  -H 'Cookie: laravel_session=YOUR_SESSION' \
  -H 'X-CSRF-TOKEN: YOUR_TOKEN' \
  -d 'name=Hacked Admin' \
  -d 'email=hacker@evil.com' \
  -d 'role=super-admin' \
  -d 'phone=666' \
  -d 'address=Hacked'

# User biasa berhasil mengubah data admin!

Fix:

public function update(UserUpdateRequest $request, $id)
{
    // Add authorization check
    $user = User::findOrFail($id);
    
    // Option 1: Using Gate
    if (Gate::denies('update-user', $user)) {
        abort(403, 'Unauthorized action.');
    }
    
    // Option 2: Using Policy
    $this->authorize('update', $user);
    
    $user->update([
        'name' => $request->name,
        'email' => $request->email,
        'phone' => $request->phone,
        'address' => $request->address,
    ]);

    if ($request->filled('role')) {
        // Additional check for role changes
        if (Gate::denies('assign-roles')) {
            abort(403, 'Unauthorized to change roles.');
        }
        $user->syncRoles([$request->role]);
    }

    ActivityLogger::log(
        'pengguna',
        'Data pengguna berhasil diperbarui: ' . $user->name,
        $user,
        'updated'
    );

    return redirect()->route('user.index')->with('success', 'Pengguna berhasil diperbarui');
}

@devopsopendesa
Copy link
Copy Markdown
Contributor

📍 app/Http/Controllers/User/UserController.php (baris 73)

[CRITICAL] 🔒 Security: IDOR - Password Change Without Authorization

Masalah: Method changePassword() tidak memiliki authorization check. Attacker bisa mengubah password user lain tanpa verifikasi password lama.

Kode:

public function changePassword(Request $request, $id)
{
    $request->validate([
        'password' => 'required|min:6|confirmed',
    ]);
    
    $user = User::findOrFail($id);
    $user->update([
        'password' => Hash::make($request->password),
    ]);
}

Risiko:

  • Account takeover - attacker bisa ganti password user lain
  • Tidak ada verifikasi password lama
  • Tidak ada verifikasi bahwa user yang login adalah pemilik akun
  • Critical security breach

Cara Reproduksi:

# Login sebagai user biasa (ID: 5)
# Ganti password admin (ID: 1)

curl -X POST 'https://opendk.local/user/1/change-password' \
  -H 'Cookie: laravel_session=YOUR_SESSION' \
  -H 'X-CSRF-TOKEN: YOUR_TOKEN' \
  -d 'password=hacked123' \
  -d 'password_confirmation=hacked123'

# Password admin berhasil diganti!
# Sekarang login sebagai admin dengan password: hacked123

Fix:

public function changePassword(Request $request, $id)
{
    $request->validate([
        'current_password' => 'required', // Add current password validation
        'password' => 'required|min:6|confirmed',
    ]);

    $user = User::findOrFail($id);
    
    // Authorization: user can only change their own password OR admin can change any
    if (Auth::id() !== $user->id && !Auth::user()->hasRole('super-admin')) {
        abort(403, 'Unauthorized action.');
    }
    
    // Verify current password if user is changing their own
    if (Auth::id() === $user->id) {
        if (!Hash::check($request->current_password, $user->password)) {
            return back()->withErrors(['current_password' => 'Password lama tidak sesuai']);
        }
    }
    
    $user->update([
        'password' => Hash::make($request->password),
    ]);

    ActivityLogger::log(
        'pengguna',
        'Password pengguna berhasil diubah: ' . $user->name,
        $user,
        'password_updated'
    );

    return redirect()->route('user.index')->with('success', 'Password berhasil diubah');
}

category: 'pengguna',
event: 'created',
message: "Membuat pengguna baru: {$user->name} ({$user->email})",
subject: $user,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🔒 Security: IDOR - User Suspend/Activate Without Authorization

Masalah: Methods suspend() dan activate() tidak memiliki authorization check. Attacker bisa suspend/activate user lain termasuk admin.

Kode:

public function suspend($id)
{
    $user = User::findOrFail($id);
    $user->update(['status' => 0]);
}

public function activate($id)
{
    $user = User::findOrFail($id);
    $user->update(['status' => 1]);
}

Risiko:

  • Attacker bisa suspend admin accounts
  • Denial of Service attack
  • Bisa suspend semua user untuk melumpuhkan sistem
  • Privilege manipulation

Cara Reproduksi:

# Login sebagai user biasa (ID: 5)
# Suspend semua admin

curl -X POST 'https://opendk.local/user/1/suspend' \
  -H 'Cookie: laravel_session=YOUR_SESSION' \
  -H 'X-CSRF-TOKEN: YOUR_TOKEN'

curl -X POST 'https://opendk.local/user/2/suspend' \
  -H 'Cookie: laravel_session=YOUR_SESSION' \
  -H 'X-CSRF-TOKEN: YOUR_TOKEN'

# Semua admin ter-suspend, sistem tidak bisa dikelola!

Fix:

public function suspend($id)
{
    $user = User::findOrFail($id);
    
    // Authorization check
    $this->authorize('suspend', $user);
    
    // Prevent self-suspension
    if (Auth::id() === $user->id) {
        return back()->with('error', 'Tidak dapat suspend akun sendiri');
    }
    
    // Prevent suspending super admin (optional)
    if ($user->hasRole('super-admin')) {
        return back()->with('error', 'Tidak dapat suspend super admin');
    }
    
    $user->update(['status' => 0]);

    ActivityLogger::log(
        'pengguna',
        'Pengguna berhasil disuspend: ' . $user->name,
        $user,
        'suspended'
    );

    return redirect()->route('user.index')->with('success', 'Pengguna berhasil disuspend');
}

public function activate($id)
{
    $user = User::findOrFail($id);
    
    // Authorization check
    $this->authorize('activate', $user);
    
    $user->update(['status' => 1]);

    ActivityLogger::log(
        'pengguna',
        'Pengguna berhasil diaktifkan: ' . $user->name,
        $user,
        'activated'
    );

    return redirect()->route('user.index')->with('success', 'Pengguna berhasil diaktifkan');
}

@devopsopendesa
Copy link
Copy Markdown
Contributor

📍 app/Http/Controllers/LogViewerController.php (baris 66)

[HIGH] 🔒 Security: IDOR - Activity Log Detail Exposure

Masalah: Method getActivityLogDetail() tidak memiliki authorization check. Attacker bisa melihat detail log aktivitas user lain termasuk IP address, browser, lokasi, dan metadata sensitif.

Kode:

public function getActivityLogDetail($id)
{
    $activity = Activity::with(['causer', 'subject'])->findOrFail($id);
    
    return response()->json([
        'success' => true,
        'data' => [
            'causer' => $activity->causer ? [
                'id' => $activity->causer->id,
                'name' => $activity->causer->name,
                'email' => $activity->causer->email,
            ] : null,
            'properties' => $activity->properties->toArray(),
        ]
    ]);
}

Risiko:

  • Information disclosure - IP address, browser, device info
  • User email exposure
  • Geolocation data leak (country, city, ISP)
  • Privacy violation
  • Bisa digunakan untuk profiling dan tracking user

Cara Reproduksi:

# Login sebagai user biasa
# Akses detail log user lain

curl 'https://opendk.local/setting/info-sistem/activity-logs/detail/1' \
  -H 'Cookie: laravel_session=YOUR_SESSION'

# Response berisi:
# - Email user lain
# - IP address
# - Browser & device info
# - Lokasi geografis (country, city)
# - ISP information

Fix:

public function getActivityLogDetail($id)
{
    $activity = Activity::with(['causer', 'subject'])->findOrFail($id);
    
    // Authorization: only admin or log owner can view
    if (!Auth::user()->hasRole('super-admin') && 
        $activity->causer_id !== Auth::id()) {
        abort(403, 'Unauthorized to view this activity log.');
    }
    
    // Sanitize sensitive data for non-admin users
    $properties = $activity->properties->toArray();
    if (!Auth::user()->hasRole('super-admin')) {
        // Mask IP address for privacy
        if (isset($properties['ip'])) {
            $properties['ip'] = $this->maskIp($properties['ip']);
        }
        // Remove geolocation data
        unset($properties['country'], $properties['city'], $properties['isp']);
    }
    
    return response()->json([
        'success' => true,
        'data' => [
            'id' => $activity->id,
            'log_name' => $activity->log_name,
            'description' => $activity->description,
            'event' => $activity->event,
            'causer' => $activity->causer ? [
                'id' => $activity->causer->id,
                'name' => $activity->causer->name,
                // Remove email for non-admin
                'email' => Auth::user()->hasRole('super-admin') ? 
                    $activity->causer->email : null,
            ] : null,
            'subject' => $activity->subject ? [
                'type' => class_basename($activity->subject_type),
                'id' => $activity->subject_id,
            ] : null,
            'properties' => $properties,
            'created_at' => $activity->created_at->format('d/m/Y H:i:s'),
        ]
    ]);
}

private function maskIp($ip)
{
    $parts = explode('.', $ip);
    if (count($parts) === 4) {
        return $parts[0] . '.' . $parts[1] . '.xxx.xxx';
    }
    return 'xxx.xxx.xxx.xxx';
}

@@ -90,6 +93,62 @@ public function index()
return $early_return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🔒 Security: Missing Authorization & Rate Limiting on Cleanup Endpoint

Masalah: Method cleanupActivityLogs() tidak memiliki authorization check dan rate limiting. Attacker bisa menghapus semua activity logs untuk menghilangkan jejak.

Kode:

public function cleanupActivityLogs(Request $request)
{
    $days = $request->input('days', 365);
    $deleted = Activity::where('created_at', '<', now()->subDays($days))->delete();
}

Risiko:

  • Attacker bisa hapus semua audit logs
  • Anti-forensics attack - menghilangkan bukti
  • Compliance violation (audit trail requirement)
  • Bisa di-abuse dengan days=0 untuk hapus semua log
  • No rate limiting - bisa di-spam

Cara Reproduksi:

# Login sebagai user biasa
# Hapus SEMUA activity logs

curl -X POST 'https://opendk.local/setting/info-sistem/activity-logs/cleanup' \
  -H 'Cookie: laravel_session=YOUR_SESSION' \
  -H 'X-CSRF-TOKEN: YOUR_TOKEN' \
  -d 'days=0'

# Response: "Berhasil menghapus 5000 log aktivitas"
# Semua audit trail hilang!

Fix:

public function cleanupActivityLogs(Request $request)
{
    // Authorization: only super-admin can cleanup logs
    if (!Auth::user()->hasRole('super-admin')) {
        abort(403, 'Unauthorized action.');
    }
    
    // Validate input with minimum days
    $request->validate([
        'days' => 'required|integer|min:30|max:3650', // Min 30 days, max 10 years
    ]);
    
    try {
        $days = $request->input('days');
        
        // Additional safety check
        if ($days < 30) {
            return response()->json([
                'success' => false,
                'message' => 'Minimal 30 hari untuk menjaga audit trail.'
            ], 400);
        }
        
        $deleted = Activity::where('created_at', '<', now()->subDays($days))->delete();
        
        ActivityLogger::log(
            'aplikasi',
            'Pembersihan log aktivitas berhasil',
            null,
            'cleanup_success',
            ['days' => $days, 'deleted_count' => $deleted]
        );
        
        return response()->json([
            'success' => true,
            'message' => "Berhasil menghapus {$deleted} log aktivitas yang lebih lama dari {$days} hari.",
            'deleted_count' => $deleted
        ]);
    } catch (\Exception $e) {
        report($e);
        
        ActivityLogger::log(
            'aplikasi',
            'Pembersihan log aktivitas gagal: ' . $e->getMessage(),
            null,
            'cleanup_failed'
        );
        
        return response()->json([
            'success' => false,
            'message' => 'Terjadi kesalahan saat membersihkan log aktivitas.'
        ], 500);
    }
}

Additional: Add rate limiting in routes/web.php:

Route::post('/activity-logs/cleanup', 'LogViewerController@cleanupActivityLogs')
    ->name('activity-logs.cleanup')
    ->middleware(['auth', 'role:super-admin', 'throttle:1,60']); // 1 request per 60 minutes

$properties = array_filter(
array_merge($metadata, $ipLocation, $additionalProperties),
static fn ($value) => ! is_null($value) && $value !== ''
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] ⚡ Performance: Blocking HTTP Call di Setiap Request

Masalah: ActivityLogger dipanggil secara synchronous di setiap request (login, logout, CRUD user, update profil, update setting) dan memanggil IpLocationResolver::resolve() yang melakukan HTTP call external ke ipwhois.app API. Ini blocking operation yang akan memperlambat response time setiap request.

Kode:

$location = IpLocationResolver::resolve($ip);

Dampak:

  • Dengan 100 juta+ request/hari, jika 10% melakukan aktivitas yang dilog = 10 juta HTTP calls/hari ke external API
  • Setiap HTTP call timeout 5 detik, jika API lambat/down = user menunggu 5 detik per request
  • Rate limiting dari ipwhois.app bisa menyebabkan semua request gagal
  • Single point of failure: jika API down, semua aktivitas user terganggu

Fix:

// Gunakan Queue untuk async logging
use Illuminate\Support\Facades\Queue;

public static function log(
    string $logName,
    string $description,
    $subject = null,
    string $event = 'success',
    array $additionalProperties = []
): Activity {
    // Dispatch ke queue untuk async processing
    dispatch(function() use ($logName, $description, $subject, $event, $additionalProperties) {
        $activity = activity($logName)
            ->event($event)
            ->withProperties(self::enrichMetadata($additionalProperties));

        if ($subject) {
            $activity->performedOn($subject);
        }

        if (Auth::check()) {
            $activity->causedBy(Auth::user());
        }

        return $activity->log($description);
    })->onQueue('activity-logs');
    
    // Return dummy activity untuk backward compatibility
    return new Activity();
}

return [
'ip_location_available' => false,
'ip_location_message' => 'IP lokal/internal',
];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ⚡ Performance: HTTP Timeout Terlalu Lama

Masalah: HTTP timeout 5 detik terlalu lama untuk synchronous call. Jika API lambat, user harus menunggu hingga 5 detik sebelum request selesai.

Kode:

$response = Http::timeout(5)->get("http://ipwhois.app/json/{$ip}");

Dampak:

  • User experience buruk: loading 5 detik untuk login/logout/update
  • Dengan traffic tinggi, banyak request akan stuck menunggu API response
  • Resource server terpakai untuk menunggu external API

Fix:

// Kurangi timeout dan tambahkan retry logic
$response = Http::timeout(2)
    ->retry(2, 100) // Retry 2x dengan delay 100ms
    ->get("http://ipwhois.app/json/{$ip}");

@devopsopendesa
Copy link
Copy Markdown
Contributor

📍 app/Http/Controllers/User/UserController.php (baris 23)

[CRITICAL] ⚡ Performance: Missing Pagination - Load Semua User

Masalah: Method getData() menggunakan User::with('roles')->get() tanpa pagination. Ini akan load SEMUA user dari database sekaligus ke memory. Dengan sistem yang besar (1000+ users), ini akan menyebabkan memory exhaustion dan query lambat.

Kode:

$data = User::with('roles')->get();

Dampak:

  • Dengan 1000 users: ~1MB memory per request
  • Dengan 10,000 users: ~10MB memory per request
  • Query time meningkat linear dengan jumlah user
  • Server bisa OOM (Out of Memory) pada traffic tinggi
  • DataTables server-side processing jadi tidak efektif karena load semua data dulu

Fix:

// Gunakan query builder langsung untuk DataTables server-side
public function getData()
{
    $query = User::with('roles')->select(['id', 'name', 'email', 'status', 'created_at']);

    return DataTables::eloquent($query)
        ->addColumn('role', function ($row) {
            return $row->roles->pluck('name')->implode(', ');
        })
        ->addColumn('aksi', function ($row) {
            $data['edit_url']   = route('user.edit', $row->id);
            $data['delete_url'] = route('user.destroy', $row->id);
            return view('forms.aksi', $data);
        })
        ->editColumn('status', function ($row) {
            if ($row->status == 1) {
                return '<span class="label label-success">Aktif</span>';
            }
            return '<span class="label label-danger">Tidak Aktif</span>';
        })
        ->rawColumns(['aksi', 'status'])
        ->make(true);
}

@devopsopendesa
Copy link
Copy Markdown
Contributor

📍 app/Http/Controllers/LogViewerController.php (baris 78)

[HIGH] ⚡ Performance: Potential N+1 Query di DataTables

Masalah: Meskipun sudah ada with(['causer', 'subject']), DataTables bisa melakukan query tambahan saat render kolom jika tidak hati-hati. Method editColumn('causer.name') bisa trigger lazy loading jika causer null.

Kode:

->editColumn('causer.name', function ($activity) {
    return $activity->causer ? $activity->causer->name : '-';
})

Dampak:

  • Dengan 10,000 activity logs dan 50% tanpa causer = 5,000 extra queries
  • Setiap page load DataTables (10-50 rows) bisa trigger 5-25 queries
  • Response time meningkat dari 100ms ke 1-2 detik

Fix:

// Pastikan eager loading bekerja dengan baik
public function getActivityLogs(Request $request)
{
    $query = Activity::with(['causer:id,name', 'subject'])
        ->select(['id', 'log_name', 'description', 'event', 'causer_id', 'causer_type', 'subject_id', 'subject_type', 'created_at'])
        ->orderBy('created_at', 'desc');

    // ... filter logic ...

    return DataTables::eloquent($query)
        ->addIndexColumn()
        ->editColumn('created_at', function ($activity) {
            return $activity->created_at->format('d/m/Y H:i:s');
        })
        ->addColumn('causer_name', function ($activity) {
            return $activity->causer?->name ?? '-';
        })
        // ... rest of columns ...
        ->make(true);
}

return [
'id' => $instance->getKey(),
'type' => $modelClass,
'name' => $name,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ⚡ Performance: Missing Index untuk Filter Queries

Masalah: Filter by log_name, event, dan causer_id tanpa memastikan ada index di database. Dengan jutaan activity logs, query filtering akan sangat lambat.

Kode:

if ($request->filled('log_name')) {
    $query->where('log_name', $request->log_name);
}
if ($request->filled('event')) {
    $query->where('event', $request->event);
}
if ($request->filled('causer_id')) {
    $query->where('causer_id', $request->causer_id);
}

Dampak:

  • Dengan 1 juta activity logs tanpa index: query bisa 5-10 detik
  • Dengan index: query < 100ms
  • User experience sangat buruk saat filtering
  • Database CPU spike saat banyak user filtering bersamaan

Fix:

// Tambahkan migration untuk composite index
Schema::table('activity_log', function (Blueprint $table) {
    $table->index(['log_name', 'event', 'created_at'], 'idx_activity_filters');
    $table->index(['causer_id', 'created_at'], 'idx_activity_causer');
});

// Atau gunakan full-text search untuk performa lebih baik
Schema::table('activity_log', function (Blueprint $table) {
    $table->fullText(['log_name', 'description', 'event']);
});

<!-- DataTable -->
<div class="table-responsive">
<table id="activity-logs-table" class="table table-bordered table-striped" style="width: 100%;">
<thead>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] ⚡ Performance: Query Database di Blade Template

Masalah: Blade template melakukan query User::all() langsung di view untuk populate dropdown filter. Ini anti-pattern dan akan load semua user setiap kali halaman dibuka, bahkan sebelum user menggunakan filter.

Kode:

@foreach(\App\Models\User::all() as $user)
    <option value="{{ $user->id }}">{{ $user->name }}</option>
@endforeach

Dampak:

  • Dengan 1000 users: extra 1MB data + 1 query setiap page load
  • Dengan 10,000 users: extra 10MB data + query bisa 500ms-1s
  • Tidak ada caching, query dijalankan setiap request
  • Memory usage tinggi untuk data yang mungkin tidak dipakai

Fix:

// Di Controller, pass data ke view dengan caching
public function index()
{
    $page_title = 'Log Viewer';
    $page_description = 'Daftar Log Sistem';
    
    // Cache user list untuk 1 jam
    $users = Cache::remember('activity_log_users', 3600, function() {
        return User::select('id', 'name')
            ->where('status', 1)
            ->orderBy('name')
            ->get();
    });

    return view('vendor.laravel-log-viewer.index', compact('page_title', 'page_description', 'users'));
}

// Di Blade, gunakan data dari controller
@foreach($users as $user)
    <option value="{{ $user->id }}">{{ $user->name }}</option>
@endforeach

// ATAU gunakan AJAX untuk lazy load dropdown
<select name="causer_id" id="causer_id" class="form-control select2">
    <option value="">Semua Pengguna</option>
</select>

<script>
$('#causer_id').select2({
    ajax: {
        url: '{{ route("users.search") }}',
        dataType: 'json',
        delay: 250,
        data: function (params) {
            return { q: params.term };
        },
        processResults: function (data) {
            return { results: data };
        }
    },
    minimumInputLength: 2
});
</script>

@devopsopendesa
Copy link
Copy Markdown
Contributor

📍 app/Http/Controllers/Auth/LoginController.php (baris 48)

[HIGH] ⚡ Performance: Synchronous Logging Memperlambat Login

Masalah: Setiap login/logout memanggil ActivityLogger::log() secara synchronous yang melakukan HTTP call ke external API. Ini menambah 2-5 detik ke login flow.

Kode:

protected function authenticated(Request $request, $user)
{
    ActivityLogger::log(
        'login',
        'Pengguna berhasil login: ' . $user->name,
        $user,
        'success'
    );
}

Dampak:

  • Login yang seharusnya 200ms jadi 2-5 detik
  • User experience buruk: loading lama setelah submit login
  • Dengan 10,000 login/hari = 10,000 blocking HTTP calls
  • Jika API down, login jadi sangat lambat atau timeout

Fix:

// Gunakan event listener dengan queue
protected function authenticated(Request $request, $user)
{
    // Dispatch event untuk async logging
    event(new UserLoggedIn($user));
    
    // Atau langsung dispatch job
    dispatch(new LogUserActivity(
        'login',
        'Pengguna berhasil login: ' . $user->name,
        $user,
        'success'
    ))->onQueue('activity-logs');
}

// Buat Job untuk async logging
class LogUserActivity implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public function handle()
    {
        ActivityLogger::log(
            $this->logName,
            $this->description,
            $this->subject,
            $this->event
        );
    }
}

@devopsopendesa
Copy link
Copy Markdown
Contributor

📍 app/Http/Controllers/LogViewerController.php (baris 175)

[HIGH] ⚡ Performance: Bulk Delete Tanpa Chunking

Masalah: Method cleanupActivityLogs() menggunakan delete() langsung yang bisa menghapus jutaan rows sekaligus. Ini akan lock table dan menyebabkan deadlock pada production.

Kode:

$deleted = Activity::where('created_at', '<', now()->subDays($days))->delete();

Dampak:

  • Dengan 1 juta logs untuk dihapus: query bisa 30-60 detik
  • Table lock selama delete: semua insert/update activity log terhambat
  • Deadlock risk tinggi pada concurrent requests
  • Memory spike untuk delete operation

Fix:

public function cleanupActivityLogs(Request $request)
{
    try {
        $days = $request->input('days', 365);
        $deleted = 0;
        
        // Gunakan chunking untuk delete bertahap
        Activity::where('created_at', '<', now()->subDays($days))
            ->chunkById(1000, function ($activities) use (&$deleted) {
                $ids = $activities->pluck('id');
                Activity::whereIn('id', $ids)->delete();
                $deleted += $ids->count();
                
                // Sleep sebentar untuk avoid lock
                usleep(100000); // 100ms
            });

        ActivityLogger::log(
            'aplikasi',
            'Activity logs berhasil dibersihkan',
            null,
            'cleanup',
            ['days' => $days, 'deleted_count' => $deleted]
        );

        return response()->json([
            'success' => true,
            'message' => "Berhasil menghapus {$deleted} log aktivitas yang lebih lama dari {$days} hari",
        ]);
    } catch (\Exception $e) {
        report($e);
        return response()->json([
            'success' => false,
            'message' => 'Gagal membersihkan log aktivitas: ' . $e->getMessage(),
        ], 500);
    }
}

// ATAU gunakan queue job untuk background processing
dispatch(new CleanupActivityLogs($days))->onQueue('maintenance');

*/
public static function log(
string $category,
string $event,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Type Hints untuk Parameter

Kategori: PHP Quality

Masalah: Parameter $subject tidak memiliki type hint. Di PHP 8+, semua parameter dan return type wajib memiliki type hints untuk type safety.

Kode: $subject = null

Fix:

public static function log(
    string $logName,
    string $description,
    ?string $event = null,
    mixed $subject = null,  // PHP 8.0+ mixed type
    array $properties = []
): Activity


class IpLocationResolver
{
private const CACHE_PREFIX = 'ip_location_';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Return Type Hint

Kategori: PHP Quality

Masalah: Method resolve() sudah ada return type ?array yang baik, namun perlu memastikan konsistensi. Method private isPrivateIp() dan ipInRange() sudah memiliki type hints yang lengkap. Ini sudah baik.

Kode: Sudah benar, tidak ada isu.

Fix: Tidak diperlukan - kode sudah memiliki type hints lengkap.

$roles = $request->input('role') ? $request->input('role') : [];
$user->assignRole($roles);

ActivityLogger::log(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Method Terlalu Panjang & Inline HTML di Controller

Kategori: Architecture

Masalah: Method getData() memiliki 40+ baris dengan inline HTML untuk action buttons. Ini melanggar Single Responsibility Principle dan membuat controller sulit di-test. HTML seharusnya di Blade component atau view partial.

Kode:

$action = '<a href="' . $edit_url . '" class="btn btn-sm btn-warning"><i class="fa fa-edit"></i> Ubah</a> ';
// ... lebih banyak HTML inline

Fix:

// Buat Blade component: resources/views/components/user-actions.blade.php
// Di Controller:
->addColumn('action', function ($user) {
    return view('components.user-actions', compact('user'))->render();
})

// Atau lebih baik, pindahkan ke View:
// resources/views/components/user-actions.blade.php
<a href="{{ route('user.edit', $user->id) }}" class="btn btn-sm btn-warning">
    <i class="fa fa-edit"></i> Ubah
</a>
@if($user->status == 1)
    <a href="{{ route('user.suspend', $user->id) }}" 
       class="btn btn-sm btn-danger"
       onclick="return confirm('Apakah Anda yakin?')">
        <i class="fa fa-ban"></i> Nonaktifkan
    </a>
@else
    <a href="{{ route('user.activate', $user->id) }}" 
       class="btn btn-sm btn-success">
        <i class="fa fa-check"></i> Aktifkan
    </a>
@endif

@devopsopendesa
Copy link
Copy Markdown
Contributor

📍 app/Http/Controllers/LogViewerController.php (baris 60)

[HIGH] 📝 Code Quality: Method Terlalu Panjang & Inline HTML

Kategori: Architecture

Masalah: Method getActivityLogs() memiliki 70+ baris dengan inline HTML untuk badges dan buttons. Sama seperti UserController, ini melanggar separation of concerns.

Kode:

->addColumn('event_badge', function ($activity) {
    $badges = [...];
    $badgeClass = $badges[$activity->event] ?? 'secondary';
    return '<span class="badge badge-' . $badgeClass . '">' . ($activity->event ?? '-') . '</span>';
})

Fix:

// Buat helper atau Blade component
// app/Helpers/ActivityLogHelper.php
class ActivityLogHelper {
    public static function getEventBadge(?string $event): string {
        return view('components.activity-event-badge', compact('event'))->render();
    }
}

// resources/views/components/activity-event-badge.blade.php
@php
$badges = [
    'success' => 'success',
    'failed' => 'danger',
    // ...
];
$class = $badges[$event] ?? 'secondary';
@endphp
<span class="badge badge-{{ $class }}">{{ $event ?? '-' }}</span>

// Di Controller:
->addColumn('event_badge', function ($activity) {
    return ActivityLogHelper::getEventBadge($activity->event);
})

@devopsopendesa
Copy link
Copy Markdown
Contributor

📍 app/Http/Controllers/LogViewerController.php (baris 155)

[CRITICAL] 📝 Code Quality: Missing Rate Limiting pada Cleanup Endpoint

Kategori: Architecture

Masalah: Method cleanupActivityLogs() dapat menghapus ribuan records tanpa rate limiting atau authorization check yang ketat. Ini bisa disalahgunakan untuk DoS attack atau menghapus audit trail penting.

Kode:

public function cleanupActivityLogs(Request $request)
{
    $request->validate(['days' => 'required|integer|min:1']);
    $count = Activity::where('created_at', '<', $date)->delete();
    // Tidak ada rate limiting atau permission check
}

Fix:

use Illuminate\Support\Facades\RateLimiter;

public function cleanupActivityLogs(Request $request)
{
    // 1. Tambahkan permission check
    $this->authorize('cleanup-activity-logs'); // Perlu setup di AuthServiceProvider
    
    // 2. Tambahkan rate limiting
    $key = 'cleanup-logs:' . $request->user()->id;
    if (RateLimiter::tooManyAttempts($key, 3)) {
        $seconds = RateLimiter::availableIn($key);
        return back()->with('error', "Terlalu banyak percobaan. Coba lagi dalam {$seconds} detik.");
    }
    
    $request->validate([
        'days' => 'required|integer|min:30|max:365', // Minimal 30 hari untuk safety
    ]);

    try {
        $date = now()->subDays($request->days);
        $count = Activity::where('created_at', '<', $date)->delete();
        
        RateLimiter::hit($key, 3600); // 1 jam cooldown
        
        // Log cleanup action
        ActivityLogger::log(
            'sistem',
            "Cleanup activity logs: {$count} records dihapus (>{$request->days} hari)",
            'cleanup',
            null,
            ['deleted_count' => $count, 'days' => $request->days]
        );

        return redirect()->back()->with('success', "Berhasil menghapus {$count} log aktivitas");
    } catch (\Exception $e) {
        report($e);
        return redirect()->back()->with('error', 'Terjadi kesalahan saat menghapus log aktivitas');
    }
}

<th>Aksi</th>
<th>Kategori</th>
<th>Peristiwa</th>
<th>Subjek Tipe</th>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 📝 Code Quality: N+1 Query Problem di Blade

Kategori: Frontend

Masalah: Query \App\Models\User::all() langsung di Blade akan menyebabkan N+1 problem dan performance issue. Database query seharusnya di Controller, bukan di View.

Kode:

@foreach(\App\Models\User::all() as $user)
    <option value="{{ $user->id }}">{{ $user->name }}</option>
@endforeach

Fix:

// Di LogViewerController::index() atau method yang render view ini
public function activityLogsView()
{
    $page_title = 'Log Aktivitas';
    $users = User::select('id', 'name')->orderBy('name')->get(); // Hanya ambil kolom yang diperlukan
    
    return view('vendor.laravel-log-viewer.activity-logs', compact('page_title', 'users'));
}

// Di Blade:
@foreach($users as $user)
    <option value="{{ $user->id }}">{{ $user->name }}</option>
@endforeach

processing: true,
serverSide: true,
ajax: {
url: '{{ route("setting.info-sistem.activity-logs.data") }}',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Inline JavaScript Seharusnya di File Terpisah

Kategori: Frontend

Masalah: 100+ baris JavaScript inline di Blade file membuat kode sulit di-maintain, tidak bisa di-cache browser, dan tidak bisa di-minify. JavaScript seharusnya di file terpisah di resources/js/.

Kode:

<script>
$(document).ready(function() {
    // 100+ baris JavaScript
});
</script>

Fix:

// resources/js/activity-logs.js
export function initActivityLogsTable() {
    const table = $('#activity-logs-table').DataTable({
        processing: true,
        serverSide: true,
        ajax: {
            url: window.activityLogsDataUrl,
            data: function(d) {
                d.log_name = $('#filter-log-name').val();
                d.event = $('#filter-event').val();
                d.causer_id = $('#filter-causer').val();
            }
        },
        // ... rest of config
    });
    
    // Event handlers
    $('#btn-filter').on('click', () => table.ajax.reload());
    
    $('#activity-logs-table').on('click', '.view-detail', function() {
        const id = $(this).data('id');
        loadActivityDetail(id);
    });
}

function loadActivityDetail(id) {
    $.ajax({
        url: window.activityLogDetailUrl.replace(':id', id),
        method: 'GET',
        success: function(response) {
            populateModal(response);
            $('#detail-modal').modal('show');
        },
        error: function() {
            alert('Gagal memuat detail log aktivitas');
        }
    });
}

// Di Blade, hanya:
<script>
window.activityLogsDataUrl = '{{ route('setting.info-sistem.activity-logs.data') }}';
window.activityLogDetailUrl = '{{ route('setting.info-sistem.activity-logs.detail', ':id') }}';
</script>
<script src="{{ asset('js/activity-logs.js') }}"></script>


if (! $payload || ($payload['success'] ?? true) === false) {
return [
'ip_location_available' => false,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🐛 Bug: JsonException Tidak Ditangani - Crash Saat API Return Invalid JSON

Kode: $data = $response->json();

Skenario: API ipwhois.app return HTML error page, malformed JSON, atau response kosong. Method json() akan throw JsonException yang tidak di-catch, menyebabkan entire request crash.

Dampak:

  • Setiap kali user login/logout/update data, jika API down atau return invalid response, aplikasi crash dengan 500 error
  • User tidak bisa login/logout sama sekali
  • Activity logging menghentikan seluruh flow bisnis

Fix:

return Cache::remember("ip_location_{$ip}", now()->addHours(24), function () use ($ip) {
    try {
        $response = Http::timeout(5)->get("http://ipwhois.app/json/{$ip}");
        
        if ($response->successful()) {
            $data = $response->json(); // Bisa throw JsonException
            
            return [
                'country' => $data['country'] ?? 'Unknown',
                'country_code' => $data['country_code'] ?? 'Unknown',
                'region' => $data['region'] ?? 'Unknown',
                'city' => $data['city'] ?? 'Unknown',
                'isp' => $data['isp'] ?? 'Unknown',
            ];
        }
    } catch (\Exception $e) {
        // Log error tapi jangan crash aplikasi
        report($e);
    }
    
    return self::getDefaultLocation();
});


$payload = $response->json();

if (! $payload || ($payload['success'] ?? true) === false) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: HTTP Request Exception Tidak Ditangani - Network Failure Crash

Kode: $response = Http::timeout(5)->get("http://ipwhois.app/json/{$ip}");

Skenario: Network timeout, DNS failure, connection refused, atau API down. Http::get() akan throw ConnectionException atau RequestException yang tidak di-catch.

Dampak:

  • Setiap aktivitas user (login, update profil, dll) akan crash jika network ke API bermasalah
  • Timeout 5 detik akan block request selama 5 detik sebelum crash
  • Production downtime karena dependency ke external API

Fix:

return Cache::remember("ip_location_{$ip}", now()->addHours(24), function () use ($ip) {
    try {
        $response = Http::timeout(5)->get("http://ipwhois.app/json/{$ip}");
        
        if ($response->successful()) {
            $data = $response->json();
            
            return [
                'country' => $data['country'] ?? 'Unknown',
                'country_code' => $data['country_code'] ?? 'Unknown',
                'region' => $data['region'] ?? 'Unknown',
                'city' => $data['city'] ?? 'Unknown',
                'isp' => $data['isp'] ?? 'Unknown',
            ];
        }
    } catch (\Exception $e) {
        report($e);
    }
    
    return self::getDefaultLocation();
});

}

$properties = array_filter(
array_merge($metadata, $ipLocation, $additionalProperties),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: IpLocationResolver Exception Tidak Ditangani - Logging Crash Request

Kode: $location = IpLocationResolver::resolve($ip);

Skenario: IpLocationResolver::resolve() throw exception (network error, JSON parse error, cache error). Exception tidak di-catch di ActivityLogger::log(), menyebabkan entire request crash.

Dampak:

  • Login berhasil tapi user dapat error 500 karena logging gagal
  • Update profil berhasil tersimpan tapi user lihat error page
  • Semua operasi yang pakai ActivityLogger akan crash jika IP resolution gagal

Fix:

public static function log(string $logName, string $description, ?Model $subject = null, ?string $event = null, array $additionalProperties = []): void
{
    try {
        $properties = self::enrichMetadata($additionalProperties);
        
        activity($logName)
            ->performedOn($subject)
            ->causedBy(Auth::user())
            ->event($event)
            ->withProperties($properties)
            ->log($description);
    } catch (\Exception $e) {
        // Log error tapi jangan crash request
        report($e);
    }
}

private static function enrichMetadata(array $additionalProperties): array
{
    // ... existing code ...
    
    try {
        $location = IpLocationResolver::resolve($ip);
        $properties = array_merge($properties, $location);
    } catch (\Exception $e) {
        report($e);
        $properties['location_error'] = 'Failed to resolve location';
    }
    
    return array_merge($properties, $additionalProperties);
}

@@ -53,7 +54,9 @@ class LoginController extends Controller
|
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Auth::user() Null Dereference - Fatal Error Setelah Login

Kode:

$user = Auth::user();
// ...
ActivityLogger::log('login', "Pengguna {$user->name} berhasil login", $user, 'success');

Skenario: Race condition dimana user berhasil authenticate tapi kemudian dihapus/disabled sebelum Auth::user() dipanggil. $user akan null, akses $user->name menyebabkan fatal error "Trying to get property 'name' of null".

Dampak:

  • User berhasil login tapi dapat white screen/500 error
  • Session sudah dibuat tapi user tidak bisa masuk
  • Harus logout manual atau clear session

Fix:

protected function authenticated(Request $request, $user)
{
    if (!$user) {
        Auth::logout();
        return redirect()->route('login')->with('error', 'Terjadi kesalahan saat login');
    }
    
    try {
        ActivityLogger::log('login', "Pengguna {$user->name} berhasil login", $user, 'success');
    } catch (\Exception $e) {
        report($e);
        // Jangan crash login flow jika logging gagal
    }
    
    return redirect()->intended($this->redirectPath());
}


use AuthenticatesUsers;
use AuthenticatesUsers {
sendFailedLoginResponse as traitSendFailedLoginResponse;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: ActivityLogger Exception Menghentikan Login Flow

Kode: ActivityLogger::log('login', "Pengguna {$user->name} berhasil login", $user, 'success');

Skenario: ActivityLogger::log() throw exception (dari IpLocationResolver network error atau cache error). Exception tidak di-catch, user sudah authenticated tapi melihat error page.

Dampak:

  • Login berhasil (session dibuat) tapi user lihat error 500
  • User bingung apakah login berhasil atau tidak
  • Harus refresh page untuk masuk

Fix:

protected function authenticated(Request $request, $user)
{
    try {
        ActivityLogger::log('login', "Pengguna {$user->name} berhasil login", $user, 'success');
    } catch (\Exception $e) {
        report($e);
        // Jangan crash login flow
    }
    
    return redirect()->intended($this->redirectPath());
}

@devopsopendesa
Copy link
Copy Markdown
Contributor

📍 app/Http/Controllers/Auth/LoginController.php (baris 76)

[HIGH] 🐛 Bug: ActivityLogger Exception Menghilangkan Error Message Login Gagal

Kode: ActivityLogger::log('login', 'Percobaan login gagal', null, 'failed', ['email' => $request->email]);

Skenario: User salah password, tapi ActivityLogger::log() throw exception. Exception tidak di-catch, user tidak melihat pesan "Email atau password salah" melainkan error 500.

Dampak:

  • User tidak tahu kenapa login gagal (salah password atau sistem error)
  • Brute force attack tidak terdeteksi karena logging gagal
  • User experience buruk

Fix:

protected function sendFailedLoginResponse(Request $request)
{
    try {
        ActivityLogger::log('login', 'Percobaan login gagal', null, 'failed', [
            'email' => $request->email,
        ]);
    } catch (\Exception $e) {
        report($e);
        // Tetap tampilkan error message ke user
    }
    
    throw ValidationException::withMessages([
        $this->username() => [trans('auth.failed')],
    ]);
}

@devopsopendesa
Copy link
Copy Markdown
Contributor

📍 app/Http/Controllers/Auth/LoginController.php (baris 95)

[HIGH] 🐛 Bug: ActivityLogger Exception Menghentikan Logout

Kode: ActivityLogger::log('login', "Pengguna {$user->name} berhasil logout", $user, 'logout');

Skenario: User klik logout, tapi ActivityLogger::log() throw exception. Session belum di-destroy, user masih login tapi melihat error page.

Dampak:

  • Logout gagal, user masih dalam keadaan login
  • Security risk: user pikir sudah logout tapi session masih aktif
  • Harus close browser untuk logout paksa

Fix:

public function logout(Request $request)
{
    $user = Auth::user();
    
    Auth::logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();
    
    try {
        if ($user) {
            ActivityLogger::log('login', "Pengguna {$user->name} berhasil logout", $user, 'logout');
        }
    } catch (\Exception $e) {
        report($e);
        // Logout sudah berhasil, logging error tidak boleh mengganggu
    }
    
    return redirect('/');
}

@devopsopendesa
Copy link
Copy Markdown
Contributor

🤖 AI Code Review — PR #1334

📊 Review Results

Area Temuan Posted
Full-Stack Security Specialist (PHP + JavaScript) 5 ✅ 5
Full-Stack Performance Analyst 8 ✅ 8
Full-Stack Code Quality & Architecture Reviewer 7 ✅ 7
Full-Stack Logic Bug Hunter (PHP + JavaScript) 7 ✅ 7

Total: 27 inline comments posted.

Setiap temuan sudah di-post sebagai inline comment pada file terkait.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants