mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 22:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50acff2e81 | ||
|
|
cb6d24dfca | ||
|
|
ab4a22d8ba | ||
|
|
bc40a15de1 | ||
|
|
2542f26d96 | ||
|
|
84edb6dbda | ||
|
|
88776e7316 | ||
|
|
f669045362 | ||
|
|
68d3efbdba | ||
|
|
22d4833496 | ||
|
|
ff14caa9f7 | ||
|
|
05647135bc | ||
|
|
ae76c2a4a8 | ||
|
|
fb2b3d44fe | ||
|
|
3868557efd | ||
|
|
9bef28c8ba | ||
|
|
f25fd3b03d | ||
|
|
d1d7571ee4 | ||
|
|
6a298a068c | ||
|
|
d4aef79b68 |
No files matched your search
+1
-1
@@ -437,7 +437,7 @@ GEM
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
retriable (3.1.2)
|
||||
rexml (3.3.8)
|
||||
rexml (3.3.9)
|
||||
rotp (6.3.0)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<h1 align="center" style="border-bottom: none">
|
||||
<div>
|
||||
<a href="https://www.docuseal.co">
|
||||
<a href="https://www.docuseal.com">
|
||||
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
||||
<br>
|
||||
</a>
|
||||
@@ -54,11 +54,11 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
- Conditional fields and formulas
|
||||
- Bulk send with CSV, XLSX spreadsheet import
|
||||
- SSO / SAML
|
||||
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
||||
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||
- [Learn more](https://www.docuseal.co/pricing)
|
||||
- Template creation with HTML API ([Guide](https://www.docuseal.com/guides/create-pdf-document-fillable-form-with-html-api))
|
||||
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
|
||||
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
|
||||
- [Learn more](https://www.docuseal.com/pricing)
|
||||
|
||||
## Deploy
|
||||
|
||||
@@ -93,7 +93,7 @@ sudo HOST=your-domain-name.com docker compose up
|
||||
|
||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||
|
||||
[Book a Meeting](https://www.docuseal.co/contact)
|
||||
[Book a Meeting](https://www.docuseal.com/contact)
|
||||
|
||||
## License
|
||||
|
||||
@@ -102,6 +102,6 @@ Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
||||
|
||||
## Tools
|
||||
|
||||
- [Signature Maker](https://www.docuseal.co/online-signature)
|
||||
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
|
||||
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
|
||||
- [Signature Maker](https://www.docuseal.com/online-signature)
|
||||
- [Sign Document Online](https://www.docuseal.com/sign-documents-online)
|
||||
- [Fill PDF Online](https://www.docuseal.com/fill-pdf)
|
||||
@@ -67,15 +67,18 @@ module Api
|
||||
|
||||
submissions = create_submissions(@template, params)
|
||||
|
||||
submissions.each do |submission|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
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
|
||||
|
||||
@@ -93,7 +96,10 @@ module Api
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.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
|
||||
|
||||
render json: @submission.as_json(only: %i[id archived_at])
|
||||
|
||||
@@ -13,7 +13,10 @@ module Api
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||
|
||||
SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.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: {}
|
||||
end
|
||||
|
||||
@@ -68,7 +68,7 @@ module Api
|
||||
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
|
||||
|
||||
@@ -25,7 +25,10 @@ module Api
|
||||
|
||||
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.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)
|
||||
end
|
||||
|
||||
@@ -65,7 +65,10 @@ module Api
|
||||
|
||||
@template.update!(template_params)
|
||||
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.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])
|
||||
end
|
||||
|
||||
@@ -104,4 +104,10 @@ class ApplicationController < ActionController::Base
|
||||
def svg_icon(icon_name, class: '')
|
||||
render_to_string(partial: "icons/#{icon_name}", locals: { class: })
|
||||
end
|
||||
|
||||
def maybe_redirect_com
|
||||
return if request.domain != 'docuseal.co'
|
||||
|
||||
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
class ErrorsController < ActionController::Base
|
||||
ENTERPRISE_FEATURE_MESSAGE =
|
||||
'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
|
||||
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
||||
|
||||
ENTERPRISE_PATHS = [
|
||||
'/templates/html',
|
||||
|
||||
@@ -7,6 +7,7 @@ class StartFormController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
around_action :with_browser_locale, only: %i[show completed]
|
||||
before_action :maybe_redirect_com, only: %i[show completed]
|
||||
before_action :load_template
|
||||
|
||||
def show
|
||||
@@ -37,7 +38,10 @@ class StartFormController < ApplicationController
|
||||
|
||||
if @submitter.save
|
||||
if is_new_record
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.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
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
|
||||
@@ -50,9 +50,7 @@ class SubmissionsController < ApplicationController
|
||||
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 @@ class SubmissionsController < ApplicationController
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.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')
|
||||
end
|
||||
@@ -85,6 +86,15 @@ class SubmissionsController < ApplicationController
|
||||
template.save!
|
||||
end
|
||||
|
||||
def enqueue_submission_created_webhooks(template, submissions)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||
end
|
||||
|
||||
@@ -4,6 +4,8 @@ class SubmissionsPreviewController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :maybe_redirect_com, only: %i[show completed]
|
||||
|
||||
TTL = 40.minutes
|
||||
|
||||
def show
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsUnarchiveController < ApplicationController
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def create
|
||||
@submission.update!(archived_at: nil)
|
||||
|
||||
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
|
||||
end
|
||||
end
|
||||
@@ -7,6 +7,8 @@ class SubmitFormController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :maybe_redirect_com, only: %i[show completed]
|
||||
|
||||
CONFIG_KEYS = [].freeze
|
||||
|
||||
def show
|
||||
|
||||
@@ -21,8 +21,14 @@ class SubmitFormDeclineController < ApplicationController
|
||||
|
||||
user = submitter.submission.created_by_user || submitter.template.author
|
||||
|
||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id)
|
||||
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
|
||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
@@ -66,7 +66,7 @@ class TemplatesController < ApplicationController
|
||||
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 @@ class TemplatesController < ApplicationController
|
||||
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 @@ class TemplatesController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue_template_created_webhooks(template)
|
||||
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)
|
||||
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
|
||||
|
||||
def load_base_template
|
||||
return if params[:base_template_id].blank?
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
@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 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
{ files: [file] }
|
||||
end
|
||||
|
||||
def enqueue_template_created_webhooks(template)
|
||||
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
|
||||
@@ -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
|
||||
@@ -1,37 +1,22 @@
|
||||
# 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
|
||||
load_and_authorize_resource :webhook_url, parent: false
|
||||
|
||||
before_action :load_account_config
|
||||
authorize_resource :account_config, 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
|
||||
|
||||
def create
|
||||
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
|
||||
|
||||
@account_config.save!
|
||||
@webhook_url.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)
|
||||
def webhook_preferences_params
|
||||
params.require(:webhook_url).permit(events: {})
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -17,20 +17,19 @@ class WebhookSettingsController < ApplicationController
|
||||
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 })
|
||||
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
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
@@ -91,7 +91,7 @@
|
||||
>
|
||||
{{ t('powered_by') }}
|
||||
<a
|
||||
href="https://www.docuseal.co/start"
|
||||
href="https://www.docuseal.com/start"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
||||
|
||||
@@ -145,6 +145,10 @@
|
||||
{{ t('clear') }}
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="isTextSignature"
|
||||
class="absolute top-0 right-0 left-0 bottom-0"
|
||||
/>
|
||||
<canvas
|
||||
v-show="!modelValue && !computedPreviousValue"
|
||||
ref="canvas"
|
||||
@@ -255,7 +259,7 @@
|
||||
class="text-base-content/60 text-xs text-center w-full mt-1"
|
||||
>
|
||||
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
|
||||
href="https://www.docuseal.co/esign-disclosure"
|
||||
href="https://www.docuseal.com/esign-disclosure"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="inline md:hidden">
|
||||
|
||||
@@ -415,6 +415,7 @@ import Contenteditable from './contenteditable'
|
||||
import DocumentPreview from './preview'
|
||||
import DocumentControls from './controls'
|
||||
import MobileFields from './mobile_fields'
|
||||
import FieldSubmitter from './field_submitter'
|
||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
|
||||
import { v4 } from 'uuid'
|
||||
import { ref, computed } from 'vue'
|
||||
@@ -558,6 +559,11 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
defineSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -677,6 +683,7 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
submitterDefaultNames: FieldSubmitter.computed.names,
|
||||
selectedAreaRef: () => ref(),
|
||||
fieldsDragFieldRef: () => ref(),
|
||||
language () {
|
||||
@@ -749,6 +756,18 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
this.defineSubmitters.forEach((name, index) => {
|
||||
const submitter = (this.template.submitters[index] ||= {})
|
||||
|
||||
submitter.name = name || this.submitterDefaultNames[index]
|
||||
|
||||
if (existingSubmittersUuids.filter(Boolean).length) {
|
||||
submitter.uuid = existingSubmittersUuids[index] || submitter.uuid || v4()
|
||||
} else {
|
||||
submitter.uuid ||= v4()
|
||||
}
|
||||
})
|
||||
|
||||
this.selectedSubmitter = this.template.submitters[0]
|
||||
},
|
||||
mounted () {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
||||
>
|
||||
<a
|
||||
href="https://www.docuseal.co/pricing"
|
||||
href="https://www.docuseal.com/pricing"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>{{ t('available_in_pro') }}</a>
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
:data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')"
|
||||
>
|
||||
<a
|
||||
href="https://www.docuseal.co/pricing"
|
||||
href="https://www.docuseal.com/pricing"
|
||||
target="_blank"
|
||||
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||
:style="{ backgroundColor }"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
||||
>
|
||||
<a
|
||||
href="https://www.docuseal.co/pricing"
|
||||
href="https://www.docuseal.com/pricing"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>{{ t('available_in_pro') }}</a>
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="w-full relative">
|
||||
<select
|
||||
class="base-select !select-sm !h-10"
|
||||
:class="{ '!text-gray-300': !mapping.column_index }"
|
||||
:class="{ '!text-gray-300': !mapping.column_index && mapping.column_index != 0 }"
|
||||
required
|
||||
@change="mapping.column_index = parseInt($event.target.value)"
|
||||
>
|
||||
@@ -285,7 +285,7 @@ export default {
|
||||
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index]
|
||||
}
|
||||
|
||||
const fieldType = this.fieldTypesIndex[mapping.submitter_uuid][mapping.field_name]
|
||||
const fieldType = this.fieldTypesIndex[mapping.submitter_uuid]?.[mapping.field_name]
|
||||
|
||||
if (fieldType && fieldType !== 'phone') {
|
||||
submittersIndex[mapping.submitter_uuid].fields.push({
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
<a
|
||||
v-if="!isConnected"
|
||||
class="block link text-center mt-1"
|
||||
href="https://www.docuseal.co/blog/accept-payments-and-request-signatures-with-ease"
|
||||
href="https://www.docuseal.com/blog/accept-payments-and-request-signatures-with-ease"
|
||||
target="_blank"
|
||||
data-turbo="false"
|
||||
>{{ t('learn_more') }}</a>
|
||||
|
||||
@@ -63,32 +63,15 @@ class ProcessSubmitterCompletionJob
|
||||
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|
|
||||
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
|
||||
|
||||
@@ -5,31 +5,30 @@ class SendFormCompletedWebhookRequestJob
|
||||
|
||||
sidekiq_options queue: :webhooks
|
||||
|
||||
USER_AGENT = 'DocuSeal.co Webhook'
|
||||
USER_AGENT = 'DocuSeal.com Webhook'
|
||||
|
||||
MAX_ATTEMPTS = 10
|
||||
|
||||
def perform(params = {})
|
||||
submitter = Submitter.find(params['submitter_id'])
|
||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||
|
||||
attempt = params['attempt'].to_i
|
||||
|
||||
url, secret = load_url_and_secret(submitter, params)
|
||||
|
||||
return if url.blank?
|
||||
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 +44,4 @@ class SendFormCompletedWebhookRequestJob
|
||||
})
|
||||
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
|
||||
@@ -5,34 +5,28 @@ class SendFormDeclinedWebhookRequestJob
|
||||
|
||||
sidekiq_options queue: :webhooks
|
||||
|
||||
USER_AGENT = 'DocuSeal.co Webhook'
|
||||
USER_AGENT = 'DocuSeal.com Webhook'
|
||||
|
||||
MAX_ATTEMPTS = 10
|
||||
|
||||
def perform(params = {})
|
||||
submitter = Submitter.find(params['submitter_id'])
|
||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||
|
||||
attempt = params['attempt'].to_i
|
||||
config = Accounts.load_webhook_config(submitter.submission.account)
|
||||
url = config&.value.presence
|
||||
|
||||
return if url.blank?
|
||||
|
||||
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
|
||||
|
||||
return if preferences['form.declined'] == false
|
||||
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 +37,7 @@ class SendFormDeclinedWebhookRequestJob
|
||||
(!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
|
||||
})
|
||||
|
||||
@@ -5,34 +5,28 @@ class SendFormStartedWebhookRequestJob
|
||||
|
||||
sidekiq_options queue: :webhooks
|
||||
|
||||
USER_AGENT = 'DocuSeal.co Webhook'
|
||||
USER_AGENT = 'DocuSeal.com Webhook'
|
||||
|
||||
MAX_ATTEMPTS = 10
|
||||
|
||||
def perform(params = {})
|
||||
submitter = Submitter.find(params['submitter_id'])
|
||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||
|
||||
attempt = params['attempt'].to_i
|
||||
config = Accounts.load_webhook_config(submitter.submission.account)
|
||||
url = config&.value.presence
|
||||
|
||||
return if url.blank?
|
||||
|
||||
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
|
||||
|
||||
return if preferences['form.started'] == false
|
||||
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 +37,7 @@ class SendFormStartedWebhookRequestJob
|
||||
(!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
|
||||
})
|
||||
|
||||
@@ -5,34 +5,28 @@ class SendFormViewedWebhookRequestJob
|
||||
|
||||
sidekiq_options queue: :webhooks
|
||||
|
||||
USER_AGENT = 'DocuSeal.co Webhook'
|
||||
USER_AGENT = 'DocuSeal.com Webhook'
|
||||
|
||||
MAX_ATTEMPTS = 10
|
||||
|
||||
def perform(params = {})
|
||||
submitter = Submitter.find(params['submitter_id'])
|
||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||
|
||||
attempt = params['attempt'].to_i
|
||||
config = Accounts.load_webhook_config(submitter.submission.account)
|
||||
url = config&.value.presence
|
||||
|
||||
return if url.blank?
|
||||
|
||||
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
|
||||
|
||||
return if preferences['form.viewed'] == false
|
||||
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 +37,7 @@ class SendFormViewedWebhookRequestJob
|
||||
(!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
|
||||
})
|
||||
|
||||
@@ -5,33 +5,26 @@ class SendSubmissionArchivedWebhookRequestJob
|
||||
|
||||
sidekiq_options queue: :webhooks
|
||||
|
||||
USER_AGENT = 'DocuSeal.co Webhook'
|
||||
USER_AGENT = 'DocuSeal.com Webhook'
|
||||
|
||||
MAX_ATTEMPTS = 10
|
||||
|
||||
def perform(params = {})
|
||||
submission = Submission.find(params['submission_id'])
|
||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||
|
||||
attempt = params['attempt'].to_i
|
||||
|
||||
config = Accounts.load_webhook_config(submission.account)
|
||||
url = config&.value.presence
|
||||
|
||||
return if url.blank?
|
||||
|
||||
preferences = Accounts.load_webhook_preferences(submission.account)
|
||||
|
||||
return if preferences['submission.archived'].blank?
|
||||
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 +35,7 @@ class SendSubmissionArchivedWebhookRequestJob
|
||||
(!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
|
||||
})
|
||||
|
||||
@@ -5,30 +5,26 @@ class SendSubmissionCompletedWebhookRequestJob
|
||||
|
||||
sidekiq_options queue: :webhooks
|
||||
|
||||
USER_AGENT = 'DocuSeal.co Webhook'
|
||||
USER_AGENT = 'DocuSeal.com Webhook'
|
||||
|
||||
MAX_ATTEMPTS = 10
|
||||
|
||||
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(params['webhook_url_id'])
|
||||
|
||||
url = webhook_url.url if webhook_url.events.include?('submission.completed')
|
||||
|
||||
return if url.blank?
|
||||
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
|
||||
|
||||
@@ -5,33 +5,26 @@ class SendSubmissionCreatedWebhookRequestJob
|
||||
|
||||
sidekiq_options queue: :webhooks
|
||||
|
||||
USER_AGENT = 'DocuSeal.co Webhook'
|
||||
USER_AGENT = 'DocuSeal.com Webhook'
|
||||
|
||||
MAX_ATTEMPTS = 10
|
||||
|
||||
def perform(params = {})
|
||||
submission = Submission.find(params['submission_id'])
|
||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||
|
||||
attempt = params['attempt'].to_i
|
||||
|
||||
config = Accounts.load_webhook_config(submission.account)
|
||||
url = config&.value.presence
|
||||
|
||||
return if url.blank?
|
||||
|
||||
preferences = Accounts.load_webhook_preferences(submission.account)
|
||||
|
||||
return if preferences['submission.created'].blank?
|
||||
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 +35,7 @@ class SendSubmissionCreatedWebhookRequestJob
|
||||
(!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
|
||||
})
|
||||
|
||||
@@ -5,33 +5,26 @@ class SendTemplateCreatedWebhookRequestJob
|
||||
|
||||
sidekiq_options queue: :webhooks
|
||||
|
||||
USER_AGENT = 'DocuSeal.co Webhook'
|
||||
USER_AGENT = 'DocuSeal.com Webhook'
|
||||
|
||||
MAX_ATTEMPTS = 10
|
||||
|
||||
def perform(params = {})
|
||||
template = Template.find(params['template_id'])
|
||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||
|
||||
attempt = params['attempt'].to_i
|
||||
|
||||
config = Accounts.load_webhook_config(template.account)
|
||||
url = config&.value.presence
|
||||
|
||||
return if url.blank?
|
||||
|
||||
preferences = Accounts.load_webhook_preferences(template.account)
|
||||
|
||||
return if preferences['template.created'].blank?
|
||||
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 +35,7 @@ class SendTemplateCreatedWebhookRequestJob
|
||||
(!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
|
||||
})
|
||||
|
||||
@@ -5,33 +5,26 @@ class SendTemplateUpdatedWebhookRequestJob
|
||||
|
||||
sidekiq_options queue: :webhooks
|
||||
|
||||
USER_AGENT = 'DocuSeal.co Webhook'
|
||||
USER_AGENT = 'DocuSeal.com Webhook'
|
||||
|
||||
MAX_ATTEMPTS = 10
|
||||
|
||||
def perform(params = {})
|
||||
template = Template.find(params['template_id'])
|
||||
webhook_url = WebhookUrl.find(params['webhook_url_id'])
|
||||
|
||||
attempt = params['attempt'].to_i
|
||||
|
||||
config = Accounts.load_webhook_config(template.account)
|
||||
url = config&.value.presence
|
||||
|
||||
return if url.blank?
|
||||
|
||||
preferences = Accounts.load_webhook_preferences(template.account)
|
||||
|
||||
return if preferences['template.updated'].blank?
|
||||
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 +35,7 @@ class SendTemplateUpdatedWebhookRequestJob
|
||||
(!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
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,6 +24,7 @@ class UserConfig < ApplicationRecord
|
||||
SIGNATURE_KEY = 'signature'
|
||||
INITIALS_KEY = 'initials'
|
||||
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
|
||||
RECEIVE_DECLINED_EMAIL = 'receive_declined_email'
|
||||
|
||||
belongs_to :user
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# events :text not null
|
||||
# secret :text not null
|
||||
# sha1 :string not null
|
||||
# url :text not null
|
||||
# created_at :datetime not null
|
||||
@@ -22,15 +23,28 @@
|
||||
# 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] }
|
||||
attribute :secret, :string, default: -> { {} }
|
||||
|
||||
serialize :events, coder: JSON
|
||||
serialize :secret, coder: JSON
|
||||
|
||||
before_validation :set_sha1
|
||||
|
||||
encrypts :url
|
||||
encrypts :url, :secret
|
||||
|
||||
def set_sha1
|
||||
self.sha1 = Digest::SHA1.hexdigest(url)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<%= content_for(:canonical_url, new_user_session_url) %>
|
||||
<div class="max-w-lg mx-auto px-2">
|
||||
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||
<h1 class="text-4xl font-bold text-center mt-8"><%= t('sign_in') %></h1>
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<% else %>
|
||||
<%= javascript_pack_tag 'application', defer: true %>
|
||||
<% end %>
|
||||
<% if canonical_url = content_for(:canonical_url) %>
|
||||
<link href="<%= canonical_url %>" rel="canonical">
|
||||
<% end %>
|
||||
<%= stylesheet_pack_tag 'application', media: 'all' %>
|
||||
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
|
||||
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
|
||||
<p class="text-base text-gray-500">
|
||||
Run on your own host using Docker container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
|
||||
Run on your own host using Docker container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.com/install" class="link link-neutral font-bold">click</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
|
||||
<img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px">
|
||||
<img alt="GitHub Repo stars" src="https://www.docuseal.com/github-badge.svg" style="height: 22px">
|
||||
</a>
|
||||
@@ -56,7 +56,7 @@
|
||||
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::WEBHOOK_URL_KEY, account: current_account)) %>
|
||||
<% if can?(:read, WebhookUrl) %>
|
||||
<li>
|
||||
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
@@ -72,13 +72,13 @@
|
||||
<% end %>
|
||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
|
||||
<li>
|
||||
<%= 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 %>
|
||||
</li>
|
||||
<% if Docuseal.multitenant? %>
|
||||
<li>
|
||||
<%= 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 %>
|
||||
</li>
|
||||
|
||||
@@ -9,14 +9,18 @@
|
||||
<span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% @submission.template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %></span>
|
||||
</a>
|
||||
<div class="space-x-3 flex items-center">
|
||||
<% last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
|
||||
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
|
||||
<% if signed_in? && can?(:create, @submission) && @submission.archived_at? && !is_all_completed %>
|
||||
<%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %>
|
||||
<% end %>
|
||||
<% if @submission.audit_trail.present? %>
|
||||
<a href="<%= ActiveStorage::Blob.proxy_url(@submission.audit_trail.blob, expires_at: 4.hours.from_now) %>" class="white-button" target="_blank">
|
||||
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
||||
<span class="hidden md:inline"><%= t('audit_log') %></span>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
|
||||
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
|
||||
<% if last_submitter %>
|
||||
<% if is_all_completed || !is_combined_enabled %>
|
||||
<div class="join relative">
|
||||
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
|
||||
@@ -54,7 +58,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? %>
|
||||
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? %>
|
||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
|
||||
<%= submitter.name || submitter.email || submitter.phone %>
|
||||
</a>
|
||||
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? %>
|
||||
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
|
||||
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
|
||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||
@@ -144,7 +144,7 @@
|
||||
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
|
||||
<%= submitter.name || submitter.email || submitter.phone %>
|
||||
</a>
|
||||
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? %>
|
||||
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
|
||||
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
|
||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||
|
||||
@@ -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') %>
|
||||
</div>
|
||||
</div>
|
||||
<%= 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' %>
|
||||
<div class="space-y-2 md:flex-nowrap mt-2">
|
||||
<%= 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' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -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| %>
|
||||
<div class="space-y-2">
|
||||
<%= 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| %>
|
||||
<div class="form-control">
|
||||
<%= ff.label :key, class: 'label' %>
|
||||
<%= ff.label :key, t('key'), class: 'label' %>
|
||||
<%= ff.text_field :key, class: 'base-input', placeholder: 'X-Example-Header' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :value, class: 'label' %>
|
||||
<%= ff.label :value, t('value'), class: 'label' %>
|
||||
<%= ff.text_field :value, class: 'base-input' %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -7,35 +7,36 @@
|
||||
</div>
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body p-6">
|
||||
<%= 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' %>
|
||||
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-2 mt-2">
|
||||
<%= 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' %>
|
||||
<a href="<%= webhook_secret_index_path %>" data-turbo-frame="modal" class="white-button w-full md:w-auto">
|
||||
<%= t('add_secret') %>
|
||||
</a>
|
||||
</div>
|
||||
<% 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| %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 mt-2 gap-y-2">
|
||||
<% events.each do |event| %>
|
||||
<%= form_for '', url: webhook_preferences_path, method: :post do |f| %>
|
||||
<%= f.hidden_field :event, value: event %>
|
||||
<% uuid = SecureRandom.uuid %>
|
||||
<div class="flex">
|
||||
<label for="<%= uuid %>" class="flex items-center space-x-2">
|
||||
<%= f.check_box :value, class: 'base-checkbox', checked: preference[event] || (!preference.key?(event) && event.starts_with?('form.')), onchange: 'this.form.requestSubmit()', id: uuid %>
|
||||
<span>
|
||||
<%= event %>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @webhook_url.persisted? %>
|
||||
<a href="<%= webhook_secret_path(@webhook_url) %>" data-turbo-frame="modal" class="white-button w-full md:w-auto">
|
||||
<%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% 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| %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 mt-4 gap-y-2">
|
||||
<% events.each do |event| %>
|
||||
<%= f.fields_for :events do |ff| %>
|
||||
<div class="flex">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<%= ff.check_box event, class: 'base-checkbox', checked: @webhook_url.events.include?(event), onchange: 'this.form.requestSubmit()', disabled: @webhook_url.url.blank? %>
|
||||
<span>
|
||||
<%= event %>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %>
|
||||
@@ -47,7 +48,7 @@
|
||||
<span>
|
||||
<%= t('submission_example_payload') %>
|
||||
</span>
|
||||
<% 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 %>
|
||||
</div>
|
||||
|
||||
+49
-14
@@ -20,6 +20,7 @@ en: &en
|
||||
language_ko: 한국어
|
||||
hi_there: Hi there
|
||||
thanks: Thanks
|
||||
unarchive: Unarchive
|
||||
first_party: 'First Party'
|
||||
docuseal_trusted_signature: DocuSeal Trusted Signature
|
||||
hello_name: Hello %{name}
|
||||
@@ -119,7 +120,7 @@ en: &en
|
||||
profile_details: Profile Details
|
||||
sign_up_with_google: Sign up with Google
|
||||
sign_up_with_microsoft: Sign up with Microsoft
|
||||
by_creating_an_account_you_agree_to_our_html: 'By creating an account, you agree to our <a target="_blank" href="https://www.docuseal.co/privacy">Privacy Policy</a> and <a target="_blank" href="https://www.docuseal.co/terms">Terms of Service</a>.'
|
||||
by_creating_an_account_you_agree_to_our_html: 'By creating an account, you agree to our <a target="_blank" href="https://www.docuseal.com/privacy">Privacy Policy</a> and <a target="_blank" href="https://www.docuseal.com/terms">Terms of Service</a>.'
|
||||
enter_email_to_continue: Enter email to continue
|
||||
account: Account
|
||||
preferences: Preferences
|
||||
@@ -473,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
|
||||
@@ -498,6 +500,7 @@ en: &en
|
||||
document_template_has_been_moved: Document template has been moved.
|
||||
unable_to_move_template_into_folder: Unable to move template into folder.
|
||||
template_has_been_unarchived: Template has been unarchived.
|
||||
submission_has_been_unarchived: Submission has been unarchived.
|
||||
unable_to_update_file: Unable to upload file.
|
||||
user_has_been_invited: User has been invited.
|
||||
unable_to_update_user: Unable to update user.
|
||||
@@ -621,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: '<b>Email sent</b> to %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
|
||||
@@ -658,6 +663,7 @@ en: &en
|
||||
read: Read your data
|
||||
|
||||
es: &es
|
||||
unarchive: Desarchivar
|
||||
awaiting_completion_by_the_other_party: "Esperando la finalización por la otra parte"
|
||||
enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios'
|
||||
first_party: 'Primera Parte'
|
||||
@@ -759,7 +765,7 @@ es: &es
|
||||
profile_details: Detalles del perfil
|
||||
sign_up_with_google: Registrarse con Google
|
||||
sign_up_with_microsoft: Registrarse con Microsoft
|
||||
by_creating_an_account_you_agree_to_our_html: 'Al crear una cuenta, aceptas nuestra <a target="_blank" href="https://www.docuseal.co/privacy">Política de Privacidad</a> y <a target="_blank" href="https://www.docuseal.co/terms">Términos de Servicio</a>.'
|
||||
by_creating_an_account_you_agree_to_our_html: 'Al crear una cuenta, aceptas nuestra <a target="_blank" href="https://www.docuseal.com/privacy">Política de Privacidad</a> y <a target="_blank" href="https://www.docuseal.com/terms">Términos de Servicio</a>.'
|
||||
enter_email_to_continue: Ingresa tu correo electrónico para continuar
|
||||
account: Cuenta
|
||||
preferences: Preferencias
|
||||
@@ -1113,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
|
||||
@@ -1138,6 +1145,7 @@ es: &es
|
||||
document_template_has_been_moved: La plantilla de documento ha sido movida.
|
||||
unable_to_move_template_into_folder: No se pudo mover la plantilla a la carpeta.
|
||||
template_has_been_unarchived: La plantilla ha sido desarchivada.
|
||||
submission_has_been_unarchived: La presentación ha sido desarchivada.
|
||||
unable_to_update_file: No se pudo subir el archivo.
|
||||
user_has_been_invited: El usuario ha sido invitado.
|
||||
unable_to_update_user: No se pudo actualizar el usuario.
|
||||
@@ -1261,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: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
|
||||
@@ -1297,6 +1308,7 @@ es: &es
|
||||
read: Leer tus datos
|
||||
|
||||
it: &it
|
||||
unarchive: Ripristina
|
||||
awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte"
|
||||
enforce_recipients_order: Aplicar el orden de los destinatarios
|
||||
first_party: 'Prima parte'
|
||||
@@ -1398,7 +1410,7 @@ it: &it
|
||||
profile_details: Dettagli del profilo
|
||||
sign_up_with_google: Registrati con Google
|
||||
sign_up_with_microsoft: Registrati con Microsoft
|
||||
by_creating_an_account_you_agree_to_our_html: 'Creando un account, accetti la nostra <a target="_blank" href="https://www.docuseal.co/privacy">Privacy Policy</a> e i nostri <a target="_blank" href="https://www.docuseal.co/terms">Termini di servizio</a>.'
|
||||
by_creating_an_account_you_agree_to_our_html: 'Creando un account, accetti la nostra <a target="_blank" href="https://www.docuseal.com/privacy">Privacy Policy</a> e i nostri <a target="_blank" href="https://www.docuseal.com/terms">Termini di servizio</a>.'
|
||||
enter_email_to_continue: "Inserisci l'email per continuare"
|
||||
account: Account
|
||||
preferences: Preferenze
|
||||
@@ -1752,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
|
||||
@@ -1777,6 +1790,7 @@ it: &it
|
||||
document_template_has_been_moved: Il modello di documento è stato spostato.
|
||||
unable_to_move_template_into_folder: Impossibile spostare il modello nella cartella.
|
||||
template_has_been_unarchived: Il modello è stato disarchiviato.
|
||||
submission_has_been_unarchived: La sottomissione è stata dearchiviata.
|
||||
unable_to_update_file: Impossibile caricare il file.
|
||||
user_has_been_invited: "L'utente è stato invitato."
|
||||
unable_to_update_user: Impossibile aggiornare l'utente.
|
||||
@@ -1900,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: '<b>E-mail inviato</b> a %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
|
||||
@@ -1936,6 +1953,7 @@ it: &it
|
||||
read: Leggi i tuoi dati
|
||||
|
||||
fr: &fr
|
||||
unarchive: Désarchiver
|
||||
awaiting_completion_by_the_other_party: "En attente de la complétion par l'autre partie"
|
||||
enforce_recipients_order: "Respecter l'ordre des destinataires"
|
||||
first_party: 'Première partie'
|
||||
@@ -2038,7 +2056,7 @@ fr: &fr
|
||||
sign_up_with_google: S'inscrire avec Google
|
||||
sign_up_with_microsoft: S'inscrire avec Microsoft
|
||||
by_creating_an_account_you_agree_to_our_html: |
|
||||
En créant un compte, vous acceptez notre <a target="_blank" href="https://www.docuseal.co/privacy">Politique de Confidentialité</a> et nos <a target="_blank" href="https://www.docuseal.co/terms">Conditions d'Utilisation</a>.
|
||||
En créant un compte, vous acceptez notre <a target="_blank" href="https://www.docuseal.com/privacy">Politique de Confidentialité</a> et nos <a target="_blank" href="https://www.docuseal.com/terms">Conditions d'Utilisation</a>.
|
||||
enter_email_to_continue: Entrez votre e-mail pour continuer
|
||||
account: Compte
|
||||
preferences: Préférences
|
||||
@@ -2392,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
|
||||
@@ -2417,6 +2436,7 @@ fr: &fr
|
||||
document_template_has_been_moved: Le modèle de document a été déplacé.
|
||||
unable_to_move_template_into_folder: Impossible de déplacer le modèle dans le dossier.
|
||||
template_has_been_unarchived: Le modèle a été désarchivé.
|
||||
submission_has_been_unarchived: La soumission a été désarchivée.
|
||||
unable_to_update_file: Impossible de télécharger le fichier.
|
||||
user_has_been_updated: "L'utilisateur a été mis à jour."
|
||||
unable_to_remove_user: Impossible de supprimer l'utilisateur.
|
||||
@@ -2540,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: '<b>E-mail envoyé</b> à %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
|
||||
@@ -2576,6 +2599,7 @@ fr: &fr
|
||||
read: Lire vos données
|
||||
|
||||
pt: &pt
|
||||
unarchive: Desarquivar
|
||||
awaiting_completion_by_the_other_party: "Aguardando a conclusão pela outra parte"
|
||||
enforce_recipients_order: 'Forçar a ordem dos recipientes'
|
||||
first_party: 'Primeira Parte'
|
||||
@@ -2677,7 +2701,7 @@ pt: &pt
|
||||
profile_details: Detalhes do perfil
|
||||
sign_up_with_google: Inscrever-se com Google
|
||||
sign_up_with_microsoft: Inscrever-se com Microsoft
|
||||
by_creating_an_account_you_agree_to_our_html: 'Ao criar uma conta, você concorda com nossa <a target="_blank" href="https://www.docuseal.co/privacy">Política de Privacidade</a> e <a target="_blank" href="https://www.docuseal.co/terms">Termos de Serviço</a>.'
|
||||
by_creating_an_account_you_agree_to_our_html: 'Ao criar uma conta, você concorda com nossa <a target="_blank" href="https://www.docuseal.com/privacy">Política de Privacidade</a> e <a target="_blank" href="https://www.docuseal.com/terms">Termos de Serviço</a>.'
|
||||
enter_email_to_continue: Insira o e-mail para continuar
|
||||
account: Conta
|
||||
preferences: Preferências
|
||||
@@ -3031,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
|
||||
@@ -3056,6 +3081,7 @@ pt: &pt
|
||||
document_template_has_been_moved: O modelo de documento foi movido.
|
||||
unable_to_move_template_into_folder: Não foi possível mover o modelo para a pasta.
|
||||
template_has_been_unarchived: O modelo foi desarquivado.
|
||||
submission_has_been_unarchived: A submissao foi desarquivada.
|
||||
unable_to_update_file: Não foi possível atualizar o arquivo.
|
||||
user_has_been_updated: O usuário foi atualizado.
|
||||
unable_to_remove_user: Não foi possível remover o usuário.
|
||||
@@ -3179,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: '<b>E-mail enviado</b> para %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
|
||||
@@ -3215,6 +3244,7 @@ pt: &pt
|
||||
read: Ler seus dados
|
||||
|
||||
de: &de
|
||||
unarchive: Wiederherstellen
|
||||
awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei"
|
||||
enforce_recipients_order: 'Empfängerreihenfolge durchsetzen'
|
||||
first_party: 'Erste Partei'
|
||||
@@ -3316,7 +3346,7 @@ de: &de
|
||||
profile_details: Profildetails
|
||||
sign_up_with_google: Mit Google registrieren
|
||||
sign_up_with_microsoft: Mit Microsoft registrieren
|
||||
by_creating_an_account_you_agree_to_our_html: 'Durch die Erstellung eines Kontos stimmst du unseren <a target="_blank" href="https://www.docuseal.co/privacy">Datenschutzrichtlinien</a> und <a target="_blank" href="https://www.docuseal.co/terms">Nutzungsbedingungen</a> zu.'
|
||||
by_creating_an_account_you_agree_to_our_html: 'Durch die Erstellung eines Kontos stimmst du unseren <a target="_blank" href="https://www.docuseal.com/privacy">Datenschutzrichtlinien</a> und <a target="_blank" href="https://www.docuseal.com/terms">Nutzungsbedingungen</a> zu.'
|
||||
enter_email_to_continue: E-Mail eingeben, um fortzufahren
|
||||
account: Konto
|
||||
preferences: Einstellungen
|
||||
@@ -3670,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
|
||||
@@ -3695,6 +3726,7 @@ de: &de
|
||||
document_template_has_been_moved: Die Dokumentvorlage wurde verschoben.
|
||||
unable_to_move_template_into_folder: Vorlage konnte nicht in den Ordner verschoben werden.
|
||||
template_has_been_unarchived: Die Vorlage wurde entarchiviert.
|
||||
submission_has_been_unarchived: Die Einreichung wurde wiederhergestellt.
|
||||
unable_to_update_file: Datei konnte nicht hochgeladen werden.
|
||||
user_has_been_updated: Der Benutzer wurde aktualisiert.
|
||||
unable_to_remove_user: Benutzer konnte nicht entfernt werden.
|
||||
@@ -3818,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: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
|
||||
@@ -3904,7 +3939,7 @@ pl:
|
||||
profile_details: Szczegóły profilu
|
||||
sign_up_with_google: Zarejestruj się przez Google
|
||||
sign_up_with_microsoft: Zarejestruj się przez Microsoft
|
||||
by_creating_an_account_you_agree_to_our_html: 'Tworząc konto, akceptujesz naszą <a target="_blank" href="https://www.docuseal.co/privacy">Politykę Prywatności</a> i <a target="_blank" href="https://www.docuseal.co/terms">Regulamin</a>.'
|
||||
by_creating_an_account_you_agree_to_our_html: 'Tworząc konto, akceptujesz naszą <a target="_blank" href="https://www.docuseal.com/privacy">Politykę Prywatności</a> i <a target="_blank" href="https://www.docuseal.com/terms">Regulamin</a>.'
|
||||
enter_email_to_continue: Wprowadź e-mail, aby kontynuować
|
||||
|
||||
uk:
|
||||
@@ -3958,7 +3993,7 @@ uk:
|
||||
profile_details: Дані профілю
|
||||
sign_up_with_google: Зареєструватися через Google
|
||||
sign_up_with_microsoft: Зареєструватися через Microsoft
|
||||
by_creating_an_account_you_agree_to_our_html: 'Створюючи акаунт, ви погоджуєтесь з нашою <a target="_blank" href="https://www.docuseal.co/privacy">Політикою конфіденційності</a> і <a target="_blank" href="https://www.docuseal.co/terms">Умовами надання послуг</a>.'
|
||||
by_creating_an_account_you_agree_to_our_html: 'Створюючи акаунт, ви погоджуєтесь з нашою <a target="_blank" href="https://www.docuseal.com/privacy">Політикою конфіденційності</a> і <a target="_blank" href="https://www.docuseal.com/terms">Умовами надання послуг</a>.'
|
||||
enter_email_to_continue: Введіть електронну пошту, щоб продовжити
|
||||
|
||||
cs:
|
||||
@@ -4012,7 +4047,7 @@ cs:
|
||||
profile_details: Detaily profilu
|
||||
sign_up_with_google: Zaregistrovat se pomocí Googlu
|
||||
sign_up_with_microsoft: Zaregistrovat se pomocí Microsoftu
|
||||
by_creating_an_account_you_agree_to_our_html: 'Vytvořením účtu souhlasíte s našimi <a target="_blank" href="https://www.docuseal.co/privacy">Zásadami ochrany osobních údajů</a> a <a target="_blank" href="https://www.docuseal.co/terms">Podmínkami služby</a>.'
|
||||
by_creating_an_account_you_agree_to_our_html: 'Vytvořením účtu souhlasíte s našimi <a target="_blank" href="https://www.docuseal.com/privacy">Zásadami ochrany osobních údajů</a> a <a target="_blank" href="https://www.docuseal.com/terms">Podmínkami služby</a>.'
|
||||
enter_email_to_continue: Zadejte e-mail pro pokračování
|
||||
|
||||
he:
|
||||
@@ -4066,7 +4101,7 @@ he:
|
||||
profile_details: פרטי הפרופיל
|
||||
sign_up_with_google: הירשם עם גוגל
|
||||
sign_up_with_microsoft: הירשם עם מיקרוסופט
|
||||
by_creating_an_account_you_agree_to_our_html: 'על ידי יצירת חשבון, אתה מסכים ל<a target="_blank" href="https://www.docuseal.co/privacy">מדיניות הפרטיות</a> ול<a target="_blank" href="https://www.docuseal.co/terms">תנאי השירות</a> שלנו.'
|
||||
by_creating_an_account_you_agree_to_our_html: 'על ידי יצירת חשבון, אתה מסכים ל<a target="_blank" href="https://www.docuseal.com/privacy">מדיניות הפרטיות</a> ול<a target="_blank" href="https://www.docuseal.com/terms">תנאי השירות</a> שלנו.'
|
||||
enter_email_to_continue: הכנס דוא"ל כדי להמשיך
|
||||
|
||||
nl:
|
||||
@@ -4120,7 +4155,7 @@ nl:
|
||||
profile_details: Profielgegevens
|
||||
sign_up_with_google: Aanmelden met Google
|
||||
sign_up_with_microsoft: Aanmelden met Microsoft
|
||||
by_creating_an_account_you_agree_to_our_html: 'Door een account aan te maken, ga je akkoord met ons <a target="_blank" href="https://www.docuseal.co/privacy">Privacybeleid</a> en onze <a target="_blank" href="https://www.docuseal.co/terms">Gebruiksvoorwaarden</a>.'
|
||||
by_creating_an_account_you_agree_to_our_html: 'Door een account aan te maken, ga je akkoord met ons <a target="_blank" href="https://www.docuseal.com/privacy">Privacybeleid</a> en onze <a target="_blank" href="https://www.docuseal.com/terms">Gebruiksvoorwaarden</a>.'
|
||||
enter_email_to_continue: Voer e-mail in om door te gaan
|
||||
|
||||
ar:
|
||||
@@ -4174,7 +4209,7 @@ ar:
|
||||
profile_details: تفاصيل الملف الشخصي
|
||||
sign_up_with_google: الاشتراك باستخدام جوجل
|
||||
sign_up_with_microsoft: الاشتراك باستخدام مايكروسوفت
|
||||
by_creating_an_account_you_agree_to_our_html: 'من خلال إنشاء حساب، فإنك توافق على <a target="_blank" href="https://www.docuseal.co/privacy">سياسة الخصوصية</a> و<a target="_blank" href="https://www.docuseal.co/terms">شروط الخدمة</a> الخاصة بنا.'
|
||||
by_creating_an_account_you_agree_to_our_html: 'من خلال إنشاء حساب، فإنك توافق على <a target="_blank" href="https://www.docuseal.com/privacy">سياسة الخصوصية</a> و<a target="_blank" href="https://www.docuseal.com/terms">شروط الخدمة</a> الخاصة بنا.'
|
||||
enter_email_to_continue: أدخل البريد الإلكتروني للمتابعة
|
||||
|
||||
ko:
|
||||
@@ -4228,7 +4263,7 @@ ko:
|
||||
profile_details: 프로필 세부 정보
|
||||
sign_up_with_google: Google로 가입
|
||||
sign_up_with_microsoft: Microsoft로 가입
|
||||
by_creating_an_account_you_agree_to_our_html: '계정을 생성함으로써, 귀하는 우리의 <a target="_blank" href="https://www.docuseal.co/privacy">개인정보 보호정책</a> 및 <a target="_blank" href="https://www.docuseal.co/terms">서비스 약관</a>에 동의하는 것입니다.'
|
||||
by_creating_an_account_you_agree_to_our_html: '계정을 생성함으로써, 귀하는 우리의 <a target="_blank" href="https://www.docuseal.com/privacy">개인정보 보호정책</a> 및 <a target="_blank" href="https://www.docuseal.com/terms">서비스 약관</a>에 동의하는 것입니다.'
|
||||
enter_email_to_continue: 계속하려면 이메일을 입력하세요
|
||||
|
||||
en-US:
|
||||
|
||||
+5
-3
@@ -69,7 +69,9 @@ Rails.application.routes.draw do
|
||||
resource :user_initials, only: %i[edit update destroy]
|
||||
resources :submissions_archived, only: %i[index], path: 'submissions/archived'
|
||||
resources :submissions, only: %i[index], controller: 'submissions_dashboard'
|
||||
resources :submissions, only: %i[show destroy]
|
||||
resources :submissions, only: %i[show destroy] do
|
||||
resources :unarchive, only: %i[create], controller: 'submissions_unarchive'
|
||||
end
|
||||
resources :submitters, only: %i[edit update]
|
||||
resources :console_redirect, only: %i[index]
|
||||
resources :upgrade, only: %i[index], controller: 'console_redirect'
|
||||
@@ -78,8 +80,8 @@ Rails.application.routes.draw do
|
||||
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]
|
||||
resources :webhook_preferences, only: %i[update]
|
||||
resource :templates_upload, only: %i[create]
|
||||
authenticated do
|
||||
resource :templates_upload, only: %i[show], path: 'new'
|
||||
|
||||
@@ -0,0 +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
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PopulateWebhookUrls < ActiveRecord::Migration[7.2]
|
||||
disable_ddl_transaction
|
||||
|
||||
class MigrationWebhookUrl < ActiveRecord::Base
|
||||
self.table_name = 'webhook_urls'
|
||||
|
||||
serialize :events, coder: JSON
|
||||
serialize :secret, coder: JSON
|
||||
|
||||
encrypts :url, :secret
|
||||
end
|
||||
|
||||
class MigrationEncryptedConfig < ActiveRecord::Base
|
||||
self.table_name = 'encrypted_configs'
|
||||
|
||||
encrypts :value
|
||||
serialize :value, coder: JSON
|
||||
end
|
||||
|
||||
class MigrationAccountConfig < ActiveRecord::Base
|
||||
self.table_name = 'account_configs'
|
||||
|
||||
serialize :value, coder: JSON
|
||||
end
|
||||
|
||||
def up
|
||||
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
|
||||
end
|
||||
|
||||
def down
|
||||
nil
|
||||
end
|
||||
end
|
||||
+2
-1
@@ -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 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_26_161207) do
|
||||
t.string "sha1", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
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
|
||||
|
||||
@@ -22,5 +22,6 @@ class Ability
|
||||
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
|
||||
@@ -78,30 +78,6 @@ module Accounts
|
||||
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?
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
module Docuseal
|
||||
URL_CACHE = ActiveSupport::Cache::MemoryStore.new
|
||||
PRODUCT_URL = 'https://www.docuseal.co'
|
||||
PRODUCT_URL = 'https://www.docuseal.com'
|
||||
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
|
||||
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
|
||||
PRODUCT_NAME = 'DocuSeal'
|
||||
|
||||
@@ -291,7 +291,16 @@ module Submissions
|
||||
),
|
||||
if field['type'].in?(%w[image signature initials stamp])
|
||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
||||
|
||||
image =
|
||||
begin
|
||||
Vips::Image.new_from_buffer(attachment.download, '').autorot
|
||||
rescue Vips::Error
|
||||
next unless attachment.content_type.starts_with?('image/')
|
||||
next if attachment.byte_size.zero?
|
||||
|
||||
raise
|
||||
end
|
||||
|
||||
scale = [600.0 / image.width, 600.0 / image.height].min
|
||||
|
||||
|
||||
@@ -103,6 +103,10 @@ module Submissions
|
||||
name: column_name(I18n.t('phone'), submitter_name, submitters_count),
|
||||
value: submitter.phone
|
||||
},
|
||||
{
|
||||
name: column_name(I18n.t('status'), submitter_name, submitters_count),
|
||||
value: submitter.status
|
||||
},
|
||||
{
|
||||
name: column_name(I18n.t('completed_at'), submitter_name, submitters_count),
|
||||
value: submitter.completed_at.to_s
|
||||
|
||||
@@ -10,7 +10,7 @@ module Submissions
|
||||
'Helvetica'
|
||||
end
|
||||
|
||||
SIGN_REASON = 'Signed by %<name>s with DocuSeal.co'
|
||||
SIGN_REASON = 'Signed by %<name>s with DocuSeal.com'
|
||||
|
||||
RTL_REGEXP = TextUtils::RTL_REGEXP
|
||||
|
||||
@@ -19,7 +19,6 @@ module Submissions
|
||||
MAX_PAGE_ROTATE = 20
|
||||
|
||||
A4_SIZE = [595, 842].freeze
|
||||
SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze
|
||||
|
||||
TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING'
|
||||
|
||||
@@ -273,7 +272,15 @@ module Submissions
|
||||
|
||||
attachments_data_cache[attachment.uuid] ||= attachment.download
|
||||
|
||||
image = Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot
|
||||
image =
|
||||
begin
|
||||
Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot
|
||||
rescue Vips::Error
|
||||
next unless attachment.content_type.starts_with?('image/')
|
||||
next if attachment.byte_size.zero?
|
||||
|
||||
raise
|
||||
end
|
||||
|
||||
scale = [(area['w'] * width) / image.width,
|
||||
(area['h'] * height) / image.height].min
|
||||
|
||||
@@ -6,10 +6,10 @@ module Submissions
|
||||
|
||||
TimestampError = Class.new(StandardError)
|
||||
|
||||
attr_reader :tsa_url
|
||||
attr_reader :tsa_url, :tsa_fallback_url
|
||||
|
||||
def initialize(tsa_url:)
|
||||
@tsa_url = tsa_url
|
||||
@tsa_url, @tsa_fallback_url = tsa_url.split(',')
|
||||
end
|
||||
|
||||
def finalize_objects(_signature_field, signature)
|
||||
@@ -37,7 +37,16 @@ module Submissions
|
||||
response = conn.post(uri.path, build_payload(digest.digest),
|
||||
'content-type' => 'application/timestamp-query')
|
||||
|
||||
raise TimestampError if response.status != 200 || response.body.blank?
|
||||
if response.status != 200 || response.body.blank?
|
||||
raise TimestampError if tsa_fallback_url.blank?
|
||||
|
||||
Rollbar.error('TimestampError: use fallback URL') if defined?(Rollbar)
|
||||
|
||||
response = Faraday.post(tsa_fallback_url, build_payload(digest.digest),
|
||||
'content-type' => 'application/timestamp-query')
|
||||
|
||||
raise TimestampError if response.status != 200 || response.body.blank?
|
||||
end
|
||||
|
||||
OpenSSL::Timestamp::Response.new(response.body).token.to_der
|
||||
end
|
||||
|
||||
@@ -14,14 +14,17 @@ module Submitters
|
||||
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 })
|
||||
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
|
||||
|
||||
update_submitter!(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
|
||||
|
||||
@@ -25,9 +25,15 @@ module Templates
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
|
||||
json[:documents] = template.schema.map do |item|
|
||||
json[:documents] = template.schema.filter_map do |item|
|
||||
attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
||||
|
||||
unless attachment
|
||||
Rollbar.error("Documents missing: #{template.id}") if defined?(Rollbar)
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
first_page_blob = preview_image_attachments.find { |e| e.record_id == attachment.id }&.blob
|
||||
first_page_blob ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: ['0.jpg', '0.png'] })&.blob
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :webhook_url do
|
||||
account
|
||||
url { Faker::Internet.url }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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.com 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.com Webhook',
|
||||
'X-Secret-Header' => 'secret_value'
|
||||
}
|
||||
).once
|
||||
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 '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
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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.com 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.com Webhook',
|
||||
'X-Secret-Header' => 'secret_value'
|
||||
}
|
||||
).once
|
||||
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 '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
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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.com 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.com Webhook',
|
||||
'X-Secret-Header' => 'secret_value'
|
||||
}
|
||||
).once
|
||||
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 '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
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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.com 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.com Webhook',
|
||||
'X-Secret-Header' => 'secret_value'
|
||||
}
|
||||
).once
|
||||
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 '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
|
||||
@@ -0,0 +1,92 @@
|
||||
# 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.com 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.com Webhook',
|
||||
'X-Secret-Header' => 'secret_value'
|
||||
}
|
||||
).once
|
||||
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 '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
|
||||
@@ -0,0 +1,92 @@
|
||||
# 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.com 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.com Webhook',
|
||||
'X-Secret-Header' => 'secret_value'
|
||||
}
|
||||
).once
|
||||
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 '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
|
||||
@@ -0,0 +1,92 @@
|
||||
# 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.com 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.com Webhook',
|
||||
'X-Secret-Header' => 'secret_value'
|
||||
}
|
||||
).once
|
||||
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 '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
|
||||
@@ -0,0 +1,90 @@
|
||||
# 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.com 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.com Webhook',
|
||||
'X-Secret-Header' => 'secret_value'
|
||||
}
|
||||
).once
|
||||
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 '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
|
||||
@@ -0,0 +1,90 @@
|
||||
# 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.com 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.com Webhook',
|
||||
'X-Secret-Header' => 'secret_value'
|
||||
}
|
||||
).once
|
||||
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 '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
|
||||
@@ -67,3 +67,19 @@ RSpec.configure do |config|
|
||||
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
|
||||
@@ -8,24 +8,110 @@ RSpec.describe 'Webhook Settings' do
|
||||
|
||||
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 'updates the webhook URL' do
|
||||
fill_in 'Webhook URL', with: 'https://example.com'
|
||||
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(EncryptedConfig, :count).by(1)
|
||||
end.to change(WebhookUrl, :count).by(1)
|
||||
|
||||
encrypted_config = EncryptedConfig.find_by(account:, key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||
webhook_url = account.webhook_urls.first
|
||||
|
||||
expect(encrypted_config.value).to eq('https://example.com')
|
||||
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 '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(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 eq({})
|
||||
|
||||
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'
|
||||
|
||||
webhook_url.reload
|
||||
|
||||
expect(webhook_url.secret).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user