diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index e94158aa38..dd74e67907 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -31,14 +31,15 @@ namespace App\Http\Controllers\Auth; -use App\Http\Controllers\Controller; use App\Models\User; use App\Notifications\SendToken2FA; use App\Providers\RouteServiceProvider; +use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\View; +use App\Services\ActivityLogger; class LoginController extends Controller { @@ -53,7 +54,9 @@ class LoginController extends Controller | */ - use AuthenticatesUsers; + use AuthenticatesUsers { + sendFailedLoginResponse as traitSendFailedLoginResponse; + } /** * Where to redirect users after login. @@ -134,6 +137,20 @@ protected function validateLogin(Request $request) } protected function authenticated(Request $request, $user) { + // Log successful login + ActivityLogger::log( + category: 'login', + event: 'success', + message: 'Pengguna berhasil masuk ke sistem', + subject: $user, + causer: $user, + additionalProperties: [ + 'user_id' => $user->id, + 'email' => $user->email, + 'status' => 'authenticated', + ] + ); + if (($this->settings['login_2fa'] ?? false)) { return $this->startTwoFactorAuthProcess($request, $user); } @@ -141,6 +158,71 @@ protected function authenticated(Request $request, $user) return redirect()->intended($this->redirectPath()); } + /** + * Handle a failed authentication attempt. + * + * @param \Illuminate\Http\Request $request + * @return void + * + * @throws \Illuminate\Validation\ValidationException + */ + protected function sendFailedLoginResponse(Request $request) + { + // Log failed login attempt + ActivityLogger::log( + category: 'login', + event: 'failed', + message: 'Percobaan login gagal', + subject: null, + causer: null, + additionalProperties: [ + 'email' => $request->input($this->username()), + 'status' => 'invalid_credentials', + ] + ); + + return $this->traitSendFailedLoginResponse($request); + } + + /** + * The user has logged out of the application. + * + * @param \Illuminate\Http\Request $request + * @return mixed + */ + public function logout(Request $request) + { + // Log the logout event before logging out + if (auth()->check()) { + ActivityLogger::log( + category: 'login', + event: 'logout', + message: 'Pengguna keluar dari sistem', + subject: auth()->user(), + causer: auth()->user(), + additionalProperties: [ + 'user_id' => auth()->id(), + 'email' => auth()->user()->email ?? null, + 'status' => 'logged_out', + ] + ); + } + + $this->guard()->logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + + return $this->loggedOut($request) ?: redirect('/'); + } + + protected function loggedOut(Request $request) + { + // This method can be used for additional logic after logout + // For example, flashing a session message. + } + /** * Log out the user and start the two factor authentication state. * diff --git a/app/Http/Controllers/Data/ProfilController.php b/app/Http/Controllers/Data/ProfilController.php index fa223ee041..c5d8f6e6a7 100644 --- a/app/Http/Controllers/Data/ProfilController.php +++ b/app/Http/Controllers/Data/ProfilController.php @@ -37,6 +37,7 @@ use App\Models\Profil; use Illuminate\Http\Response; use Illuminate\Support\Facades\Cache; +use App\Services\ActivityLogger; class ProfilController extends Controller { @@ -110,11 +111,34 @@ public function update(ProfilRequest $request, $id) $profil->update(); $dataumum->update(); + ActivityLogger::log( + category: 'profil', + event: 'updated', + message: "Mengubah data profil kecamatan: {$profil->nama_kecamatan}", + subject: $profil, + causer: auth()->user(), + additionalProperties: [ + 'profil_id' => $profil->id, + 'changes' => array_merge($profil->getChanges(), $dataumum?->getChanges() ?? []), + ] + ); + // Clear cache setelah update data kecamatan $this->clearProfilCache(); } catch (\Exception $e) { report($e); + ActivityLogger::log( + category: 'profil', + event: 'failed', + message: 'Gagal mengubah profil kecamatan', + causer: auth()->user(), + additionalProperties: [ + 'error' => $e->getMessage(), + 'profil_id' => $id, + ] + ); + return back()->withInput()->with('error', 'Update Profil gagal!'); } diff --git a/app/Http/Controllers/LogViewerController.php b/app/Http/Controllers/LogViewerController.php index 741b512e23..fa2c7078f5 100644 --- a/app/Http/Controllers/LogViewerController.php +++ b/app/Http/Controllers/LogViewerController.php @@ -34,12 +34,15 @@ use App\Http\Requests\EmailSmtpRequest; use App\Mail\SmtpTestEmail; use App\Models\EmailSmtp; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Mail; use RachidLaasri\LaravelInstaller\Helpers\RequirementsChecker; use Rap2hpoutre\LaravelLogViewer\LaravelLogViewer; +use Spatie\Activitylog\Models\Activity; use Symfony\Component\HttpFoundation\Response; +use Yajra\DataTables\DataTables; /** * Class LogViewerController @@ -90,6 +93,62 @@ public function index() return $early_return; } + $activityCategories = Activity::query() + ->select('log_name') + ->whereNotNull('log_name') + ->distinct() + ->orderBy('log_name') + ->pluck('log_name') + ->filter() + ->values(); + + $activityEvents = Activity::query() + ->select('event') + ->whereNotNull('event') + ->distinct() + ->orderBy('event') + ->pluck('event') + ->filter() + ->values(); + + $activityUsers = Activity::query() + ->whereNotNull('causer_id') + ->whereNotNull('causer_type') + ->select('causer_id', 'causer_type') + ->distinct() + ->get() + ->groupBy('causer_type') + ->flatMap(function ($activities, $modelClass) { + if (! class_exists($modelClass)) { + return collect(); + } + + $ids = $activities->pluck('causer_id')->unique()->toArray(); + + try { + $instances = $modelClass::whereIn('id', $ids)->get(); + } catch (\Throwable $e) { + report($e); + + return collect(); + } + + return $instances->map(function ($instance) use ($modelClass) { + $name = $instance->name ?? ($instance->email ?? ('ID: '.$instance->getKey())); + + return [ + 'id' => $instance->getKey(), + 'type' => $modelClass, + 'name' => $name, + ]; + }); + }) + ->unique(function ($item) { + return $item['type'].'|'.$item['id']; + }) + ->sortBy('name') + ->values(); + $data = [ 'tab' => session('tab', 'log_viewer'), 'logs' => $this->log_viewer->all(), @@ -101,7 +160,9 @@ public function index() 'standardFormat' => true, 'structure' => $this->log_viewer->foldersAndFiles(), 'storage_path' => $this->log_viewer->getStoragePath(), - + 'activityCategories' => $activityCategories, + 'activityEvents' => $activityEvents, + 'activityUsers' => $activityUsers, ]; if ($this->request->wantsJson()) { @@ -273,4 +334,148 @@ public function sendTestEmailSmtp($email) 'success' => true, ], Response::HTTP_OK); } + + /** + * Menampilkan data activity logs untuk DataTables + */ + public function getActivityLogs(Request $request) + { + $query = Activity::with('causer')->latest(); + + // Filter berdasarkan kategori (log_name) + if ($request->filled('category')) { + $query->where('log_name', $request->category); + } + + // Filter berdasarkan peristiwa (event) + if ($request->filled('event')) { + $query->where('event', $request->event); + } + + // Filter berdasarkan pengguna + if ($request->filled('user')) { + [$causerType, $causerId] = array_pad(explode('|', $request->user), 2, null); + + if ($causerId) { + $query->where('causer_id', $causerId); + } + + if ($causerType) { + $query->where('causer_type', $causerType); + } + } + + return DataTables::of($query) + ->addIndexColumn() + ->addColumn('category', function ($log) { + return $log->log_name ? ucfirst($log->log_name) : '-'; + }) + ->addColumn('user_display', function ($log) { + return $log->causer ? $log->causer->name : 'System'; + }) + ->addColumn('event_badge', function ($log) { + $badges = [ + 'login' => 'success', + 'logout' => 'info', + 'created' => 'primary', + 'updated' => 'warning', + 'deleted' => 'danger', + 'retrieved' => 'secondary' + ]; + $event = $log->event ?? 'N/A'; + $badge = $badges[$event] ?? 'secondary'; + + return '' . ucfirst($event) . ''; + }) + ->addColumn('subject_type', function ($log) { + return $log->subject_type ? class_basename($log->subject_type) : '-'; + }) + ->addColumn('causer_type', function ($log) { + return $log->causer_type ? class_basename($log->causer_type) : 'System'; + }) + ->addColumn('formatted_date', function ($log) { + return $log->created_at->format('d/m/Y H:i:s'); + }) + ->addColumn('aksi', function ($log) { + return ''; + }) + ->rawColumns(['event_badge', 'aksi']) + ->make(true); + } + + /** + * Menampilkan detail activity log + */ + public function getActivityLogDetail($id) + { + $log = Activity::with('causer')->findOrFail($id); + $properties = $log->properties ? $log->properties->toArray() : []; + + $userLabel = 'System'; + if ($log->causer) { + $name = $log->causer->name ?? null; + $email = $log->causer->email ?? null; + $identifier = $name ?: ($email ?: ('ID: '.$log->causer->getKey())); + $userLabel = $email && $name ? $name.' ('.$email.')' : $identifier; + } + + $subjectType = $log->subject_type ? class_basename($log->subject_type) : null; + $causerType = $log->causer_type ? class_basename($log->causer_type) : null; + + return response()->json([ + 'success' => true, + 'data' => [ + 'id' => $log->id, + 'user' => $userLabel, + 'event' => $log->event, + 'description' => $log->description, + 'category' => $log->log_name, + 'subject_type' => $subjectType, + 'subject_id' => $log->subject_id, + 'causer_type' => $causerType, + 'causer_id' => $log->causer_id, + 'created_at' => $log->created_at->format('d/m/Y H:i:s'), + 'ip_address' => data_get($properties, 'ip_address'), + 'user_agent' => data_get($properties, 'user_agent'), + 'url' => data_get($properties, 'url'), + 'slug' => data_get($properties, 'slug'), + 'method' => data_get($properties, 'method'), + 'browser' => data_get($properties, 'browser'), + 'platform' => data_get($properties, 'platform'), + 'ip_country' => data_get($properties, 'ip_country'), + 'ip_country_code' => data_get($properties, 'ip_country_code'), + 'ip_region' => data_get($properties, 'ip_region'), + 'ip_city' => data_get($properties, 'ip_city'), + 'ip_location_available' => data_get($properties, 'ip_location_available'), + 'ip_location_message' => data_get($properties, 'ip_location_message'), + 'referer' => data_get($properties, 'referer'), + 'properties' => $properties, + ] + ]); + } + + /** + * Menghapus activity logs lama (cleanup) + */ + public function cleanupActivityLogs(Request $request) + { + try { + $days = $request->input('days', 30); // Default 30 hari + $cutoffDate = now()->subDays($days); + + $deletedCount = Activity::where('created_at', '<', $cutoffDate)->delete(); + + return response()->json([ + 'success' => true, + 'message' => "Berhasil menghapus {$deletedCount} log aktivitas yang lebih dari {$days} hari." + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal menghapus log aktivitas: ' . $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } } diff --git a/app/Http/Controllers/Setting/AplikasiController.php b/app/Http/Controllers/Setting/AplikasiController.php index 8fa4806ad2..59fb1ef61e 100644 --- a/app/Http/Controllers/Setting/AplikasiController.php +++ b/app/Http/Controllers/Setting/AplikasiController.php @@ -61,14 +61,33 @@ public function update(Request $request, $id) ]); try { - $penyakit = SettingAplikasi::findOrFail($id); - $penyakit->fill($request->only(['value'])); - $penyakit->save(); + $setting = SettingAplikasi::findOrFail($id); + $oldValue = $setting->value; + $setting->fill($request->only(['value'])); + $setting->save(); + + // Log activity + activity() + ->causedBy(auth()->user()) + ->performedOn($setting) + ->withProperties([ + 'setting_name' => $setting->key, + 'old_value' => $oldValue, + 'new_value' => $request->input('value') + ]) + ->event('updated') + ->log("Mengubah pengaturan {$setting->key}"); $this->browser_title = $request->input('value'); } catch (\Exception $e) { report($e); + // Log failed activity + activity() + ->causedBy(auth()->user()) + ->withProperties(['error' => $e->getMessage(), 'setting_id' => $id]) + ->log('Gagal mengubah pengaturan aplikasi'); + return back()->with('error', 'Pengaturan aplikasi gagal diubah!'); } diff --git a/app/Http/Controllers/User/UserController.php b/app/Http/Controllers/User/UserController.php index 1cdee03aea..4d290a8799 100644 --- a/app/Http/Controllers/User/UserController.php +++ b/app/Http/Controllers/User/UserController.php @@ -41,6 +41,7 @@ use Spatie\Permission\Models\Role; use App\Http\Controllers\Controller; use App\Http\Requests\UserUpdateRequest; +use App\Services\ActivityLogger; class UserController extends Controller { @@ -91,10 +92,33 @@ public function store(UserRequest $request) $roles = $request->input('role') ? $request->input('role') : []; $user->assignRole($roles); + ActivityLogger::log( + category: 'pengguna', + event: 'created', + message: "Membuat pengguna baru: {$user->name} ({$user->email})", + subject: $user, + causer: auth()->user(), + additionalProperties: [ + 'user_id' => $user->id, + 'roles' => $roles, + ] + ); + return redirect()->route('setting.user.index')->with('success', 'User berhasil ditambahkan!'); } catch (\Exception $e) { report($e); + ActivityLogger::log( + category: 'pengguna', + event: 'failed', + message: 'Gagal membuat pengguna baru', + causer: auth()->user(), + additionalProperties: [ + 'error' => $e->getMessage(), + 'input' => $request->except(['password', 'password_confirmation', '_token']), + ] + ); + return back()->withInput()->with('error', $e->getMessage()); } } @@ -141,17 +165,42 @@ public function update(UserRequest $request, $id) try { $input = $request->validated(); $user = User::findOrFail($id); + $this->handleFileUpload($request, $input, 'image', 'user', false); $user->update($input); - if (! empty($request->role)) { - $roles = $request->input('role') ? $request->input('role') : []; + $roles = $request->input('role') ? $request->input('role') : []; + if (! empty($roles)) { $user->syncRoles($roles); } + ActivityLogger::log( + category: 'pengguna', + event: 'updated', + message: "Mengubah data pengguna: {$user->name} ({$user->email})", + subject: $user, + causer: auth()->user(), + additionalProperties: [ + 'user_id' => $user->id, + 'roles' => $roles, + 'changes' => $user->getChanges(), + ] + ); + return redirect()->route('setting.user.index')->with('success', 'User berhasil diperbarui!'); } catch (\Exception $e) { report($e); + ActivityLogger::log( + category: 'pengguna', + event: 'failed', + message: 'Gagal mengubah data pengguna', + causer: auth()->user(), + additionalProperties: [ + 'error' => $e->getMessage(), + 'user_id' => $id, + ] + ); + return back()->withInput()->with('error', $e->getMessage()); } } @@ -169,16 +218,39 @@ public function updatePassword(UserUpdateRequest $request, $id) $user_find = User::findOrFail($id); $user = $user_find->update($request->all()); - $user->update([ + $user_find->update([ 'password' => bcrypt($request->password), ]); - flash()->success(trans('message.user.update-success')); + ActivityLogger::log( + category: 'pengguna', + event: 'password_updated', + message: "Mengubah password pengguna: {$user_find->name} ({$user_find->email})", + subject: $user_find, + causer: auth()->user(), + additionalProperties: [ + 'user_id' => $user_find->id, + ] + ); + + flash()->success('Password pengguna berhasil diperbarui'); return redirect()->route('setting.user.index'); } catch (\Exception $e) { report($e); - flash()->error(trans('message.user.update-error')); + + ActivityLogger::log( + category: 'pengguna', + event: 'failed', + message: 'Gagal mengubah password pengguna', + causer: auth()->user(), + additionalProperties: [ + 'error' => $e->getMessage(), + 'user_id' => $id, + ] + ); + + flash()->error('Gagal mengubah password pengguna'); return back()->withInput(); } @@ -197,12 +269,37 @@ public function destroy($id) $user->status = 0; $user->save(); - flash()->success(trans('general.suspend-success')); + ActivityLogger::log( + category: 'pengguna', + event: 'suspended', + message: "Menonaktifkan pengguna: {$user->name} ({$user->email})", + subject: $user, + causer: auth()->user(), + additionalProperties: [ + 'user_id' => $user->id, + 'previous_status' => 1, + 'new_status' => 0, + ] + ); + + flash()->success('Pengguna berhasil dinonaktifkan'); return redirect()->route('setting.user.index'); } catch (\Exception $e) { report($e); - flash()->success(trans('general.suspend-error')); + + ActivityLogger::log( + category: 'pengguna', + event: 'failed', + message: 'Gagal menonaktifkan pengguna', + causer: auth()->user(), + additionalProperties: [ + 'error' => $e->getMessage(), + 'user_id' => $id, + ] + ); + + flash()->error('Gagal menonaktifkan pengguna'); return redirect()->route('setting.user.index'); } @@ -221,12 +318,37 @@ public function active($id) $user->status = 1; $user->save(); - flash()->success(trans('general.active-success')); + ActivityLogger::log( + category: 'pengguna', + event: 'activated', + message: "Mengaktifkan pengguna: {$user->name} ({$user->email})", + subject: $user, + causer: auth()->user(), + additionalProperties: [ + 'user_id' => $user->id, + 'previous_status' => 0, + 'new_status' => 1, + ] + ); + + flash()->success('Pengguna berhasil diaktifkan'); return redirect()->route('setting.user.index'); } catch (\Exception $e) { report($e); - flash()->success(trans('general.active-error')); + + ActivityLogger::log( + category: 'pengguna', + event: 'failed', + message: 'Gagal mengaktifkan pengguna', + causer: auth()->user(), + additionalProperties: [ + 'error' => $e->getMessage(), + 'user_id' => $id, + ] + ); + + flash()->error('Gagal mengaktifkan pengguna'); return redirect()->route('setting.user.index'); } diff --git a/app/Models/User.php b/app/Models/User.php index 307ef60253..08cc1dd297 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -43,6 +43,8 @@ use Illuminate\Auth\Authenticatable as AuthenticableTrait; use Illuminate\Database\Eloquent\Factories\HasFactory; use MichaelDzjap\TwoFactorAuth\TwoFactorAuthenticable; +use Spatie\Activitylog\Traits\LogsActivity; +use Spatie\Activitylog\LogOptions; class User extends Authenticatable implements JWTSubject { @@ -52,6 +54,7 @@ class User extends Authenticatable implements JWTSubject use Notifiable; use HandlesResourceDeletion; use TwoFactorAuthenticable; + use LogsActivity; /** * Default password. @@ -60,6 +63,17 @@ class User extends Authenticatable implements JWTSubject */ public const DEFAULT_PASSWORD = '12345678'; + /** + * Configure activity logging options + */ + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logOnly(['name', 'email', 'status', 'address', 'phone']) + ->logOnlyDirty() + ->dontSubmitEmptyLogs(); + } + /** * {@inheritDoc} */ diff --git a/app/Services/ActivityLogger.php b/app/Services/ActivityLogger.php new file mode 100644 index 0000000000..fd7e40f701 --- /dev/null +++ b/app/Services/ActivityLogger.php @@ -0,0 +1,83 @@ + $request?->ip(), + 'user_agent' => $request?->userAgent(), + 'url' => $request?->fullUrl(), + 'slug' => $request?->path(), + 'method' => $request?->method(), + 'referer' => $request?->headers->get('referer'), + ]; + + if ($request && $request->userAgent()) { + $agent = new Agent(); + $agent->setUserAgent($request->userAgent()); + + $metadata['browser'] = $agent->browser(); + $metadata['platform'] = $agent->platform(); + $metadata['device'] = $agent->device(); + } + + $ipLocation = []; + if (! empty($metadata['ip_address'])) { + $ipLocation = IpLocationResolver::resolve($metadata['ip_address']); + } + + $properties = array_filter( + array_merge($metadata, $ipLocation, $additionalProperties), + static fn ($value) => ! is_null($value) && $value !== '' + ); + + $logger = activity() + ->useLog($category) + ->event($event); + + $causer = $causer ?? Auth::user(); + if ($causer) { + $logger->causedBy($causer); + } + + if ($subject) { + $logger->performedOn($subject); + } + + return $logger + ->withProperties($properties) + ->log($message); + } + + /** + * Helper untuk menambahkan properti tambahan setelah log dibuat. + */ + public static function appendProperties(ActivityModel $activity, array $properties): ActivityModel + { + $merged = array_merge($activity->properties?->toArray() ?? [], $properties); + $activity->properties = Arr::except($merged, [null, '']); + $activity->save(); + + return $activity; + } +} diff --git a/app/Services/IpLocationResolver.php b/app/Services/IpLocationResolver.php new file mode 100644 index 0000000000..8327dddd66 --- /dev/null +++ b/app/Services/IpLocationResolver.php @@ -0,0 +1,84 @@ + false, + 'ip_location_message' => 'IP lokal/internal', + ]; + } + + $cacheKey = self::CACHE_PREFIX . $ip; + + return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($ip) { + try { + $response = Http::timeout(5)->get(self::API_ENDPOINT . $ip); + + if (! $response->ok()) { + return [ + 'ip_location_available' => false, + 'ip_location_message' => 'Gagal mengambil lokasi IP', + ]; + } + + $payload = $response->json(); + + if (! $payload || ($payload['success'] ?? true) === false) { + return [ + 'ip_location_available' => false, + 'ip_location_message' => $payload['message'] ?? 'Lokasi IP tidak ditemukan', + ]; + } + + return [ + 'ip_location_available' => true, + 'ip_country' => $payload['country'] ?? null, + 'ip_country_code' => $payload['country_code'] ?? null, + 'ip_region' => $payload['region'] ?? null, + 'ip_city' => $payload['city'] ?? null, + 'ip_isp' => $payload['isp'] ?? null, + ]; + } catch (\Throwable $e) { + report($e); + + return [ + 'ip_location_available' => false, + 'ip_location_message' => 'Kesalahan saat memproses lokasi IP', + ]; + } + }); + } + + private static function isPrivateIp(string $ip): bool + { + foreach (self::PRIVATE_IP_PATTERNS as $pattern) { + if (preg_match($pattern, $ip)) { + return true; + } + } + + return false; + } +} diff --git a/composer.json b/composer.json index 24d22bf8ce..c7dff826fa 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "rachidlaasri/laravel-installer": "^4.1", "rap2hpoutre/laravel-log-viewer": "^2.0", "sentry/sentry-laravel": "^3.8", + "spatie/laravel-activitylog": "^4.10", "spatie/laravel-backup": "^8.2", "spatie/laravel-permission": "^5.5", "stevebauman/purify": "^6.2", diff --git a/composer.lock b/composer.lock index d318404731..306801c686 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e78ec08298d7f3ac82d7e052b4c71aa7", + "content-hash": "071c319bbe4f1461f70f18201414bcc9", "packages": [ { "name": "bensampo/laravel-enum", @@ -6655,6 +6655,97 @@ ], "time": "2025-02-14T15:04:22+00:00" }, + { + "name": "spatie/laravel-activitylog", + "version": "4.10.2", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-activitylog.git", + "reference": "bb879775d487438ed9a99e64f09086b608990c10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/bb879775d487438ed9a99e64f09086b608990c10", + "reference": "bb879775d487438ed9a99e64f09086b608990c10", + "shasum": "" + }, + "require": { + "illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", + "illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0", + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.6.3" + }, + "require-dev": { + "ext-json": "*", + "orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.0 || ^10.0", + "pestphp/pest": "^1.20 || ^2.0 || ^3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Activitylog\\ActivitylogServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Activitylog\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Sebastian De Deyne", + "email": "sebastian@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Tom Witkowski", + "email": "dev.gummibeer@gmail.com", + "homepage": "https://gummibeer.de", + "role": "Developer" + } + ], + "description": "A very simple activity logger to monitor the users of your website or application", + "homepage": "https://github.com/spatie/activitylog", + "keywords": [ + "activity", + "laravel", + "log", + "spatie", + "user" + ], + "support": { + "issues": "https://github.com/spatie/laravel-activitylog/issues", + "source": "https://github.com/spatie/laravel-activitylog/tree/4.10.2" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-06-15T06:59:49+00:00" + }, { "name": "spatie/laravel-backup", "version": "8.8.2", @@ -13348,15 +13439,15 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.1" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.1" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.6.0" } diff --git a/config/activitylog.php b/config/activitylog.php new file mode 100644 index 0000000000..f1262f5425 --- /dev/null +++ b/config/activitylog.php @@ -0,0 +1,52 @@ + env('ACTIVITY_LOGGER_ENABLED', true), + + /* + * When the clean-command is executed, all recording activities older than + * the number of days specified here will be deleted. + */ + 'delete_records_older_than_days' => 365, + + /* + * If no log name is passed to the activity() helper + * we use this default log name. + */ + 'default_log_name' => 'default', + + /* + * You can specify an auth driver here that gets user models. + * If this is null we'll use the current Laravel auth driver. + */ + 'default_auth_driver' => null, + + /* + * If set to true, the subject returns soft deleted models. + */ + 'subject_returns_soft_deleted_models' => false, + + /* + * This model will be used to log activity. + * It should implement the Spatie\Activitylog\Contracts\Activity interface + * and extend Illuminate\Database\Eloquent\Model. + */ + 'activity_model' => \Spatie\Activitylog\Models\Activity::class, + + /* + * This is the name of the table that will be created by the migration and + * used by the Activity model shipped with this package. + */ + 'table_name' => env('ACTIVITY_LOGGER_TABLE_NAME', 'activity_log'), + + /* + * This is the database connection that will be used by the migration and + * the Activity model shipped with this package. In case it's not set + * Laravel's database.default will be used instead. + */ + 'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'), +]; diff --git a/database/migrations/2025_09_22_061449_create_activity_log_table.php b/database/migrations/2025_09_22_061449_create_activity_log_table.php new file mode 100644 index 0000000000..7c05bc8929 --- /dev/null +++ b/database/migrations/2025_09_22_061449_create_activity_log_table.php @@ -0,0 +1,27 @@ +create(config('activitylog.table_name'), function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('log_name')->nullable(); + $table->text('description'); + $table->nullableMorphs('subject', 'subject'); + $table->nullableMorphs('causer', 'causer'); + $table->json('properties')->nullable(); + $table->timestamps(); + $table->index('log_name'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name')); + } +} diff --git a/database/migrations/2025_09_22_061450_add_event_column_to_activity_log_table.php b/database/migrations/2025_09_22_061450_add_event_column_to_activity_log_table.php new file mode 100644 index 0000000000..7b797fd5e2 --- /dev/null +++ b/database/migrations/2025_09_22_061450_add_event_column_to_activity_log_table.php @@ -0,0 +1,22 @@ +table(config('activitylog.table_name'), function (Blueprint $table) { + $table->string('event')->nullable()->after('subject_type'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->dropColumn('event'); + }); + } +} diff --git a/database/migrations/2025_09_22_061451_add_batch_uuid_column_to_activity_log_table.php b/database/migrations/2025_09_22_061451_add_batch_uuid_column_to_activity_log_table.php new file mode 100644 index 0000000000..8f7db66542 --- /dev/null +++ b/database/migrations/2025_09_22_061451_add_batch_uuid_column_to_activity_log_table.php @@ -0,0 +1,22 @@ +table(config('activitylog.table_name'), function (Blueprint $table) { + $table->uuid('batch_uuid')->nullable()->after('properties'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->dropColumn('batch_uuid'); + }); + } +} diff --git a/resources/views/layouts/dashboard_template.blade.php b/resources/views/layouts/dashboard_template.blade.php index e4d2cca639..c3abe0005d 100644 --- a/resources/views/layouts/dashboard_template.blade.php +++ b/resources/views/layouts/dashboard_template.blade.php @@ -21,6 +21,8 @@ + + @stack('css') @@ -98,6 +100,11 @@ + + + + + @stack('scripts') diff --git a/resources/views/vendor/laravel-log-viewer/activity-logs.blade.php b/resources/views/vendor/laravel-log-viewer/activity-logs.blade.php new file mode 100644 index 0000000000..b81a3276a6 --- /dev/null +++ b/resources/views/vendor/laravel-log-viewer/activity-logs.blade.php @@ -0,0 +1,274 @@ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ + +
+ + + + + + + + + + + + + + +
NoAksiKategoriPeristiwaSubjek TipePenyebab TipePenggunaDeskripsiDibuat Pada
+
+
+
+ + + + +@push('scripts') + +@endpush diff --git a/resources/views/vendor/laravel-log-viewer/index.blade.php b/resources/views/vendor/laravel-log-viewer/index.blade.php index b7b285c78f..547416fbd1 100644 --- a/resources/views/vendor/laravel-log-viewer/index.blade.php +++ b/resources/views/vendor/laravel-log-viewer/index.blade.php @@ -19,6 +19,7 @@