From 67e13c646a33b27cc1f566d163d10d1ef7f78e2f Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Fri, 6 Feb 2026 10:55:47 -0500 Subject: [PATCH] Add admin activity logging for CRUD operations Log create, update, and delete actions performed in the admin panel using Filament's RecordCreated/RecordUpdated events and a DeleteAction before() hook. Sensitive fields (passwords, tokens) are redacted from stored properties. --- app/Listeners/AdminActivityListener.php | 64 +++++++++++++++++++ app/Providers/EventServiceProvider.php | 5 ++ .../Filament/FilamentServiceProvider.php | 16 +++++ lang/en/activity.php | 47 ++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 app/Listeners/AdminActivityListener.php diff --git a/app/Listeners/AdminActivityListener.php b/app/Listeners/AdminActivityListener.php new file mode 100644 index 000000000..42e6c44c1 --- /dev/null +++ b/app/Listeners/AdminActivityListener.php @@ -0,0 +1,64 @@ + $data */ + public function handle(Model $record, array $data, Page $page): void + { + if (Filament::getCurrentPanel()?->getId() !== 'admin') { + return; + } + + $resourceClass = $page::getResource(); + $modelClass = $resourceClass::getModel(); + $slug = Str::kebab(class_basename($modelClass)); + + $action = $page instanceof \Filament\Resources\Pages\CreateRecord ? 'create' : 'update'; + + $properties = $this->redactSensitiveFields($data); + + Activity::event("admin:$slug.$action") + ->subject($record) + ->property($properties) + ->log(); + } + + /** + * @param array $data + * @return array + */ + protected function redactSensitiveFields(array $data): array + { + $redacted = []; + + foreach ($data as $key => $value) { + if (in_array($key, self::REDACTED_FIELDS, true)) { + continue; + } + + if (is_array($value)) { + $redacted[$key] = $this->redactSensitiveFields($value); + } else { + $redacted[$key] = $value; + } + } + + return $redacted; + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 50a6e42cd..2a7336c63 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,7 +2,10 @@ namespace App\Providers; +use App\Listeners\AdminActivityListener; use App\Listeners\DispatchWebhooks; +use Filament\Resources\Events\RecordCreated; +use Filament\Resources\Events\RecordUpdated; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider @@ -15,5 +18,7 @@ class EventServiceProvider extends ServiceProvider 'eloquent.created*' => [DispatchWebhooks::class], 'eloquent.deleted*' => [DispatchWebhooks::class], 'eloquent.updated*' => [DispatchWebhooks::class], + RecordCreated::class => [AdminActivityListener::class], + RecordUpdated::class => [AdminActivityListener::class], ]; } diff --git a/app/Providers/Filament/FilamentServiceProvider.php b/app/Providers/Filament/FilamentServiceProvider.php index 5b30646aa..f0e836199 100644 --- a/app/Providers/Filament/FilamentServiceProvider.php +++ b/app/Providers/Filament/FilamentServiceProvider.php @@ -4,12 +4,14 @@ namespace App\Providers\Filament; use App\Enums\CustomizationKey; use App\Enums\TablerIcon; +use App\Facades\Activity; use Filament\Actions\Action; use Filament\Actions\CreateAction; use Filament\Actions\DeleteAction; use Filament\Actions\EditAction; use Filament\Actions\View\ActionsIconAlias; use Filament\Actions\ViewAction; +use Filament\Facades\Filament; use Filament\Forms\Components\Field; use Filament\Forms\Components\KeyValue; use Filament\Forms\Components\Repeater; @@ -29,8 +31,10 @@ use Filament\Support\View\SupportIconAlias; use Filament\Tables\View\TablesIconAlias; use Filament\View\PanelsIconAlias; use Filament\View\PanelsRenderHook; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Blade; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Str; use Livewire\Component; use Livewire\Livewire; @@ -132,6 +136,18 @@ class FilamentServiceProvider extends ServiceProvider $action->iconButton(); $action->iconSize(IconSize::ExtraLarge); } + + $action->before(function (Model $record) { + if (Filament::getCurrentPanel()?->getId() !== 'admin') { + return; + } + + $slug = Str::kebab(class_basename($record)); + + Activity::event("admin:$slug.delete") + ->subject($record) + ->log(); + }); }); CreateAction::configureUsing(function (CreateAction $action) { diff --git a/lang/en/activity.php b/lang/en/activity.php index 464854f00..c16d26932 100644 --- a/lang/en/activity.php +++ b/lang/en/activity.php @@ -122,4 +122,51 @@ return [ ], 'crashed' => 'Server crashed', ], + 'admin' => [ + 'user' => [ + 'create' => 'Created user :username', + 'update' => 'Updated user :username', + 'delete' => 'Deleted user :username', + ], + 'server' => [ + 'create' => 'Created server :name', + 'update' => 'Updated server :name', + 'delete' => 'Deleted server :name', + ], + 'node' => [ + 'create' => 'Created node :name', + 'update' => 'Updated node :name', + 'delete' => 'Deleted node :name', + ], + 'egg' => [ + 'create' => 'Created egg :name', + 'update' => 'Updated egg :name', + 'delete' => 'Deleted egg :name', + ], + 'role' => [ + 'create' => 'Created role :name', + 'update' => 'Updated role :name', + 'delete' => 'Deleted role :name', + ], + 'database-host' => [ + 'create' => 'Created database host :name', + 'update' => 'Updated database host :name', + 'delete' => 'Deleted database host :name', + ], + 'mount' => [ + 'create' => 'Created mount :name', + 'update' => 'Updated mount :name', + 'delete' => 'Deleted mount :name', + ], + 'webhook-configuration' => [ + 'create' => 'Created webhook :description', + 'update' => 'Updated webhook :description', + 'delete' => 'Deleted webhook :description', + ], + 'api-key' => [ + 'create' => 'Created API key', + 'update' => 'Updated API key', + 'delete' => 'Deleted API key', + ], + ], ];