Add admin activity logging for CRUD operations#2189
Conversation
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.
📝 WalkthroughWalkthroughAdds an admin activity logging flow for the Filament admin panel: a new listener captures RecordCreated/RecordUpdated events (and delete actions via Filament service provider), redacts sensitive fields, and dispatches activity log entries with derived resource slugs and subjects. Changes
Sequence Diagram(s)sequenceDiagram
actor Admin as Admin User
participant Filament
participant Listener as AdminActivityListener
participant Redactor as redactSensitiveFields
participant Activity as Activity Logger
participant DB as Database
Admin->>Filament: Perform Create/Update/Delete action
activate Filament
Filament->>Listener: Dispatch RecordCreated / RecordUpdated / DeleteAction event
deactivate Filament
activate Listener
Listener->>Listener: Derive resource slug from model class
Listener->>Redactor: Extract event data / properties
activate Redactor
Redactor->>Redactor: Recursively redact sensitive fields
Redactor-->>Listener: Return redacted properties
deactivate Redactor
Listener->>Activity: Dispatch ActivityLogged with slug, action, subject, properties
deactivate Listener
activate Activity
Activity->>DB: Persist activity record with redacted properties
deactivate Activity
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@app/Providers/EventServiceProvider.php`:
- Around line 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.
🧹 Nitpick comments (4)
lang/en/activity.php (1)
166-170: API key translations lack identifiers.Unlike other admin action translations that include identifying information (
:username,:name,:description), the api-key entries provide no context about which API key was affected. Consider adding an identifier placeholder for better audit trail clarity.'api-key' => [ - 'create' => 'Created API key', - 'update' => 'Updated API key', - 'delete' => 'Deleted API key', + 'create' => 'Created API key <b>:identifier</b>', + 'update' => 'Updated API key <b>:identifier</b>', + 'delete' => 'Deleted API key <b>:identifier</b>', ],app/Listeners/AdminActivityListener.php (3)
13-19: Consider expanding the sensitive fields list.The current list may miss other sensitive fields commonly found in admin panels. Consider adding fields like
daemon_token,daemon_token_id,client_secret,private_key, or pattern-based matching for fields containingtoken,secret, orkeyin their names.Additionally, the matching is case-sensitive, so
PasswordorAPI_KEYwould not be redacted.🛡️ Suggested improvement
protected const REDACTED_FIELDS = [ 'password', 'password_confirmation', 'token', 'secret', 'api_key', + 'daemon_token', + 'daemon_token_id', + 'client_secret', + 'private_key', ];Or consider pattern-based matching in
redactSensitiveFields:$sensitivePatterns = ['password', 'token', 'secret', 'key']; $shouldRedact = false; foreach ($sensitivePatterns as $pattern) { if (stripos($key, $pattern) !== false) { $shouldRedact = true; break; } }
50-53: Sensitive fields are silently dropped rather than marked as redacted.Currently, sensitive fields are completely omitted from the logged properties. This makes it impossible to distinguish between "field was not submitted" vs "field was redacted for security." Consider replacing the value with a placeholder like
'[REDACTED]'to maintain audit visibility.♻️ Suggested change
foreach ($data as $key => $value) { if (in_array($key, self::REDACTED_FIELDS, true)) { - continue; + $redacted[$key] = '[REDACTED]'; + continue; }
32-32: Action determination assumes binary create/update.The logic assumes any page that isn't
CreateRecordis anupdate. If Filament introduces other page types or if custom pages exist, this could misclassify actions. Consider being more explicit or logging the actual page type.♻️ Suggested defensive improvement
-$action = $page instanceof \Filament\Resources\Pages\CreateRecord ? 'create' : 'update'; +$action = match (true) { + $page instanceof \Filament\Resources\Pages\CreateRecord => 'create', + $page instanceof \Filament\Resources\Pages\EditRecord => 'update', + default => 'update', // fallback for compatibility +};
| RecordCreated::class => [AdminActivityListener::class], | ||
| RecordUpdated::class => [AdminActivityListener::class], |
There was a problem hiding this comment.
🧩 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 10Repository: 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 2Repository: 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:
- 1: https://filamentphp.com/api/3.x/Filament/Resources/Pages/CreateRecord.html
- 2: https://filamentphp.com/api/3.x/Filament/Resources/Pages/EditRecord.html?utm_source=openai
- 3: https://laravel.com/docs/5.7/eloquent?utm_source=openai
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:
- Define and dispatch these custom events explicitly in the appropriate resource pages, or
- Use Laravel's Eloquent model events (
created,updated) with an observer/listener, or - 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.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@app/Listeners/AdminActivityListener.php`:
- Around line 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.
🧹 Nitpick comments (3)
app/Listeners/AdminActivityListener.php (2)
46-63: Sensitive fields are silently dropped instead of replaced with a redaction marker.Currently, redacted fields are completely omitted from the logged properties. This makes it impossible to distinguish "the field wasn't submitted" from "the field was submitted but redacted." Consider replacing with a placeholder value instead of skipping.
♻️ Proposed change
foreach ($data as $key => $value) { if (in_array($key, self::REDACTED_FIELDS, true)) { - continue; + $redacted[$key] = '[REDACTED]'; + continue; }
13-19: Consider extending the redaction list withcurrent_passwordand_token.Fields like
current_password(used in password change flows) and_token(CSRF token sometimes present in form data) may also contain sensitive values.tests/Filament/Admin/AdminActivityListenerTest.php (1)
15-18:pageInstancebypasses constructor — verifygetResource()works on the resulting object.
newInstanceWithoutConstructor()skips all property initialization. SinceAdminActivityListener::handle()calls$page::getResource()(a static method), this should work as long asgetResource()doesn't rely on instance state. However, this is fragile — any future change to the page class could break these tests silently.
| 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(); |
There was a problem hiding this comment.
🧩 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 -80Repository: 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.phpRepository: 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 -100Repository: pelican-dev/panel
Length of output: 6736
🏁 Script executed:
#!/bin/bash
cat -n tests/Filament/Admin/AdminActivityListenerTest.phpRepository: 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 5Repository: 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 -20Repository: 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:
- 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
}- 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.
- Change handle() signature to accept RecordCreated|RecordUpdated event objects instead of separate params (Laravel passes the event, not constructor args) - Use [REDACTED] placeholder instead of silently dropping sensitive fields - Expand redacted fields list with current_password, daemon_token, _token - Update tests to construct proper event objects
|
From what I can see, nothing is logged in the actor's activity logs. |
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.
Resolves #1585