From 90c50da83bc2b621f9b29379495fecb8ea6a4a01 Mon Sep 17 00:00:00 2001 From: notCharles Date: Tue, 12 May 2026 10:06:33 -0400 Subject: [PATCH] wip --- app/Eloquent/BackupQueryBuilder.php | 3 +- app/Models/Egg.php | 2 + app/Models/Node.php | 2 + app/Models/Role.php | 2 + app/Models/Server.php | 2 + app/Models/User.php | 7 ++ app/Observers/AdminActivityObserver.php | 159 +++++++----------------- app/Providers/AppServiceProvider.php | 19 --- app/Traits/HasAdminActivityLogging.php | 26 ++++ lang/en/admin/log.php | 12 ++ 10 files changed, 98 insertions(+), 136 deletions(-) create mode 100644 app/Traits/HasAdminActivityLogging.php diff --git a/app/Eloquent/BackupQueryBuilder.php b/app/Eloquent/BackupQueryBuilder.php index 53f10a28f..7e119cdf8 100644 --- a/app/Eloquent/BackupQueryBuilder.php +++ b/app/Eloquent/BackupQueryBuilder.php @@ -3,9 +3,10 @@ namespace App\Eloquent; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; /** - * @template TModel of \Illuminate\Database\Eloquent\Model + * @template TModel of Model * * @extends Builder */ diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 9be069fae..221a6418e 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -6,6 +6,7 @@ use App\Contracts\Validatable; use App\Exceptions\Service\Egg\HasChildrenException; use App\Exceptions\Service\HasActiveServersException; use App\Models\Traits\HasIcon; +use App\Traits\HasAdminActivityLogging; use App\Traits\HasValidation; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -93,6 +94,7 @@ use Illuminate\Support\Str; */ class Egg extends Model implements Validatable { + use HasAdminActivityLogging; use HasFactory; use HasIcon; use HasValidation; diff --git a/app/Models/Node.php b/app/Models/Node.php index 3edf89d9d..1be3e843d 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Contracts\Validatable; use App\Exceptions\Service\HasActiveServersException; use App\Repositories\Daemon\DaemonSystemRepository; +use App\Traits\HasAdminActivityLogging; use App\Traits\HasValidation; use Exception; use Illuminate\Database\Eloquent\Collection; @@ -95,6 +96,7 @@ use Symfony\Component\Yaml\Yaml; */ class Node extends Model implements Validatable { + use HasAdminActivityLogging; use HasFactory; use HasValidation; use Notifiable; diff --git a/app/Models/Role.php b/app/Models/Role.php index ad2b242fe..c05291b8f 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Enums\RolePermissionModels; use App\Enums\RolePermissionPrefixes; use App\Enums\TablerIcon; +use App\Traits\HasAdminActivityLogging; use BackedEnum; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -41,6 +42,7 @@ use Spatie\Permission\Models\Role as BaseRole; */ class Role extends BaseRole { + use HasAdminActivityLogging; use HasFactory; public const RESOURCE_NAME = 'role'; diff --git a/app/Models/Server.php b/app/Models/Server.php index 073a39be2..61fb4f506 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -10,6 +10,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException; use App\Models\Traits\HasIcon; use App\Repositories\Daemon\DaemonServerRepository; use App\Services\Subusers\SubuserDeletionService; +use App\Traits\HasAdminActivityLogging; use App\Traits\HasValidation; use Carbon\CarbonInterface; use Exception; @@ -129,6 +130,7 @@ use Psr\Http\Message\ResponseInterface; */ class Server extends Model implements HasAvatar, Validatable { + use HasAdminActivityLogging; use HasFactory; use HasIcon; use HasValidation; diff --git a/app/Models/User.php b/app/Models/User.php index 6dd2a0c8e..eddc549cb 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,6 +9,7 @@ use App\Events\User\Deleting; use App\Exceptions\DisplayException; use App\Extensions\Avatar\AvatarService; use App\Models\Traits\HasAccessTokens; +use App\Traits\HasAdminActivityLogging; use App\Traits\HasValidation; use BackedEnum; use DateTimeZone; @@ -119,6 +120,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac use Authorizable { can as protected canned; } use CanResetPassword; use HasAccessTokens; + use HasAdminActivityLogging; use HasFactory; use HasRoles; use HasValidation { getRules as getValidationRules; } @@ -538,4 +540,9 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac { $this->update(['mfa_email_enabled' => $condition]); } + + public function getAdminActivityName(): string + { + return $this->username; + } } diff --git a/app/Observers/AdminActivityObserver.php b/app/Observers/AdminActivityObserver.php index 5a48a345b..cde55e82c 100644 --- a/app/Observers/AdminActivityObserver.php +++ b/app/Observers/AdminActivityObserver.php @@ -3,13 +3,9 @@ namespace App\Observers; use App\Facades\Activity; -use App\Models\Egg; -use App\Models\Node; -use App\Models\Role; -use App\Models\Server; -use App\Models\User; use Filament\Facades\Filament; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Str; class AdminActivityObserver { @@ -49,7 +45,7 @@ class AdminActivityObserver } // Deduplicate identical events for the same record within a single request. - $key = $event . ':' . get_class($model) . ':' . $model->getKey(); + $key = $event . ':' . $model::class . ':' . $model->getKey(); if (isset(self::$logged[$key])) { return; } @@ -65,135 +61,66 @@ class AdminActivityObserver $log->log(); } - // ------------------------------------------------------------------------- - // User events - // ------------------------------------------------------------------------- - public function userCreated(User $user): void + public function created(Model $model): void { - $this->log('admin:user.create', $user, ['name' => $user->username]); + $this->log($this->eventFor($model, 'create'), $model, [ + 'name' => $this->displayNameFor($model), + ]); } - public function userUpdated(User $user): void + public function updated(Model $model): void { - $changedFields = $this->changedFieldsFor($user); + $changedFields = $this->changedFieldsFor($model); + $name = $this->displayNameFor($model); - $this->log('admin:user.update', $user, [ - 'name' => empty($changedFields) ? $user->username : sprintf('%s (%s)', $user->username, implode(', ', $changedFields)), + $this->log($this->eventFor($model, 'update'), $model, [ + 'name' => empty($changedFields) ? $name : sprintf('%s (%s)', $name, implode(', ', $changedFields)), 'count' => count($changedFields), 'changes' => implode(', ', $changedFields), ]); } - public function userDeleted(User $user): void + public function deleted(Model $model): void { - $this->log('admin:user.delete', $user, ['name' => $user->username]); - } - - // ------------------------------------------------------------------------- - // Server events - // ------------------------------------------------------------------------- - - public function serverCreated(Server $server): void - { - $this->log('admin:server.create', $server, ['name' => $server->name]); - } - - public function serverUpdated(Server $server): void - { - $changedFields = $this->changedFieldsFor($server); - - $this->log('admin:server.update', $server, [ - 'name' => empty($changedFields) ? $server->name : sprintf('%s (%s)', $server->name, implode(', ', $changedFields)), - 'count' => count($changedFields), - 'changes' => implode(', ', $changedFields), + $this->log($this->eventFor($model, 'delete'), $model, [ + 'name' => $this->displayNameFor($model), ]); } - public function serverDeleted(Server $server): void - { - $this->log('admin:server.delete', $server, ['name' => $server->name]); - } - - // ------------------------------------------------------------------------- - // Node events - // ------------------------------------------------------------------------- - - public function nodeCreated(Node $node): void - { - $this->log('admin:node.create', $node, ['name' => $node->name]); - } - - public function nodeUpdated(Node $node): void - { - $changedFields = $this->changedFieldsFor($node); - - $this->log('admin:node.update', $node, [ - 'name' => empty($changedFields) ? $node->name : sprintf('%s (%s)', $node->name, implode(', ', $changedFields)), - 'count' => count($changedFields), - 'changes' => implode(', ', $changedFields), - ]); - } - - public function nodeDeleted(Node $node): void - { - $this->log('admin:node.delete', $node, ['name' => $node->name]); - } - - // ------------------------------------------------------------------------- - // Egg events - // ------------------------------------------------------------------------- - - public function eggCreated(Egg $egg): void - { - $this->log('admin:egg.create', $egg, ['name' => $egg->name]); - } - - public function eggUpdated(Egg $egg): void - { - $changedFields = $this->changedFieldsFor($egg); - - $this->log('admin:egg.update', $egg, [ - 'name' => empty($changedFields) ? $egg->name : sprintf('%s (%s)', $egg->name, implode(', ', $changedFields)), - 'count' => count($changedFields), - 'changes' => implode(', ', $changedFields), - ]); - } - - public function eggDeleted(Egg $egg): void - { - $this->log('admin:egg.delete', $egg, ['name' => $egg->name]); - } - - // ------------------------------------------------------------------------- - // Role events - // ------------------------------------------------------------------------- - - public function roleCreated(Role $role): void - { - $this->log('admin:role.create', $role, ['name' => $role->name]); - } - - public function roleUpdated(Role $role): void - { - $changedFields = $this->changedFieldsFor($role); - - $this->log('admin:role.update', $role, [ - 'name' => empty($changedFields) ? $role->name : sprintf('%s (%s)', $role->name, implode(', ', $changedFields)), - 'count' => count($changedFields), - 'changes' => implode(', ', $changedFields), - ]); - } - - public function roleDeleted(Role $role): void - { - $this->log('admin:role.delete', $role, ['name' => $role->name]); - } - // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- + private function eventFor(Model $model, string $action): string + { + return sprintf('admin:%s.%s', $this->resourceNameFor($model), $action); + } + + private function resourceNameFor(Model $model): string + { + $constant = $model::class . '::RESOURCE_NAME'; + + if (defined($constant)) { + $value = constant($constant); + + if (is_string($value) && $value !== '') { + return $value; + } + } + + return Str::of(class_basename($model))->snake()->toString(); + } + + private function displayNameFor(Model $model): string + { + if (method_exists($model, 'getAdminActivityName')) { + return $model->getAdminActivityName(); + } + + return (string) $model->getKey(); + } + /** * Returns the sorted list of attribute names that changed on the given model, * excluding internal timestamps. diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7fa11df89..6d3f89cf3 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -18,13 +18,11 @@ use App\Models\Database; use App\Models\Egg; use App\Models\EggVariable; use App\Models\Node; -use App\Models\Role; use App\Models\Schedule; use App\Models\Server; use App\Models\Task; use App\Models\User; use App\Models\UserSSHKey; -use App\Observers\AdminActivityObserver; use App\Services\Helpers\PluginService; use App\Services\Helpers\SoftwareVersionService; use Dedoc\Scramble\Scramble; @@ -111,23 +109,6 @@ class AppServiceProvider extends ServiceProvider ]); } - $observer = new AdminActivityObserver(); - User::created(fn (User $user) => $observer->userCreated($user)); - User::updated(fn (User $user) => $observer->userUpdated($user)); - User::deleted(fn (User $user) => $observer->userDeleted($user)); - Server::created(fn (Server $server) => $observer->serverCreated($server)); - Server::updated(fn (Server $server) => $observer->serverUpdated($server)); - Server::deleted(fn (Server $server) => $observer->serverDeleted($server)); - Node::created(fn (Node $node) => $observer->nodeCreated($node)); - Node::updated(fn (Node $node) => $observer->nodeUpdated($node)); - Node::deleted(fn (Node $node) => $observer->nodeDeleted($node)); - Egg::created(fn (Egg $egg) => $observer->eggCreated($egg)); - Egg::updated(fn (Egg $egg) => $observer->eggUpdated($egg)); - Egg::deleted(fn (Egg $egg) => $observer->eggDeleted($egg)); - Role::created(fn (Role $role) => $observer->roleCreated($role)); - Role::updated(fn (Role $role) => $observer->roleUpdated($role)); - Role::deleted(fn (Role $role) => $observer->roleDeleted($role)); - Gate::before(fn (User $user, $ability) => $user->isRootAdmin() ? true : null); AboutCommand::add('Pelican', [ diff --git a/app/Traits/HasAdminActivityLogging.php b/app/Traits/HasAdminActivityLogging.php new file mode 100644 index 000000000..50d441cdb --- /dev/null +++ b/app/Traits/HasAdminActivityLogging.php @@ -0,0 +1,26 @@ + $observer->created($model)); + static::updated(fn ($model) => $observer->updated($model)); + static::deleted(fn ($model) => $observer->deleted($model)); + } + + public function getAdminActivityName(): string + { + if (isset($this->attributes['name'])) { + return (string) $this->attributes['name']; + } + + return (string) $this->getKey(); + } +} diff --git a/lang/en/admin/log.php b/lang/en/admin/log.php index 4c1a13159..41c3d0d89 100644 --- a/lang/en/admin/log.php +++ b/lang/en/admin/log.php @@ -2,6 +2,9 @@ return [ 'empty_table' => 'Yay! No Errors!', + 'empty_audit_log' => 'No admin actions have been logged yet.', + 'model_label' => 'Admin Audit Log', + 'model_label_plural' => 'Admin Audit Logs', 'total_logs' => 'Total Logs', 'error' => 'Error', 'warning' => 'Warning', @@ -10,6 +13,15 @@ return [ 'debug' => 'Debug', 'navigation' => [ 'panel_logs' => 'Panel Logs', + 'admin_audit_log' => 'Admin Audit', + ], + 'table' => [ + 'actor' => 'Actor', + 'event' => 'Event', + 'description' => 'Description', + 'ip' => 'IP Address', + 'timestamp' => 'Time', + 'system' => 'System', ], 'actions' => [ 'upload_logs' => 'Upload Logs?',