From 84edb6dbdaceaab42869d66a0b40468a74908cce Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Mon, 28 Oct 2024 20:39:09 +0200 Subject: [PATCH 1/5] use webhook urls instead of encrypted configs --- app/controllers/api/submissions_controller.rb | 12 +- .../api/submitter_form_views_controller.rb | 5 +- .../api/templates_clone_controller.rb | 5 +- app/controllers/api/templates_controller.rb | 5 +- app/controllers/start_form_controller.rb | 5 +- app/controllers/submissions_controller.rb | 18 ++- .../submit_form_decline_controller.rb | 5 +- app/controllers/templates_controller.rb | 18 ++- .../templates_uploads_controller.rb | 9 +- .../testing_api_settings_controller.rb | 5 +- .../webhook_preferences_controller.rb | 37 ------ app/controllers/webhook_secret_controller.rb | 22 ++-- .../webhook_settings_controller.rb | 19 ++- app/jobs/process_submitter_completion_job.rb | 19 +-- ...send_form_completed_webhook_request_job.rb | 32 +----- .../send_form_declined_webhook_request_job.rb | 15 +-- .../send_form_started_webhook_request_job.rb | 15 +-- .../send_form_viewed_webhook_request_job.rb | 15 +-- ...submission_archived_webhook_request_job.rb | 16 +-- ...ubmission_completed_webhook_request_job.rb | 12 +- ..._submission_created_webhook_request_job.rb | 16 +-- ...nd_template_created_webhook_request_job.rb | 16 +-- ...nd_template_updated_webhook_request_job.rb | 16 +-- app/models/account_config.rb | 1 - app/models/encrypted_config.rb | 4 +- app/models/webhook_url.rb | 24 +++- app/views/shared/_settings_nav.html.erb | 2 +- app/views/testing_api_settings/index.html.erb | 6 +- .../{index.html.erb => show.erb} | 10 +- app/views/webhook_settings/show.html.erb | 37 +++--- config/locales/i18n.yml | 25 +++- config/routes.rb | 3 +- ...241028162000_add_secret_to_webhook_urls.rb | 7 ++ .../20241029192232_populate_webhook_urls.rb | 50 ++++++++ db/schema.rb | 3 +- lib/ability.rb | 1 + lib/accounts.rb | 24 ---- lib/submitters/submit_values.rb | 5 +- spec/factories/webhook_urls.rb | 8 ++ ...form_completed_webhook_request_job_spec.rb | 108 ++++++++++++++++++ ..._form_declined_webhook_request_job_spec.rb | 108 ++++++++++++++++++ ...d_form_started_webhook_request_job_spec.rb | 108 ++++++++++++++++++ ...nd_form_viewed_webhook_request_job_spec.rb | 108 ++++++++++++++++++ ...ssion_archived_webhook_request_job_spec.rb | 106 +++++++++++++++++ ...sion_completed_webhook_request_job_spec.rb | 106 +++++++++++++++++ ...ission_created_webhook_request_job_spec.rb | 106 +++++++++++++++++ ...mplate_created_webhook_request_job_spec.rb | 104 +++++++++++++++++ ...mplate_updated_webhook_request_job_spec.rb | 104 +++++++++++++++++ spec/rails_helper.rb | 16 +++ spec/system/webhook_settings_spec.rb | 98 +++++++++++++++- 50 files changed, 1350 insertions(+), 269 deletions(-) delete mode 100644 app/controllers/webhook_preferences_controller.rb rename app/views/webhook_secret/{index.html.erb => show.erb} (50%) create mode 100644 db/migrate/20241028162000_add_secret_to_webhook_urls.rb create mode 100644 db/migrate/20241029192232_populate_webhook_urls.rb create mode 100644 spec/factories/webhook_urls.rb create mode 100644 spec/jobs/send_form_completed_webhook_request_job_spec.rb create mode 100644 spec/jobs/send_form_declined_webhook_request_job_spec.rb create mode 100644 spec/jobs/send_form_started_webhook_request_job_spec.rb create mode 100644 spec/jobs/send_form_viewed_webhook_request_job_spec.rb create mode 100644 spec/jobs/send_submission_archived_webhook_request_job_spec.rb create mode 100644 spec/jobs/send_submission_completed_webhook_request_job_spec.rb create mode 100644 spec/jobs/send_submission_created_webhook_request_job_spec.rb create mode 100644 spec/jobs/send_template_created_webhook_request_job_spec.rb create mode 100644 spec/jobs/send_template_updated_webhook_request_job_spec.rb diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index bda564c0c..c1f95d314 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -67,8 +67,11 @@ def create submissions = create_submissions(@template, params) - submissions.each do |submission| - SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id }) + @template.account.webhook_urls.with_event('submission.created').each do |webhook_url| + submissions.each do |submission| + SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id, + 'webhook_url_id' => webhook_url.id }) + end end Submissions.send_signature_requests(submissions) @@ -93,7 +96,10 @@ def destroy else @submission.update!(archived_at: Time.current) - SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id) + @submission.account.webhook_urls.with_event('submission.archived').each do |webhook_url| + SendSubmissionArchivedWebhookRequestJob.perform_async({ 'submission_id' => @submission.id, + 'webhook_url_id' => webhook_url.id }) + end end render json: @submission.as_json(only: %i[id archived_at]) diff --git a/app/controllers/api/submitter_form_views_controller.rb b/app/controllers/api/submitter_form_views_controller.rb index b582095a4..e24df6b84 100644 --- a/app/controllers/api/submitter_form_views_controller.rb +++ b/app/controllers/api/submitter_form_views_controller.rb @@ -13,7 +13,10 @@ def create SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request) - SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id }) + submitter.account.webhook_urls.with_event('form.viewed').each do |webhook_url| + SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, + 'webhook_url_id' => webhook_url.id }) + end render json: {} end diff --git a/app/controllers/api/templates_clone_controller.rb b/app/controllers/api/templates_clone_controller.rb index 52c0c30bc..922739a42 100644 --- a/app/controllers/api/templates_clone_controller.rb +++ b/app/controllers/api/templates_clone_controller.rb @@ -25,7 +25,10 @@ def create schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template) - SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id) + cloned_template.account.webhook_urls.with_event('template.created').each do |webhook_url| + SendTemplateCreatedWebhookRequestJob.perform_async({ 'template_id' => cloned_template.id, + 'webhook_url_id' => webhook_url.id }) + end render json: Templates::SerializeForApi.call(cloned_template, schema_documents) end diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index add4bcf85..c8c43e4ad 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -65,7 +65,10 @@ def update @template.update!(template_params) - SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id) + @template.account.webhook_urls.with_event('template.updated').each do |webhook_url| + SendTemplateUpdatedWebhookRequestJob.perform_async({ 'template_id' => @template.id, + 'webhook_url_id' => webhook_url.id }) + end render json: @template.as_json(only: %i[id updated_at]) end diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index c7fc9ae8f..63d0bb3bf 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -38,7 +38,10 @@ def update if @submitter.save if is_new_record - SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id }) + @submitter.account.webhook_urls.with_event('submission.created').each do |webhook_url| + SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission_id, + 'webhook_url_id' => webhook_url.id }) + end end redirect_to submit_form_path(@submitter.slug) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 6f92a874d..2c47dca7c 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -50,9 +50,7 @@ def create params: params.merge('send_completed_email' => true)) end - submissions.each do |submission| - SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id }) - end + enqueue_submission_created_webhooks(@template, submissions) Submissions.send_signature_requests(submissions) @@ -68,7 +66,10 @@ def destroy else @submission.update!(archived_at: Time.current) - SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id) + @submission.account.webhook_urls.with_event('submission.archived').each do |webhook_url| + SendSubmissionArchivedWebhookRequestJob.perform_async({ 'submission_id' => @submission.id, + 'webhook_url_id' => webhook_url.id }) + end I18n.t('submission_has_been_archived') end @@ -85,6 +86,15 @@ def save_template_message(template, params) template.save! end + def enqueue_submission_created_webhooks(template, submissions) + template.account.webhook_urls.with_event('submission.created').each do |webhook_url| + submissions.each do |submission| + SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id, + 'webhook_url_id' => webhook_url.id }) + end + end + end + def submissions_params params.permit(submission: { submitters: [%i[uuid email phone name]] }) end diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb index a04f5ca28..1c4e05633 100644 --- a/app/controllers/submit_form_decline_controller.rb +++ b/app/controllers/submit_form_decline_controller.rb @@ -25,7 +25,10 @@ def create SubmitterMailer.declined_email(submitter, user).deliver_later! end - SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id) + submitter.account.webhook_urls.with_event('form.declined').each do |webhook_url| + SendFormDeclinedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, + 'webhook_url_id' => webhook_url.id }) + end redirect_to submit_form_path(submitter.slug) end diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index d5c06ace2..ae63d3d0a 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -66,7 +66,7 @@ def create if @template.save Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template - SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id) + enqueue_template_created_webhooks(@template) maybe_redirect_to_template(@template) else @@ -77,7 +77,7 @@ def create def update @template.update!(template_params) - SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id) + enqueue_template_updated_webhooks(@template) head :ok end @@ -128,6 +128,20 @@ def maybe_redirect_to_template(template) end end + def enqueue_template_created_webhooks(template) + template.account.webhook_urls.with_event('template.updated').each do |webhook_url| + SendTemplateCreatedWebhookRequestJob.perform_async({ 'template_id' => template.id, + 'webhook_url_id' => webhook_url.id }) + end + end + + def enqueue_template_updated_webhooks(template) + template.account.webhook_urls.with_event('template.updated').each do |webhook_url| + SendTemplateUpdatedWebhookRequestJob.perform_async({ 'template_id' => template.id, + 'webhook_url_id' => webhook_url.id }) + end + end + def load_base_template return if params[:base_template_id].blank? diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index daefcc678..069b316f9 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -23,7 +23,7 @@ def create @template.update!(schema:) - SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id) + enqueue_template_created_webhooks(@template) redirect_to edit_template_path(@template) rescue Templates::CreateAttachments::PdfEncrypted @@ -65,4 +65,11 @@ def create_file_params_from_url { files: [file] } end + + def enqueue_template_created_webhooks(template) + template.account.webhook_urls.with_event('template.created').each do |webhook_url| + SendTemplateCreatedWebhookRequestJob.perform_async({ 'template_id' => template.id, + 'webhook_url_id' => webhook_url.id }) + end + end end diff --git a/app/controllers/testing_api_settings_controller.rb b/app/controllers/testing_api_settings_controller.rb index 5eade6479..6c34fed7f 100644 --- a/app/controllers/testing_api_settings_controller.rb +++ b/app/controllers/testing_api_settings_controller.rb @@ -4,9 +4,8 @@ class TestingApiSettingsController < ApplicationController def index authorize!(:manage, current_user.access_token) - @webhook_config = - current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY) + @webhook_url = current_account.webhook_urls.first_or_initialize - authorize!(:manage, @webhook_config) + authorize!(:manage, @webhook_url) end end diff --git a/app/controllers/webhook_preferences_controller.rb b/app/controllers/webhook_preferences_controller.rb deleted file mode 100644 index d584fdf9a..000000000 --- a/app/controllers/webhook_preferences_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -class WebhookPreferencesController < ApplicationController - EVENTS = %w[ - form.viewed - form.started - form.completed - form.declined - template.created - template.updated - submission.created - submission.archived - ].freeze - - before_action :load_account_config - authorize_resource :account_config, parent: false - - def create - @account_config.value[account_config_params[:event]] = account_config_params[:value] == '1' - - @account_config.save! - - head :ok - end - - private - - def load_account_config - @account_config = - current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY) - @account_config.value ||= {} - end - - def account_config_params - params.permit(:event, :value) - end -end diff --git a/app/controllers/webhook_secret_controller.rb b/app/controllers/webhook_secret_controller.rb index cd5bbefa2..27f35b260 100644 --- a/app/controllers/webhook_secret_controller.rb +++ b/app/controllers/webhook_secret_controller.rb @@ -1,29 +1,21 @@ # frozen_string_literal: true class WebhookSecretController < ApplicationController - before_action :load_encrypted_config - authorize_resource :encrypted_config, parent: false + load_and_authorize_resource :webhook_url, parent: false - def index; end + def show; end - def create - @encrypted_config.assign_attributes(value: { - encrypted_config_params[:key] => encrypted_config_params[:value] + def update + @webhook_url.update!(secret: { + webhook_secret_params[:key] => webhook_secret_params[:value] }.compact_blank) - @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete - redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved')) end private - def load_encrypted_config - @encrypted_config = - current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY) - end - - def encrypted_config_params - params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {}) + def webhook_secret_params + params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {}) end end diff --git a/app/controllers/webhook_settings_controller.rb b/app/controllers/webhook_settings_controller.rb index 475e830f6..5482b4782 100644 --- a/app/controllers/webhook_settings_controller.rb +++ b/app/controllers/webhook_settings_controller.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true class WebhookSettingsController < ApplicationController - before_action :load_encrypted_config - authorize_resource :encrypted_config, parent: false + before_action :load_webhook_url + authorize_resource :webhook_url, parent: false def show; end def create - @encrypted_config.assign_attributes(encrypted_config_params) + @webhook_url.assign_attributes(webhook_params) - @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete + @webhook_url.url.present? ? @webhook_url.save! : @webhook_url.delete redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')) end @@ -18,19 +18,18 @@ def update submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, - 'encrypted_config_id' => @encrypted_config.id }) + 'webhook_url_id' => @webhook_url.id }) redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent')) end private - def load_encrypted_config - @encrypted_config = - current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY) + def load_webhook_url + @webhook_url = current_account.webhook_urls.first_or_initialize end - def encrypted_config_params - params.require(:encrypted_config).permit(:value) + def webhook_params + params.require(:webhook_url).permit(:url, events: []) end end diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index 19c278d64..0b8c3e0b2 100644 --- a/app/jobs/process_submitter_completion_job.rb +++ b/app/jobs/process_submitter_completion_job.rb @@ -63,24 +63,7 @@ def create_completed_documents!(submitter) end def enqueue_completed_webhooks(submitter, is_all_completed: false) - webhook_config = Accounts.load_webhook_config(submitter.account) - - if webhook_config - SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, - 'encrypted_config_id' => webhook_config.id }) - end - - webhook_urls = submitter.account.webhook_urls - - webhook_urls = webhook_urls.where( - Arel::Table.new(:webhook_urls)[:events].matches('%"form.completed"%') - ).or( - webhook_urls.where( - Arel::Table.new(:webhook_urls)[:events].matches('%"submission.completed"%') - ) - ) - - webhook_urls.each do |webhook| + submitter.account.webhook_urls.with_events(%w[form.completed submission.completed]).each do |webhook| if webhook.events.include?('form.completed') SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, 'webhook_url_id' => webhook.id }) diff --git a/app/jobs/send_form_completed_webhook_request_job.rb b/app/jobs/send_form_completed_webhook_request_job.rb index 62c6e1d57..9d6c4c292 100644 --- a/app/jobs/send_form_completed_webhook_request_job.rb +++ b/app/jobs/send_form_completed_webhook_request_job.rb @@ -14,22 +14,23 @@ def perform(params = {}) attempt = params['attempt'].to_i - url, secret = load_url_and_secret(submitter, params) + webhook_url = submitter.account.webhook_urls.find_by(id: params['webhook_url_id']) - return if url.blank? + return unless webhook_url + return if webhook_url.url.blank? || webhook_url.events.exclude?('form.completed') Submissions::EnsureResultGenerated.call(submitter) ActiveStorage::Current.url_options = Docuseal.default_url_options resp = begin - Faraday.post(url, + Faraday.post(webhook_url.url, { event_type: 'form.completed', timestamp: Time.current, data: Submitters::SerializeForWebhook.call(submitter) }.to_json, - **secret.to_h, + **webhook_url.secret.to_h, 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT) rescue Faraday::Error @@ -45,27 +46,4 @@ def perform(params = {}) }) end end - - def load_url_and_secret(submitter, params) - if params['encrypted_config_id'] - config = EncryptedConfig.find(params['encrypted_config_id']) - - url = config.value - - return if url.blank? - - preferences = Accounts.load_webhook_preferences(submitter.submission.account) - - return if preferences['form.completed'] == false - - secret = EncryptedConfig.find_or_initialize_by(account_id: config.account_id, - key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h - - [url, secret] - elsif params['webhook_url_id'] - webhook_url = submitter.account.webhook_urls.find(params['webhook_url_id']) - - webhook_url.url if webhook_url.events.include?('form.completed') - end - end end diff --git a/app/jobs/send_form_declined_webhook_request_job.rb b/app/jobs/send_form_declined_webhook_request_job.rb index b4f158b6d..58ee86406 100644 --- a/app/jobs/send_form_declined_webhook_request_job.rb +++ b/app/jobs/send_form_declined_webhook_request_job.rb @@ -13,26 +13,22 @@ def perform(params = {}) submitter = Submitter.find(params['submitter_id']) attempt = params['attempt'].to_i - config = Accounts.load_webhook_config(submitter.submission.account) - url = config&.value.presence - return if url.blank? + webhook_url = submitter.submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - preferences = Accounts.load_webhook_preferences(submitter.submission.account) - - return if preferences['form.declined'] == false + return unless webhook_url + return if webhook_url.url.blank? || webhook_url.events.exclude?('form.declined') ActiveStorage::Current.url_options = Docuseal.default_url_options resp = begin - Faraday.post(url, + Faraday.post(webhook_url.url, { event_type: 'form.declined', timestamp: Time.current, data: Submitters::SerializeForWebhook.call(submitter) }.to_json, - **EncryptedConfig.find_or_initialize_by(account_id: config.account_id, - key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h, + **webhook_url.secret.to_h, 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT) rescue Faraday::Error @@ -43,6 +39,7 @@ def perform(params = {}) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, { 'submitter_id' => submitter.id, + 'webhook_url_id' => webhook_url.id, 'attempt' => attempt + 1, 'last_status' => resp&.status.to_i }) diff --git a/app/jobs/send_form_started_webhook_request_job.rb b/app/jobs/send_form_started_webhook_request_job.rb index 747e93388..5a7a6d380 100644 --- a/app/jobs/send_form_started_webhook_request_job.rb +++ b/app/jobs/send_form_started_webhook_request_job.rb @@ -13,26 +13,22 @@ def perform(params = {}) submitter = Submitter.find(params['submitter_id']) attempt = params['attempt'].to_i - config = Accounts.load_webhook_config(submitter.submission.account) - url = config&.value.presence - return if url.blank? + webhook_url = submitter.submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - preferences = Accounts.load_webhook_preferences(submitter.submission.account) - - return if preferences['form.started'] == false + return unless webhook_url + return if webhook_url.url.blank? || webhook_url.events.exclude?('form.started') ActiveStorage::Current.url_options = Docuseal.default_url_options resp = begin - Faraday.post(url, + Faraday.post(webhook_url.url, { event_type: 'form.started', timestamp: Time.current, data: Submitters::SerializeForWebhook.call(submitter) }.to_json, - **EncryptedConfig.find_or_initialize_by(account_id: config.account_id, - key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h, + **webhook_url.secret.to_h, 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT) rescue Faraday::Error @@ -43,6 +39,7 @@ def perform(params = {}) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, { 'submitter_id' => submitter.id, + 'webhook_url_id' => webhook_url.id, 'attempt' => attempt + 1, 'last_status' => resp&.status.to_i }) diff --git a/app/jobs/send_form_viewed_webhook_request_job.rb b/app/jobs/send_form_viewed_webhook_request_job.rb index 391064e31..a47c57b79 100644 --- a/app/jobs/send_form_viewed_webhook_request_job.rb +++ b/app/jobs/send_form_viewed_webhook_request_job.rb @@ -13,26 +13,22 @@ def perform(params = {}) submitter = Submitter.find(params['submitter_id']) attempt = params['attempt'].to_i - config = Accounts.load_webhook_config(submitter.submission.account) - url = config&.value.presence - return if url.blank? + webhook_url = submitter.submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - preferences = Accounts.load_webhook_preferences(submitter.submission.account) - - return if preferences['form.viewed'] == false + return unless webhook_url + return if webhook_url.url.blank? || webhook_url.events.exclude?('form.viewed') ActiveStorage::Current.url_options = Docuseal.default_url_options resp = begin - Faraday.post(url, + Faraday.post(webhook_url.url, { event_type: 'form.viewed', timestamp: Time.current, data: Submitters::SerializeForWebhook.call(submitter) }.to_json, - **EncryptedConfig.find_or_initialize_by(account_id: config.account_id, - key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h, + **webhook_url.secret.to_h, 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT) rescue Faraday::Error @@ -43,6 +39,7 @@ def perform(params = {}) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, { 'submitter_id' => submitter.id, + 'webhook_url_id' => webhook_url.id, 'attempt' => attempt + 1, 'last_status' => resp&.status.to_i }) diff --git a/app/jobs/send_submission_archived_webhook_request_job.rb b/app/jobs/send_submission_archived_webhook_request_job.rb index ff62419eb..a0141f500 100644 --- a/app/jobs/send_submission_archived_webhook_request_job.rb +++ b/app/jobs/send_submission_archived_webhook_request_job.rb @@ -14,24 +14,19 @@ def perform(params = {}) attempt = params['attempt'].to_i - config = Accounts.load_webhook_config(submission.account) - url = config&.value.presence + webhook_url = submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - return if url.blank? - - preferences = Accounts.load_webhook_preferences(submission.account) - - return if preferences['submission.archived'].blank? + return unless webhook_url + return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived') resp = begin - Faraday.post(url, + Faraday.post(webhook_url.url, { event_type: 'submission.archived', timestamp: Time.current, data: submission.as_json(only: %i[id archived_at]) }.to_json, - **EncryptedConfig.find_or_initialize_by(account_id: config.account_id, - key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h, + **webhook_url.secret.to_h, 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT) rescue Faraday::Error @@ -42,6 +37,7 @@ def perform(params = {}) (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, { 'submission_id' => submission.id, + 'webhook_url_id' => webhook_url.id, 'attempt' => attempt + 1, 'last_status' => resp&.status.to_i }) diff --git a/app/jobs/send_submission_completed_webhook_request_job.rb b/app/jobs/send_submission_completed_webhook_request_job.rb index 5a58c514a..06ab92c98 100644 --- a/app/jobs/send_submission_completed_webhook_request_job.rb +++ b/app/jobs/send_submission_completed_webhook_request_job.rb @@ -14,21 +14,19 @@ def perform(params = {}) attempt = params['attempt'].to_i - webhook_url = submission.account.webhook_urls.find(params['webhook_url_id']) + webhook_url = submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - url = webhook_url.url if webhook_url.events.include?('submission.completed') - - return if url.blank? + return unless webhook_url + return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed') resp = begin - Faraday.post(url, + Faraday.post(webhook_url.url, { event_type: 'submission.completed', timestamp: Time.current, data: Submissions::SerializeForApi.call(submission) }.to_json, - **EncryptedConfig.find_or_initialize_by(account_id: submission.account_id, - key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h, + **webhook_url.secret.to_h, 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT) rescue Faraday::Error diff --git a/app/jobs/send_submission_created_webhook_request_job.rb b/app/jobs/send_submission_created_webhook_request_job.rb index aeb3f18ff..e43ce493a 100644 --- a/app/jobs/send_submission_created_webhook_request_job.rb +++ b/app/jobs/send_submission_created_webhook_request_job.rb @@ -14,24 +14,19 @@ def perform(params = {}) attempt = params['attempt'].to_i - config = Accounts.load_webhook_config(submission.account) - url = config&.value.presence + webhook_url = submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - return if url.blank? - - preferences = Accounts.load_webhook_preferences(submission.account) - - return if preferences['submission.created'].blank? + return unless webhook_url + return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created') resp = begin - Faraday.post(url, + Faraday.post(webhook_url.url, { event_type: 'submission.created', timestamp: Time.current, data: Submissions::SerializeForApi.call(submission) }.to_json, - **EncryptedConfig.find_or_initialize_by(account_id: config.account_id, - key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h, + **webhook_url.secret.to_h, 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT) rescue Faraday::Error @@ -42,6 +37,7 @@ def perform(params = {}) (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, { 'submission_id' => submission.id, + 'webhook_url_id' => webhook_url.id, 'attempt' => attempt + 1, 'last_status' => resp&.status.to_i }) diff --git a/app/jobs/send_template_created_webhook_request_job.rb b/app/jobs/send_template_created_webhook_request_job.rb index e2d7ac714..4a6cb896a 100644 --- a/app/jobs/send_template_created_webhook_request_job.rb +++ b/app/jobs/send_template_created_webhook_request_job.rb @@ -14,24 +14,19 @@ def perform(params = {}) attempt = params['attempt'].to_i - config = Accounts.load_webhook_config(template.account) - url = config&.value.presence + webhook_url = template.account.webhook_urls.find_by(id: params['webhook_url_id']) - return if url.blank? - - preferences = Accounts.load_webhook_preferences(template.account) - - return if preferences['template.created'].blank? + return unless webhook_url + return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created') resp = begin - Faraday.post(url, + Faraday.post(webhook_url.url, { event_type: 'template.created', timestamp: Time.current, data: Templates::SerializeForApi.call(template) }.to_json, - **EncryptedConfig.find_or_initialize_by(account_id: config.account_id, - key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h, + **webhook_url.secret.to_h, 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT) rescue Faraday::Error @@ -42,6 +37,7 @@ def perform(params = {}) (!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan)) SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, { 'template_id' => template.id, + 'webhook_url_id' => webhook_url.id, 'attempt' => attempt + 1, 'last_status' => resp&.status.to_i }) diff --git a/app/jobs/send_template_updated_webhook_request_job.rb b/app/jobs/send_template_updated_webhook_request_job.rb index 8d969eb5b..e36f53b39 100644 --- a/app/jobs/send_template_updated_webhook_request_job.rb +++ b/app/jobs/send_template_updated_webhook_request_job.rb @@ -14,24 +14,19 @@ def perform(params = {}) attempt = params['attempt'].to_i - config = Accounts.load_webhook_config(template.account) - url = config&.value.presence + webhook_url = template.account.webhook_urls.find_by(id: params['webhook_url_id']) - return if url.blank? - - preferences = Accounts.load_webhook_preferences(template.account) - - return if preferences['template.updated'].blank? + return unless webhook_url + return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated') resp = begin - Faraday.post(url, + Faraday.post(webhook_url.url, { event_type: 'template.updated', timestamp: Time.current, data: Templates::SerializeForApi.call(template) }.to_json, - **EncryptedConfig.find_or_initialize_by(account_id: config.account_id, - key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h, + **webhook_url.secret.to_h, 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT) rescue Faraday::Error @@ -42,6 +37,7 @@ def perform(params = {}) (!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan)) SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, { 'template_id' => template.id, + 'webhook_url_id' => webhook_url.id, 'attempt' => attempt + 1, 'last_status' => resp&.status.to_i }) diff --git a/app/models/account_config.rb b/app/models/account_config.rb index 1e7e2d617..112ca4666 100644 --- a/app/models/account_config.rb +++ b/app/models/account_config.rb @@ -35,7 +35,6 @@ class AccountConfig < ApplicationRecord FORM_WITH_CONFETTI_KEY = 'form_with_confetti' FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature' ESIGNING_PREFERENCE_KEY = 'esigning_preference' - WEBHOOK_PREFERENCES_KEY = 'webhook_preferences' DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth' FORCE_SSO_AUTH_KEY = 'force_sso_auth' FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf' diff --git a/app/models/encrypted_config.rb b/app/models/encrypted_config.rb index 5d2422af9..e61923b4f 100644 --- a/app/models/encrypted_config.rb +++ b/app/models/encrypted_config.rb @@ -26,9 +26,7 @@ class EncryptedConfig < ApplicationRecord EMAIL_SMTP_KEY = 'action_mailer_smtp', ESIGN_CERTS_KEY = 'esign_certs', TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url', - APP_URL_KEY = 'app_url', - WEBHOOK_URL_KEY = 'webhook_url', - WEBHOOK_SECRET_KEY = 'webhook_secret' + APP_URL_KEY = 'app_url' ].freeze belongs_to :account diff --git a/app/models/webhook_url.rb b/app/models/webhook_url.rb index 74d70bb47..18794c824 100644 --- a/app/models/webhook_url.rb +++ b/app/models/webhook_url.rb @@ -6,6 +6,7 @@ # # id :bigint not null, primary key # events :text not null +# secret :text # sha1 :string not null # url :text not null # created_at :datetime not null @@ -22,15 +23,34 @@ # fk_rails_... (account_id => accounts.id) # class WebhookUrl < ApplicationRecord + EVENTS = %w[ + form.viewed + form.started + form.completed + form.declined + template.created + template.updated + submission.created + submission.archived + ].freeze + belongs_to :account - attribute :events, :string, default: -> { [] } + attribute :events, :string, default: -> { %w[form.viewed form.started form.completed form.declined] } serialize :events, coder: JSON + serialize :secret, coder: JSON + + scope :with_event, ->(event) { with_events([event]) } + scope :with_events, lambda { |events| + where(events.map do |event| + Arel::Table.new(:webhook_urls)[:events].matches("%\"#{event}\"%") + end.reduce(:or)) + } before_validation :set_sha1 - encrypts :url + encrypts :url, :secret def set_sha1 self.sha1 = Digest::SHA1.hexdigest(url) diff --git a/app/views/shared/_settings_nav.html.erb b/app/views/shared/_settings_nav.html.erb index f70791c3d..15e58efb4 100644 --- a/app/views/shared/_settings_nav.html.erb +++ b/app/views/shared/_settings_nav.html.erb @@ -56,7 +56,7 @@ <%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %> <% end %> - <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::WEBHOOK_URL_KEY, account: current_account)) %> + <% if can?(:read, WebhookUrl) %>
  • <%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
  • diff --git a/app/views/testing_api_settings/index.html.erb b/app/views/testing_api_settings/index.html.erb index c85f2613f..b02cad316 100644 --- a/app/views/testing_api_settings/index.html.erb +++ b/app/views/testing_api_settings/index.html.erb @@ -8,10 +8,10 @@ <%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %> - <%= form_for @webhook_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> - <%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %> + <%= form_for @webhook_url, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> + <%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
    - <%= f.url_field :value, class: 'base-input w-full', placeholder: 'https://example.com/hook' %> + <%= f.url_field :url, class: 'base-input w-full', placeholder: 'https://example.com/hook' %> <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full' %>
    <% end %> diff --git a/app/views/webhook_secret/index.html.erb b/app/views/webhook_secret/show.erb similarity index 50% rename from app/views/webhook_secret/index.html.erb rename to app/views/webhook_secret/show.erb index 5c7ad9edb..88b996c5e 100644 --- a/app/views/webhook_secret/index.html.erb +++ b/app/views/webhook_secret/show.erb @@ -1,13 +1,13 @@ -<%= render 'shared/turbo_modal', title: 'Webhook Secret' do %> - <%= form_for @encrypted_config, url: webhook_secret_index_path, method: :post, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %> +<%= render 'shared/turbo_modal', title: t('webhook_secret') do %> + <%= form_for @webhook_url, url: webhook_secret_path, method: :patch, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
    - <%= f.fields_for :value, Struct.new(:key, :value).new(*@encrypted_config.value.to_a.first) do |ff| %> + <%= f.fields_for :secret, Struct.new(:key, :value).new(*@webhook_url.secret.to_a.first) do |ff| %>
    - <%= ff.label :key, class: 'label' %> + <%= ff.label :key, t('key'), class: 'label' %> <%= ff.text_field :key, class: 'base-input', placeholder: 'X-Example-Header' %>
    - <%= ff.label :value, class: 'label' %> + <%= ff.label :value, t('value'), class: 'label' %> <%= ff.text_field :value, class: 'base-input' %>
    <% end %> diff --git a/app/views/webhook_settings/show.html.erb b/app/views/webhook_settings/show.html.erb index 746d3958e..eee894931 100644 --- a/app/views/webhook_settings/show.html.erb +++ b/app/views/webhook_settings/show.html.erb @@ -7,34 +7,31 @@
    - <%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %> - <%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %> + <%= form_for @webhook_url, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %> + <%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
    - <%= f.url_field :value, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %> + <%= f.url_field :url, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %> <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full md:w-32' %> - - <%= t('add_secret') %> - + <% if @webhook_url.persisted? %> + + <%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %> + + <% end %>
    - <% end %> - <% preference = current_account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)&.value || {} %> - <% WebhookPreferencesController::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %> -
    - <% events.each do |event| %> - <%= form_for '', url: webhook_preferences_path, method: :post do |f| %> - <%= f.hidden_field :event, value: event %> - <% uuid = SecureRandom.uuid %> + <% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %> +
    + <%= f.collection_check_boxes(:events, events, :to_s, :to_s, include_hidden: false) do |b| %>
    -
    <% end %> - <% end %> -
    +
    + <% end %> <% end %>
    @@ -47,7 +44,7 @@ <%= t('submission_example_payload') %> - <% if @encrypted_config.value.present? %> + <% if @webhook_url.url.present? && @webhook_url.events.include?('form.completed') %> <%= button_to button_title(title: 'Test Webhook', disabled_with: t('sending'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %> <% end %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 0ac26e8ab..e92205909 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -474,6 +474,7 @@ en: &en logo: Logo back: Back add_secret: Add Secret + edit_secret: Edit Secret submission_example_payload: Submission example payload there_are_no_signatures: There are no signatures signed_with_trusted_certificate: Signed with trusted certificate @@ -623,7 +624,9 @@ en: &en archived_users: Archived Users embedding_users: Embedding Users view_embedding_users: View Embedding Users - view_users: View Users + key: Key + value: Value + webhook_secret: Webhook Secret submission_event_names: send_email_to_html: 'Email sent to %{submitter_name}' send_reminder_email_to_html: 'Reminder email sent to %{submitter_name}' @@ -1116,6 +1119,7 @@ es: &es logo: Logotipo back: Atrás add_secret: Agregar secreto + edit_secret: Editar secreto submission_example_payload: Ejemplo de payload de envío there_are_no_signatures: No hay firmas signed_with_trusted_certificate: Firmado con certificado de confianza @@ -1265,6 +1269,9 @@ es: &es archived_users: Usuarios Archivados embedding_users: Usuarios Integrados view_embedding_users: Ver Usuarios Integrado + key: Clave + value: Valor + webhook_secret: Secreto del Webhook submission_event_names: send_email_to_html: 'Correo electrónico enviado a %{submitter_name}' send_reminder_email_to_html: 'Correo de recordatorio enviado a %{submitter_name}' @@ -1757,6 +1764,7 @@ it: &it logo: Logo back: Indietro add_secret: Aggiungi segreto + edit_secret: Modifica segreto submission_example_payload: Esempio di payload di invio there_are_no_signatures: Non ci sono firme signed_with_trusted_certificate: Firmato con certificato affidabile @@ -1906,6 +1914,9 @@ it: &it archived_users: Utenti Archiviati embedding_users: Utenti Incorporati view_embedding_users: Visualizza Utenti Incorporati + key: Chiave + value: Valore + webhook_secret: Segreto del Webhook submission_event_names: send_email_to_html: 'E-mail inviato a %{submitter_name}' send_reminder_email_to_html: 'E-mail di promemoria inviato a %{submitter_name}' @@ -2399,6 +2410,7 @@ fr: &fr logo: Logo back: Retour add_secret: Ajouter un secret + edit_secret: Modifier le Secret submission_example_payload: Exemple de payload de soumission there_are_no_signatures: "Il n'y a pas de signatures" signed_with_trusted_certificate: Signé avec un certificat de confiance @@ -2548,6 +2560,9 @@ fr: &fr archived_users: Utilisateurs Archivés embedding_users: Utilisateurs Intégrés view_embedding_users: Voir les Utilisateurs Intégrés + key: Clé + value: Valeur + webhook_secret: Secret du Webhook submission_event_names: send_email_to_html: 'E-mail envoyé à %{submitter_name}' send_reminder_email_to_html: 'E-mail de rappel envoyé à %{submitter_name}' @@ -3040,6 +3055,7 @@ pt: &pt logo: Logotipo back: Voltar add_secret: Adicionar segredo + edit_secret: Editar Segredo submission_example_payload: Exemplo de payload de submissão there_are_no_signatures: Não há assinaturas signed_with_trusted_certificate: Assinado com certificado confiável @@ -3189,6 +3205,9 @@ pt: &pt archived_users: Usuários Arquivados embedding_users: Usuários Incorporados view_embedding_users: Ver Usuários Incorporados + key: Chave + value: Valor + webhook_secret: Segredo do Webhook submission_event_names: send_email_to_html: 'E-mail enviado para %{submitter_name}' send_reminder_email_to_html: 'E-mail de lembrete enviado para %{submitter_name}' @@ -3681,6 +3700,7 @@ de: &de logo: Logo back: Zurück add_secret: Geheimnis hinzufügen + edit_secret: Geheimnis bearbeiten submission_example_payload: Beispiel-Payload für Einreichung there_are_no_signatures: Es gibt keine Unterschriften signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat @@ -3830,6 +3850,9 @@ de: &de archived_users: Archivierte Benutzer embedding_users: Einbettende Benutzer view_embedding_users: Einbettende Benutzer anzeigen + key: Schlüssel + value: Wert + webhook_secret: Webhook-Geheimnis submission_event_names: send_email_to_html: 'E-Mail gesendet an %{submitter_name}' send_reminder_email_to_html: 'Erinnerungs-E-Mail gesendet an %{submitter_name}' diff --git a/config/routes.rb b/config/routes.rb index 420859fcb..c2b6e7622 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,8 +80,7 @@ resources :testing_api_settings, only: %i[index] resources :submitters_autocomplete, only: %i[index] resources :template_folders_autocomplete, only: %i[index] - resources :webhook_preferences, only: %i[create] - resources :webhook_secret, only: %i[index create] + resources :webhook_secret, only: %i[show update] resource :templates_upload, only: %i[create] authenticated do resource :templates_upload, only: %i[show], path: 'new' diff --git a/db/migrate/20241028162000_add_secret_to_webhook_urls.rb b/db/migrate/20241028162000_add_secret_to_webhook_urls.rb new file mode 100644 index 000000000..0befc6467 --- /dev/null +++ b/db/migrate/20241028162000_add_secret_to_webhook_urls.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddSecretToWebhookUrls < ActiveRecord::Migration[7.2] + def change + add_column :webhook_urls, :secret, :text + end +end diff --git a/db/migrate/20241029192232_populate_webhook_urls.rb b/db/migrate/20241029192232_populate_webhook_urls.rb new file mode 100644 index 000000000..36c393db9 --- /dev/null +++ b/db/migrate/20241029192232_populate_webhook_urls.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class PopulateWebhookUrls < ActiveRecord::Migration[7.2] + disable_ddl_transaction + + class MigrationWebhookUrl < ApplicationRecord + self.table_name = 'webhook_urls' + + serialize :events, coder: JSON + serialize :secret, coder: JSON + encrypts :url, :secret + + before_validation -> { self.sha1 = Digest::SHA1.hexdigest(url) } + end + + class MigrationEncryptedConfig < ApplicationRecord + self.table_name = 'encrypted_configs' + + encrypts :value + serialize :value, coder: JSON + end + + class MigrationAccountConfig < ApplicationRecord + self.table_name = 'account_configs' + + serialize :value, coder: JSON + end + + def up + MigrationEncryptedConfig.joins('INNER JOIN accounts a ON a.id = encrypted_configs.account_id') + .where(key: 'webhook_url') + .find_each do |config| + webhook_url = MigrationWebhookUrl.find_or_initialize_by(account_id: config.account_id, url: config.value) + webhook_url.secret = MigrationEncryptedConfig.find_by(account_id: config.account_id, key: 'webhook_secret')&.value + + preferences = MigrationAccountConfig.find_by(account_id: config.account_id, + key: 'webhook_preferences')&.value.to_h + events = %w[form.viewed form.started form.completed form.declined].reject { |event| preferences[event] == false } + events += preferences.compact_blank.keys + + webhook_url.events = events.uniq + + webhook_url.save! + end + end + + def down + nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 3fd0ab117..6521993de 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_10_26_161207) do +ActiveRecord::Schema[7.2].define(version: 2024_10_29_192232) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -367,6 +367,7 @@ t.string "sha1", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.text "secret" t.index ["account_id"], name: "index_webhook_urls_on_account_id" t.index ["sha1"], name: "index_webhook_urls_on_sha1" end diff --git a/lib/ability.rb b/lib/ability.rb index b48e866a5..960a8beba 100644 --- a/lib/ability.rb +++ b/lib/ability.rb @@ -22,5 +22,6 @@ def initialize(user) can :manage, UserConfig, user_id: user.id can :manage, Account, id: user.account_id can :manage, AccessToken, user_id: user.id + can :manage, WebhookUrl, account_id: user.account_id end end diff --git a/lib/accounts.rb b/lib/accounts.rb index 644032b1e..67427cf6d 100644 --- a/lib/accounts.rb +++ b/lib/accounts.rb @@ -78,30 +78,6 @@ def create_default_template(account) new_template end - def load_webhook_url(account) - load_webhook_config(account)&.value.presence - end - - def load_webhook_config(account) - configs = account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY) - - if !configs && !Docuseal.multitenant? && !account.testing? - configs = Account.order(:id).first.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY) - end - - configs - end - - def load_webhook_preferences(account) - configs = account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY) - - unless Docuseal.multitenant? - configs ||= Account.order(:id).first.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY) - end - - configs&.value.presence || {} - end - def load_signing_pkcs(account) cert_data = if Docuseal.multitenant? diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 2a320bc8e..be32fb911 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -14,7 +14,10 @@ def call(submitter, params, request) unless submitter.submission_events.exists?(event_type: 'start_form') SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request) - SendFormStartedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id }) + submitter.account.webhook_urls.with_event('form.started').each do |webhook_url| + SendFormStartedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, + 'webhook_url_id' => webhook_url.id }) + end end update_submitter!(submitter, params, request) diff --git a/spec/factories/webhook_urls.rb b/spec/factories/webhook_urls.rb new file mode 100644 index 000000000..45a44a7a3 --- /dev/null +++ b/spec/factories/webhook_urls.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :webhook_url do + account + url { Faker::Internet.url } + end +end diff --git a/spec/jobs/send_form_completed_webhook_request_job_spec.rb b/spec/jobs/send_form_completed_webhook_request_job_spec.rb new file mode 100644 index 000000000..f9fd7cad6 --- /dev/null +++ b/spec/jobs/send_form_completed_webhook_request_job_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SendFormCompletedWebhookRequestJob do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:submission) { create(:submission, template:, created_by_user: user) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current) + end + let(:webhook_url) { create(:webhook_url, account:, events: ['form.completed']) } + + before do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + before do + stub_request(:post, webhook_url.url).to_return(status: 200) + end + + it 'sends a webhook request' do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'form.completed', + 'timestamp' => Time.current, + 'data' => Submitters::SerializeForWebhook.call(submitter.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook' + } + ).once + end + + it 'sends a webhook request with the secret' do + webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' }) + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'form.completed', + 'timestamp' => Time.current, + 'data' => Submitters::SerializeForWebhook.call(submitter.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook', + 'X-Secret-Header' => 'secret_value' + } + ).once + end + + it "doesn't send a webhook request if the submitter doesn't exist" do + expect do + described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) + end.to raise_error ActiveRecord::RecordNotFound + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the event is not in the webhook's events" do + webhook_url.update!(events: ['form.declined']) + + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the webhook doesn't exist" do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it 'sends again if the response status is 400 or higher' do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + end.to change(described_class.jobs, :size).by(1) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + + args = described_class.jobs.last['args'].first + + expect(args['attempt']).to eq(1) + expect(args['last_status']).to eq(401) + expect(args['webhook_url_id']).to eq(webhook_url.id) + expect(args['submitter_id']).to eq(submitter.id) + end + + it "doesn't send again if the max attempts is reached" do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11) + end.not_to change(described_class.jobs, :size) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + end + end +end diff --git a/spec/jobs/send_form_declined_webhook_request_job_spec.rb b/spec/jobs/send_form_declined_webhook_request_job_spec.rb new file mode 100644 index 000000000..e6900fcad --- /dev/null +++ b/spec/jobs/send_form_declined_webhook_request_job_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SendFormDeclinedWebhookRequestJob do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:submission) { create(:submission, template:, created_by_user: user) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current) + end + let(:webhook_url) { create(:webhook_url, account:, events: ['form.declined']) } + + before do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + before do + stub_request(:post, webhook_url.url).to_return(status: 200) + end + + it 'sends a webhook request' do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'form.declined', + 'timestamp' => Time.current, + 'data' => Submitters::SerializeForWebhook.call(submitter.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook' + } + ).once + end + + it 'sends a webhook request with the secret' do + webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' }) + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'form.declined', + 'timestamp' => Time.current, + 'data' => Submitters::SerializeForWebhook.call(submitter.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook', + 'X-Secret-Header' => 'secret_value' + } + ).once + end + + it "doesn't send a webhook request if the submitter doesn't exist" do + expect do + described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) + end.to raise_error ActiveRecord::RecordNotFound + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the event is not in the webhook's events" do + webhook_url.update!(events: ['form.completed']) + + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the webhook doesn't exist" do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it 'sends again if the response status is 400 or higher' do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + end.to change(described_class.jobs, :size).by(1) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + + args = described_class.jobs.last['args'].first + + expect(args['attempt']).to eq(1) + expect(args['last_status']).to eq(401) + expect(args['webhook_url_id']).to eq(webhook_url.id) + expect(args['submitter_id']).to eq(submitter.id) + end + + it "doesn't send again if the max attempts is reached" do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11) + end.not_to change(described_class.jobs, :size) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + end + end +end diff --git a/spec/jobs/send_form_started_webhook_request_job_spec.rb b/spec/jobs/send_form_started_webhook_request_job_spec.rb new file mode 100644 index 000000000..258e72743 --- /dev/null +++ b/spec/jobs/send_form_started_webhook_request_job_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SendFormStartedWebhookRequestJob do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:submission) { create(:submission, template:, created_by_user: user) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current) + end + let(:webhook_url) { create(:webhook_url, account:, events: ['form.started']) } + + before do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + before do + stub_request(:post, webhook_url.url).to_return(status: 200) + end + + it 'sends a webhook request' do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'form.started', + 'timestamp' => Time.current, + 'data' => Submitters::SerializeForWebhook.call(submitter.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook' + } + ).once + end + + it 'sends a webhook request with the secret' do + webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' }) + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'form.started', + 'timestamp' => Time.current, + 'data' => Submitters::SerializeForWebhook.call(submitter.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook', + 'X-Secret-Header' => 'secret_value' + } + ).once + end + + it "doesn't send a webhook request if the submitter doesn't exist" do + expect do + described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) + end.to raise_error ActiveRecord::RecordNotFound + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the event is not in the webhook's events" do + webhook_url.update!(events: ['form.declined']) + + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the webhook doesn't exist" do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it 'sends again if the response status is 400 or higher' do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + end.to change(described_class.jobs, :size).by(1) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + + args = described_class.jobs.last['args'].first + + expect(args['attempt']).to eq(1) + expect(args['last_status']).to eq(401) + expect(args['webhook_url_id']).to eq(webhook_url.id) + expect(args['submitter_id']).to eq(submitter.id) + end + + it "doesn't send again if the max attempts is reached" do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11) + end.not_to change(described_class.jobs, :size) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + end + end +end diff --git a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb new file mode 100644 index 000000000..cb23dcaa4 --- /dev/null +++ b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SendFormViewedWebhookRequestJob do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:submission) { create(:submission, template:, created_by_user: user) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current) + end + let(:webhook_url) { create(:webhook_url, account:, events: ['form.viewed']) } + + before do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + before do + stub_request(:post, webhook_url.url).to_return(status: 200) + end + + it 'sends a webhook request' do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'form.viewed', + 'timestamp' => Time.current, + 'data' => Submitters::SerializeForWebhook.call(submitter.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook' + } + ).once + end + + it 'sends a webhook request with the secret' do + webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' }) + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'form.viewed', + 'timestamp' => Time.current, + 'data' => Submitters::SerializeForWebhook.call(submitter.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook', + 'X-Secret-Header' => 'secret_value' + } + ).once + end + + it "doesn't send a webhook request if the submitter doesn't exist" do + expect do + described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) + end.to raise_error ActiveRecord::RecordNotFound + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the event is not in the webhook's events" do + webhook_url.update!(events: ['form.started']) + + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the webhook doesn't exist" do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it 'sends again if the response status is 400 or higher' do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id) + end.to change(described_class.jobs, :size).by(1) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + + args = described_class.jobs.last['args'].first + + expect(args['attempt']).to eq(1) + expect(args['last_status']).to eq(401) + expect(args['webhook_url_id']).to eq(webhook_url.id) + expect(args['submitter_id']).to eq(submitter.id) + end + + it "doesn't send again if the max attempts is reached" do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11) + end.not_to change(described_class.jobs, :size) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + end + end +end diff --git a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb new file mode 100644 index 000000000..3753ece8b --- /dev/null +++ b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SendSubmissionArchivedWebhookRequestJob do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:submission) { create(:submission, template:, created_by_user: user) } + let(:webhook_url) { create(:webhook_url, account:, events: ['submission.archived']) } + + before do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + before do + stub_request(:post, webhook_url.url).to_return(status: 200) + end + + it 'sends a webhook request' do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'submission.archived', + 'timestamp' => Time.current, + 'data' => submission.reload.as_json(only: %i[id archived_at]) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook' + } + ).once + end + + it 'sends a webhook request with the secret' do + webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' }) + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'submission.archived', + 'timestamp' => Time.current, + 'data' => submission.reload.as_json(only: %i[id archived_at]) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook', + 'X-Secret-Header' => 'secret_value' + } + ).once + end + + it "doesn't send a webhook request if the submission doesn't exist" do + expect do + described_class.new.perform('submission_id' => 100_500, 'webhook_url_id' => webhook_url.id) + end.to raise_error ActiveRecord::RecordNotFound + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the event is not in the webhook's events" do + webhook_url.update!(events: ['submission.created']) + + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the webhook doesn't exist" do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => 100_500) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it 'sends again if the response status is 400 or higher' do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + end.to change(described_class.jobs, :size).by(1) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + + args = described_class.jobs.last['args'].first + + expect(args['attempt']).to eq(1) + expect(args['last_status']).to eq(401) + expect(args['webhook_url_id']).to eq(webhook_url.id) + expect(args['submission_id']).to eq(submission.id) + end + + it "doesn't send again if the max attempts is reached" do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id, + 'attempt' => 11) + end.not_to change(described_class.jobs, :size) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + end + end +end diff --git a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb new file mode 100644 index 000000000..999ffbb58 --- /dev/null +++ b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SendSubmissionCompletedWebhookRequestJob do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:submission) { create(:submission, :with_submitters, template:, created_by_user: user) } + let(:webhook_url) { create(:webhook_url, account:, events: ['submission.completed']) } + + before do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + before do + stub_request(:post, webhook_url.url).to_return(status: 200) + end + + it 'sends a webhook request' do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'submission.completed', + 'timestamp' => Time.current, + 'data' => Submissions::SerializeForApi.call(submission.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook' + } + ).once + end + + it 'sends a webhook request with the secret' do + webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' }) + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'submission.completed', + 'timestamp' => Time.current, + 'data' => Submissions::SerializeForApi.call(submission.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook', + 'X-Secret-Header' => 'secret_value' + } + ).once + end + + it "doesn't send a webhook request if the submission doesn't exist" do + expect do + described_class.new.perform('submission_id' => 100_500, 'webhook_url_id' => webhook_url.id) + end.to raise_error ActiveRecord::RecordNotFound + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the event is not in the webhook's events" do + webhook_url.update!(events: ['submission.archived']) + + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the webhook doesn't exist" do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => 100_500) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it 'sends again if the response status is 400 or higher' do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + end.to change(described_class.jobs, :size).by(1) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + + args = described_class.jobs.last['args'].first + + expect(args['attempt']).to eq(1) + expect(args['last_status']).to eq(401) + expect(args['webhook_url_id']).to eq(webhook_url.id) + expect(args['submission_id']).to eq(submission.id) + end + + it "doesn't send again if the max attempts is reached" do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id, + 'attempt' => 11) + end.not_to change(described_class.jobs, :size) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + end + end +end diff --git a/spec/jobs/send_submission_created_webhook_request_job_spec.rb b/spec/jobs/send_submission_created_webhook_request_job_spec.rb new file mode 100644 index 000000000..a3f804c1f --- /dev/null +++ b/spec/jobs/send_submission_created_webhook_request_job_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SendSubmissionCreatedWebhookRequestJob do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:submission) { create(:submission, :with_submitters, template:, created_by_user: user) } + let(:webhook_url) { create(:webhook_url, account:, events: ['submission.created']) } + + before do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + before do + stub_request(:post, webhook_url.url).to_return(status: 200) + end + + it 'sends a webhook request' do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'submission.created', + 'timestamp' => Time.current, + 'data' => Submissions::SerializeForApi.call(submission.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook' + } + ).once + end + + it 'sends a webhook request with the secret' do + webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' }) + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'submission.created', + 'timestamp' => Time.current, + 'data' => Submissions::SerializeForApi.call(submission.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook', + 'X-Secret-Header' => 'secret_value' + } + ).once + end + + it "doesn't send a webhook request if the submission doesn't exist" do + expect do + described_class.new.perform('submission_id' => 100_500, 'webhook_url_id' => webhook_url.id) + end.to raise_error ActiveRecord::RecordNotFound + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the event is not in the webhook's events" do + webhook_url.update!(events: ['submission.completed']) + + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the webhook doesn't exist" do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => 100_500) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it 'sends again if the response status is 400 or higher' do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id) + end.to change(described_class.jobs, :size).by(1) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + + args = described_class.jobs.last['args'].first + + expect(args['attempt']).to eq(1) + expect(args['last_status']).to eq(401) + expect(args['webhook_url_id']).to eq(webhook_url.id) + expect(args['submission_id']).to eq(submission.id) + end + + it "doesn't send again if the max attempts is reached" do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id, + 'attempt' => 11) + end.not_to change(described_class.jobs, :size) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + end + end +end diff --git a/spec/jobs/send_template_created_webhook_request_job_spec.rb b/spec/jobs/send_template_created_webhook_request_job_spec.rb new file mode 100644 index 000000000..83a36c480 --- /dev/null +++ b/spec/jobs/send_template_created_webhook_request_job_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SendTemplateCreatedWebhookRequestJob do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:webhook_url) { create(:webhook_url, account:, events: ['template.created']) } + + before do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + before do + stub_request(:post, webhook_url.url).to_return(status: 200) + end + + it 'sends a webhook request' do + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'template.created', + 'timestamp' => Time.current, + 'data' => Templates::SerializeForApi.call(template.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook' + } + ).once + end + + it 'sends a webhook request with the secret' do + webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' }) + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'template.created', + 'timestamp' => Time.current, + 'data' => Templates::SerializeForApi.call(template.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook', + 'X-Secret-Header' => 'secret_value' + } + ).once + end + + it "doesn't send a webhook request if the template doesn't exist" do + expect do + described_class.new.perform('template_id' => 100_500, 'webhook_url_id' => webhook_url.id) + end.to raise_error ActiveRecord::RecordNotFound + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the event is not in the webhook's events" do + webhook_url.update!(events: ['template.updated']) + + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the webhook doesn't exist" do + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => 100_500) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it 'sends again if the response status is 400 or higher' do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) + end.to change(described_class.jobs, :size).by(1) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + + args = described_class.jobs.last['args'].first + + expect(args['attempt']).to eq(1) + expect(args['last_status']).to eq(401) + expect(args['webhook_url_id']).to eq(webhook_url.id) + expect(args['template_id']).to eq(template.id) + end + + it "doesn't send again if the max attempts is reached" do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11) + end.not_to change(described_class.jobs, :size) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + end + end +end diff --git a/spec/jobs/send_template_updated_webhook_request_job_spec.rb b/spec/jobs/send_template_updated_webhook_request_job_spec.rb new file mode 100644 index 000000000..bf463eca6 --- /dev/null +++ b/spec/jobs/send_template_updated_webhook_request_job_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SendTemplateUpdatedWebhookRequestJob do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:webhook_url) { create(:webhook_url, account:, events: ['template.updated']) } + + before do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + before do + stub_request(:post, webhook_url.url).to_return(status: 200) + end + + it 'sends a webhook request' do + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'template.updated', + 'timestamp' => Time.current, + 'data' => Templates::SerializeForApi.call(template.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook' + } + ).once + end + + it 'sends a webhook request with the secret' do + webhook_url.update(secret: { 'X-Secret-Header' => 'secret_value' }) + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).to have_requested(:post, webhook_url.url).with( + body: replace_timestamps({ + 'event_type' => 'template.updated', + 'timestamp' => Time.current, + 'data' => Templates::SerializeForApi.call(template.reload) + }.deep_stringify_keys), + headers: { + 'Content-Type' => 'application/json', + 'User-Agent' => 'DocuSeal.co Webhook', + 'X-Secret-Header' => 'secret_value' + } + ).once + end + + it "doesn't send a webhook request if the template doesn't exist" do + expect do + described_class.new.perform('template_id' => 100_500, 'webhook_url_id' => webhook_url.id) + end.to raise_error ActiveRecord::RecordNotFound + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the event is not in the webhook's events" do + webhook_url.update!(events: ['template.created']) + + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it "doesn't send a webhook request if the webhook doesn't exist" do + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => 100_500) + + expect(WebMock).not_to have_requested(:post, webhook_url.url) + end + + it 'sends again if the response status is 400 or higher' do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id) + end.to change(described_class.jobs, :size).by(1) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + + args = described_class.jobs.last['args'].first + + expect(args['attempt']).to eq(1) + expect(args['last_status']).to eq(401) + expect(args['webhook_url_id']).to eq(webhook_url.id) + expect(args['template_id']).to eq(template.id) + end + + it "doesn't send again if the max attempts is reached" do + stub_request(:post, webhook_url.url).to_return(status: 401) + + expect do + described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id, 'attempt' => 11) + end.not_to change(described_class.jobs, :size) + + expect(WebMock).to have_requested(:post, webhook_url.url).once + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 86a15a9f8..94f071be6 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -67,3 +67,19 @@ Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline end end + +def replace_timestamps(data, replace = /.*/) + timestamp_fields = %w[created_at updated_at completed_at sent_at opened_at timestamp] + + data.each do |key, value| + if timestamp_fields.include?(key) && (value.is_a?(String) || value.is_a?(Time)) + data[key] = replace + elsif value.is_a?(Hash) + replace_timestamps(value) + elsif value.is_a?(Array) + value.each { |item| replace_timestamps(item) if item.is_a?(Hash) } + end + end + + data +end diff --git a/spec/system/webhook_settings_spec.rb b/spec/system/webhook_settings_spec.rb index 1e94eb62c..a6c1e2140 100644 --- a/spec/system/webhook_settings_spec.rb +++ b/spec/system/webhook_settings_spec.rb @@ -8,24 +8,110 @@ before do sign_in(user) - visit settings_webhooks_path end it 'shows webhook settings page' do + visit settings_webhooks_path + expect(page).to have_content('Webhooks') expect(page).to have_field('Webhook URL') expect(page).to have_button('Save') + + WebhookUrl::EVENTS.each do |event| + expect(page).to have_field(event, type: 'checkbox', disabled: true) + end + end + + it 'creates the webhook' do + visit settings_webhooks_path + + fill_in 'Webhook URL', with: 'https://example.com/webhook' + + expect do + click_button 'Save' + end.to change(WebhookUrl, :count).by(1) + + webhook_url = account.webhook_urls.first + + expect(webhook_url.url).to eq('https://example.com/webhook') + end + + it 'updates the webhook' do + webhook_url = create(:webhook_url, account:, url: 'https://example.com/webhook') + + visit settings_webhooks_path + + fill_in 'Webhook URL', with: 'https://example.org/webhook' + click_button 'Save' + + webhook_url.reload + + expect(webhook_url.url).to eq('https://example.org/webhook') end - it 'updates the webhook URL' do - fill_in 'Webhook URL', with: 'https://example.com' + it 'deletes the webhook' do + create(:webhook_url, account:) + + visit settings_webhooks_path + + fill_in 'Webhook URL', with: '' expect do click_button 'Save' - end.to change(EncryptedConfig, :count).by(1) + end.to change(WebhookUrl, :count).by(-1) + end + + it 'updates the webhook events' do + webhook_url = create(:webhook_url, account:) + + visit settings_webhooks_path + + expect(webhook_url.events).not_to include('submission.created') + + check('submission.created') + + webhook_url.reload + + expect(webhook_url.events).to include('submission.created') + end + + it 'adds a secret to the webhook' do + webhook_url = create(:webhook_url, account:) + + visit settings_webhooks_path + + expect(webhook_url.secret).to be_nil + + click_link 'Add Secret' + + within '#modal' do + fill_in 'Key', with: 'X-Signature' + fill_in 'Value', with: 'secret-value' + + click_button 'Submit' + + webhook_url.reload + + expect(webhook_url.secret).to eq({ 'X-Signature' => 'secret-value' }) + end + end + + it 'removes a secret from the webhook' do + webhook_url = create(:webhook_url, account:, secret: { 'X-Signature' => 'secret-value' }) + + visit settings_webhooks_path + + click_link 'Edit Secret' + + within '#modal' do + fill_in 'Key', with: '' + fill_in 'Value', with: '' + + click_button 'Submit' - encrypted_config = EncryptedConfig.find_by(account:, key: EncryptedConfig::WEBHOOK_URL_KEY) + webhook_url.reload - expect(encrypted_config.value).to eq('https://example.com') + expect(webhook_url.secret).to eq({}) + end end end From 2542f26d96cffda95ca0f46c4d5fac321891048f Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 1 Nov 2024 15:35:02 +0200 Subject: [PATCH 2/5] adjust webhooks --- app/controllers/api/submissions_controller.rb | 14 ++++----- .../api/submitter_form_views_controller.rb | 6 ++-- app/controllers/api/submitters_controller.rb | 2 +- .../api/templates_clone_controller.rb | 6 ++-- app/controllers/api/templates_controller.rb | 6 ++-- app/controllers/start_form_controller.rb | 6 ++-- app/controllers/submissions_controller.rb | 12 ++++---- .../submit_form_decline_controller.rb | 6 ++-- app/controllers/templates_controller.rb | 12 ++++---- .../templates_uploads_controller.rb | 6 ++-- .../webhook_preferences_controller.rb | 22 ++++++++++++++ .../webhook_settings_controller.rb | 6 ++-- app/jobs/process_submitter_completion_job.rb | 10 +++---- ...send_form_completed_webhook_request_job.rb | 4 +-- .../send_form_declined_webhook_request_job.rb | 4 +-- .../send_form_started_webhook_request_job.rb | 4 +-- .../send_form_viewed_webhook_request_job.rb | 4 +-- ...submission_archived_webhook_request_job.rb | 4 +-- ...ubmission_completed_webhook_request_job.rb | 4 +-- ..._submission_created_webhook_request_job.rb | 4 +-- ...nd_template_created_webhook_request_job.rb | 4 +-- ...nd_template_updated_webhook_request_job.rb | 4 +-- app/models/webhook_url.rb | 10 ++----- app/views/webhook_settings/show.html.erb | 24 ++++++++------- config/routes.rb | 1 + ...241028162000_add_secret_to_webhook_urls.rb | 14 +++++++++ .../20241029192232_populate_webhook_urls.rb | 29 ++++++++++--------- db/schema.rb | 2 +- lib/submitters/submit_values.rb | 8 ++--- lib/webhook_urls.rb | 27 +++++++++++++++++ ...form_completed_webhook_request_job_spec.rb | 14 --------- ..._form_declined_webhook_request_job_spec.rb | 14 --------- ...d_form_started_webhook_request_job_spec.rb | 14 --------- ...nd_form_viewed_webhook_request_job_spec.rb | 14 --------- ...ssion_archived_webhook_request_job_spec.rb | 14 --------- ...sion_completed_webhook_request_job_spec.rb | 14 --------- ...ission_created_webhook_request_job_spec.rb | 14 --------- ...mplate_created_webhook_request_job_spec.rb | 14 --------- ...mplate_updated_webhook_request_job_spec.rb | 14 --------- spec/system/webhook_settings_spec.rb | 2 +- 40 files changed, 157 insertions(+), 236 deletions(-) create mode 100644 app/controllers/webhook_preferences_controller.rb create mode 100644 lib/webhook_urls.rb diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index c1f95d314..6337a1236 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -67,10 +67,10 @@ def create submissions = create_submissions(@template, params) - @template.account.webhook_urls.with_event('submission.created').each do |webhook_url| + WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url| submissions.each do |submission| - SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id, - 'webhook_url_id' => webhook_url.id }) + SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id, + 'webhook_url_id' => webhook_url.id) end end @@ -78,7 +78,7 @@ def create submissions.each do |submission| submission.submitters.each do |submitter| - ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submitter.id }) if submitter.completed_at? + ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at? end end @@ -96,9 +96,9 @@ def destroy else @submission.update!(archived_at: Time.current) - @submission.account.webhook_urls.with_event('submission.archived').each do |webhook_url| - SendSubmissionArchivedWebhookRequestJob.perform_async({ 'submission_id' => @submission.id, - 'webhook_url_id' => webhook_url.id }) + WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url| + SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id, + 'webhook_url_id' => webhook_url.id) end end diff --git a/app/controllers/api/submitter_form_views_controller.rb b/app/controllers/api/submitter_form_views_controller.rb index e24df6b84..98d7f5b52 100644 --- a/app/controllers/api/submitter_form_views_controller.rb +++ b/app/controllers/api/submitter_form_views_controller.rb @@ -13,9 +13,9 @@ def create SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request) - submitter.account.webhook_urls.with_event('form.viewed').each do |webhook_url| - SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, - 'webhook_url_id' => webhook_url.id }) + WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url| + SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id, + 'webhook_url_id' => webhook_url.id) end render json: {} diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index 210b783d5..f85e2f60a 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -68,7 +68,7 @@ def update end if @submitter.completed_at? - ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id }) + ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id) elsif normalized_params[:send_email] || normalized_params[:send_sms] Submitters.send_signature_requests([@submitter]) end diff --git a/app/controllers/api/templates_clone_controller.rb b/app/controllers/api/templates_clone_controller.rb index 922739a42..f869fa28a 100644 --- a/app/controllers/api/templates_clone_controller.rb +++ b/app/controllers/api/templates_clone_controller.rb @@ -25,9 +25,9 @@ def create schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template) - cloned_template.account.webhook_urls.with_event('template.created').each do |webhook_url| - SendTemplateCreatedWebhookRequestJob.perform_async({ 'template_id' => cloned_template.id, - 'webhook_url_id' => webhook_url.id }) + WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url| + SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id, + 'webhook_url_id' => webhook_url.id) end render json: Templates::SerializeForApi.call(cloned_template, schema_documents) diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index c8c43e4ad..80af5acf9 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -65,9 +65,9 @@ def update @template.update!(template_params) - @template.account.webhook_urls.with_event('template.updated').each do |webhook_url| - SendTemplateUpdatedWebhookRequestJob.perform_async({ 'template_id' => @template.id, - 'webhook_url_id' => webhook_url.id }) + WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url| + SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id, + 'webhook_url_id' => webhook_url.id) end render json: @template.as_json(only: %i[id updated_at]) diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index 63d0bb3bf..209c81b49 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -38,9 +38,9 @@ def update if @submitter.save if is_new_record - @submitter.account.webhook_urls.with_event('submission.created').each do |webhook_url| - SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission_id, - 'webhook_url_id' => webhook_url.id }) + WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url| + SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id, + 'webhook_url_id' => webhook_url.id) end end diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 2c47dca7c..a7a7f216a 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -66,9 +66,9 @@ def destroy else @submission.update!(archived_at: Time.current) - @submission.account.webhook_urls.with_event('submission.archived').each do |webhook_url| - SendSubmissionArchivedWebhookRequestJob.perform_async({ 'submission_id' => @submission.id, - 'webhook_url_id' => webhook_url.id }) + WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url| + SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id, + 'webhook_url_id' => webhook_url.id) end I18n.t('submission_has_been_archived') @@ -87,10 +87,10 @@ def save_template_message(template, params) end def enqueue_submission_created_webhooks(template, submissions) - template.account.webhook_urls.with_event('submission.created').each do |webhook_url| + WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url| submissions.each do |submission| - SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id, - 'webhook_url_id' => webhook_url.id }) + SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id, + 'webhook_url_id' => webhook_url.id) end end end diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb index 1c4e05633..94099b0df 100644 --- a/app/controllers/submit_form_decline_controller.rb +++ b/app/controllers/submit_form_decline_controller.rb @@ -25,9 +25,9 @@ def create SubmitterMailer.declined_email(submitter, user).deliver_later! end - submitter.account.webhook_urls.with_event('form.declined').each do |webhook_url| - SendFormDeclinedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, - 'webhook_url_id' => webhook_url.id }) + WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url| + SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id, + 'webhook_url_id' => webhook_url.id) end redirect_to submit_form_path(submitter.slug) diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index ae63d3d0a..5a578685b 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -129,16 +129,16 @@ def maybe_redirect_to_template(template) end def enqueue_template_created_webhooks(template) - template.account.webhook_urls.with_event('template.updated').each do |webhook_url| - SendTemplateCreatedWebhookRequestJob.perform_async({ 'template_id' => template.id, - 'webhook_url_id' => webhook_url.id }) + WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url| + SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id, + 'webhook_url_id' => webhook_url.id) end end def enqueue_template_updated_webhooks(template) - template.account.webhook_urls.with_event('template.updated').each do |webhook_url| - SendTemplateUpdatedWebhookRequestJob.perform_async({ 'template_id' => template.id, - 'webhook_url_id' => webhook_url.id }) + WebhookUrls.for_account_id(template.account_id, 'template.updated').each do |webhook_url| + SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id, + 'webhook_url_id' => webhook_url.id) end end diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index 069b316f9..75b4e620a 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -67,9 +67,9 @@ def create_file_params_from_url end def enqueue_template_created_webhooks(template) - template.account.webhook_urls.with_event('template.created').each do |webhook_url| - SendTemplateCreatedWebhookRequestJob.perform_async({ 'template_id' => template.id, - 'webhook_url_id' => webhook_url.id }) + WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url| + SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id, + 'webhook_url_id' => webhook_url.id) end end end diff --git a/app/controllers/webhook_preferences_controller.rb b/app/controllers/webhook_preferences_controller.rb new file mode 100644 index 000000000..1070f7fe4 --- /dev/null +++ b/app/controllers/webhook_preferences_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class WebhookPreferencesController < ApplicationController + load_and_authorize_resource :webhook_url, parent: false + + def update + webhook_preferences_params[:events].each do |event, val| + @webhook_url.events.delete(event) if val == '0' + @webhook_url.events.push(event) if val == '1' && @webhook_url.events.exclude?(event) + end + + @webhook_url.save! + + head :ok + end + + private + + def webhook_preferences_params + params.require(:webhook_url).permit(events: {}) + end +end diff --git a/app/controllers/webhook_settings_controller.rb b/app/controllers/webhook_settings_controller.rb index 5482b4782..2c6362af3 100644 --- a/app/controllers/webhook_settings_controller.rb +++ b/app/controllers/webhook_settings_controller.rb @@ -17,8 +17,8 @@ def create def update submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last - SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, - 'webhook_url_id' => @webhook_url.id }) + SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id, + 'webhook_url_id' => @webhook_url.id) redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent')) end @@ -30,6 +30,6 @@ def load_webhook_url end def webhook_params - params.require(:webhook_url).permit(:url, events: []) + params.require(:webhook_url).permit(:url) end end diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index 0b8c3e0b2..c23c7f827 100644 --- a/app/jobs/process_submitter_completion_job.rb +++ b/app/jobs/process_submitter_completion_job.rb @@ -63,15 +63,15 @@ def create_completed_documents!(submitter) end def enqueue_completed_webhooks(submitter, is_all_completed: false) - submitter.account.webhook_urls.with_events(%w[form.completed submission.completed]).each do |webhook| + WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook| if webhook.events.include?('form.completed') - SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, - 'webhook_url_id' => webhook.id }) + SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id, + 'webhook_url_id' => webhook.id) end if webhook.events.include?('submission.completed') && is_all_completed - SendSubmissionCompletedWebhookRequestJob.perform_async({ 'submission_id' => submitter.submission_id, - 'webhook_url_id' => webhook.id }) + SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id, + 'webhook_url_id' => webhook.id) end end end diff --git a/app/jobs/send_form_completed_webhook_request_job.rb b/app/jobs/send_form_completed_webhook_request_job.rb index 9d6c4c292..7e935e8d9 100644 --- a/app/jobs/send_form_completed_webhook_request_job.rb +++ b/app/jobs/send_form_completed_webhook_request_job.rb @@ -11,12 +11,10 @@ class SendFormCompletedWebhookRequestJob def perform(params = {}) submitter = Submitter.find(params['submitter_id']) + webhook_url = WebhookUrl.find(params['webhook_url_id']) attempt = params['attempt'].to_i - webhook_url = submitter.account.webhook_urls.find_by(id: params['webhook_url_id']) - - return unless webhook_url return if webhook_url.url.blank? || webhook_url.events.exclude?('form.completed') Submissions::EnsureResultGenerated.call(submitter) diff --git a/app/jobs/send_form_declined_webhook_request_job.rb b/app/jobs/send_form_declined_webhook_request_job.rb index 58ee86406..2bf7b7f74 100644 --- a/app/jobs/send_form_declined_webhook_request_job.rb +++ b/app/jobs/send_form_declined_webhook_request_job.rb @@ -11,12 +11,10 @@ class SendFormDeclinedWebhookRequestJob def perform(params = {}) submitter = Submitter.find(params['submitter_id']) + webhook_url = WebhookUrl.find(params['webhook_url_id']) attempt = params['attempt'].to_i - webhook_url = submitter.submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - - return unless webhook_url return if webhook_url.url.blank? || webhook_url.events.exclude?('form.declined') ActiveStorage::Current.url_options = Docuseal.default_url_options diff --git a/app/jobs/send_form_started_webhook_request_job.rb b/app/jobs/send_form_started_webhook_request_job.rb index 5a7a6d380..72dc40f7a 100644 --- a/app/jobs/send_form_started_webhook_request_job.rb +++ b/app/jobs/send_form_started_webhook_request_job.rb @@ -11,12 +11,10 @@ class SendFormStartedWebhookRequestJob def perform(params = {}) submitter = Submitter.find(params['submitter_id']) + webhook_url = WebhookUrl.find(params['webhook_url_id']) attempt = params['attempt'].to_i - webhook_url = submitter.submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - - return unless webhook_url return if webhook_url.url.blank? || webhook_url.events.exclude?('form.started') ActiveStorage::Current.url_options = Docuseal.default_url_options diff --git a/app/jobs/send_form_viewed_webhook_request_job.rb b/app/jobs/send_form_viewed_webhook_request_job.rb index a47c57b79..f71cf45f1 100644 --- a/app/jobs/send_form_viewed_webhook_request_job.rb +++ b/app/jobs/send_form_viewed_webhook_request_job.rb @@ -11,12 +11,10 @@ class SendFormViewedWebhookRequestJob def perform(params = {}) submitter = Submitter.find(params['submitter_id']) + webhook_url = WebhookUrl.find(params['webhook_url_id']) attempt = params['attempt'].to_i - webhook_url = submitter.submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - - return unless webhook_url return if webhook_url.url.blank? || webhook_url.events.exclude?('form.viewed') ActiveStorage::Current.url_options = Docuseal.default_url_options diff --git a/app/jobs/send_submission_archived_webhook_request_job.rb b/app/jobs/send_submission_archived_webhook_request_job.rb index a0141f500..bc6f4897d 100644 --- a/app/jobs/send_submission_archived_webhook_request_job.rb +++ b/app/jobs/send_submission_archived_webhook_request_job.rb @@ -11,12 +11,10 @@ class SendSubmissionArchivedWebhookRequestJob def perform(params = {}) submission = Submission.find(params['submission_id']) + webhook_url = WebhookUrl.find(params['webhook_url_id']) attempt = params['attempt'].to_i - webhook_url = submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - - return unless webhook_url return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived') resp = begin diff --git a/app/jobs/send_submission_completed_webhook_request_job.rb b/app/jobs/send_submission_completed_webhook_request_job.rb index 06ab92c98..0477a1b4d 100644 --- a/app/jobs/send_submission_completed_webhook_request_job.rb +++ b/app/jobs/send_submission_completed_webhook_request_job.rb @@ -11,12 +11,10 @@ class SendSubmissionCompletedWebhookRequestJob def perform(params = {}) submission = Submission.find(params['submission_id']) + webhook_url = WebhookUrl.find(params['webhook_url_id']) attempt = params['attempt'].to_i - webhook_url = submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - - return unless webhook_url return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed') resp = begin diff --git a/app/jobs/send_submission_created_webhook_request_job.rb b/app/jobs/send_submission_created_webhook_request_job.rb index e43ce493a..31f83f23e 100644 --- a/app/jobs/send_submission_created_webhook_request_job.rb +++ b/app/jobs/send_submission_created_webhook_request_job.rb @@ -11,12 +11,10 @@ class SendSubmissionCreatedWebhookRequestJob def perform(params = {}) submission = Submission.find(params['submission_id']) + webhook_url = WebhookUrl.find(params['webhook_url_id']) attempt = params['attempt'].to_i - webhook_url = submission.account.webhook_urls.find_by(id: params['webhook_url_id']) - - return unless webhook_url return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created') resp = begin diff --git a/app/jobs/send_template_created_webhook_request_job.rb b/app/jobs/send_template_created_webhook_request_job.rb index 4a6cb896a..49b344bce 100644 --- a/app/jobs/send_template_created_webhook_request_job.rb +++ b/app/jobs/send_template_created_webhook_request_job.rb @@ -11,12 +11,10 @@ class SendTemplateCreatedWebhookRequestJob def perform(params = {}) template = Template.find(params['template_id']) + webhook_url = WebhookUrl.find(params['webhook_url_id']) attempt = params['attempt'].to_i - webhook_url = template.account.webhook_urls.find_by(id: params['webhook_url_id']) - - return unless webhook_url return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created') resp = begin diff --git a/app/jobs/send_template_updated_webhook_request_job.rb b/app/jobs/send_template_updated_webhook_request_job.rb index e36f53b39..0766a14d0 100644 --- a/app/jobs/send_template_updated_webhook_request_job.rb +++ b/app/jobs/send_template_updated_webhook_request_job.rb @@ -11,12 +11,10 @@ class SendTemplateUpdatedWebhookRequestJob def perform(params = {}) template = Template.find(params['template_id']) + webhook_url = WebhookUrl.find(params['webhook_url_id']) attempt = params['attempt'].to_i - webhook_url = template.account.webhook_urls.find_by(id: params['webhook_url_id']) - - return unless webhook_url return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated') resp = begin diff --git a/app/models/webhook_url.rb b/app/models/webhook_url.rb index 18794c824..011bf5a1c 100644 --- a/app/models/webhook_url.rb +++ b/app/models/webhook_url.rb @@ -6,7 +6,7 @@ # # id :bigint not null, primary key # events :text not null -# secret :text +# secret :text not null # sha1 :string not null # url :text not null # created_at :datetime not null @@ -37,17 +37,11 @@ class WebhookUrl < ApplicationRecord belongs_to :account attribute :events, :string, default: -> { %w[form.viewed form.started form.completed form.declined] } + attribute :secret, :string, default: -> { {} } serialize :events, coder: JSON serialize :secret, coder: JSON - scope :with_event, ->(event) { with_events([event]) } - scope :with_events, lambda { |events| - where(events.map do |event| - Arel::Table.new(:webhook_urls)[:events].matches("%\"#{event}\"%") - end.reduce(:or)) - } - before_validation :set_sha1 encrypts :url, :secret diff --git a/app/views/webhook_settings/show.html.erb b/app/views/webhook_settings/show.html.erb index eee894931..6c305d88d 100644 --- a/app/views/webhook_settings/show.html.erb +++ b/app/views/webhook_settings/show.html.erb @@ -12,23 +12,27 @@
    <%= f.url_field :url, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %> <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full md:w-32' %> - <% if @webhook_url.persisted? %> + <% if @webhook_url.persisted? %> <%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %> <% end %>
    + <% end %> + <%= form_for @webhook_url, url: @webhook_url.url.present? ? webhook_preference_path(@webhook_url) : '', method: :put, html: { autocomplete: 'off' } do |f| %> <% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
    - <%= f.collection_check_boxes(:events, events, :to_s, :to_s, include_hidden: false) do |b| %> -
    - -
    + <% events.each do |event| %> + <%= f.fields_for :events do |ff| %> +
    + +
    + <% end %> <% end %>
    <% end %> diff --git a/config/routes.rb b/config/routes.rb index c2b6e7622..a2d5af4b0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -81,6 +81,7 @@ resources :submitters_autocomplete, only: %i[index] resources :template_folders_autocomplete, only: %i[index] resources :webhook_secret, only: %i[show update] + resources :webhook_preferences, only: %i[update] resource :templates_upload, only: %i[create] authenticated do resource :templates_upload, only: %i[show], path: 'new' diff --git a/db/migrate/20241028162000_add_secret_to_webhook_urls.rb b/db/migrate/20241028162000_add_secret_to_webhook_urls.rb index 0befc6467..5da2b6bda 100644 --- a/db/migrate/20241028162000_add_secret_to_webhook_urls.rb +++ b/db/migrate/20241028162000_add_secret_to_webhook_urls.rb @@ -1,7 +1,21 @@ # frozen_string_literal: true class AddSecretToWebhookUrls < ActiveRecord::Migration[7.2] + class MigrationWebhookUrl < ApplicationRecord + self.table_name = 'webhook_urls' + + serialize :secret, coder: JSON + + encrypts :url, :secret + end + def change add_column :webhook_urls, :secret, :text + + MigrationWebhookUrl.all.each do |url| + url.update_columns(secret: {}) + end + + change_column_null :webhook_urls, :secret, false end end diff --git a/db/migrate/20241029192232_populate_webhook_urls.rb b/db/migrate/20241029192232_populate_webhook_urls.rb index 36c393db9..cb8b55460 100644 --- a/db/migrate/20241029192232_populate_webhook_urls.rb +++ b/db/migrate/20241029192232_populate_webhook_urls.rb @@ -3,42 +3,45 @@ class PopulateWebhookUrls < ActiveRecord::Migration[7.2] disable_ddl_transaction - class MigrationWebhookUrl < ApplicationRecord + class MigrationWebhookUrl < ActiveRecord::Base self.table_name = 'webhook_urls' serialize :events, coder: JSON serialize :secret, coder: JSON - encrypts :url, :secret - before_validation -> { self.sha1 = Digest::SHA1.hexdigest(url) } + encrypts :url, :secret end - class MigrationEncryptedConfig < ApplicationRecord + class MigrationEncryptedConfig < ActiveRecord::Base self.table_name = 'encrypted_configs' encrypts :value serialize :value, coder: JSON end - class MigrationAccountConfig < ApplicationRecord + class MigrationAccountConfig < ActiveRecord::Base self.table_name = 'account_configs' serialize :value, coder: JSON end def up - MigrationEncryptedConfig.joins('INNER JOIN accounts a ON a.id = encrypted_configs.account_id') - .where(key: 'webhook_url') - .find_each do |config| - webhook_url = MigrationWebhookUrl.find_or_initialize_by(account_id: config.account_id, url: config.value) - webhook_url.secret = MigrationEncryptedConfig.find_by(account_id: config.account_id, key: 'webhook_secret')&.value - - preferences = MigrationAccountConfig.find_by(account_id: config.account_id, - key: 'webhook_preferences')&.value.to_h + MigrationEncryptedConfig.where(key: 'webhook_url').find_each do |config| + webhook_url = MigrationWebhookUrl.find_or_initialize_by(account_id: config.account_id, + sha1: Digest::SHA1.hexdigest(config.value)) + + webhook_url.secret = + MigrationEncryptedConfig.find_by(account_id: config.account_id, key: 'webhook_secret')&.value.to_h + + preferences = + MigrationAccountConfig.find_by(account_id: config.account_id, key: 'webhook_preferences')&.value.to_h + events = %w[form.viewed form.started form.completed form.declined].reject { |event| preferences[event] == false } + events += preferences.compact_blank.keys webhook_url.events = events.uniq + webhook_url.url = config.value webhook_url.save! end diff --git a/db/schema.rb b/db/schema.rb index 6521993de..92e6a9829 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -367,7 +367,7 @@ t.string "sha1", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.text "secret" + t.text "secret", null: false t.index ["account_id"], name: "index_webhook_urls_on_account_id" t.index ["sha1"], name: "index_webhook_urls_on_sha1" end diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index be32fb911..40e96618c 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -14,9 +14,9 @@ def call(submitter, params, request) unless submitter.submission_events.exists?(event_type: 'start_form') SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request) - submitter.account.webhook_urls.with_event('form.started').each do |webhook_url| - SendFormStartedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, - 'webhook_url_id' => webhook_url.id }) + WebhookUrls.for_account_id(submitter.account_id, 'form.started').each do |webhook_url| + SendFormStartedWebhookRequestJob.perform_async('submitter_id' => submitter.id, + 'webhook_url_id' => webhook_url.id) end end @@ -24,7 +24,7 @@ def call(submitter, params, request) submitter.submission.save! - ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submitter.id }) if submitter.completed_at? + ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at? submitter end diff --git a/lib/webhook_urls.rb b/lib/webhook_urls.rb new file mode 100644 index 000000000..482e66a70 --- /dev/null +++ b/lib/webhook_urls.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module WebhookUrls + module_function + + def for_account_id(account_id, events) + events = Array.wrap(events) + + rel = WebhookUrl.where(account_id:) + + event_arel = events.map { |event| Arel::Table.new(:webhook_urls)[:events].matches("%\"#{event}\"%") }.reduce(:or) + + if Docuseal.multitenant? + rel.where(event_arel) + else + linked_account_rel = + AccountLinkedAccount.where(linked_account_id: account_id).where.not(account_type: :testing).select(:account_id) + + webhook_urls = rel.or(WebhookUrl.where(account_id: linked_account_rel).where(event_arel)) + + account_urls, linked_urls = webhook_urls.partition { |w| w.account_id == account_id } + + account_urls.select { |w| w.events.intersect?(events) }.presence || + (account_urls.present? ? WebhookUrl.none : linked_urls) + end + end +end diff --git a/spec/jobs/send_form_completed_webhook_request_job_spec.rb b/spec/jobs/send_form_completed_webhook_request_job_spec.rb index f9fd7cad6..e4046cd92 100644 --- a/spec/jobs/send_form_completed_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_completed_webhook_request_job_spec.rb @@ -56,14 +56,6 @@ ).once end - it "doesn't send a webhook request if the submitter doesn't exist" do - expect do - described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) - end.to raise_error ActiveRecord::RecordNotFound - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it "doesn't send a webhook request if the event is not in the webhook's events" do webhook_url.update!(events: ['form.declined']) @@ -72,12 +64,6 @@ expect(WebMock).not_to have_requested(:post, webhook_url.url) end - it "doesn't send a webhook request if the webhook doesn't exist" do - described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it 'sends again if the response status is 400 or higher' do stub_request(:post, webhook_url.url).to_return(status: 401) diff --git a/spec/jobs/send_form_declined_webhook_request_job_spec.rb b/spec/jobs/send_form_declined_webhook_request_job_spec.rb index e6900fcad..be2872b2d 100644 --- a/spec/jobs/send_form_declined_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_declined_webhook_request_job_spec.rb @@ -56,14 +56,6 @@ ).once end - it "doesn't send a webhook request if the submitter doesn't exist" do - expect do - described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) - end.to raise_error ActiveRecord::RecordNotFound - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it "doesn't send a webhook request if the event is not in the webhook's events" do webhook_url.update!(events: ['form.completed']) @@ -72,12 +64,6 @@ expect(WebMock).not_to have_requested(:post, webhook_url.url) end - it "doesn't send a webhook request if the webhook doesn't exist" do - described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it 'sends again if the response status is 400 or higher' do stub_request(:post, webhook_url.url).to_return(status: 401) diff --git a/spec/jobs/send_form_started_webhook_request_job_spec.rb b/spec/jobs/send_form_started_webhook_request_job_spec.rb index 258e72743..3bff34eb1 100644 --- a/spec/jobs/send_form_started_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_started_webhook_request_job_spec.rb @@ -56,14 +56,6 @@ ).once end - it "doesn't send a webhook request if the submitter doesn't exist" do - expect do - described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) - end.to raise_error ActiveRecord::RecordNotFound - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it "doesn't send a webhook request if the event is not in the webhook's events" do webhook_url.update!(events: ['form.declined']) @@ -72,12 +64,6 @@ expect(WebMock).not_to have_requested(:post, webhook_url.url) end - it "doesn't send a webhook request if the webhook doesn't exist" do - described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it 'sends again if the response status is 400 or higher' do stub_request(:post, webhook_url.url).to_return(status: 401) diff --git a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb index cb23dcaa4..66e1accb1 100644 --- a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb @@ -56,14 +56,6 @@ ).once end - it "doesn't send a webhook request if the submitter doesn't exist" do - expect do - described_class.new.perform('submitter_id' => 100_500, 'webhook_url_id' => webhook_url.id) - end.to raise_error ActiveRecord::RecordNotFound - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it "doesn't send a webhook request if the event is not in the webhook's events" do webhook_url.update!(events: ['form.started']) @@ -72,12 +64,6 @@ expect(WebMock).not_to have_requested(:post, webhook_url.url) end - it "doesn't send a webhook request if the webhook doesn't exist" do - described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => 100_500) - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it 'sends again if the response status is 400 or higher' do stub_request(:post, webhook_url.url).to_return(status: 401) diff --git a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb index 3753ece8b..879702143 100644 --- a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb @@ -53,14 +53,6 @@ ).once end - it "doesn't send a webhook request if the submission doesn't exist" do - expect do - described_class.new.perform('submission_id' => 100_500, 'webhook_url_id' => webhook_url.id) - end.to raise_error ActiveRecord::RecordNotFound - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it "doesn't send a webhook request if the event is not in the webhook's events" do webhook_url.update!(events: ['submission.created']) @@ -69,12 +61,6 @@ expect(WebMock).not_to have_requested(:post, webhook_url.url) end - it "doesn't send a webhook request if the webhook doesn't exist" do - described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => 100_500) - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it 'sends again if the response status is 400 or higher' do stub_request(:post, webhook_url.url).to_return(status: 401) diff --git a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb index 999ffbb58..f4ba884c3 100644 --- a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb @@ -53,14 +53,6 @@ ).once end - it "doesn't send a webhook request if the submission doesn't exist" do - expect do - described_class.new.perform('submission_id' => 100_500, 'webhook_url_id' => webhook_url.id) - end.to raise_error ActiveRecord::RecordNotFound - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it "doesn't send a webhook request if the event is not in the webhook's events" do webhook_url.update!(events: ['submission.archived']) @@ -69,12 +61,6 @@ expect(WebMock).not_to have_requested(:post, webhook_url.url) end - it "doesn't send a webhook request if the webhook doesn't exist" do - described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => 100_500) - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it 'sends again if the response status is 400 or higher' do stub_request(:post, webhook_url.url).to_return(status: 401) diff --git a/spec/jobs/send_submission_created_webhook_request_job_spec.rb b/spec/jobs/send_submission_created_webhook_request_job_spec.rb index a3f804c1f..ba635493f 100644 --- a/spec/jobs/send_submission_created_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_created_webhook_request_job_spec.rb @@ -53,14 +53,6 @@ ).once end - it "doesn't send a webhook request if the submission doesn't exist" do - expect do - described_class.new.perform('submission_id' => 100_500, 'webhook_url_id' => webhook_url.id) - end.to raise_error ActiveRecord::RecordNotFound - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it "doesn't send a webhook request if the event is not in the webhook's events" do webhook_url.update!(events: ['submission.completed']) @@ -69,12 +61,6 @@ expect(WebMock).not_to have_requested(:post, webhook_url.url) end - it "doesn't send a webhook request if the webhook doesn't exist" do - described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => 100_500) - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it 'sends again if the response status is 400 or higher' do stub_request(:post, webhook_url.url).to_return(status: 401) diff --git a/spec/jobs/send_template_created_webhook_request_job_spec.rb b/spec/jobs/send_template_created_webhook_request_job_spec.rb index 83a36c480..f6a09ba6b 100644 --- a/spec/jobs/send_template_created_webhook_request_job_spec.rb +++ b/spec/jobs/send_template_created_webhook_request_job_spec.rb @@ -52,14 +52,6 @@ ).once end - it "doesn't send a webhook request if the template doesn't exist" do - expect do - described_class.new.perform('template_id' => 100_500, 'webhook_url_id' => webhook_url.id) - end.to raise_error ActiveRecord::RecordNotFound - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it "doesn't send a webhook request if the event is not in the webhook's events" do webhook_url.update!(events: ['template.updated']) @@ -68,12 +60,6 @@ expect(WebMock).not_to have_requested(:post, webhook_url.url) end - it "doesn't send a webhook request if the webhook doesn't exist" do - described_class.new.perform('template_id' => template.id, 'webhook_url_id' => 100_500) - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it 'sends again if the response status is 400 or higher' do stub_request(:post, webhook_url.url).to_return(status: 401) diff --git a/spec/jobs/send_template_updated_webhook_request_job_spec.rb b/spec/jobs/send_template_updated_webhook_request_job_spec.rb index bf463eca6..58e46b639 100644 --- a/spec/jobs/send_template_updated_webhook_request_job_spec.rb +++ b/spec/jobs/send_template_updated_webhook_request_job_spec.rb @@ -52,14 +52,6 @@ ).once end - it "doesn't send a webhook request if the template doesn't exist" do - expect do - described_class.new.perform('template_id' => 100_500, 'webhook_url_id' => webhook_url.id) - end.to raise_error ActiveRecord::RecordNotFound - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it "doesn't send a webhook request if the event is not in the webhook's events" do webhook_url.update!(events: ['template.created']) @@ -68,12 +60,6 @@ expect(WebMock).not_to have_requested(:post, webhook_url.url) end - it "doesn't send a webhook request if the webhook doesn't exist" do - described_class.new.perform('template_id' => template.id, 'webhook_url_id' => 100_500) - - expect(WebMock).not_to have_requested(:post, webhook_url.url) - end - it 'sends again if the response status is 400 or higher' do stub_request(:post, webhook_url.url).to_return(status: 401) diff --git a/spec/system/webhook_settings_spec.rb b/spec/system/webhook_settings_spec.rb index a6c1e2140..a12b98f8d 100644 --- a/spec/system/webhook_settings_spec.rb +++ b/spec/system/webhook_settings_spec.rb @@ -80,7 +80,7 @@ visit settings_webhooks_path - expect(webhook_url.secret).to be_nil + expect(webhook_url.secret).to eq({}) click_link 'Add Secret' From bc40a15de1e35df4db55da416074f1c189398fce Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 3 Nov 2024 10:19:59 +0200 Subject: [PATCH 3/5] use com --- app/jobs/send_form_completed_webhook_request_job.rb | 2 +- app/jobs/send_form_declined_webhook_request_job.rb | 2 +- app/jobs/send_form_started_webhook_request_job.rb | 2 +- app/jobs/send_form_viewed_webhook_request_job.rb | 2 +- app/jobs/send_submission_archived_webhook_request_job.rb | 2 +- app/jobs/send_submission_completed_webhook_request_job.rb | 2 +- app/jobs/send_submission_created_webhook_request_job.rb | 2 +- app/jobs/send_template_created_webhook_request_job.rb | 2 +- app/jobs/send_template_updated_webhook_request_job.rb | 2 +- spec/jobs/send_form_completed_webhook_request_job_spec.rb | 4 ++-- spec/jobs/send_form_declined_webhook_request_job_spec.rb | 4 ++-- spec/jobs/send_form_started_webhook_request_job_spec.rb | 4 ++-- spec/jobs/send_form_viewed_webhook_request_job_spec.rb | 4 ++-- .../jobs/send_submission_archived_webhook_request_job_spec.rb | 4 ++-- .../send_submission_completed_webhook_request_job_spec.rb | 4 ++-- spec/jobs/send_submission_created_webhook_request_job_spec.rb | 4 ++-- spec/jobs/send_template_created_webhook_request_job_spec.rb | 4 ++-- spec/jobs/send_template_updated_webhook_request_job_spec.rb | 4 ++-- 18 files changed, 27 insertions(+), 27 deletions(-) diff --git a/app/jobs/send_form_completed_webhook_request_job.rb b/app/jobs/send_form_completed_webhook_request_job.rb index 7e935e8d9..30810c328 100644 --- a/app/jobs/send_form_completed_webhook_request_job.rb +++ b/app/jobs/send_form_completed_webhook_request_job.rb @@ -5,7 +5,7 @@ class SendFormCompletedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.co Webhook' + USER_AGENT = 'DocuSeal.com Webhook' MAX_ATTEMPTS = 10 diff --git a/app/jobs/send_form_declined_webhook_request_job.rb b/app/jobs/send_form_declined_webhook_request_job.rb index 2bf7b7f74..86fdae2aa 100644 --- a/app/jobs/send_form_declined_webhook_request_job.rb +++ b/app/jobs/send_form_declined_webhook_request_job.rb @@ -5,7 +5,7 @@ class SendFormDeclinedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.co Webhook' + USER_AGENT = 'DocuSeal.com Webhook' MAX_ATTEMPTS = 10 diff --git a/app/jobs/send_form_started_webhook_request_job.rb b/app/jobs/send_form_started_webhook_request_job.rb index 72dc40f7a..3a6e0eaeb 100644 --- a/app/jobs/send_form_started_webhook_request_job.rb +++ b/app/jobs/send_form_started_webhook_request_job.rb @@ -5,7 +5,7 @@ class SendFormStartedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.co Webhook' + USER_AGENT = 'DocuSeal.com Webhook' MAX_ATTEMPTS = 10 diff --git a/app/jobs/send_form_viewed_webhook_request_job.rb b/app/jobs/send_form_viewed_webhook_request_job.rb index f71cf45f1..c70cf776c 100644 --- a/app/jobs/send_form_viewed_webhook_request_job.rb +++ b/app/jobs/send_form_viewed_webhook_request_job.rb @@ -5,7 +5,7 @@ class SendFormViewedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.co Webhook' + USER_AGENT = 'DocuSeal.com Webhook' MAX_ATTEMPTS = 10 diff --git a/app/jobs/send_submission_archived_webhook_request_job.rb b/app/jobs/send_submission_archived_webhook_request_job.rb index bc6f4897d..334d047b0 100644 --- a/app/jobs/send_submission_archived_webhook_request_job.rb +++ b/app/jobs/send_submission_archived_webhook_request_job.rb @@ -5,7 +5,7 @@ class SendSubmissionArchivedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.co Webhook' + USER_AGENT = 'DocuSeal.com Webhook' MAX_ATTEMPTS = 10 diff --git a/app/jobs/send_submission_completed_webhook_request_job.rb b/app/jobs/send_submission_completed_webhook_request_job.rb index 0477a1b4d..085e721c1 100644 --- a/app/jobs/send_submission_completed_webhook_request_job.rb +++ b/app/jobs/send_submission_completed_webhook_request_job.rb @@ -5,7 +5,7 @@ class SendSubmissionCompletedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.co Webhook' + USER_AGENT = 'DocuSeal.com Webhook' MAX_ATTEMPTS = 10 diff --git a/app/jobs/send_submission_created_webhook_request_job.rb b/app/jobs/send_submission_created_webhook_request_job.rb index 31f83f23e..a4dba4a67 100644 --- a/app/jobs/send_submission_created_webhook_request_job.rb +++ b/app/jobs/send_submission_created_webhook_request_job.rb @@ -5,7 +5,7 @@ class SendSubmissionCreatedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.co Webhook' + USER_AGENT = 'DocuSeal.com Webhook' MAX_ATTEMPTS = 10 diff --git a/app/jobs/send_template_created_webhook_request_job.rb b/app/jobs/send_template_created_webhook_request_job.rb index 49b344bce..6b2493dc6 100644 --- a/app/jobs/send_template_created_webhook_request_job.rb +++ b/app/jobs/send_template_created_webhook_request_job.rb @@ -5,7 +5,7 @@ class SendTemplateCreatedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.co Webhook' + USER_AGENT = 'DocuSeal.com Webhook' MAX_ATTEMPTS = 10 diff --git a/app/jobs/send_template_updated_webhook_request_job.rb b/app/jobs/send_template_updated_webhook_request_job.rb index 0766a14d0..68479f1b5 100644 --- a/app/jobs/send_template_updated_webhook_request_job.rb +++ b/app/jobs/send_template_updated_webhook_request_job.rb @@ -5,7 +5,7 @@ class SendTemplateUpdatedWebhookRequestJob sidekiq_options queue: :webhooks - USER_AGENT = 'DocuSeal.co Webhook' + USER_AGENT = 'DocuSeal.com Webhook' MAX_ATTEMPTS = 10 diff --git a/spec/jobs/send_form_completed_webhook_request_job_spec.rb b/spec/jobs/send_form_completed_webhook_request_job_spec.rb index e4046cd92..ceec4c00c 100644 --- a/spec/jobs/send_form_completed_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_completed_webhook_request_job_spec.rb @@ -33,7 +33,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook' + 'User-Agent' => 'DocuSeal.com Webhook' } ).once end @@ -50,7 +50,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook', + 'User-Agent' => 'DocuSeal.com Webhook', 'X-Secret-Header' => 'secret_value' } ).once diff --git a/spec/jobs/send_form_declined_webhook_request_job_spec.rb b/spec/jobs/send_form_declined_webhook_request_job_spec.rb index be2872b2d..88a88906b 100644 --- a/spec/jobs/send_form_declined_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_declined_webhook_request_job_spec.rb @@ -33,7 +33,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook' + 'User-Agent' => 'DocuSeal.com Webhook' } ).once end @@ -50,7 +50,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook', + 'User-Agent' => 'DocuSeal.com Webhook', 'X-Secret-Header' => 'secret_value' } ).once diff --git a/spec/jobs/send_form_started_webhook_request_job_spec.rb b/spec/jobs/send_form_started_webhook_request_job_spec.rb index 3bff34eb1..863ce826f 100644 --- a/spec/jobs/send_form_started_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_started_webhook_request_job_spec.rb @@ -33,7 +33,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook' + 'User-Agent' => 'DocuSeal.com Webhook' } ).once end @@ -50,7 +50,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook', + 'User-Agent' => 'DocuSeal.com Webhook', 'X-Secret-Header' => 'secret_value' } ).once diff --git a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb index 66e1accb1..495e62395 100644 --- a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb @@ -33,7 +33,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook' + 'User-Agent' => 'DocuSeal.com Webhook' } ).once end @@ -50,7 +50,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook', + 'User-Agent' => 'DocuSeal.com Webhook', 'X-Secret-Header' => 'secret_value' } ).once diff --git a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb index 879702143..468430f74 100644 --- a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb @@ -30,7 +30,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook' + 'User-Agent' => 'DocuSeal.com Webhook' } ).once end @@ -47,7 +47,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook', + 'User-Agent' => 'DocuSeal.com Webhook', 'X-Secret-Header' => 'secret_value' } ).once diff --git a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb index f4ba884c3..1101b12af 100644 --- a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb @@ -30,7 +30,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook' + 'User-Agent' => 'DocuSeal.com Webhook' } ).once end @@ -47,7 +47,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook', + 'User-Agent' => 'DocuSeal.com Webhook', 'X-Secret-Header' => 'secret_value' } ).once diff --git a/spec/jobs/send_submission_created_webhook_request_job_spec.rb b/spec/jobs/send_submission_created_webhook_request_job_spec.rb index ba635493f..e4004dc74 100644 --- a/spec/jobs/send_submission_created_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_created_webhook_request_job_spec.rb @@ -30,7 +30,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook' + 'User-Agent' => 'DocuSeal.com Webhook' } ).once end @@ -47,7 +47,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook', + 'User-Agent' => 'DocuSeal.com Webhook', 'X-Secret-Header' => 'secret_value' } ).once diff --git a/spec/jobs/send_template_created_webhook_request_job_spec.rb b/spec/jobs/send_template_created_webhook_request_job_spec.rb index f6a09ba6b..532a1beab 100644 --- a/spec/jobs/send_template_created_webhook_request_job_spec.rb +++ b/spec/jobs/send_template_created_webhook_request_job_spec.rb @@ -29,7 +29,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook' + 'User-Agent' => 'DocuSeal.com Webhook' } ).once end @@ -46,7 +46,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook', + 'User-Agent' => 'DocuSeal.com Webhook', 'X-Secret-Header' => 'secret_value' } ).once diff --git a/spec/jobs/send_template_updated_webhook_request_job_spec.rb b/spec/jobs/send_template_updated_webhook_request_job_spec.rb index 58e46b639..1bef3eb2a 100644 --- a/spec/jobs/send_template_updated_webhook_request_job_spec.rb +++ b/spec/jobs/send_template_updated_webhook_request_job_spec.rb @@ -29,7 +29,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook' + 'User-Agent' => 'DocuSeal.com Webhook' } ).once end @@ -46,7 +46,7 @@ }.deep_stringify_keys), headers: { 'Content-Type' => 'application/json', - 'User-Agent' => 'DocuSeal.co Webhook', + 'User-Agent' => 'DocuSeal.com Webhook', 'X-Secret-Header' => 'secret_value' } ).once From ab4a22d8bae14f99d9496d644891013815edea68 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 3 Nov 2024 13:43:28 +0200 Subject: [PATCH 4/5] fix test links --- app/views/shared/_settings_nav.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_settings_nav.html.erb b/app/views/shared/_settings_nav.html.erb index 15e58efb4..fddae67f3 100644 --- a/app/views/shared/_settings_nav.html.erb +++ b/app/views/shared/_settings_nav.html.erb @@ -72,13 +72,13 @@ <% end %> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
  • - <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> + <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %> <% end %>
  • <% if Docuseal.multitenant? %>
  • - <%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> + <%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <%= t('embedding') %> <% end %>
  • From cb6d24dfca26ae502f409854d25aa16c9cd0e22b Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 4 Nov 2024 11:37:58 +0200 Subject: [PATCH 5/5] add canonical urls --- app/views/devise/sessions/new.html.erb | 1 + app/views/layouts/application.html.erb | 3 +++ 2 files changed, 4 insertions(+) diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index baca3e563..0cbd57e82 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -1,3 +1,4 @@ +<%= content_for(:canonical_url, new_user_session_url) %>
    <%= render 'devise/shared/select_server' if Docuseal.multitenant? %>

    <%= t('sign_in') %>

    diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 4de5c8c76..590e0b23a 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,6 +11,9 @@ <% else %> <%= javascript_pack_tag 'application', defer: true %> <% end %> + <% if canonical_url = content_for(:canonical_url) %> + + <% end %> <%= stylesheet_pack_tag 'application', media: 'all' %> <%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %> <%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>