Update backup logic to use activity logs, not audit logs
This commit is contained in:
		
							parent
							
								
									cbecfff6da
								
							
						
					
					
						commit
						2fc5a734f9
					
				| @ -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,8 +77,6 @@ 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') ?? '')); | ||||
| 
 | ||||
| @ -92,10 +90,10 @@ class BackupController extends ClientApiController | ||||
| 
 | ||||
|         $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->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); | ||||
|         }); | ||||
| 
 | ||||
|         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) { | ||||
|             $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.
 | ||||
|             $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]); | ||||
|             }); | ||||
|                 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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DaneEveritt
						DaneEveritt