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'); }); });