Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions app/Listeners/AdminActivityListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace App\Listeners;

use App\Facades\Activity;
use Filament\Facades\Filament;
use Filament\Resources\Pages\Page;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class AdminActivityListener
{
protected const REDACTED_FIELDS = [
'password',
'password_confirmation',
'token',
'secret',
'api_key',
];

/** @param array<string, mixed> $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();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how RecordCreated/RecordUpdated are defined and what constructor params they take
echo "=== RecordCreated definition ==="
rg -n "class RecordCreated" --type php -A 20

echo ""
echo "=== RecordUpdated definition ==="
rg -n "class RecordUpdated" --type php -A 20

echo ""
echo "=== How these events are dispatched ==="
rg -n "RecordCreated\b" --type php -C 3 | head -80
echo ""
rg -n "RecordUpdated\b" --type php -C 3 | head -80

Repository: pelican-dev/panel

Length of output: 2420


🏁 Script executed:

#!/bin/bash
# Check if Filament events exist in vendor
echo "=== Checking vendor directory for Filament events ==="
find vendor -path "*Filament/Resources/Events*" -name "*.php" 2>/dev/null | head -20

echo ""
echo "=== Check the full AdminActivityListener implementation ==="
cat -n app/Listeners/AdminActivityListener.php

Repository: pelican-dev/panel

Length of output: 2246


🏁 Script executed:

#!/bin/bash
# Check for other listeners and their signatures
echo "=== Other listener implementations ==="
find app/Listeners -name "*.php" -exec echo "File: {}" \; -exec head -30 {} \;

echo ""
echo "=== Check test files for event dispatching patterns ==="
find tests -name "*Event*" -o -name "*Listener*" 2>/dev/null | head -10

echo ""
echo "=== Search for any Filament event usage in tests ==="
rg "RecordCreated|RecordUpdated" --type php -B 5 -A 5 tests/ 2>/dev/null | head -100

Repository: pelican-dev/panel

Length of output: 6736


🏁 Script executed:

#!/bin/bash
cat -n tests/Filament/Admin/AdminActivityListenerTest.php

Repository: pelican-dev/panel

Length of output: 5485


🏁 Script executed:

#!/bin/bash
# Search for event dispatch calls
echo "=== Search for RecordCreated/RecordUpdated event instantiation ==="
rg "RecordCreated|RecordUpdated" --type php -B 3 -A 3 | grep -v "tests/" | grep -v ".phpt" | head -100

echo ""
echo "=== Search for event dispatching ==="
rg "event\(" --type php | grep -i "record" | head -20

echo ""
echo "=== Check for any Filament event handling documentation ==="
rg "handle.*RecordCreated|handle.*RecordUpdated" --type php -B 5 -A 5

Repository: pelican-dev/panel

Length of output: 1430


🌐 Web query:

Filament RecordCreated RecordUpdated event listener signature

💡 Result:

Filament doesn’t expose Laravel events named RecordCreated / RecordUpdated that you can register listeners for. Instead, record creation/update in a Resource page is meant to be customized via lifecycle hook methods (no parameters, void return):

Create page (CreateRecord)

protected function afterCreate(): void
{
    // Runs after the record is created.
}

(Also available: beforeFill, afterFill, beforeValidate, afterValidate, beforeCreate, afterCreate.) [1]

The created model is stored on the page as $record (type Model|null). [3]

Edit page (EditRecord)

protected function afterSave(): void
{
    // Runs after the record is saved (updated).
}

(Also available: beforeFill, afterFill, beforeValidate, afterValidate, beforeSave, afterSave.) [2]

If you truly need Laravel-style events, dispatch your own Laravel events from afterCreate() / afterSave() and then use the normal listener signature public function handle(YourEvent $event): void.

Sources: [1] [2] [3]


🏁 Script executed:

#!/bin/bash
echo "=== Check composer.json for Filament version ==="
grep -A 2 "filament" composer.json | head -10

echo ""
echo "=== Search for any dispatch() calls ==="
rg "dispatch.*Record" --type php

echo ""
echo "=== Search for all imports of Filament events ==="
rg "use Filament.*Events" --type php

echo ""
echo "=== Check if there's any custom event handling ==="
rg "Event::" --type php | head -20

Repository: pelican-dev/panel

Length of output: 2262


🌐 Web query:

Filament 4.5 Events RecordCreated RecordUpdated Laravel event listener

💡 Result:

Filament v4.x (including 4.5) does not expose global Laravel events named RecordCreated, RecordUpdated, etc. for Resource page CRUD in a way you can listen to via EventServiceProvider.

What to use instead

1) Filament Resource page lifecycle hooks (recommended for “only when done via Filament UI”)

On Create pages you have hooks like beforeCreate() / afterCreate(); on Edit pages you have beforeSave() / afterSave(). Use these hooks, and if you need a Laravel event, dispatch your own from there. [1] [2]

// app/Filament/Resources/PostResource/Pages/CreatePost.php
use App\Events\PostCreatedFromFilament;
use Filament\Resources\Pages\CreateRecord;

class CreatePost extends CreateRecord
{
    protected function afterCreate(): void
    {
        PostCreatedFromFilament::dispatch($this->record);
    }
}
// app/Filament/Resources/PostResource/Pages/EditPost.php
use App\Events\PostUpdatedFromFilament;
use Filament\Resources\Pages\EditRecord;

class EditPost extends EditRecord
{
    protected function afterSave(): void
    {
        PostUpdatedFromFilament::dispatch($this->record);
    }
}

Then register normal Laravel listeners for PostCreatedFromFilament / PostUpdatedFromFilament.

2) Eloquent model events / observers (recommended for “no matter where it’s created/updated”)

If your goal is “whenever the model changes (Filament or not)”, use Eloquent created / updated (Observer) instead of Filament-specific hooks.


Sources: Filament Resource Create lifecycle hooks (includes afterCreate). [1] Filament Resource Edit lifecycle hooks (includes afterSave). [2]

[1] https://docs.laravel-filament.cn/docs/panels/resources/creating-records/
[2] https://docs.laravel-filament.cn/docs/4.x/resources/editing-records


This listener will never be triggered in production — Filament v4.5 does not expose RecordCreated/RecordUpdated as dispatchable Laravel events.

The code registers AdminActivityListener for these non-existent events in EventServiceProvider. Filament 4.5 uses lifecycle hook methods (afterCreate(), afterSave()) on Resource pages instead. If you need to log record creation/updates:

  1. Add hooks to your Resource pages:
// In CreateRecord page
protected function afterCreate(): void
{
    // dispatch custom event or call listener directly
}

// In EditRecord page  
protected function afterSave(): void
{
    // dispatch custom event or call listener directly
}
  1. Or use Eloquent model events (created, updated) if activity should be logged regardless of how records are created.

The listener tests pass only because they manually invoke handle() directly—they don't validate that the listener is actually called through the event system.

🤖 Prompt for AI Agents
In `@app/Listeners/AdminActivityListener.php` around lines 22 - 39, The
AdminActivityListener is registered for Filament RecordCreated/RecordUpdated
events which are not dispatched in v4.5, so it never runs in production; update
wiring by either invoking AdminActivityListener from Resource page lifecycle
hooks (call or dispatch from CreateRecord::afterCreate() and
EditRecord::afterSave()) or attach it to Eloquent model events (created/updated)
instead; modify EventServiceProvider to stop registering non-existent Filament
events and instead dispatch a custom event or call
AdminActivityListener::handle(...) from the Resource pages (or register
listeners for the model class events) so the listener actually executes in real
requests.

}

/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
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;
}
}
5 changes: 5 additions & 0 deletions app/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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],
Comment on lines +21 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Find AdminActivityListener and check its handle method
echo "=== AdminActivityListener location and handle signature ==="
find . -name "*AdminActivityListener*" -type f

echo ""
echo "=== AdminActivityListener handle method signature ==="
rg -n "class AdminActivityListener|function handle|public function handle" app/ --type php -A 3 | grep -A 10 "AdminActivityListener"

Repository: pelican-dev/panel

Length of output: 1470


🏁 Script executed:

#!/bin/bash

# Search for RecordCreated and RecordUpdated event definitions
echo "=== Searching for RecordCreated event definition ==="
rg -n "class RecordCreated" --type php -A 10

echo ""
echo "=== Searching for RecordUpdated event definition ==="
rg -n "class RecordUpdated" --type php -A 10

Repository: pelican-dev/panel

Length of output: 167


🏁 Script executed:

#!/bin/bash

# Check how RecordCreated/RecordUpdated are dispatched to see actual parameters
echo "=== How RecordCreated is dispatched ==="
rg -n "RecordCreated::dispatch|dispatch.*RecordCreated" --type php -B 2 -A 2

echo ""
echo "=== How RecordUpdated is dispatched ==="
rg -n "RecordUpdated::dispatch|dispatch.*RecordUpdated" --type php -B 2 -A 2

Repository: pelican-dev/panel

Length of output: 141


🌐 Web query:

Filament RecordCreated RecordUpdated event listener signature parameters

💡 Result:

Filament doesn’t ship Laravel events named RecordCreated / RecordUpdated for resource CRUD. Instead, you typically hook into:

1) Filament resource page hooks (recommended)

Create page (CreateRecord)

After a record is created, you can use the page’s $record property (the created Eloquent model) and $data (form state). (filamentphp.com)

use Filament\Resources\Pages\CreateRecord;

class CreateUser extends CreateRecord
{
    protected function afterCreate(): void
    {
        $record = $this->record; // Eloquent model
        $data   = $this->data;   // validated form data array
    }
}

Edit page (EditRecord)

Updates happen via handleRecordUpdate(Model $record, array $data) (and you can use $this->record as well). (filamentphp.com)

use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model;

class EditUser extends EditRecord
{
    protected function afterSave(): void
    {
        $record = $this->record; // updated model
    }

    protected function handleRecordUpdate(Model $record, array $data): Model
    {
        // $record = model being updated, $data = form data
        return parent::handleRecordUpdate($record, $data);
    }
}

2) Laravel / Eloquent model events (framework-level)

If you want true “created/updated” listeners, use Eloquent events/observers (created(Model $model), updated(Model $model), etc.). (laravel.com)

If you meant a third-party Filament plugin that defines RecordCreated / RecordUpdated, tell me the package name/namespace and Filament major version (v2/v3/v4), and I’ll pull the exact event class + listener method signature.

Citations:


Remove or implement RecordCreated and RecordUpdated events—they do not exist in Filament core or this codebase.

The listener registrations on lines 21-22 reference RecordCreated and RecordUpdated events that are not defined anywhere in the codebase or Filament's core library. These events are never dispatched, so the AdminActivityListener will never be triggered. Either:

  1. Define and dispatch these custom events explicitly in the appropriate resource pages, or
  2. Use Laravel's Eloquent model events (created, updated) with an observer/listener, or
  3. Hook into Filament's page lifecycle methods (afterCreate(), afterSave()) directly on resource pages.

Without dispatching these events, admin activity logging will not function.

🤖 Prompt for AI Agents
In `@app/Providers/EventServiceProvider.php` around lines 21 - 22, The
EventServiceProvider registers non-existent events RecordCreated and
RecordUpdated for AdminActivityListener so the listener is never invoked; either
remove those registrations from EventServiceProvider and instead register a
model observer for the Eloquent events (created/updated) that points to your
AdminActivityListener (or create a dedicated AdminActivityObserver and register
it via the EventServiceProvider or in a service provider), or implement and
dispatch custom RecordCreated and RecordUpdated events where records are
created/updated (e.g., from your Filament resource pages) or move the logging
logic into Filament resource lifecycle hooks like afterCreate() / afterSave() on
the relevant Resource/Page classes to call the AdminActivityListener
functionality.

];
}
16 changes: 16 additions & 0 deletions app/Providers/Filament/FilamentServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

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;
Expand All @@ -29,8 +31,10 @@
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;

Expand Down Expand Up @@ -132,6 +136,18 @@ public function boot(): void
$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) {
Expand Down
47 changes: 47 additions & 0 deletions lang/en/activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,51 @@
],
'crashed' => 'Server crashed',
],
'admin' => [
'user' => [
'create' => 'Created user <b>:username</b>',
'update' => 'Updated user <b>:username</b>',
'delete' => 'Deleted user <b>:username</b>',
],
'server' => [
'create' => 'Created server <b>:name</b>',
'update' => 'Updated server <b>:name</b>',
'delete' => 'Deleted server <b>:name</b>',
],
'node' => [
'create' => 'Created node <b>:name</b>',
'update' => 'Updated node <b>:name</b>',
'delete' => 'Deleted node <b>:name</b>',
],
'egg' => [
'create' => 'Created egg <b>:name</b>',
'update' => 'Updated egg <b>:name</b>',
'delete' => 'Deleted egg <b>:name</b>',
],
'role' => [
'create' => 'Created role <b>:name</b>',
'update' => 'Updated role <b>:name</b>',
'delete' => 'Deleted role <b>:name</b>',
],
'database-host' => [
'create' => 'Created database host <b>:name</b>',
'update' => 'Updated database host <b>:name</b>',
'delete' => 'Deleted database host <b>:name</b>',
],
'mount' => [
'create' => 'Created mount <b>:name</b>',
'update' => 'Updated mount <b>:name</b>',
'delete' => 'Deleted mount <b>:name</b>',
],
'webhook-configuration' => [
'create' => 'Created webhook <b>:description</b>',
'update' => 'Updated webhook <b>:description</b>',
'delete' => 'Deleted webhook <b>:description</b>',
],
'api-key' => [
'create' => 'Created API key',
'update' => 'Updated API key',
'delete' => 'Deleted API key',
],
],
];