Conversation
|
mas @hasanlq69 silahkan perbaiki PR dengan menghapus file-file yang tidak diperlukan untuk masuk ke repo. |
|
mas @hasanlq69 masih ditemukan error saat kondisi masuk dengan user & password salah : |
| @@ -41,6 +41,7 @@ | |||
| use Spatie\Permission\Models\Role; | |||
| use App\Http\Controllers\Controller; | |||
There was a problem hiding this comment.
[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');
}|
📍 [CRITICAL] 🔒 Security: IDOR - Password Change Without Authorization Masalah: Method 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:
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: hacked123Fix: 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, |
There was a problem hiding this comment.
[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');
}|
📍 [HIGH] 🔒 Security: IDOR - Activity Log Detail Exposure Masalah: Method 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:
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 informationFix: 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; | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
[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 !== '' | ||
| ); |
There was a problem hiding this comment.
[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', | ||
| ]; |
There was a problem hiding this comment.
[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}");|
📍 [CRITICAL] ⚡ Performance: Missing Pagination - Load Semua User Masalah: Method Kode: $data = User::with('roles')->get();Dampak:
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);
} |
|
📍 [HIGH] ⚡ Performance: Potential N+1 Query di DataTables Masalah: Meskipun sudah ada Kode: ->editColumn('causer.name', function ($activity) {
return $activity->causer ? $activity->causer->name : '-';
})Dampak:
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, |
There was a problem hiding this comment.
[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> |
There was a problem hiding this comment.
[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>
@endforeachDampak:
- 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>|
📍 [HIGH] ⚡ Performance: Synchronous Logging Memperlambat Login Masalah: Setiap login/logout memanggil Kode: protected function authenticated(Request $request, $user)
{
ActivityLogger::log(
'login',
'Pengguna berhasil login: ' . $user->name,
$user,
'success'
);
}Dampak:
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
);
}
} |
|
📍 [HIGH] ⚡ Performance: Bulk Delete Tanpa Chunking Masalah: Method Kode: $deleted = Activity::where('created_at', '<', now()->subDays($days))->delete();Dampak:
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, |
There was a problem hiding this comment.
[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_'; |
There was a problem hiding this comment.
[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( |
There was a problem hiding this comment.
[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 inlineFix:
// 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|
📍 [HIGH] 📝 Code Quality: Method Terlalu Panjang & Inline HTML Kategori: Architecture Masalah: Method 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);
}) |
|
📍 [CRITICAL] 📝 Code Quality: Missing Rate Limiting pada Cleanup Endpoint Kategori: Architecture Masalah: Method 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> |
There was a problem hiding this comment.
[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>
@endforeachFix:
// 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") }}', |
There was a problem hiding this comment.
[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, |
There was a problem hiding this comment.
[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) { |
There was a problem hiding this comment.
[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), |
There was a problem hiding this comment.
[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 | |||
| | | |||
| */ | |||
|
|
|||
There was a problem hiding this comment.
[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; |
There was a problem hiding this comment.
[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());
}|
📍 [HIGH] 🐛 Bug: ActivityLogger Exception Menghilangkan Error Message Login Gagal Kode: Skenario: User salah password, tapi Dampak:
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')],
]);
} |
|
📍 [HIGH] 🐛 Bug: ActivityLogger Exception Menghentikan Logout Kode: Skenario: User klik logout, tapi Dampak:
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('/');
} |
🤖 AI Code Review — PR #1334📊 Review Results
Total: 27 inline comments posted. Setiap temuan sudah di-post sebagai inline comment pada file terkait. |

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
Usersecara otomatis mencatat perubahan (create, update, delete).Implementasi Teknis
Best Practice Pencatatan Log
Struktur ideal untuk mencatat log secara manual adalah sebagai berikut:
Contoh Implementasi di Controller
LoginController (
authenticated&logout)UserController (
store&update)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.Modal Detail
Modal detail akan menampilkan data dari server, termasuk
ip_addressdanuser_agentyang diambil dari kolompropertiespada log.Troubleshooting
Berikut adalah beberapa masalah umum yang mungkin terjadi dan solusinya.
Masalah: Tabel log aktivitas kosong dan di console muncul error
Uncaught TypeError: $ is not a function.@push('scripts')di file view Blade.Masalah: Kolom
EventatauSubjectmenampilkanN/A.event()atauperformedOn()tidak dipanggil.activity()menyertakan->event('nama_event')dan->performedOn($model)jika relevan.Masalah: Saat klik tombol "Detail", muncul error
Cannot read properties of undefined (reading 'ip_address').propertiesatau tidak berisiip_address. Kode JavaScript mencoba membaca properti dari objek yangundefined.var properties = response.properties || {};.Masalah: Di console muncul error CORS saat mencoba memuat file
Indonesian.jsondari CDN.opendk.test).Indonesian.jsondan simpan secara lokal di dalam direktoripublic/, lalu ubah URL di konfigurasi DataTables untuk menunjuk ke file lokal tersebut.API Endpoints
Get Activity Logs Data
GET /setting/info-sistem/activity-logs/dataaction(optional) - Filter by event typeuser_id(optional) - Filter by userdate_from(optional) - Start date filterdate_to(optional) - End date filterGet Activity Log Detail
GET /setting/info-sistem/activity-logs/detail/{id}Maintenance
Performance Considerations
Security
Permission Control
super-adminatauadministrator-websiteyang bisa aksesData Privacy
Troubleshooting
Common Issues
Log tidak muncul
LogsActivitytraitgetActivitylogOptions()configurationPerformance lambat
Memory issues
Future Enhancements
Testing
Manual Testing
Verification
SELECT * FROM activity_log ORDER BY created_at DESC LIMIT 10;http://localhost:8000/setting/info-sistem