feat(audit): add user activity logging with View Logs modal and CSV export on User Management page
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
46
src/app/Models/ActivityLog.php
Normal file
46
src/app/Models/ActivityLog.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ActivityLog extends Model
|
||||
{
|
||||
protected $table = 'activity_log';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'user_name',
|
||||
'action',
|
||||
'subject_type',
|
||||
'subject_id',
|
||||
'properties',
|
||||
'ip_address',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'properties' => '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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('activity_log', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
105
src/resources/views/layout/admin/userlogs.blade.php
Normal file
105
src/resources/views/layout/admin/userlogs.blade.php
Normal file
@@ -0,0 +1,105 @@
|
||||
@extends('layout.app')
|
||||
|
||||
@section('content1')
|
||||
<section id="container">
|
||||
<div class="container mt-3" id="table-container">
|
||||
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{route('dashboard')}}">@lang('messages.home')</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{route('usermgmt')}}">User Management</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Activity Logs — {{ $user->name ?? 'Unknown' }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0">Activity Logs: <strong>{{ $user->name ?? 'Unknown' }}</strong></h5>
|
||||
<a href="{{ route('usermgmt.logs.export', $user->id) }}" class="btn btn-outline-success btn-sm"><i class='bx bx-export'></i> Export CSV</a>
|
||||
</div>
|
||||
|
||||
@if($logs->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>Date & Time</th>
|
||||
<th>Action</th>
|
||||
<th>Subject</th>
|
||||
<th>Details</th>
|
||||
<th>IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($logs as $log)
|
||||
@php
|
||||
$actionLabel = match($log->action) {
|
||||
'login' => '<span class="badge bg-success">Login</span>',
|
||||
'logout' => '<span class="badge bg-secondary">Logout</span>',
|
||||
'create_station' => '<span class="badge bg-info">Create Station</span>',
|
||||
'update_station' => '<span class="badge bg-primary">Update Station</span>',
|
||||
'delete_station' => '<span class="badge bg-danger">Delete Station</span>',
|
||||
'import_stations_csv' => '<span class="badge bg-warning text-dark">Import CSV</span>',
|
||||
'export_stations_csv' => '<span class="badge bg-warning text-dark">Export CSV</span>',
|
||||
'update_cctv_link' => '<span class="badge bg-primary">Update CCTV Link</span>',
|
||||
'create_user' => '<span class="badge bg-info">Create User</span>',
|
||||
'update_user' => '<span class="badge bg-primary">Update User</span>',
|
||||
'update_password' => '<span class="badge bg-warning text-dark">Update Password</span>',
|
||||
'delete_user' => '<span class="badge bg-danger">Delete User</span>',
|
||||
'export_user_logs' => '<span class="badge bg-warning text-dark">Export Logs</span>',
|
||||
default => '<span class="badge bg-secondary">' . e($log->action) . '</span>',
|
||||
};
|
||||
$details = '';
|
||||
if ($log->properties) {
|
||||
$parts = [];
|
||||
foreach ($log->properties as $key => $val) {
|
||||
if ($key === 'password') continue;
|
||||
$parts[] = "$key: $val";
|
||||
}
|
||||
$details = implode(', ', $parts);
|
||||
}
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $loop->iteration }}</td>
|
||||
<td>{{ $log->created_at ? \Carbon\Carbon::parse($log->created_at)->format('d/m/Y H:i:s') : '-' }}</td>
|
||||
<td>{!! $actionLabel !!}</td>
|
||||
<td>{{ $log->subject_type ? ucfirst($log->subject_type) . ' ' . $log->subject_id : '-' }}</td>
|
||||
<td>{{ $details }}</td>
|
||||
<td>{{ $log->ip_address ?? '-' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($logs->hasPages())
|
||||
<nav aria-label="Page navigation" class="d-flex justify-content-center mt-3">
|
||||
<ul class="pagination">
|
||||
@if ($logs->onFirstPage())
|
||||
<li class="page-item disabled"><span class="page-link">«</span></li>
|
||||
@else
|
||||
<li class="page-item"><a class="page-link" href="{{ $logs->previousPageUrl() }}" rel="prev">«</a></li>
|
||||
@endif
|
||||
|
||||
@foreach ($logs->getUrlRange(1, $logs->lastPage()) as $page => $url)
|
||||
<li class="page-item {{ $page == $logs->currentPage() ? 'active' : '' }}">
|
||||
<a class="page-link" href="{{ $url }}">{{ $page }}</a>
|
||||
</li>
|
||||
@endforeach
|
||||
|
||||
@if ($logs->hasMorePages())
|
||||
<li class="page-item"><a class="page-link" href="{{ $logs->nextPageUrl() }}" rel="next">»</a></li>
|
||||
@else
|
||||
<li class="page-item disabled"><span class="page-link">»</span></li>
|
||||
@endif
|
||||
</ul>
|
||||
</nav>
|
||||
@endif
|
||||
|
||||
@else
|
||||
<div class="alert alert-light text-center mt-4" role="alert">
|
||||
No activity logs found for this user.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -89,6 +89,9 @@
|
||||
data-bs-target="#userModal{{$row->id}}">
|
||||
<i class='bx bx-edit'></i> Edit
|
||||
</button>
|
||||
<a href="{{ route('usermgmt.logs', $row->id) }}" class="btn btn-outline-secondary btn-sm me-1">
|
||||
<i class='bx bx-file'></i> View Logs
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal"
|
||||
data-bs-target="#DeleteModal{{$row->id}}">
|
||||
<i class='bx bx-trash'></i> @lang('messages.delete')
|
||||
|
||||
@@ -79,6 +79,7 @@ Route::middleware(['admin'])->group(function () {
|
||||
|
||||
// Station CSV Import/Export
|
||||
Route::get('/stationmanagement/export-csv',[App\Http\Controllers\AdminController::class, 'exportStations'])->name('stationmanagement.export.csv');
|
||||
Route::get('/stationmanagement/sample-csv',[App\Http\Controllers\AdminController::class, 'sampleCsv'])->name('stationmanagement.sample.csv');
|
||||
Route::post('/stationmanagement/import-csv',[App\Http\Controllers\AdminController::class, 'importStations'])->name('stationmanagement.import.csv');
|
||||
|
||||
// User Management
|
||||
@@ -87,6 +88,8 @@ Route::middleware(['admin'])->group(function () {
|
||||
Route::post('/usermgmt/{userid}/update',[App\Http\Controllers\AdminController::class, 'updateUsers'])->name('usermgmt.update');
|
||||
Route::post('/usermgmt/{userid}/updatePassword',[App\Http\Controllers\AdminController::class, 'updatePassword'])->name('usermgmt.updatePassword');
|
||||
Route::delete('/usermgmt/{userid}/delete',[App\Http\Controllers\AdminController::class, 'deleteUser'])->name('usermgmt.delete');
|
||||
Route::get('/usermgmt/{userid}/logs',[App\Http\Controllers\AdminController::class, 'userLogs'])->name('usermgmt.logs');
|
||||
Route::get('/usermgmt/{userid}/logs/export-csv',[App\Http\Controllers\AdminController::class, 'exportUserLogs'])->name('usermgmt.logs.export');
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user