Update backup logic to use activity logs, not audit logs
This commit is contained in:
parent
cbecfff6da
commit
2fc5a734f9
12 changed files with 222 additions and 159 deletions
|
@ -10,11 +10,10 @@ use Pterodactyl\Services\Activity\ActivityLogService;
|
|||
* @method static ActivityLogService anonymous()
|
||||
* @method static ActivityLogService event(string $action)
|
||||
* @method static ActivityLogService description(?string $description)
|
||||
* @method static ActivityLogService subject(Model $subject)
|
||||
* @method static ActivityLogService subject(Model|Model[] $subject)
|
||||
* @method static ActivityLogService actor(Model $actor)
|
||||
* @method static ActivityLogService withProperties(\Illuminate\Support\Collection|array $properties)
|
||||
* @method static ActivityLogService withRequestMetadata()
|
||||
* @method static ActivityLogService property(string $key, mixed $value)
|
||||
* @method static ActivityLogService property(string|array $key, mixed $value = null)
|
||||
* @method static \Pterodactyl\Models\ActivityLog log(string $description = null)
|
||||
* @method static ActivityLogService clone()
|
||||
* @method static mixed transaction(\Closure $callback)
|
||||
|
|
|
@ -5,8 +5,8 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
|||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\AuditLog;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Pterodactyl\Services\Backups\DeleteBackupService;
|
||||
|
@ -77,25 +77,23 @@ class BackupController extends ClientApiController
|
|||
*/
|
||||
public function store(StoreBackupRequest $request, Server $server): array
|
||||
{
|
||||
/** @var \Pterodactyl\Models\Backup $backup */
|
||||
$backup = $server->audit(AuditLog::SERVER__BACKUP_STARTED, function (AuditLog $model, Server $server) use ($request) {
|
||||
$action = $this->initiateBackupService
|
||||
->setIgnoredFiles(explode(PHP_EOL, $request->input('ignored') ?? ''));
|
||||
$action = $this->initiateBackupService
|
||||
->setIgnoredFiles(explode(PHP_EOL, $request->input('ignored') ?? ''));
|
||||
|
||||
// Only set the lock status if the user even has permission to delete backups,
|
||||
// otherwise ignore this status. This gets a little funky since it isn't clear
|
||||
// how best to allow a user to create a backup that is locked without also preventing
|
||||
// them from just filling up a server with backups that can never be deleted?
|
||||
if ($request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
|
||||
$action->setIsLocked((bool) $request->input('is_locked'));
|
||||
}
|
||||
// Only set the lock status if the user even has permission to delete backups,
|
||||
// otherwise ignore this status. This gets a little funky since it isn't clear
|
||||
// how best to allow a user to create a backup that is locked without also preventing
|
||||
// them from just filling up a server with backups that can never be deleted?
|
||||
if ($request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
|
||||
$action->setIsLocked((bool) $request->input('is_locked'));
|
||||
}
|
||||
|
||||
$backup = $action->handle($server, $request->input('name'));
|
||||
$backup = $action->handle($server, $request->input('name'));
|
||||
|
||||
$model->metadata = ['backup_uuid' => $backup->uuid];
|
||||
|
||||
return $backup;
|
||||
});
|
||||
Activity::event('server:backup.start')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'locked' => (bool) $request->input('is_locked')])
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($backup)
|
||||
->transformWith($this->getTransformer(BackupTransformer::class))
|
||||
|
@ -114,14 +112,11 @@ class BackupController extends ClientApiController
|
|||
throw new AuthorizationException();
|
||||
}
|
||||
|
||||
$action = $backup->is_locked ? AuditLog::SERVER__BACKUP_UNLOCKED : AuditLog::SERVER__BACKUP_LOCKED;
|
||||
$server->audit($action, function (AuditLog $audit) use ($backup) {
|
||||
$audit->metadata = ['backup_uuid' => $backup->uuid];
|
||||
$action = $backup->is_locked ? 'server:backup.unlock' : 'server:backup.lock';
|
||||
|
||||
$backup->update(['is_locked' => !$backup->is_locked]);
|
||||
});
|
||||
$backup->update(['is_locked' => !$backup->is_locked]);
|
||||
|
||||
$backup->refresh();
|
||||
Activity::event($action)->subject($backup)->property('name', $backup->name)->log();
|
||||
|
||||
return $this->fractal->item($backup)
|
||||
->transformWith($this->getTransformer(BackupTransformer::class))
|
||||
|
@ -156,11 +151,12 @@ class BackupController extends ClientApiController
|
|||
throw new AuthorizationException();
|
||||
}
|
||||
|
||||
$server->audit(AuditLog::SERVER__BACKUP_DELETED, function (AuditLog $audit) use ($backup) {
|
||||
$audit->metadata = ['backup_uuid' => $backup->uuid];
|
||||
$this->deleteBackupService->handle($backup);
|
||||
|
||||
$this->deleteBackupService->handle($backup);
|
||||
});
|
||||
Activity::event('server:backup.delete')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'failed' => !$backup->is_successful])
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
@ -184,9 +180,8 @@ class BackupController extends ClientApiController
|
|||
}
|
||||
|
||||
$url = $this->downloadLinkService->handle($backup, $request->user());
|
||||
$server->audit(AuditLog::SERVER__BACKUP_DOWNLOADED, function (AuditLog $audit) use ($backup) {
|
||||
$audit->metadata = ['backup_uuid' => $backup->uuid];
|
||||
});
|
||||
|
||||
Activity::event('server:backup.download')->subject($backup)->property('name', $backup->name)->log();
|
||||
|
||||
return new JsonResponse([
|
||||
'object' => 'signed_url',
|
||||
|
@ -221,9 +216,11 @@ class BackupController extends ClientApiController
|
|||
throw new BadRequestHttpException('This backup cannot be restored at this time: not completed or failed.');
|
||||
}
|
||||
|
||||
$server->audit(AuditLog::SERVER__BACKUP_RESTORE_STARTED, function (AuditLog $audit, Server $server) use ($backup, $request) {
|
||||
$audit->metadata = ['backup_uuid' => $backup->uuid];
|
||||
$log = Activity::event('server:backup.restore')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'truncate' => $request->input('truncate')]);
|
||||
|
||||
$log->transaction(function () use ($backup, $server, $request) {
|
||||
// If the backup is for an S3 file we need to generate a unique Download link for
|
||||
// it that will allow Wings to actually access the file.
|
||||
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
|
||||
|
|
|
@ -5,9 +5,8 @@ namespace Pterodactyl\Http\Controllers\Api\Remote\Backups;
|
|||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\AuditLog;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use League\Flysystem\AwsS3v3\AwsS3Adapter;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
|
@ -46,15 +45,12 @@ class BackupStatusController extends Controller
|
|||
throw new BadRequestHttpException('Cannot update the status of a backup that is already marked as completed.');
|
||||
}
|
||||
|
||||
$action = $request->input('successful')
|
||||
? AuditLog::SERVER__BACKUP_COMPELTED
|
||||
: AuditLog::SERVER__BACKUP_FAILED;
|
||||
|
||||
$model->server->audit($action, function (AuditLog $audit) use ($model, $request) {
|
||||
$audit->is_system = true;
|
||||
$audit->metadata = ['backup_uuid' => $model->uuid];
|
||||
$action = $request->boolean('successful') ? 'server:backup.complete' : 'server:backup.failed';
|
||||
$log = Activity::event($action)->subject($model, $model->server)->property('name', $model->name);
|
||||
|
||||
$log->transaction(function () use ($model, $request) {
|
||||
$successful = $request->boolean('successful');
|
||||
|
||||
$model->fill([
|
||||
'is_successful' => $successful,
|
||||
// Change the lock state to unlocked if this was a failed backup so that it can be
|
||||
|
@ -93,17 +89,13 @@ class BackupStatusController extends Controller
|
|||
{
|
||||
/** @var \Pterodactyl\Models\Backup $model */
|
||||
$model = Backup::query()->where('uuid', $backup)->firstOrFail();
|
||||
$action = $request->get('successful')
|
||||
? AuditLog::SERVER__BACKUP_RESTORE_COMPLETED
|
||||
: AuditLog::SERVER__BACKUP_RESTORE_FAILED;
|
||||
|
||||
// Just create a new audit entry for this event and update the server state
|
||||
// so that power actions, file management, and backups can resume as normal.
|
||||
$model->server->audit($action, function (AuditLog $audit, Server $server) use ($backup) {
|
||||
$audit->is_system = true;
|
||||
$audit->metadata = ['backup_uuid' => $backup];
|
||||
$server->update(['status' => null]);
|
||||
});
|
||||
$model->server->update(['status' => null]);
|
||||
|
||||
Activity::event($request->boolean('successful') ? 'server:backup.restore-complete' : 'server.backup.restore-failed')
|
||||
->subject($model, $model->server)
|
||||
->property('name', $model->name)
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
|
|
@ -4,8 +4,10 @@ namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\AuditLog;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
|
@ -107,7 +109,6 @@ class ServerDetailsController extends Controller
|
|||
//
|
||||
// For each of those servers we'll track a new audit log entry to mark them as
|
||||
// failed and then update them all to be in a valid state.
|
||||
/** @var \Pterodactyl\Models\Server[] $servers */
|
||||
$servers = Server::query()
|
||||
->select('servers.*')
|
||||
->selectRaw('JSON_UNQUOTE(JSON_EXTRACT(started.metadata, "$.backup_uuid")) as backup_uuid')
|
||||
|
@ -130,14 +131,17 @@ class ServerDetailsController extends Controller
|
|||
->where('servers.status', Server::STATUS_RESTORING_BACKUP)
|
||||
->get();
|
||||
|
||||
$backups = Backup::query()->whereIn('uuid', $servers->pluck('backup_uuid'))->get();
|
||||
|
||||
/** @var \Pterodactyl\Models\Server $server */
|
||||
foreach ($servers as $server) {
|
||||
// Just create a new audit entry for this event and update the server state
|
||||
// so that power actions, file management, and backups can resume as normal.
|
||||
$server->audit(AuditLog::SERVER__BACKUP_RESTORE_FAILED, function (AuditLog $audit, Server $server) {
|
||||
$audit->is_system = true;
|
||||
$audit->metadata = ['backup_uuid' => $server->getAttribute('backup_uuid')];
|
||||
$server->update(['status' => null]);
|
||||
});
|
||||
$server->update(['status' => null]);
|
||||
|
||||
if ($backup = $backups->where('uuid', $server->getAttribute('backup_uuid'))->first()) {
|
||||
// Just create a new audit entry for this event and update the server state
|
||||
// so that power actions, file management, and backups can resume as normal.
|
||||
Activity::event('server:backup.restore-failed')->subject($server, $backup)->log();
|
||||
}
|
||||
}
|
||||
|
||||
// Update any server marked as installing or restoring as being in a normal state
|
||||
|
|
|
@ -16,16 +16,13 @@ use Illuminate\Database\Eloquent\Model as IlluminateModel;
|
|||
* @property string|null $description
|
||||
* @property string|null $actor_type
|
||||
* @property int|null $actor_id
|
||||
* @property string|null $subject_type
|
||||
* @property int|null $subject_id
|
||||
* @property \Illuminate\Support\Collection $properties
|
||||
* @property string $timestamp
|
||||
* @property IlluminateModel|\Eloquent $actor
|
||||
* @property IlluminateModel|\Eloquent $subject
|
||||
*
|
||||
* @method static Builder|ActivityLog forAction(string $action)
|
||||
* @method static Builder|ActivityLog forEvent(string $event)
|
||||
* @method static Builder|ActivityLog forActor(\Illuminate\Database\Eloquent\Model $actor)
|
||||
* @method static Builder|ActivityLog forSubject(\Illuminate\Database\Eloquent\Model $subject)
|
||||
* @method static Builder|ActivityLog newModelQuery()
|
||||
* @method static Builder|ActivityLog newQuery()
|
||||
* @method static Builder|ActivityLog query()
|
||||
|
@ -37,8 +34,6 @@ use Illuminate\Database\Eloquent\Model as IlluminateModel;
|
|||
* @method static Builder|ActivityLog whereId($value)
|
||||
* @method static Builder|ActivityLog whereIp($value)
|
||||
* @method static Builder|ActivityLog whereProperties($value)
|
||||
* @method static Builder|ActivityLog whereSubjectId($value)
|
||||
* @method static Builder|ActivityLog whereSubjectType($value)
|
||||
* @method static Builder|ActivityLog whereTimestamp($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
|
@ -68,14 +63,9 @@ class ActivityLog extends Model
|
|||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function subject(): MorphTo
|
||||
public function scopeForEvent(Builder $builder, string $action): Builder
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function scopeForAction(Builder $builder, string $action): Builder
|
||||
{
|
||||
return $builder->where('action', $action);
|
||||
return $builder->where('event', $action);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,12 +75,4 @@ class ActivityLog extends Model
|
|||
{
|
||||
return $builder->whereMorphedTo('actor', $actor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes a query to only return results where the subject is the given model.
|
||||
*/
|
||||
public function scopeForSubject(Builder $builder, IlluminateModel $subject): Builder
|
||||
{
|
||||
return $builder->whereMorphedTo('subject', $subject);
|
||||
}
|
||||
}
|
||||
|
|
40
app/Models/ActivityLogSubject.php
Normal file
40
app/Models/ActivityLogSubject.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
|
||||
/**
|
||||
* \Pterodactyl\Models\ActivityLogSubject.
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $activity_log_id
|
||||
* @property int $subject_id
|
||||
* @property string $subject_type
|
||||
* @property \Pterodactyl\Models\ActivityLog|null $activityLog
|
||||
* @property \Illuminate\Database\Eloquent\Model|\Eloquent $subject
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ActivityLogSubject newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ActivityLogSubject newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ActivityLogSubject query()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ActivityLogSubject extends Pivot
|
||||
{
|
||||
public $incrementing = true;
|
||||
public $timestamps = false;
|
||||
|
||||
protected $table = 'activity_log_subjects';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public function activityLog()
|
||||
{
|
||||
return $this->belongsTo(ActivityLog::class);
|
||||
}
|
||||
|
||||
public function subject()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Znck\Eloquent\Traits\BelongsToThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;
|
||||
|
||||
/**
|
||||
|
@ -41,8 +41,6 @@ use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;
|
|||
* @property \Pterodactyl\Models\Allocation|null $allocation
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Allocation[] $allocations
|
||||
* @property int|null $allocations_count
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\AuditLog[] $audits
|
||||
* @property int|null $audits_count
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Backup[] $backups
|
||||
* @property int|null $backups_count
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Database[] $databases
|
||||
|
@ -373,48 +371,11 @@ class Server extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns a fresh AuditLog model for the server. This model is not saved to the
|
||||
* database when created, so it is up to the caller to correctly store it as needed.
|
||||
*
|
||||
* @return \Pterodactyl\Models\AuditLog
|
||||
* Returns all of the activity log entries where the server is the subject.
|
||||
*/
|
||||
public function newAuditEvent(string $action, array $metadata = []): AuditLog
|
||||
public function activity(): MorphToMany
|
||||
{
|
||||
return AuditLog::instance($action, $metadata)->fill([
|
||||
'server_id' => $this->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a new audit event for a server by using a transaction. If the transaction
|
||||
* fails for any reason everything executed within will be rolled back. The callback
|
||||
* passed in will receive the AuditLog model before it is saved and the second argument
|
||||
* will be the current server instance. The callback should modify the audit entry as
|
||||
* needed before finishing, any changes will be persisted.
|
||||
*
|
||||
* The response from the callback is returned to the caller.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function audit(string $action, Closure $callback)
|
||||
{
|
||||
return $this->getConnection()->transaction(function () use ($action, $callback) {
|
||||
$model = $this->newAuditEvent($action);
|
||||
$response = $callback($model, $this);
|
||||
$model->save();
|
||||
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function audits()
|
||||
{
|
||||
return $this->hasMany(AuditLog::class);
|
||||
return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,6 +14,7 @@ use Illuminate\Auth\Passwords\CanResetPassword;
|
|||
use Pterodactyl\Traits\Helpers\AvailableLanguages;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\Access\Authorizable;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
|
@ -273,6 +274,15 @@ class User extends Model implements
|
|||
return $this->hasMany(UserSSHKey::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the activity logs where this user is the subject — not to
|
||||
* be confused by activity logs where this user is the _actor_.
|
||||
*/
|
||||
public function activity(): MorphToMany
|
||||
{
|
||||
return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the servers that a user can access by way of being the owner of the
|
||||
* server, or because they are assigned as a subuser for that server.
|
||||
|
|
|
@ -5,11 +5,15 @@ namespace Pterodactyl\Providers;
|
|||
use View;
|
||||
use Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Pterodactyl\Extensions\Themes\Theme;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
@ -33,6 +37,12 @@ class AppServiceProvider extends ServiceProvider
|
|||
if (Str::startsWith(config('app.url') ?? '', 'https://')) {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
|
||||
Relation::enforceMorphMap([
|
||||
'backup' => Backup::class,
|
||||
'server' => Server::class,
|
||||
'user' => User::class,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,20 +2,28 @@
|
|||
|
||||
namespace Pterodactyl\Services\Activity;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Illuminate\Support\Collection;
|
||||
use Pterodactyl\Models\ActivityLog;
|
||||
use Illuminate\Contracts\Auth\Factory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Pterodactyl\Models\ActivityLogSubject;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
|
||||
class ActivityLogService
|
||||
{
|
||||
protected ?ActivityLog $activity = null;
|
||||
|
||||
protected array $subjects = [];
|
||||
|
||||
protected Factory $manager;
|
||||
|
||||
protected ConnectionInterface $connection;
|
||||
|
||||
protected AcitvityLogBatchService $batch;
|
||||
|
||||
protected ActivityLogTargetableService $targetable;
|
||||
|
||||
public function __construct(
|
||||
|
@ -65,10 +73,22 @@ class ActivityLogService
|
|||
|
||||
/**
|
||||
* Sets the subject model instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Model[] $subjects
|
||||
*/
|
||||
public function subject(Model $subject): self
|
||||
public function subject(...$subjects): self
|
||||
{
|
||||
$this->getActivity()->subject()->associate($subject);
|
||||
foreach (Arr::wrap($subjects) as $subject) {
|
||||
foreach ($this->subjects as $entry) {
|
||||
// If this subject is already tracked in our array of subjects just skip over
|
||||
// it and move on to the next one in the list.
|
||||
if ($entry->is($subject)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$this->subjects[] = $subject;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -83,26 +103,18 @@ class ActivityLogService
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom properties for the activity log instance.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection|array $properties
|
||||
*/
|
||||
public function withProperties($properties): self
|
||||
{
|
||||
$this->getActivity()->properties = Collection::make($properties);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a custom property on the activty log instance.
|
||||
*
|
||||
* @param string|array $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function property(string $key, $value): self
|
||||
public function property($key, $value = null): self
|
||||
{
|
||||
$this->getActivity()->properties = $this->getActivity()->properties->put($key, $value);
|
||||
$properties = $this->getActivity()->properties;
|
||||
$this->activity->properties = is_array($key)
|
||||
? $properties->merge($key)
|
||||
: $properties->put($key, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -112,10 +124,10 @@ class ActivityLogService
|
|||
*/
|
||||
public function withRequestMetadata(): self
|
||||
{
|
||||
$this->property('ip', Request::getClientIp());
|
||||
$this->property('useragent', Request::userAgent());
|
||||
|
||||
return $this;
|
||||
return $this->property([
|
||||
'ip' => Request::getClientIp(),
|
||||
'useragent' => Request::userAgent(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -130,11 +142,7 @@ class ActivityLogService
|
|||
$activity->description = $description;
|
||||
}
|
||||
|
||||
$activity->save();
|
||||
|
||||
$this->activity = null;
|
||||
|
||||
return $activity;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -155,17 +163,12 @@ class ActivityLogService
|
|||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function transaction(\Closure $callback, string $description = null)
|
||||
public function transaction(\Closure $callback)
|
||||
{
|
||||
if (!is_null($description)) {
|
||||
$this->description($description);
|
||||
}
|
||||
|
||||
return $this->connection->transaction(function () use ($callback) {
|
||||
$response = $callback($activity = $this->getActivity());
|
||||
|
||||
$activity->save();
|
||||
$this->activity = null;
|
||||
$this->save($activity);
|
||||
|
||||
return $response;
|
||||
});
|
||||
|
@ -200,4 +203,38 @@ class ActivityLogService
|
|||
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the activity log instance and attaches all of the subject models.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function save(ActivityLog $activity = null): ActivityLog
|
||||
{
|
||||
$activity = $activity ?? $this->activity;
|
||||
|
||||
Assert::notNull($activity);
|
||||
|
||||
$response = $this->connection->transaction(function () use ($activity) {
|
||||
$activity->save();
|
||||
|
||||
$subjects = Collection::make($this->subjects)
|
||||
->map(fn (Model $subject) => [
|
||||
'activity_log_id' => $this->activity->id,
|
||||
'subject_id' => $subject->getKey(),
|
||||
'subject_type' => $subject->getMorphClass(),
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
ActivityLogSubject::insert($subjects);
|
||||
|
||||
return $activity;
|
||||
});
|
||||
|
||||
$this->activity = null;
|
||||
$this->subjects = [];
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ class CreateActivityLogsTable extends Migration
|
|||
$table->string('ip');
|
||||
$table->text('description')->nullable();
|
||||
$table->nullableNumericMorphs('actor');
|
||||
$table->nullableNumericMorphs('subject');
|
||||
$table->json('properties');
|
||||
$table->timestamp('timestamp')->useCurrent()->onUpdate(null);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateActivityLogActorsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('activity_log_subjects', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('activity_log_id')->references('id')->on('activity_logs');
|
||||
$table->numericMorphs('subject');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('activity_log_subject');
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue