feat: add app and database modules
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user