mirror of
https://github.com/pelican-dev/panel.git
synced 2026-02-25 03:14:39 +03:00
Compare commits
14 Commits
charles/sp
...
lance/1585
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c048ccf2d | ||
|
|
bdf8a9151c | ||
|
|
67e13c646a | ||
|
|
b1a39f1724 | ||
|
|
6a548c09a0 | ||
|
|
55bda569cc | ||
|
|
adf1249086 | ||
|
|
dbf77bf146 | ||
|
|
a34bf9fd49 | ||
|
|
7a9deba0e1 | ||
|
|
159bfe2210 | ||
|
|
a821db8aae | ||
|
|
1556f8efb8 | ||
|
|
57c2aa6f21 |
@@ -22,6 +22,7 @@ use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Fieldset;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
@@ -113,12 +114,44 @@ class ApiKeyResource extends Resource
|
||||
*/
|
||||
public static function defaultForm(Schema $schema): Schema
|
||||
{
|
||||
$permissionList = ApiKey::getPermissionList();
|
||||
|
||||
return $schema
|
||||
->components([
|
||||
Section::make(trans('admin/apikey.permissions.all'))
|
||||
->description(trans('admin/apikey.permissions.all_description'))
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('permissions_all')
|
||||
->hiddenLabel()
|
||||
->inline()
|
||||
->options([
|
||||
0 => trans('admin/apikey.permissions.none'),
|
||||
1 => trans('admin/apikey.permissions.read'),
|
||||
3 => trans('admin/apikey.permissions.read_write'),
|
||||
])
|
||||
->icons([
|
||||
0 => TablerIcon::BookOff,
|
||||
1 => TablerIcon::Book,
|
||||
3 => TablerIcon::Writing,
|
||||
])
|
||||
->colors([
|
||||
0 => 'success',
|
||||
1 => 'warning',
|
||||
3 => 'danger',
|
||||
])
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, callable $set) use ($permissionList) {
|
||||
foreach ($permissionList as $resource) {
|
||||
$set('permissions_' . $resource, $state);
|
||||
}
|
||||
})
|
||||
->default(0),
|
||||
]),
|
||||
Fieldset::make('Permissions')
|
||||
->columnSpanFull()
|
||||
->schema(
|
||||
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
|
||||
collect($permissionList)->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
|
||||
->label(str($resource)->replace('_', ' ')->title())->inline()
|
||||
->options([
|
||||
0 => trans('admin/apikey.permissions.none'),
|
||||
|
||||
@@ -450,17 +450,7 @@ class EditEgg extends EditRecord
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
|
||||
->tooltip(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use'))
|
||||
->successNotification(fn (Egg $egg) => Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/egg.delete_success'))
|
||||
->body(trans('admin/egg.deleted', ['egg' => $egg->name]))
|
||||
)
|
||||
->failureNotification(fn (Egg $egg) => Notification::make()
|
||||
->danger()
|
||||
->title(trans('admin/egg.delete_failed'))
|
||||
->body(trans('admin/egg.could_not_delete', ['egg' => $egg->name]))
|
||||
),
|
||||
->tooltip(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use')),
|
||||
ExportEggAction::make(),
|
||||
ImportEggAction::make()
|
||||
->multiple(false),
|
||||
|
||||
@@ -278,6 +278,14 @@ class CreateNode extends CreateRecord
|
||||
->default(256)
|
||||
->minValue(1)
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
|
||||
TextInput::make('daemon_base')
|
||||
->label(trans('admin/node.daemon_base'))
|
||||
->placeholder('/var/lib/pelican/volumes')
|
||||
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.daemon_base_help'))
|
||||
->columnSpan(1)
|
||||
->required()
|
||||
->default('/var/lib/pelican/volumes')
|
||||
->rule('regex:/^([\/][\d\w.\-\/]+)$/'),
|
||||
TextInput::make('daemon_sftp')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.sftp_port'))
|
||||
@@ -287,7 +295,7 @@ class CreateNode extends CreateRecord
|
||||
->required()
|
||||
->integer(),
|
||||
TextInput::make('daemon_sftp_alias')
|
||||
->columnSpan(2)
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.sftp_alias'))
|
||||
->helperText(trans('admin/node.sftp_alias_help')),
|
||||
Grid::make()
|
||||
|
||||
@@ -314,7 +314,7 @@ class EditNode extends EditRecord
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
'lg' => 3,
|
||||
]),
|
||||
TextInput::make('upload_size')
|
||||
->columnSpan([
|
||||
@@ -329,12 +329,24 @@ class EditNode extends EditRecord
|
||||
->required()
|
||||
->minValue(1)
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
|
||||
TextInput::make('daemon_base')
|
||||
->label(trans('admin/node.daemon_base'))
|
||||
->placeholder('/var/lib/pelican/volumes')
|
||||
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.daemon_base_help'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->rule('regex:/^([\/][\d\w.\-\/]+)$/'),
|
||||
TextInput::make('daemon_sftp')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 3,
|
||||
'md' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label(trans('admin/node.sftp_port'))
|
||||
->minValue(1)
|
||||
@@ -346,8 +358,8 @@ class EditNode extends EditRecord
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 3,
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
])
|
||||
->label(trans('admin/node.sftp_alias'))
|
||||
->helperText(trans('admin/node.sftp_alias_help')),
|
||||
@@ -356,7 +368,7 @@ class EditNode extends EditRecord
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 3,
|
||||
'lg' => 2,
|
||||
])
|
||||
->label(trans('admin/node.use_for_deploy'))
|
||||
->inline()
|
||||
@@ -374,7 +386,7 @@ class EditNode extends EditRecord
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 3,
|
||||
'lg' => 2,
|
||||
])
|
||||
->label(trans('admin/node.maintenance_mode'))
|
||||
->inline()
|
||||
@@ -572,7 +584,7 @@ class EditNode extends EditRecord
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Actions::make([
|
||||
Action::make('autoDeploy')
|
||||
Action::make('exclude_autoDeploy')
|
||||
->label(trans('admin/node.auto_deploy'))
|
||||
->color('primary')
|
||||
->modalHeading(trans('admin/node.auto_deploy'))
|
||||
@@ -610,7 +622,7 @@ class EditNode extends EditRecord
|
||||
}),
|
||||
])->fullWidth(),
|
||||
Actions::make([
|
||||
Action::make('resetKey')
|
||||
Action::make('exclude_resetKey')
|
||||
->label(trans('admin/node.reset_token'))
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
|
||||
@@ -66,6 +66,14 @@ class Login extends BaseLogin
|
||||
->extraInputAttributes(['tabindex' => 1]);
|
||||
}
|
||||
|
||||
protected function getPasswordFormComponent(): Component
|
||||
{
|
||||
/** @var TextInput $component */
|
||||
$component = parent::getPasswordFormComponent();
|
||||
|
||||
return $component->extraInputAttributes(['tabindex' => 2]);
|
||||
}
|
||||
|
||||
protected function getOAuthFormComponent(): Component
|
||||
{
|
||||
$actions = [];
|
||||
|
||||
@@ -78,11 +78,15 @@ class Console extends Page
|
||||
$feature = data_get($data, 'key');
|
||||
|
||||
$feature = $this->featureService->get($feature);
|
||||
if (!$feature || $this->getMountedAction()) {
|
||||
if (!$feature) {
|
||||
return;
|
||||
}
|
||||
$this->mountAction($feature->getId());
|
||||
sleep(2); // TODO find a better way
|
||||
|
||||
if ($this->getMountedAction()) {
|
||||
$this->replaceMountedAction($feature->getId());
|
||||
} else {
|
||||
$this->mountAction($feature->getId());
|
||||
}
|
||||
}
|
||||
|
||||
public function getWidgetData(): array
|
||||
|
||||
@@ -209,16 +209,18 @@ class ListFiles extends ListRecords
|
||||
->required()
|
||||
->live(),
|
||||
TextEntry::make('new_location')
|
||||
->state(fn (Get $get, File $file) => resolve_path(join_paths($this->path, $get('location') ?? '/', $file->name))),
|
||||
->state(fn (Get $get, File $file) => resolve_path(join_paths($this->path, str_ends_with($get('location') ?? '/', '/') ? join_paths($get('location') ?? '/', $file->name) : $get('location') ?? '/'))),
|
||||
])
|
||||
->action(function ($data, File $file) {
|
||||
$location = $data['location'];
|
||||
$files = [['to' => join_paths($location, $file->name), 'from' => $file->name]];
|
||||
$endsWithSlash = str_ends_with($location, '/');
|
||||
$to = $endsWithSlash ? join_paths($location, $file->name) : $location;
|
||||
$files = [['to' => $to, 'from' => $file->name]];
|
||||
|
||||
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
|
||||
|
||||
$oldLocation = join_paths($this->path, $file->name);
|
||||
$newLocation = resolve_path(join_paths($this->path, $location, $file->name));
|
||||
$newLocation = resolve_path(join_paths($this->path, $to));
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
|
||||
@@ -74,7 +74,7 @@ class OAuthController extends Controller
|
||||
$email = $oauthUser->getEmail();
|
||||
|
||||
if (!$email) {
|
||||
return $this->errorRedirect();
|
||||
return $this->errorRedirect('No email was linked to your account on the OAuth provider.');
|
||||
}
|
||||
|
||||
$user = User::whereEmail($email)->first();
|
||||
|
||||
72
app/Listeners/AdminActivityListener.php
Normal file
72
app/Listeners/AdminActivityListener.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Events\RecordCreated;
|
||||
use Filament\Resources\Events\RecordUpdated;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AdminActivityListener
|
||||
{
|
||||
protected const REDACTED_FIELDS = [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
'current_password',
|
||||
'token',
|
||||
'secret',
|
||||
'api_key',
|
||||
'daemon_token',
|
||||
'_token',
|
||||
];
|
||||
|
||||
public function handle(RecordCreated|RecordUpdated $event): void
|
||||
{
|
||||
if (Filament::getCurrentPanel()?->getId() !== 'admin') {
|
||||
return;
|
||||
}
|
||||
|
||||
$record = $event->getRecord();
|
||||
$page = $event->getPage();
|
||||
$data = $event->getData();
|
||||
|
||||
$resourceClass = $page::getResource();
|
||||
$modelClass = $resourceClass::getModel();
|
||||
$slug = Str::kebab(class_basename($modelClass));
|
||||
|
||||
$action = $event instanceof RecordCreated ? 'create' : 'update';
|
||||
|
||||
$properties = $this->redactSensitiveFields($data);
|
||||
|
||||
Activity::event("admin:$slug.$action")
|
||||
->subject($record)
|
||||
->property($properties)
|
||||
->log();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)) {
|
||||
$redacted[$key] = '[REDACTED]';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$redacted[$key] = $this->redactSensitiveFields($value);
|
||||
} else {
|
||||
$redacted[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $redacted;
|
||||
}
|
||||
}
|
||||
@@ -358,7 +358,7 @@ class Node extends Model implements Validatable
|
||||
'disk_used' => 0,
|
||||
];
|
||||
|
||||
return cache()->remember("nodes.$this->id.statistics", now()->addSeconds(360), function () use ($default) {
|
||||
return cache()->flexible("nodes.$this->id.statistics", [5, 30], function () use ($default) {
|
||||
try {
|
||||
|
||||
$data = Http::daemon($this)
|
||||
|
||||
@@ -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],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -122,4 +122,51 @@ return [
|
||||
],
|
||||
'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',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -20,6 +20,8 @@ return [
|
||||
'never_used' => 'Never Used',
|
||||
],
|
||||
'permissions' => [
|
||||
'all' => 'Set All Permissions',
|
||||
'all_description' => 'Quickly set all permissions below to the same level.',
|
||||
'none' => 'None',
|
||||
'read' => 'Read',
|
||||
'read_write' => 'Read & Write',
|
||||
|
||||
@@ -115,10 +115,6 @@ return [
|
||||
'no_update_url' => 'The following eggs do not have a working update URL set: :eggs',
|
||||
'cannot_delete' => 'Cannot delete :count egg(s)',
|
||||
'eggs_have_servers' => 'The following eggs have servers and cannot be deleted: :eggs',
|
||||
'delete_success' => 'Egg deleted successfully',
|
||||
'deleted' => 'Deleted: :egg',
|
||||
'delete_failed' => 'Failed to delete egg',
|
||||
'could_not_delete' => 'Could not delete: :egg',
|
||||
'updated_from' => 'Successfully updated from: :url',
|
||||
'update_error' => 'Error: :error',
|
||||
'updated_eggs' => 'Updated: :eggs',
|
||||
|
||||
@@ -65,6 +65,8 @@ return [
|
||||
'sftp_port' => 'SFTP Port',
|
||||
'sftp_alias' => 'SFTP Alias',
|
||||
'sftp_alias_help' => 'Display alias for the SFTP address. Leave empty to use the Node FQDN.',
|
||||
'daemon_base' => 'Daemon Base Directory',
|
||||
'daemon_base_help' => 'The directory where server data will be stored.',
|
||||
'use_for_deploy' => 'Use for Deployments?',
|
||||
'maintenance_mode' => 'Maintenance Mode',
|
||||
'maintenance_mode_help' => 'If the node is marked \'Under Maintenance\' users won\'t be able to access servers that are on that node',
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle X-Forwarded-Proto Header
|
||||
RewriteCond %{HTTP:X-Forwarded-Proto} =https [NC]
|
||||
RewriteRule .* - [E=HTTPS:on]
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
<x-filament-panels::page>
|
||||
@once
|
||||
<style>
|
||||
.files-selection-merged .fi-ta-header-ctn {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
</style>
|
||||
@endonce
|
||||
|
||||
<div
|
||||
x-data="
|
||||
{
|
||||
|
||||
158
tests/Filament/Admin/AdminActivityListenerTest.php
Normal file
158
tests/Filament/Admin/AdminActivityListenerTest.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
use App\Events\ActivityLogged;
|
||||
use App\Filament\Admin\Resources\Eggs\Pages\CreateEgg;
|
||||
use App\Filament\Admin\Resources\Eggs\Pages\EditEgg;
|
||||
use App\Filament\Admin\Resources\Nodes\Pages\CreateNode;
|
||||
use App\Filament\Admin\Resources\Nodes\Pages\EditNode;
|
||||
use App\Listeners\AdminActivityListener;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
use App\Models\Role;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Events\RecordCreated;
|
||||
use Filament\Resources\Events\RecordUpdated;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
function pageInstance(string $class): object
|
||||
{
|
||||
return (new ReflectionClass($class))->newInstanceWithoutConstructor();
|
||||
}
|
||||
|
||||
function createEvent(object $record, array $data, object $page): RecordCreated
|
||||
{
|
||||
return new RecordCreated($record, $data, $page);
|
||||
}
|
||||
|
||||
function updateEvent(object $record, array $data, object $page): RecordUpdated
|
||||
{
|
||||
return new RecordUpdated($record, $data, $page);
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
[$this->admin] = generateTestAccount([]);
|
||||
$this->admin = $this->admin->syncRoles(Role::getRootAdmin());
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Filament::setCurrentPanel('admin');
|
||||
});
|
||||
|
||||
it('logs create activity for an egg', function () {
|
||||
$egg = Egg::first();
|
||||
|
||||
$listener = new AdminActivityListener();
|
||||
$listener->handle(createEvent($egg, ['name' => 'Test Egg'], pageInstance(CreateEgg::class)));
|
||||
|
||||
$this->assertActivityLogged('admin:egg.create');
|
||||
});
|
||||
|
||||
it('logs update activity for an egg', function () {
|
||||
$egg = Egg::first();
|
||||
|
||||
$listener = new AdminActivityListener();
|
||||
$listener->handle(updateEvent($egg, ['name' => 'Updated Egg'], pageInstance(EditEgg::class)));
|
||||
|
||||
$this->assertActivityLogged('admin:egg.update');
|
||||
});
|
||||
|
||||
it('logs create activity for a node', function () {
|
||||
$node = Node::first();
|
||||
|
||||
$listener = new AdminActivityListener();
|
||||
$listener->handle(createEvent($node, ['name' => 'Test Node'], pageInstance(CreateNode::class)));
|
||||
|
||||
$this->assertActivityLogged('admin:node.create');
|
||||
});
|
||||
|
||||
it('logs update activity for a node', function () {
|
||||
$node = Node::first();
|
||||
|
||||
$listener = new AdminActivityListener();
|
||||
$listener->handle(updateEvent($node, ['name' => 'Updated Node'], pageInstance(EditNode::class)));
|
||||
|
||||
$this->assertActivityLogged('admin:node.update');
|
||||
});
|
||||
|
||||
it('does not log activity for non-admin panels', function () {
|
||||
Filament::setCurrentPanel('app');
|
||||
|
||||
$egg = Egg::first();
|
||||
|
||||
$listener = new AdminActivityListener();
|
||||
$listener->handle(createEvent($egg, ['name' => 'Test'], pageInstance(CreateEgg::class)));
|
||||
|
||||
Event::assertNotDispatched(ActivityLogged::class);
|
||||
});
|
||||
|
||||
it('sets the record as the activity subject', function () {
|
||||
$egg = Egg::first();
|
||||
|
||||
$listener = new AdminActivityListener();
|
||||
$listener->handle(createEvent($egg, ['name' => 'Test'], pageInstance(CreateEgg::class)));
|
||||
|
||||
$this->assertActivityFor('admin:egg.create', $this->admin, $egg);
|
||||
});
|
||||
|
||||
it('redacts sensitive fields from activity properties', function () {
|
||||
$egg = Egg::first();
|
||||
|
||||
$data = [
|
||||
'name' => 'Visible',
|
||||
'password' => 'should-be-redacted',
|
||||
'password_confirmation' => 'should-be-redacted',
|
||||
'token' => 'should-be-redacted',
|
||||
'secret' => 'should-be-redacted',
|
||||
'api_key' => 'should-be-redacted',
|
||||
];
|
||||
|
||||
$listener = new AdminActivityListener();
|
||||
$listener->handle(updateEvent($egg, $data, pageInstance(EditEgg::class)));
|
||||
|
||||
Event::assertDispatched(ActivityLogged::class, function (ActivityLogged $event) {
|
||||
$properties = $event->model->properties;
|
||||
|
||||
expect($properties)->toHaveKey('name', 'Visible')
|
||||
->toHaveKey('password', '[REDACTED]')
|
||||
->toHaveKey('password_confirmation', '[REDACTED]')
|
||||
->toHaveKey('token', '[REDACTED]')
|
||||
->toHaveKey('secret', '[REDACTED]')
|
||||
->toHaveKey('api_key', '[REDACTED]');
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it('redacts sensitive fields in nested arrays', function () {
|
||||
$egg = Egg::first();
|
||||
|
||||
$data = [
|
||||
'name' => 'Visible',
|
||||
'nested' => [
|
||||
'safe' => 'value',
|
||||
'password' => 'should-be-redacted',
|
||||
'token' => 'should-be-redacted',
|
||||
],
|
||||
];
|
||||
|
||||
$listener = new AdminActivityListener();
|
||||
$listener->handle(updateEvent($egg, $data, pageInstance(EditEgg::class)));
|
||||
|
||||
Event::assertDispatched(ActivityLogged::class, function (ActivityLogged $event) {
|
||||
$properties = $event->model->properties;
|
||||
|
||||
expect($properties['nested'])->toHaveKey('safe', 'value')
|
||||
->toHaveKey('password', '[REDACTED]')
|
||||
->toHaveKey('token', '[REDACTED]');
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it('generates kebab-case event names from model class names', function () {
|
||||
$node = Node::first();
|
||||
|
||||
$listener = new AdminActivityListener();
|
||||
$listener->handle(createEvent($node, ['name' => 'Test'], pageInstance(CreateNode::class)));
|
||||
|
||||
$this->assertActivityLogged('admin:node.create');
|
||||
});
|
||||
Reference in New Issue
Block a user