From 4e7d73e6e21ae421a5079865af1de18bf54247b8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:24:52 +0200 Subject: [PATCH] fix(webhooks): save manual webhook secret when generated Generate now stores the new secret at once. The unsaved bar compares fields with the saved secrets, so other drafts stay marked as unsaved. Co-Authored-By: Claude Opus 5.5 --- app/Livewire/Project/Shared/Webhooks.php | 30 ++++++++++++--- .../project/shared/webhooks.blade.php | 5 ++- .../ManualWebhookSecretStrengthTest.php | 38 +++++++++---------- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/app/Livewire/Project/Shared/Webhooks.php b/app/Livewire/Project/Shared/Webhooks.php index b43ea471b0..4a21654838 100644 --- a/app/Livewire/Project/Shared/Webhooks.php +++ b/app/Livewire/Project/Shared/Webhooks.php @@ -42,6 +42,14 @@ class Webhooks extends Component public ?string $giteaManualWebhookSecret = null; + /** + * Stored secret per component property. The unsaved bar compares the fields + * with these values, because Livewire syncs unsaved field edits with every request. + * + * @var array + */ + public array $savedSecrets = []; + /** * Maps each manual webhook provider to its component property and model attribute. * @@ -63,6 +71,7 @@ class Webhooks extends Component $this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab'); $this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket'); $this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea'); + $this->syncSavedSecrets(); } $this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github'); @@ -91,6 +100,7 @@ class Webhooks extends Component 'manual_webhook_secret_bitbucket' => $this->bitbucketManualWebhookSecret, 'manual_webhook_secret_gitea' => $this->giteaManualWebhookSecret, ]); + $this->syncSavedSecrets(); $this->dispatch('success', 'Webhook secrets saved.'); } catch (ValidationException $e) { throw $e; @@ -100,10 +110,10 @@ class Webhooks extends Component } /** - * Return a random secret for the provider's field. The browser fills the field - * as an unsaved change, and the user saves it with the form. + * Generate a random secret for the provider and save it immediately. + * Other unsaved secret edits in the form are kept as they are. */ - public function generateSecret(string $provider): ?string + public function generateSecret(string $provider): void { try { $this->authorize('update', $this->resource); @@ -112,11 +122,21 @@ class Webhooks extends Component throw new \InvalidArgumentException('Unknown webhook provider.'); } - return Str::random(40); + $field = self::SECRET_FIELDS[$provider]; + $secret = Str::random(40); + $this->resource->update([$field['attribute'] => $secret]); + $this->{$field['property']} = $secret; + $this->savedSecrets[$field['property']] = $secret; + $this->dispatch('success', "New {$field['label']} generated and saved."); } catch (\Exception $e) { handleError($e, $this); + } + } - return null; + private function syncSavedSecrets(): void + { + foreach (self::SECRET_FIELDS as $field) { + $this->savedSecrets[$field['property']] = data_get($this->resource, $field['attribute']); } } diff --git a/resources/views/livewire/project/shared/webhooks.blade.php b/resources/views/livewire/project/shared/webhooks.blade.php index a004d83796..f137d658ed 100644 --- a/resources/views/livewire/project/shared/webhooks.blade.php +++ b/resources/views/livewire/project/shared/webhooks.blade.php @@ -50,7 +50,8 @@ @if ($githubManualWebhook && $gitlabManualWebhook)
- + @@ -84,7 +85,7 @@ helper="Must exactly match the secret configured in {{ $provider['name'] }}. New secrets need at least 16 characters. Leave empty to disable this webhook." autocomplete="new-password" copyable /> Generate diff --git a/tests/Feature/Webhook/ManualWebhookSecretStrengthTest.php b/tests/Feature/Webhook/ManualWebhookSecretStrengthTest.php index 508b90d558..1b65e9b58d 100644 --- a/tests/Feature/Webhook/ManualWebhookSecretStrengthTest.php +++ b/tests/Feature/Webhook/ManualWebhookSecretStrengthTest.php @@ -112,34 +112,30 @@ describe('Webhooks component', function () { expect($this->application->fresh()->manual_webhook_secret_github)->toBeEmpty(); }); - test('generate returns a random secret of at least 32 characters that saves with the form', function () { - $generated = null; - + test('generate saves a random secret of at least 32 characters for that provider only', function () { $component = manualWebhookSecretComponent() + ->set('githubManualWebhookSecret', 'unsaved-github-edit') ->call('generateSecret', 'gitlab') ->assertHasNoErrors() - ->assertReturned(function ($secret) use (&$generated) { - $generated = $secret; - - return is_string($secret) && strlen($secret) >= 32; - }) - ->assertSet('gitlabManualWebhookSecret', 'gitlab-legacy'); - - expect($this->application->fresh()->manual_webhook_secret_gitlab)->toBe('gitlab-legacy'); - - $component->set('gitlabManualWebhookSecret', $generated) - ->call('submit') - ->assertHasNoErrors() ->assertDispatched('success'); - expect($this->application->fresh()->manual_webhook_secret_gitlab)->toBe($generated); + $generated = $component->get('gitlabManualWebhookSecret'); + $application = $this->application->fresh(); + + expect($generated)->toBeString()->not->toBe('gitlab-legacy') + ->and(strlen($generated))->toBeGreaterThanOrEqual(32) + ->and($application->manual_webhook_secret_gitlab)->toBe($generated) + ->and($application->manual_webhook_secret_github)->toBe('short-legacy'); + + $component->assertSet('githubManualWebhookSecret', 'unsaved-github-edit') + ->assertSet('savedSecrets.gitlabManualWebhookSecret', $generated) + ->assertSet('savedSecrets.githubManualWebhookSecret', 'short-legacy'); }); test('generate rejects an unknown provider', function () { manualWebhookSecretComponent() ->call('generateSecret', 'deploywebhook') - ->assertDispatched('error') - ->assertReturned(null); + ->assertDispatched('error'); }); test('shows generate buttons only to users who can edit secrets', function () { @@ -158,9 +154,11 @@ describe('Webhooks component', function () { session(['currentTeam' => $this->team]); Livewire::test(Webhooks::class, ['resource' => $this->application->fresh()]) + ->assertSet('savedSecrets', []) ->call('generateSecret', 'github') - ->assertDispatched('error') - ->assertReturned(null); + ->assertDispatched('error'); + + expect($this->application->fresh()->manual_webhook_secret_github)->toBe('short-legacy'); }); });