This commit is contained in:
notCharles
2026-05-12 10:06:33 -04:00
parent 396d71c4a5
commit 90c50da83b
10 changed files with 98 additions and 136 deletions

View File

@@ -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<TModel>
*/

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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', [

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Traits;
use App\Observers\AdminActivityObserver;
trait HasAdminActivityLogging
{
public static function bootHasAdminActivityLogging(): void
{
$observer = new AdminActivityObserver();
static::created(fn ($model) => $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();
}
}

View File

@@ -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?',