diff --git a/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php b/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php index 8a19545211..b32ca0c849 100644 --- a/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php +++ b/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php @@ -2,6 +2,7 @@ namespace App\Filament\Server\Resources\ActivityResource\Pages; +use App\Filament\Admin\Resources\UserResource\Pages\EditUser; use App\Filament\Server\Resources\ActivityResource; use App\Models\ActivityLog; use App\Models\User; @@ -20,12 +21,16 @@ public function table(Table $table): Table ->columns([ TextColumn::make('event') ->html() - ->formatStateUsing(fn ($state, ActivityLog $activityLog) => __('activity.'.str($state)->replace(':', '.'))) // TODO: convert properties to a format that trans likes, see ActivityLogEntry.tsx - wrapProperties - ->description(fn ($state) => $state), + ->description(fn ($state) => $state) + ->formatStateUsing(function ($state, ActivityLog $activityLog) { + $properties = $activityLog->wrapProperties(); + + return trans_choice('activity.'.str($state)->replace(':', '.'), array_get($properties, 'count', 1), $properties); + }), TextColumn::make('user') ->state(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User ? $activityLog->actor->username : 'System') ->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '') - ->url(fn (ActivityLog $activityLog): string => $activityLog->actor instanceof User ? route('filament.admin.resources.users.edit', ['record' => $activityLog->actor]) : ''), + ->url(fn (ActivityLog $activityLog): string => $activityLog->actor instanceof User ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin', tenant: null) : ''), DateTimeColumn::make('timestamp') ->since() ->sortable(), diff --git a/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php index 87e087b42d..6db4fc4e0e 100644 --- a/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php +++ b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php @@ -130,13 +130,17 @@ public function table(Table $table): Table ->required(), ]) ->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) { + $files = [['to' => $data['name'], 'from' => $file->name]]; + $fileRepository ->setServer($server) - ->renameFiles($this->path, [['to' => $data['name'], 'from' => $file->name]]); + ->renameFiles($this->path, $files); Activity::event('server:file.rename') ->property('directory', $this->path) - ->property('files', [['to' => $data['name'], 'from' => $file->name]]) + ->property('files', $files) + ->property('to', $data['name']) + ->property('from', $file->name) ->log(); Notification::make() @@ -204,13 +208,17 @@ public function table(Table $table): Table ->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) { $location = resolve_path(join_paths($this->path, $data['location'])); + $files = [['to' => $location, 'from' => $file->name]]; + $fileRepository ->setServer($server) - ->renameFiles($this->path, [['to' => $location, 'from' => $file->name]]); + ->renameFiles($this->path, $files); Activity::event('server:file.rename') ->property('directory', $this->path) - ->property('files', [['to' => $location, 'from' => $file->name]]) + ->property('files', $files) + ->property('to', $location) + ->property('from', $file->name) ->log(); Notification::make() @@ -309,7 +317,7 @@ public function table(Table $table): Table Activity::event('server:file.decompress') ->property('directory', $this->path) - ->property('files', $file->name) + ->property('file', $file->name) ->log(); Notification::make() @@ -342,6 +350,7 @@ public function table(Table $table): Table BulkActionGroup::make([ BulkAction::make('move') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) + ->hidden() // TODO ->form([ TextInput::make('location') ->label('File name') @@ -366,7 +375,7 @@ public function table(Table $table): Table ->log(); Notification::make() - ->title(count($files) . ' Files were moved from to ' . $location) + ->title(count($files) . ' Files were moved from ' . $location) ->success() ->send(); }), diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php index 6fc443e8fd..38acc49a15 100644 --- a/app/Http/Controllers/Api/Client/Servers/FileController.php +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -146,13 +146,17 @@ public function create(CreateFolderRequest $request, Server $server): JsonRespon */ public function rename(RenameFileRequest $request, Server $server): JsonResponse { + $files = $request->input('files'); + $this->fileRepository ->setServer($server) - ->renameFiles($request->input('root'), $request->input('files')); + ->renameFiles($request->input('root'), $files); Activity::event('server:file.rename') ->property('directory', $request->input('root')) - ->property('files', $request->input('files')) + ->property('files', $files) + ->property('to', $files['to']) + ->property('from', $files['from']) ->log(); return new JsonResponse([], Response::HTTP_NO_CONTENT); @@ -210,7 +214,7 @@ public function decompress(DecompressFilesRequest $request, Server $server): Jso Activity::event('server:file.decompress') ->property('directory', $request->input('root')) - ->property('files', $request->input('file')) + ->property('file', $request->input('file')) ->log(); return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); diff --git a/app/Models/ActivityLog.php b/app/Models/ActivityLog.php index 83daf066c0..15e2fd1431 100644 --- a/app/Models/ActivityLog.php +++ b/app/Models/ActivityLog.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Model as IlluminateModel; +use Illuminate\Support\Str; /** * \App\Models\ActivityLog. @@ -151,8 +152,8 @@ public function htmlable(): string 'username' => 'system', ]); } - - $event = __('activity.'.str($this->event)->replace(':', '.')); + $properties = $this->wrapProperties(); + $event = trans_choice('activity.'.str($this->event)->replace(':', '.'), array_key_exists('count', $properties) ? $properties['count'] : 1, $properties); return "
@@ -166,4 +167,38 @@ public function htmlable(): string
"; } + + public function wrapProperties(): array + { + if (!$this->properties || $this->properties->isEmpty()) { + return []; + } + + $properties = $this->properties->mapWithKeys(function ($value, $key) { + if (!is_array($value)) { + // Perform some directory normalization at this point. + if ($key === 'directory') { + $value = str_replace('//', '/', '/' . trim($value, '/') . '/'); + } + + return [$key => $value]; + } + + $first = array_first($value); + + // Backwards compatibility for old logs + if (is_array($first)) { + return ["{$key}_count" => count($value)]; + } + + return [$key => $first, "{$key}_count" => count($value)]; + }); + + $keys = $properties->keys()->filter(fn ($key) => Str::endsWith($key, '_count'))->values(); + if ($keys->containsOneItem()) { + $properties = $properties->merge(['count' => $properties->get($keys[0])])->except([$keys[0]]); + } + + return $properties->toArray(); + } } diff --git a/app/Transformers/Api/Client/ActivityLogTransformer.php b/app/Transformers/Api/Client/ActivityLogTransformer.php index a54cb1f304..a5d3128d54 100644 --- a/app/Transformers/Api/Client/ActivityLogTransformer.php +++ b/app/Transformers/Api/Client/ActivityLogTransformer.php @@ -2,7 +2,6 @@ namespace App\Transformers\Api\Client; -use Illuminate\Support\Str; use App\Models\User; use App\Models\ActivityLog; use Illuminate\Database\Eloquent\Model; @@ -29,7 +28,7 @@ public function transform(ActivityLog $model): array 'is_api' => !is_null($model->api_key_id), 'ip' => $this->canViewIP($model->actor) ? $model->ip : null, 'description' => $model->description, - 'properties' => $this->properties($model), + 'properties' => $model->wrapProperties(), 'has_additional_metadata' => $this->hasAdditionalMetadata($model), 'timestamp' => $model->timestamp->toAtomString(), ]; @@ -44,42 +43,6 @@ public function includeActor(ActivityLog $model): ResourceAbstract return $this->item($model->actor, $this->makeTransformer(UserTransformer::class), User::RESOURCE_NAME); } - /** - * Transforms any array values in the properties into a countable field for easier - * use within the translation outputs. - */ - protected function properties(ActivityLog $model): object - { - if (!$model->properties || $model->properties->isEmpty()) { - return (object) []; - } - - $properties = $model->properties - ->mapWithKeys(function ($value, $key) use ($model) { - if ($key === 'ip' && $model->actor instanceof User && !$model->actor->is($this->request->user())) { - return [$key => '[hidden]']; - } - - if (!is_array($value)) { - // Perform some directory normalization at this point. - if ($key === 'directory') { - $value = str_replace('//', '/', '/' . trim($value, '/') . '/'); - } - - return [$key => $value]; - } - - return [$key => $value, "{$key}_count" => count($value)]; - }); - - $keys = $properties->keys()->filter(fn ($key) => Str::endsWith($key, '_count'))->values(); - if ($keys->containsOneItem()) { - $properties = $properties->merge(['count' => $properties->get($keys[0])])->except([$keys[0]]); - } - - return (object) $properties->toArray(); - } - /** * Determines if there are any log properties that we've not already exposed * in the response language string and that are not just the IP address or diff --git a/lang/en/activity.php b/lang/en/activity.php index 3158063b7d..fa712e40d5 100644 --- a/lang/en/activity.php +++ b/lang/en/activity.php @@ -15,7 +15,7 @@ 'checkpoint' => 'Two-factor authentication requested', 'recovery-token' => 'Used two-factor recovery token', 'token' => 'Solved two-factor challenge', - 'ip-blocked' => 'Blocked request from unlisted IP address for :identifier', + 'ip-blocked' => 'Blocked request from unlisted IP address for :identifier', 'sftp' => [ 'fail' => 'Failed SFTP log in', ], @@ -26,12 +26,12 @@ 'password-changed' => 'Changed password', ], 'api-key' => [ - 'create' => 'Created new API key :identifier', - 'delete' => 'Deleted API key :identifier', + 'create' => 'Created new API key :identifier', + 'delete' => 'Deleted API key :identifier', ], 'ssh-key' => [ - 'create' => 'Added SSH key :fingerprint to account', - 'delete' => 'Removed SSH key :fingerprint from account', + 'create' => 'Added SSH key :fingerprint to account', + 'delete' => 'Removed SSH key :fingerprint from account', ], 'two-factor' => [ 'create' => 'Enabled two-factor auth', @@ -41,7 +41,7 @@ 'server' => [ 'reinstall' => 'Reinstalled server', 'console' => [ - 'command' => 'Executed ":command" on the server', + 'command' => 'Executed ":command" on the server', ], 'power' => [ 'start' => 'Started the server', @@ -52,7 +52,7 @@ 'backup' => [ 'download' => 'Downloaded the :name backup', 'delete' => 'Deleted the :name backup', - 'restore' => 'Restored the :name backup (deleted files: :truncate)', + 'restore' => 'Restored the :name backup (deleted files: :truncate)', 'restore-complete' => 'Completed restoration of the :name backup', 'restore-failed' => 'Failed to complete restoration of the :name backup', 'start' => 'Started a new backup :name', @@ -67,40 +67,32 @@ 'delete' => 'Deleted database :name', ], 'file' => [ - 'compress_one' => 'Compressed :directory:file', - 'compress_other' => 'Compressed :count files in :directory', - 'read' => 'Viewed the contents of :file', - 'copy' => 'Created a copy of :file', - 'create-directory' => 'Created directory :directory:name', - 'decompress' => 'Decompressed :files in :directory', - 'delete_one' => 'Deleted :directory:files.0', - 'delete_other' => 'Deleted :count files in :directory', - 'download' => 'Downloaded :file', - 'pull' => 'Downloaded a remote file from :url to :directory', - 'rename_one' => 'Renamed :directory:files.0.from to :directory:files.0.to', - 'rename_other' => 'Renamed :count files in :directory', - 'write' => 'Wrote new content to :file', + 'compress' => 'Compressed :directory:files|Compressed :count files in :directory', + 'read' => 'Viewed the contents of :file', + 'copy' => 'Created a copy of :file', + 'create-directory' => 'Created directory :directory:name', + 'decompress' => 'Decompressed :file in :directory', + 'delete' => 'Deleted :directory:files|Deleted :count files in :directory', + 'download' => 'Downloaded :file', + 'pull' => 'Downloaded a remote file from :url to :directory', + 'rename' => 'Renamed :directory:from to :directory:to|Renamed :count files in :directory', + 'write' => 'Wrote new content to :file', 'upload' => 'Began a file upload', - 'uploaded' => 'Uploaded :directory:file', + 'uploaded' => 'Uploaded :directory:file', ], 'sftp' => [ 'denied' => 'Blocked SFTP access due to permissions', - 'create_one' => 'Created :files.0', - 'create_other' => 'Created :count new files', - 'write_one' => 'Modified the contents of :files.0', - 'write_other' => 'Modified the contents of :count files', - 'delete_one' => 'Deleted :files.0', - 'delete_other' => 'Deleted :count files', - 'create-directory_one' => 'Created the :files.0 directory', - 'create-directory_other' => 'Created :count directories', - 'rename_one' => 'Renamed :files.0.from to :files.0.to', - 'rename_other' => 'Renamed or moved :count files', + 'create' => 'Created :files|Created :count new files', + 'write' => 'Modified the contents of :files|Modified the contents of :count files', + 'delete' => 'Deleted :files|Deleted :count files', + 'create-directory' => 'Created the :files directory|Created :count directories', + 'rename' => 'Renamed :from to :to|Renamed or moved :count files', ], 'allocation' => [ - 'create' => 'Added :allocation to the server', - 'notes' => 'Updated the notes for :allocation from ":old" to ":new"', - 'primary' => 'Set :allocation as the primary server allocation', - 'delete' => 'Deleted the :allocation allocation', + 'create' => 'Added :allocation to the server', + 'notes' => 'Updated the notes for :allocation from ":old" to ":new"', + 'primary' => 'Set :allocation as the primary server allocation', + 'delete' => 'Deleted the :allocation allocation', ], 'schedule' => [ 'create' => 'Created the :name schedule', @@ -114,8 +106,8 @@ 'delete' => 'Deleted a task for the :name schedule', ], 'settings' => [ - 'rename' => 'Renamed the server from :old to :new', - 'description' => 'Changed the server description from :old to :new', + 'rename' => 'Renamed the server from ":old" to ":new"', + 'description' => 'Changed the server description from ":old" to ":new"', ], 'startup' => [ 'edit' => 'Changed the :variable variable from ":old" to ":new"',