Files

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);
}
}