From 33e172ac24aa43ae89f1f93783f87abd77e6673d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 23 May 2026 21:06:22 +0200 Subject: [PATCH] fix(backups): revalidate S3 storage on scheduled backup submit Check the selected S3 storage against the database at submit time so stale Livewire state cannot schedule backups with storage that was reassigned or marked unusable after the component mounted. --- .../Database/CreateScheduledBackup.php | 9 ++++- .../CreateScheduledBackupValidationTest.php | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php index bb8b8b9c7..7384adcff 100644 --- a/app/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Livewire/Project/Database/CreateScheduledBackup.php @@ -2,6 +2,7 @@ namespace App\Livewire\Project\Database; +use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\ServiceDatabase; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; @@ -50,7 +51,13 @@ public function submit() $this->validate(); if ($this->saveToS3) { - if (is_null($this->s3StorageId) || ! $this->definedS3s->contains('id', $this->s3StorageId)) { + $s3StorageExists = ! is_null($this->s3StorageId) + && S3Storage::where('team_id', currentTeam()->id) + ->where('is_usable', true) + ->whereKey($this->s3StorageId) + ->exists(); + + if (! $s3StorageExists) { $this->dispatch('error', 'Please select a valid S3 storage to enable S3 backups.'); return; diff --git a/tests/Feature/CreateScheduledBackupValidationTest.php b/tests/Feature/CreateScheduledBackupValidationTest.php index a167511e2..4b5eec24c 100644 --- a/tests/Feature/CreateScheduledBackupValidationTest.php +++ b/tests/Feature/CreateScheduledBackupValidationTest.php @@ -84,6 +84,42 @@ function createS3StorageForTeam(Team $team, string $name = 'Test S3'): S3Storage expect(ScheduledDatabaseBackup::count())->toBe(0); }); +it('rejects an S3 storage that is reassigned after the component is mounted', function () { + $database = createDatabaseForScheduledBackupTest($this->team); + $s3 = createS3StorageForTeam($this->team); + + $component = Livewire::test(CreateScheduledBackup::class, ['database' => $database]) + ->set('frequency', '0 0 * * *') + ->set('saveToS3', true) + ->set('s3StorageId', $s3->id); + + $s3->update(['team_id' => Team::factory()->create()->id]); + + $component + ->call('submit') + ->assertDispatched('error'); + + expect(ScheduledDatabaseBackup::count())->toBe(0); +}); + +it('rejects an S3 storage that becomes unusable after the component is mounted', function () { + $database = createDatabaseForScheduledBackupTest($this->team); + $s3 = createS3StorageForTeam($this->team); + + $component = Livewire::test(CreateScheduledBackup::class, ['database' => $database]) + ->set('frequency', '0 0 * * *') + ->set('saveToS3', true) + ->set('s3StorageId', $s3->id); + + $s3->update(['is_usable' => false]); + + $component + ->call('submit') + ->assertDispatched('error'); + + expect(ScheduledDatabaseBackup::count())->toBe(0); +}); + it('creates a scheduled backup with a valid team-owned S3 storage', function () { $database = createDatabaseForScheduledBackupTest($this->team); $s3 = createS3StorageForTeam($this->team);