Files
website/website/app/Http/Controllers/Admin/AuditLogController.php
Claude Dev 40c1ecc6fe Remove old Vuexy wrapper components (AppTextField, AppSelect, AppTextarea, FlashMessages, NotificationBell)
All pages now use native Vuetify components directly. Flash messages are handled
by the ToastStack component via Pinia store. Notifications use NotificationPanel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 17:10:23 -04:00

259 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AuditLog;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
class AuditLogController extends Controller
{
public function index(Request $request): Response
{
$query = $this->applyFilters($request);
$auditLogs = $query->paginate(25)->withQueryString();
// Get distinct actions for the filter dropdown
$actions = AuditLog::query()
->distinct()
->orderBy('action')
->pluck('action');
return Inertia::render('Admin/AuditLogs/Index', [
'auditLogs' => $auditLogs,
'actions' => $actions,
'filters' => [
'search' => $request->input('search', ''),
'action' => $request->input('action', ''),
'date_from' => $request->input('date_from', ''),
'date_to' => $request->input('date_to', ''),
],
]);
}
public function export(Request $request): StreamedResponse
{
$request->validate([
'format' => ['required', 'in:csv,json'],
'search' => ['nullable', 'string', 'max:255'],
'action' => ['nullable', 'string', 'max:255'],
'date_from' => ['nullable', 'date'],
'date_to' => ['nullable', 'date'],
]);
$format = $request->input('format', 'csv');
$query = $this->applyFilters($request);
if ($format === 'json') {
return $this->exportJson($query);
}
return $this->exportCsv($query);
}
/**
* @param Builder<AuditLog> $query
*/
private function exportCsv(Builder $query): StreamedResponse
{
$filename = 'audit-logs-'.now()->format('Y-m-d-His').'.csv';
return response()->streamDownload(function () use ($query): void {
$handle = fopen('php://output', 'w');
if ($handle === false) {
return;
}
fputcsv($handle, [
'ID',
'Date',
'User',
'User Email',
'Action',
'Resource Type',
'Resource ID',
'IP Address',
'User Agent',
'Changes Summary',
]);
$query->chunk(500, function ($logs) use ($handle): void {
foreach ($logs as $log) {
fputcsv($handle, [
$log->id,
$log->created_at->format('Y-m-d H:i:s'),
$log->user?->name ?? 'System',
$log->user?->email ?? '-',
$log->action,
$log->resource_type ?? '-',
$log->resource_id ?? '-',
$log->ip_address ?? '-',
$log->user_agent ?? '-',
$this->summarizeChanges($log->changes),
]);
}
});
fclose($handle);
}, $filename, [
'Content-Type' => 'text/csv',
]);
}
/**
* @param Builder<AuditLog> $query
*/
private function exportJson(Builder $query): StreamedResponse
{
$filename = 'audit-logs-'.now()->format('Y-m-d-His').'.json';
return response()->streamDownload(function () use ($query): void {
echo '[';
$first = true;
$query->chunk(500, function ($logs) use (&$first): void {
foreach ($logs as $log) {
if (! $first) {
echo ',';
}
echo json_encode([
'id' => $log->id,
'date' => $log->created_at->format('Y-m-d H:i:s'),
'user' => $log->user ? [
'id' => $log->user->id,
'name' => $log->user->name,
'email' => $log->user->email,
] : null,
'action' => $log->action,
'resource_type' => $log->resource_type,
'resource_id' => $log->resource_id,
'ip_address' => $log->ip_address,
'user_agent' => $log->user_agent,
'changes' => $log->changes,
'created_at' => $log->created_at->toIso8601String(),
], JSON_PRETTY_PRINT);
$first = false;
}
});
echo ']';
}, $filename, [
'Content-Type' => 'application/json',
]);
}
/**
* @param array<string, mixed>|null $changes
*/
private function summarizeChanges(?array $changes): string
{
if (! $changes || count($changes) === 0) {
return '-';
}
// Check for per-field old/new format: {"plan": {"old": "Basic", "new": "Pro"}, ...}
$hasPerFieldOldNew = false;
foreach ($changes as $value) {
if (is_array($value) && (array_key_exists('old', $value) || array_key_exists('new', $value))) {
$hasPerFieldOldNew = true;
break;
}
}
if ($hasPerFieldOldNew) {
$changedFields = [];
foreach ($changes as $field => $value) {
if (is_array($value) && array_key_exists('old', $value) && array_key_exists('new', $value)) {
if ($value['old'] !== $value['new']) {
$changedFields[] = $field;
}
}
}
return $changedFields ? 'Changed: '.implode(', ', $changedFields) : 'No changes';
}
// Top-level old/new structure: {"old": {...}, "new": {...}}
if (isset($changes['old']) || isset($changes['new'])) {
$fields = [];
if (isset($changes['new']) && is_array($changes['new'])) {
$fields = array_keys($changes['new']);
} elseif (isset($changes['old']) && is_array($changes['old'])) {
$fields = array_keys($changes['old']);
}
return 'Changed: '.implode(', ', $fields);
}
// Top-level before/after structure
if (isset($changes['before']) || isset($changes['after'])) {
$fields = [];
if (isset($changes['after']) && is_array($changes['after'])) {
$fields = array_keys($changes['after']);
} elseif (isset($changes['before']) && is_array($changes['before'])) {
$fields = array_keys($changes['before']);
}
return 'Changed: '.implode(', ', $fields);
}
// Flat key-value pairs
return 'Fields: '.implode(', ', array_keys($changes));
}
/**
* @return Builder<AuditLog>
*/
private function applyFilters(Request $request): Builder
{
$query = AuditLog::query()
->with('user:id,name,email')
->latest();
// Search by user name/email, action, or resource type
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search): void {
$q->where('action', 'like', "%{$search}%")
->orWhere('resource_type', 'like', "%{$search}%")
->orWhere('ip_address', 'like', "%{$search}%")
->orWhereHas('user', function ($userQuery) use ($search): void {
$userQuery->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
});
}
// Filter by action
if ($action = $request->input('action')) {
$query->where('action', $action);
}
// Filter by date range
if ($dateFrom = $request->input('date_from')) {
$query->whereDate('created_at', '>=', $dateFrom);
}
if ($dateTo = $request->input('date_to')) {
$query->whereDate('created_at', '<=', $dateTo);
}
return $query;
}
}