From c5270926c760bde228e8e63e7d05d10d1cb30bc0 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 30 May 2026 23:18:49 +0800 Subject: [PATCH] feat(audit): add user activity logging with View Logs modal and CSV export on User Management page --- src/app/Http/Controllers/AdminController.php | 76 +++++++++++++ .../Auth/AuthenticatedSessionController.php | 18 ++- src/app/Http/Controllers/cctvController.php | 19 +++- src/app/Models/ActivityLog.php | 46 ++++++++ ...05_30_000000_create_activity_log_table.php | 32 ++++++ .../views/layout/admin/userlogs.blade.php | 105 ++++++++++++++++++ .../views/layout/admin/usermgmt.blade.php | 3 + src/routes/web.php | 3 + 8 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 src/app/Models/ActivityLog.php create mode 100644 src/database/migrations/2026_05_30_000000_create_activity_log_table.php create mode 100644 src/resources/views/layout/admin/userlogs.blade.php diff --git a/src/app/Http/Controllers/AdminController.php b/src/app/Http/Controllers/AdminController.php index e24d5c8c..8d2138f0 100644 --- a/src/app/Http/Controllers/AdminController.php +++ b/src/app/Http/Controllers/AdminController.php @@ -9,6 +9,7 @@ use Illuminate\Validation\Rules\Password; use Maatwebsite\Excel\Facades\Excel; use App\Exports\StationExport; use App\Imports\StationImport; +use App\Models\ActivityLog; class AdminController extends Controller { @@ -90,6 +91,8 @@ class AdminController extends Controller ]); }); + ActivityLog::record('create_station', $validated['stationid'], 'station', ['name' => $validated['stationname']]); + return redirect()->back()->with('success',__('toast.stationsuccess')); } @@ -118,6 +121,8 @@ class AdminController extends Controller ]); }); + ActivityLog::record('create_user', (string) $validated['name'], 'user', ['name' => $validated['name'], 'access_level' => $validated['access_level']]); + return redirect()->back()->with('success', __('toast.usersuccess')); } catch (ValidationException $e) { @@ -163,6 +168,8 @@ class AdminController extends Controller ]); + ActivityLog::record('update_station', $stationid, 'station', ['name' => $validated['stationname']]); + return redirect()->back()->with('success',__('toast.stationupdated')); } @@ -208,6 +215,8 @@ class AdminController extends Controller ]); }); + ActivityLog::record('update_user', (string) $userid, 'user', ['name' => $validated['name'], 'access_level' => $validated['access_level']]); + return redirect()->back()->with('success',__('toast.userupdated')); } catch (ValidationException $e) { @@ -243,6 +252,8 @@ class AdminController extends Controller ]); + ActivityLog::record('update_password', (string) $userid, 'user'); + return redirect()->back()->with('success',__('toast.passwordupdated')); @@ -263,6 +274,8 @@ class AdminController extends Controller // Function Delete Station public function deleteStation($stationid) { + $station = DB::table('station')->where('stationid', $stationid)->first(); + ActivityLog::record('delete_station', $stationid, 'station', ['name' => $station?->name]); DB::table('station')->where('stationid',$stationid)->delete(); return redirect()->back()->with('success',__('toast.stationdeleted')); } @@ -270,6 +283,8 @@ class AdminController extends Controller // Function Delete User public function deleteUser ($userid) { + $user = DB::table('users')->where('id', $userid)->first(); + ActivityLog::record('delete_user', (string) $userid, 'user', ['name' => $user?->name]); DB::table('users')->where('id',$userid)->delete(); return redirect()->back()->with('success',__('toast.userdeleted')); } @@ -277,9 +292,26 @@ class AdminController extends Controller // Function Export Stations to CSV public function exportStations() { + ActivityLog::record('export_stations_csv'); return Excel::download(new StationExport, 'stations.csv', \Maatwebsite\Excel\Excel::CSV); } + // Function Download Sample CSV + public function sampleCsv() + { + $headers = [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="sample_stations.csv"', + ]; + $callback = function () { + $file = fopen('php://output', 'w'); + fputcsv($file, ['Station ID', 'Name', 'District', 'Longitude', 'Latitude', 'Main River Basin', 'Sub River Basin', 'Rainfall', 'Water Level', 'Siren', 'CCTV Link']); + fputcsv($file, ['KBLG0001', 'Sg. Example at Kg. Example', 'Baling', '100.879344', '5.589411', 'Sg. Muda', 'Sg. Kupang', '1', '1', '0', '']); + fclose($file); + }; + return response()->stream($callback, 200, $headers); + } + // Function Import Stations from CSV public function importStations(Request $request) { @@ -289,6 +321,7 @@ class AdminController extends Controller try { Excel::import(new StationImport, $request->file('csv_file')); + ActivityLog::record('import_stations_csv', null, 'station', ['file' => $request->file('csv_file')->getClientOriginalName()]); return redirect()->route('stationmanagement')->with('success', __('toast.stationsimported')); } catch (\Maatwebsite\Excel\Validators\ValidationException $e) { $failures = $e->failures(); @@ -303,4 +336,47 @@ class AdminController extends Controller } } + public function userLogs($userid) + { + $user = DB::table('users')->where('id', $userid)->first(); + $logs = ActivityLog::where('user_id', $userid) + ->orderByDesc('created_at') + ->paginate(20); + + return view('layout.admin.userlogs', compact('logs', 'user')); + } + + public function exportUserLogs($userid) + { + $user = DB::table('users')->where('id', $userid)->first(); + $logs = ActivityLog::where('user_id', $userid) + ->orderByDesc('created_at') + ->get(); + + $headers = [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="user_logs_' . ($user?->name ?? $userid) . '.csv"', + ]; + + $callback = function () use ($logs) { + $file = fopen('php://output', 'w'); + fputcsv($file, ['Timestamp', 'Action', 'Subject Type', 'Subject ID', 'Details', 'IP Address']); + foreach ($logs as $log) { + fputcsv($file, [ + $log->created_at, + $log->action, + $log->subject_type ?? '-', + $log->subject_id ?? '-', + $log->properties ? json_encode($log->properties) : '-', + $log->ip_address ?? '-', + ]); + } + fclose($file); + }; + + ActivityLog::record('export_user_logs', (string) $userid, 'user', ['name' => $user?->name]); + + return response()->stream($callback, 200, $headers); + } + } diff --git a/src/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/src/app/Http/Controllers/Auth/AuthenticatedSessionController.php index f051634b..9cf8c1b5 100644 --- a/src/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/src/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -9,6 +9,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\View\View; use Illuminate\Support\Facades\DB; +use App\Models\ActivityLog; class AuthenticatedSessionController extends Controller { @@ -90,7 +91,9 @@ class AuthenticatedSessionController extends Controller $request->session()->regenerate(); - $loggedInUser = Auth::user(); // Always use this after Auth::attempt() + $loggedInUser = Auth::user(); + + ActivityLog::record('login', null, null, ['name' => $loggedInUser->name]); return redirect()->intended('/') ->with('success', __('auth.success'). $loggedInUser->name . '.'); @@ -101,6 +104,19 @@ class AuthenticatedSessionController extends Controller */ public function destroy(Request $request): RedirectResponse { + $userName = Auth::user()?->name; + $userId = Auth::id(); + + if ($userId) { + ActivityLog::create([ + 'user_id' => $userId, + 'user_name' => $userName, + 'action' => 'logout', + 'ip_address' => $request->ip(), + 'created_at' => now(), + ]); + } + Auth::guard('web')->logout(); $request->session()->invalidate(); diff --git a/src/app/Http/Controllers/cctvController.php b/src/app/Http/Controllers/cctvController.php index 12be7037..f435a6b7 100644 --- a/src/app/Http/Controllers/cctvController.php +++ b/src/app/Http/Controllers/cctvController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use App\Models\ActivityLog; class cctvController extends Controller { @@ -11,11 +12,27 @@ class cctvController extends Controller public function index() { $stationdata = DB::select(" - SELECT name,district,cctv_link FROM station + SELECT stationid, name, district, cctv_link FROM station WHERE cctv_link IS NOT NULL AND waterlevel = 1 ORDER BY name ASC "); return view('layout.cctv',compact('stationdata')); } + + // Function Update CCTV Link + public function updateCctvLink(Request $request, $stationid) + { + $validated = $request->validate([ + 'cctv_link' => 'nullable|string|max:500', + ]); + + DB::table('station')->where('stationid', $stationid)->update([ + 'cctv_link' => $validated['cctv_link'] ?? '', + ]); + + ActivityLog::record('update_cctv_link', $stationid, 'station', ['cctv_link' => $validated['cctv_link']]); + + return redirect()->route('cctv')->with('success', __('toast.linksupdated')); + } } diff --git a/src/app/Models/ActivityLog.php b/src/app/Models/ActivityLog.php new file mode 100644 index 00000000..7ebd9b52 --- /dev/null +++ b/src/app/Models/ActivityLog.php @@ -0,0 +1,46 @@ + 'array', + 'created_at' => 'datetime', + ]; + + public static function record(string $action, ?string $subjectId = null, ?string $subjectType = null, ?array $properties = null): self + { + $user = Auth::user(); + + return self::create([ + 'user_id' => $user?->id, + 'user_name' => $user?->name, + 'action' => $action, + 'subject_type' => $subjectType, + 'subject_id' => $subjectId, + 'properties' => $properties, + 'ip_address' => request()->ip(), + 'created_at' => now(), + ]); + } +} diff --git a/src/database/migrations/2026_05_30_000000_create_activity_log_table.php b/src/database/migrations/2026_05_30_000000_create_activity_log_table.php new file mode 100644 index 00000000..d42ee0b1 --- /dev/null +++ b/src/database/migrations/2026_05_30_000000_create_activity_log_table.php @@ -0,0 +1,32 @@ +id(); + $table->unsignedBigInteger('user_id')->nullable(); + $table->string('user_name')->nullable(); + $table->string('action'); + $table->string('subject_type')->nullable(); + $table->string('subject_id')->nullable(); + $table->json('properties')->nullable(); + $table->string('ip_address', 45)->nullable(); + $table->timestamp('created_at')->useCurrent(); + + $table->index('user_id'); + $table->index('action'); + $table->index('created_at'); + }); + } + + public function down(): void + { + Schema::dropIfExists('activity_log'); + } +}; diff --git a/src/resources/views/layout/admin/userlogs.blade.php b/src/resources/views/layout/admin/userlogs.blade.php new file mode 100644 index 00000000..d4051559 --- /dev/null +++ b/src/resources/views/layout/admin/userlogs.blade.php @@ -0,0 +1,105 @@ +@extends('layout.app') + +@section('content1') +
+
+ + +
+
Activity Logs: {{ $user->name ?? 'Unknown' }}
+ Export CSV +
+ + @if($logs->isNotEmpty()) +
+ + + + + + + + + + + + + @foreach ($logs as $log) + @php + $actionLabel = match($log->action) { + 'login' => 'Login', + 'logout' => 'Logout', + 'create_station' => 'Create Station', + 'update_station' => 'Update Station', + 'delete_station' => 'Delete Station', + 'import_stations_csv' => 'Import CSV', + 'export_stations_csv' => 'Export CSV', + 'update_cctv_link' => 'Update CCTV Link', + 'create_user' => 'Create User', + 'update_user' => 'Update User', + 'update_password' => 'Update Password', + 'delete_user' => 'Delete User', + 'export_user_logs' => 'Export Logs', + default => '' . e($log->action) . '', + }; + $details = ''; + if ($log->properties) { + $parts = []; + foreach ($log->properties as $key => $val) { + if ($key === 'password') continue; + $parts[] = "$key: $val"; + } + $details = implode(', ', $parts); + } + @endphp + + + + + + + + + @endforeach + +
NoDate & TimeActionSubjectDetailsIP Address
{{ $loop->iteration }}{{ $log->created_at ? \Carbon\Carbon::parse($log->created_at)->format('d/m/Y H:i:s') : '-' }}{!! $actionLabel !!}{{ $log->subject_type ? ucfirst($log->subject_type) . ' ' . $log->subject_id : '-' }}{{ $details }}{{ $log->ip_address ?? '-' }}
+
+ + @if ($logs->hasPages()) + + @endif + + @else + + @endif +
+
+@endsection diff --git a/src/resources/views/layout/admin/usermgmt.blade.php b/src/resources/views/layout/admin/usermgmt.blade.php index d9452791..090f58a1 100644 --- a/src/resources/views/layout/admin/usermgmt.blade.php +++ b/src/resources/views/layout/admin/usermgmt.blade.php @@ -89,6 +89,9 @@ data-bs-target="#userModal{{$row->id}}"> Edit + + View Logs +