268 lines
11 KiB
PHP
268 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\AccessControl;
|
|
|
|
use App\Services\System\ActivityFormatter;
|
|
use App\Support\DataTable;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Routing\Controller;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Spatie\Activitylog\Models\Activity;
|
|
|
|
class ActionLogController extends Controller
|
|
{
|
|
public function __construct()
|
|
{
|
|
// Middleware handled in web.php
|
|
}
|
|
|
|
public function index(Request $request)
|
|
{
|
|
if (DataTable::isDataTableRequest($request)) {
|
|
return $this->dataTable($request);
|
|
}
|
|
|
|
return view('pages.access_control.action-logs');
|
|
}
|
|
|
|
protected function dataTable(Request $request)
|
|
{
|
|
try {
|
|
$query = Activity::query()->with('causer');
|
|
|
|
// Fast count without eager loading or ordering
|
|
$recordsTotal = Activity::count();
|
|
|
|
$globalSearch = DataTable::globalSearch($request);
|
|
|
|
if ($event = $request->input('event')) {
|
|
if ($event === 'auth') {
|
|
$query->whereIn('description', ['login', 'logout', 'login_attempt', 'password_changed', 'failed login', 'password reset']);
|
|
} elseif ($event === 'data') {
|
|
$query->whereIn('description', ['created', 'updated', 'deleted', 'restored', 'force deleted', 'permanent_deleted']);
|
|
} elseif ($event === 'system') {
|
|
$query->whereIn('log_name', ['system', 'maintenance', 'backup']);
|
|
}
|
|
}
|
|
|
|
if ($user = DataTable::columnSearch($request, 0)) {
|
|
$query->whereHas('causer', function ($causerQuery) use ($user) {
|
|
$causerQuery->where(function ($q) use ($user) {
|
|
$q->where('name', 'like', "%{$user}%")
|
|
->orWhere('email', 'like', "%{$user}%");
|
|
});
|
|
});
|
|
}
|
|
|
|
if ($action = DataTable::columnSearch($request, 1)) {
|
|
$query->where('description', 'like', "%{$action}%");
|
|
}
|
|
|
|
if ($details = DataTable::columnSearch($request, 2)) {
|
|
$query->where('properties', 'like', "%{$details}%");
|
|
}
|
|
|
|
if ($module = DataTable::columnSearch($request, 3)) {
|
|
$query->where('log_name', 'like', "%{$module}%");
|
|
}
|
|
|
|
if ($executedAt = DataTable::columnSearch($request, 4)) {
|
|
$query->whereDate('created_at', $executedAt);
|
|
}
|
|
|
|
if ($ip = DataTable::columnSearch($request, 5)) {
|
|
$query->where('properties->ip', 'like', "%{$ip}%");
|
|
}
|
|
|
|
if ($agent = DataTable::columnSearch($request, 6)) {
|
|
$query->where('properties->agent', 'like', "%{$agent}%");
|
|
}
|
|
|
|
if ($properties = DataTable::columnSearch($request, 7)) {
|
|
$query->where('properties', 'like', "%{$properties}%");
|
|
}
|
|
|
|
if ($globalSearch) {
|
|
$query->where(function ($searchQuery) use ($globalSearch) {
|
|
$searchQuery
|
|
->where('description', 'like', "%{$globalSearch}%")
|
|
->orWhere('log_name', 'like', "%{$globalSearch}%")
|
|
->orWhere('properties', 'like', "%{$globalSearch}%")
|
|
->orWhereHas('causer', function ($causerQuery) use ($globalSearch) {
|
|
$causerQuery->where('email', 'like', "%{$globalSearch}%");
|
|
});
|
|
});
|
|
}
|
|
|
|
[$orderIndex, $orderDirection] = DataTable::order($request, 4, 'desc');
|
|
|
|
$sortColumn = match ($orderIndex) {
|
|
0 => 'causer_type',
|
|
1 => 'description',
|
|
3 => 'log_name',
|
|
4 => 'created_at',
|
|
default => 'created_at',
|
|
};
|
|
|
|
// Remove old global ordering and apply datatable specific ordering
|
|
$query->orderBy($sortColumn, $orderDirection);
|
|
|
|
// Perform filtered count WITHOUT eager loading or ordering
|
|
$countQuery = clone $query;
|
|
$countQuery->setEagerLoads([]);
|
|
$countQuery->orders = null;
|
|
$recordsFiltered = $countQuery->count();
|
|
|
|
$logs = $query
|
|
->skip(DataTable::start($request))
|
|
->take(DataTable::length($request))
|
|
->get();
|
|
|
|
$rows = $logs->map(function (Activity $log) {
|
|
$properties = is_array($log->properties) ? $log->properties : $log->properties?->toArray();
|
|
|
|
$eventLabel = ucfirst($log->description);
|
|
$eventBadge = ActivityFormatter::getEventBadgeClass($log->description);
|
|
$eventIcon = ActivityFormatter::getEventIcon($log->description);
|
|
$modelName = ActivityFormatter::getFriendlyModelName($log->subject_type);
|
|
$changes = ActivityFormatter::formatChanges($properties ?? []);
|
|
|
|
// User Column (Removed icon)
|
|
$userHtml = '<div>
|
|
<div class="fw-bold small">'.e($log->causer?->name ?? 'System').'</div>
|
|
<div class="text-secondary extra-small">'.e($log->causer?->email ?? 'no-email').'</div>
|
|
</div>';
|
|
|
|
// Event Column
|
|
$eventHtml = '<span class="badge rounded-pill '.$eventBadge.' px-3 py-2 small">
|
|
<i class="bi '.$eventIcon.' me-1"></i>'.$eventLabel.'
|
|
</span>';
|
|
|
|
// Information Column (Preview of changes)
|
|
$infoHtml = '<div class="small text-truncate" style="max-width: 250px;">';
|
|
if (! empty($changes)) {
|
|
$first = $changes[0];
|
|
$infoHtml .= '<strong>'.e($first['field']).':</strong> '.e($first['new']);
|
|
if (count($changes) > 1) {
|
|
$infoHtml .= ' <span class="badge text-bg-light border text-dark ms-1">+'.(count($changes) - 1).'</span>';
|
|
}
|
|
} else {
|
|
$infoHtml .= e(data_get($properties, 'details', '-'));
|
|
}
|
|
$infoHtml .= '</div>';
|
|
|
|
// Logistics Column
|
|
$ip = data_get($properties, 'ip', '-');
|
|
$agent = data_get($properties, 'agent', '-');
|
|
|
|
// Prepare JSON for modal
|
|
$modalData = [
|
|
'causer' => [
|
|
'name' => $log->causer?->name ?? 'System',
|
|
'email' => $log->causer?->email ?? '-',
|
|
],
|
|
'event' => [
|
|
'label' => $eventLabel,
|
|
'badge' => $eventBadge,
|
|
'icon' => $eventIcon,
|
|
'description' => $log->description,
|
|
],
|
|
'subject' => [
|
|
'type' => $modelName,
|
|
'id' => $log->subject_id,
|
|
'module' => $log->log_name,
|
|
],
|
|
'changes' => $changes,
|
|
'meta' => [
|
|
'ip' => $ip,
|
|
'agent' => $agent,
|
|
'time' => format_datetime($log->created_at),
|
|
],
|
|
'raw' => $log->toArray(),
|
|
];
|
|
|
|
return [
|
|
$userHtml,
|
|
$eventHtml,
|
|
$infoHtml,
|
|
'<span class="badge text-bg-theme-1-subtle text-theme-1">'.e($modelName).'</span>',
|
|
e(format_datetime($log->created_at)),
|
|
'<code class="extra-small">'.e($ip).'</code>',
|
|
'<span class="text-secondary extra-small text-truncate d-block" style="max-width:150px;" title="'.e($agent).'">'.e($agent).'</span>',
|
|
'<pre class="mb-0 extra-small text-secondary" style="max-height: 50px; overflow: hidden;">'.e(json_encode($properties)).'</pre>',
|
|
'<div class="text-end">
|
|
<button class="btn btn-square btn-outline-theme btn-sm rounded-circle btn-detail-log"
|
|
data-activity=\''.e(json_encode($modalData, JSON_HEX_APOS | JSON_HEX_QUOT)).'\'>
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
</div>',
|
|
];
|
|
})->all();
|
|
|
|
return DataTable::response($request, $recordsTotal, $recordsFiltered, $rows);
|
|
} catch (\Exception $e) {
|
|
Log::error('DataTable Error [ActionLog]: '.$e->getMessage());
|
|
|
|
return DataTable::response($request, 0, 0, []);
|
|
}
|
|
}
|
|
|
|
public function clear()
|
|
{
|
|
try {
|
|
DB::table('activity_log')->truncate();
|
|
|
|
return response()->json(['success' => true, 'message' => __('Action logs cleared successfully.')]);
|
|
} catch (\Exception $e) {
|
|
return response()->json(['success' => false, 'message' => __('Failed to clear logs.')], 500);
|
|
}
|
|
}
|
|
|
|
public function export(Request $request)
|
|
{
|
|
$query = Activity::query()->with('causer')->latest();
|
|
|
|
// Apply same filters as dataTable
|
|
if ($event = $request->input('event')) {
|
|
if ($event === 'auth') {
|
|
$query->whereIn('description', ['login', 'logout', 'login_attempt', 'password_changed', 'failed login', 'password reset']);
|
|
} elseif ($event === 'data') {
|
|
$query->whereIn('description', ['created', 'updated', 'deleted', 'restored', 'force deleted', 'permanent_deleted']);
|
|
} elseif ($event === 'system') {
|
|
$query->whereIn('log_name', ['system', 'maintenance', 'backup']);
|
|
}
|
|
}
|
|
|
|
if ($search = $request->input('search')) {
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('description', 'like', "%{$search}%")
|
|
->orWhere('log_name', 'like', "%{$search}%")
|
|
->orWhere('properties', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
$filename = 'action-logs-'.now()->format('Y-m-d-His').'.csv';
|
|
|
|
return response()->streamDownload(function () use ($query) {
|
|
$file = fopen('php://output', 'w');
|
|
fputcsv($file, ['User', 'Action', 'Module', 'Executed At', 'IP Address', 'User Agent', 'Properties']);
|
|
|
|
$query->chunk(200, function ($logs) use ($file) {
|
|
foreach ($logs as $log) {
|
|
fputcsv($file, [
|
|
$log->causer?->name ?? 'System',
|
|
ucfirst($log->description),
|
|
$log->log_name,
|
|
$log->created_at->toDateTimeString(),
|
|
data_get($log->properties, 'ip', '-'),
|
|
data_get($log->properties, 'agent', '-'),
|
|
json_encode($log->properties),
|
|
]);
|
|
}
|
|
});
|
|
fclose($file);
|
|
}, $filename);
|
|
}
|
|
}
|