Compare commits

...
35 Commits
Author SHA1 Message Date
Alex Turchyn 0c2abf211e Merge from docusealco/wip 2024-11-11 17:44:28 +02:00
Pete Matsyburka 8ecaa4fd7a adjust custom font size 2024-11-11 14:25:07 +02:00
Pete Matsyburka 6759c26d86 fix rubocop 2024-11-10 13:11:25 +02:00
Pete Matsyburka 76f1f140ac add field color 2024-11-10 12:48:53 +02:00
Pete Matsyburka ca7ab5aab7 adjust webhooks controller 2024-11-09 11:15:19 +02:00
Alex Turchyn fd3530ac62 update Webhooks UI 2024-11-09 00:41:46 +02:00
Pete Matsyburka 4ea863592b fix form 2024-11-08 22:12:07 +02:00
Pete Matsyburka 3f87d0336d do not use same reply to as to 2024-11-08 13:02:32 +02:00
Pete Matsyburka e251482481 unique party names 2024-11-08 12:15:13 +02:00
Pete Matsyburka c79fca75f3 add complete button 2024-11-07 23:41:49 +02:00
Alex Turchyn 0b0f0b497e fix acroform select field 2024-11-06 20:30:01 +02:00
Pete Matsyburka e9034d8da7 webhook secret priority 2024-11-06 14:06:33 +02:00
Pete Matsyburka 8fbb8b8f6e do not touch updated_at 2024-11-05 22:55:03 +02:00
Alex Turchyn 4635fdd32b remove replace_timestamps helper 2024-11-05 22:34:14 +02:00
Pete Matsyburka 6dd4382446 optimize webhooks query 2024-11-04 16:19:43 +02:00
Alex Turchyn 50acff2e81 Merge from docusealco/wip 2024-11-04 16:13:29 +02:00
Pete Matsyburka cb6d24dfca add canonical urls 2024-11-04 11:37:58 +02:00
Pete Matsyburka ab4a22d8ba fix test links 2024-11-03 13:43:28 +02:00
Pete Matsyburka bc40a15de1 use com 2024-11-03 10:19:59 +02:00
Pete Matsyburka 2542f26d96 adjust webhooks 2024-11-03 09:49:30 +02:00
Alex Turchyn 84edb6dbda use webhook urls instead of encrypted configs 2024-11-02 22:12:41 +02:00
Pete Matsyburka 88776e7316 Merge from docusealco/wip 2024-11-02 14:48:21 +02:00
Pete Matsyburka f669045362 redirect com 2024-11-02 09:48:47 +02:00
Pete Matsyburka 68d3efbdba use com 2024-11-02 09:02:50 +02:00
Pete Matsyburka 22d4833496 allow to unarchive submissions 2024-11-01 21:37:02 +02:00
Pete Matsyburka ff14caa9f7 fix image 2024-11-01 19:01:51 +02:00
Pete Matsyburka 05647135bc add status to export 2024-10-31 17:22:14 +02:00
Pete Matsyburka ae76c2a4a8 disable draw when type 2024-10-31 16:43:43 +02:00
Pete Matsyburka fb2b3d44fe fix bulk 2024-10-31 00:51:21 +02:00
Pete Matsyburka 3868557efd add define submitters 2024-10-30 23:07:10 +02:00
dependabot[bot] 9bef28c8ba Bump rexml from 3.3.8 to 3.3.9 in the bundler group across 1 directory (#50) 2024-10-30 12:12:36 +02:00
Pete Matsyburka f25fd3b03d do not edit if declined 2024-10-29 17:11:44 +02:00
Pete Matsyburka d1d7571ee4 disable decline email 2024-10-29 16:44:07 +02:00
Pete Matsyburka 6a298a068c fix updated templates 2024-10-29 12:11:21 +02:00
Pete Matsyburka d4aef79b68 use tsa fallback url 2024-10-29 08:29:26 +02:00
96 changed files with 1849 additions and 385 deletions

No files matched your search

+2 -2
View File
@@ -51,10 +51,10 @@ Metrics/AbcSize:
- spec/**/*
Metrics/ModuleLength:
Max: 500
Max: 1000
Metrics/ClassLength:
Max: 500
Max: 1000
RSpec/NestedGroups:
Max: 6
+1 -1
View File
@@ -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)
+10 -10
View File
@@ -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)
+10 -4
View File
@@ -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
+1 -1
View File
@@ -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
+4 -1
View File
@@ -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
+1 -1
View File
@@ -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',
+5 -1
View File
@@ -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)
+14 -4
View File
@@ -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
+16 -2
View File
@@ -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
+9 -16
View File
@@ -1,29 +1,22 @@
# 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'))
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
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
+37 -13
View File
@@ -1,36 +1,60 @@
# frozen_string_literal: true
class WebhookSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false
load_and_authorize_resource :webhook_url, parent: false, only: %i[index show new create update destroy]
load_and_authorize_resource :webhook_url, only: %i[resend], id_param: :webhook_id
def index
@webhook_urls = @webhook_urls.order(id: :desc)
@webhook_url = @webhook_urls.first_or_initialize
render @webhook_urls.size > 1 ? 'index' : 'show'
end
def show; end
def new; end
def create
@encrypted_config.assign_attributes(encrypted_config_params)
@webhook_url.save!
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
end
def update
@webhook_url.update!(update_params)
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
notice: I18n.t('webhook_url_has_been_updated'))
end
def destroy
@webhook_url.destroy!
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_deleted')
end
def resend
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 })
if submitter.blank? || @webhook_url.blank?
return redirect_back(fallback_location: settings_webhooks_path,
alert: I18n.t('unable_to_resend_webhook_request'))
end
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 create_params
params.require(:webhook_url).permit(:url, events: []).reverse_merge(events: [])
end
def encrypted_config_params
params.require(:encrypted_config).permit(:value)
def update_params
params.require(:webhook_url).permit(:url)
end
end
+4
View File
@@ -12,6 +12,10 @@ window.customElements.define('draw-signature', class extends HTMLElement {
this.pad = new SignaturePad(this.canvas)
if (this.dataset.color) {
this.pad.penColor = this.dataset.color
}
this.pad.addEventListener('endStroke', () => {
this.updateSubmitButtonVisibility()
})
+6 -3
View File
@@ -39,7 +39,6 @@
</template>
</div>
<div
v-if="isActive"
ref="scrollToElem"
class="absolute"
:style="{ top: scrollPadding }"
@@ -405,8 +404,12 @@ export default {
}
if (this.field.preferences?.font_size) {
style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)`
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)`
style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
}
if (this.field.preferences?.color) {
style.color = this.field.preferences.color
}
return style
+1 -1
View File
@@ -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') }}
+64 -4
View File
@@ -24,6 +24,44 @@
:fields="formulaFields"
:values="values"
/>
<Teleport
v-if="completeButtonToRef"
:to="completeButtonToRef"
>
<span
v-if="(emptyValueRequiredStep && ((stepFields.length - 1) !== currentStep || currentStepFields !== emptyValueRequiredStep)) || isCompleted"
class="tooltip-left"
:class="{ tooltip: !isCompleted }"
:data-tip="t('fill_all_required_fields_to_complete')"
>
<button
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center btn-disabled pointer-events-auto"
@click="[isFormVisible = true, !isCompleted && goToStep(stepFields.indexOf(emptyValueRequiredStep), true, false)]"
>
{{ t('complete') }}
</button>
</span>
<button
v-else
id="complete_form_button"
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
form="steps_form"
type="submit"
name="completed"
value="true"
:disabled="isSubmittingComplete"
>
<span class="flex items-center">
<IconInnerShadowTop
v-if="isSubmittingComplete"
class="mr-1 animate-spin w-5 h-5"
/>
<span>
{{ t('complete') }}
</span>
</span>
</button>
</Teleport>
<button
v-if="!isFormVisible"
id="expand_form_button"
@@ -74,6 +112,7 @@
>
<form
v-if="!isCompleted && !isInvite"
id="steps_form"
ref="form"
:action="submitPath"
method="post"
@@ -592,6 +631,11 @@ export default {
required: false,
default: false
},
completeButtonToRef: {
type: Object,
required: false,
default: null
},
attachments: {
type: Array,
required: false,
@@ -772,6 +816,7 @@ export default {
phoneVerifiedValues: {},
orientation: screen?.orientation?.type,
isSubmitting: false,
isSubmittingComplete: false,
submittedValues: {},
recalculateButtonDisabledKey: ''
}
@@ -780,6 +825,13 @@ export default {
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
},
emptyValueRequiredStep () {
return this.stepFields.find((fields, index) => {
return fields.some((f) => {
return f.required && isEmpty(this.values[f.uuid])
})
})
},
submitButtonText () {
if (this.alwaysMinimize) {
return this.t('submit')
@@ -1146,9 +1198,15 @@ export default {
scrollIntoArea (area) {
return this.$refs.areas.scrollIntoArea(area)
},
async submitStep () {
async submitStep (e) {
this.isSubmitting = true
const forceComplete = e?.submitter?.getAttribute('name') === 'completed'
if (forceComplete) {
this.isSubmittingComplete = true
}
const submitStep = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
@@ -1157,7 +1215,7 @@ export default {
stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => {
if (index >= submitStep) {
if (forceComplete ? index === submitStep : index >= submitStep) {
return false
}
@@ -1167,7 +1225,7 @@ export default {
})
const formData = new FormData(this.$refs.form)
const isLastStep = submitStep === this.stepFields.length - 1
const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
formData.append('completed', 'true')
@@ -1192,7 +1250,7 @@ export default {
return Promise.reject(new Error(data.error))
}
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[submitStep + 1]
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
if (nextStep) {
if (this.alwaysMinimize) {
@@ -1213,6 +1271,7 @@ export default {
console.error(error)
}).finally(() => {
this.isSubmitting = false
this.isSubmittingComplete = false
})
}).catch(error => {
if (error?.message === 'Image too small') {
@@ -1222,6 +1281,7 @@ export default {
}
}).finally(() => {
this.isSubmitting = false
this.isSubmittingComplete = false
})
},
minimizeForm () {
+13
View File
@@ -1,5 +1,6 @@
const en = {
complete: 'Complete',
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
sign_and_complete: 'Sign and Complete',
text: 'Text',
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
@@ -93,6 +94,7 @@ const en = {
const es = {
complete: 'Completar',
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
sign_and_complete: 'Firmar y Completar',
invite: 'Invitar',
email: 'Correo electrónico',
@@ -185,6 +187,7 @@ const es = {
const it = {
complete: 'Completa',
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
sign_and_complete: 'Firma e Completa',
invite: 'Invita',
email: 'Email',
@@ -277,6 +280,7 @@ const it = {
const de = {
complete: 'Abschließen',
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
sign_and_complete: 'Signieren und Abschließen',
invite: 'Einladen',
email: 'E-Mail',
@@ -369,6 +373,7 @@ const de = {
const fr = {
complete: 'Terminer',
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
sign_and_complete: 'Signer et Terminer',
invite: 'Inviter',
email: 'Courriel',
@@ -461,6 +466,7 @@ const fr = {
const pl = {
complete: 'Zakończ',
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
sign_and_complete: 'Podpisz i zakończ',
invite: 'Zaproś',
email: 'E-mail',
@@ -553,6 +559,7 @@ const pl = {
const uk = {
complete: 'Завершити',
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
sign_and_complete: 'Підписати і завершити',
invite: 'Запросити',
email: 'Електронна пошта',
@@ -645,6 +652,7 @@ const uk = {
const cs = {
complete: 'Dokončit',
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
sign_and_complete: 'Podepsat a dokončit',
invite: 'Pozvat',
email: 'E-mail',
@@ -737,6 +745,7 @@ const cs = {
const pt = {
complete: 'Completar',
preencher_todos_os_campos_obrigatórios_para_concluir: 'Preencher todos os campos obrigatórios para concluir',
sign_and_complete: 'Assinar e Completar',
invite: 'Convidar',
email: 'E-mail',
@@ -829,6 +838,7 @@ const pt = {
const he = {
complete: 'השלם',
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
sign_and_complete: 'חתום והשלם',
invite: 'הזמן',
email: 'דוא"ל',
@@ -922,6 +932,7 @@ const he = {
const nl = {
complete: 'Voltooien',
vul_alle_verplichte_velden_in_om_te_voltooien: 'Vul alle verplichte velden in om te voltooien',
sign_and_complete: 'Ondertekenen en voltooien',
invite: 'Uitnodigen',
email: 'E-mail',
@@ -1015,6 +1026,7 @@ const nl = {
const ar = {
complete: 'اكتمال',
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
sign_and_complete: 'التوقيع والاكتمال',
invite: 'دعوة',
email: 'البريد الإلكتروني',
@@ -1107,6 +1119,7 @@ const ar = {
const ko = {
complete: '완료',
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
sign_and_complete: '서명하고 완료하기',
invite: '초대하기',
email: '이메일',
@@ -226,6 +226,10 @@ export default {
if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas)
if (this.field.preferences?.color) {
this.pad.penColor = this.field.preferences.color
}
this.pad.addEventListener('beginStroke', () => {
this.isInitialsStarted = true
@@ -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">
@@ -419,6 +423,10 @@ export default {
if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas)
if (this.field.preferences?.color) {
this.pad.penColor = this.field.preferences.color
}
this.pad.addEventListener('endStroke', () => {
this.isSignatureStarted = true
+1
View File
@@ -175,6 +175,7 @@
<div
ref="textContainer"
class="flex items-center px-0.5"
:style="{ color: field.preferences?.color }"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
>
<IconCheck
@@ -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>
@@ -79,7 +79,7 @@
:stroke-width="1.6"
/>
<span class="py-1">
{{ t('add') }} {{ names[submitters.length] }}
{{ t('add') }} {{ names[lastPartyIndex] }}
</span>
</a>
</li>
@@ -202,7 +202,7 @@
:stroke-width="1.6"
/>
<span class="py-1">
{{ t('add') }} {{ names[submitters.length] }}
{{ t('add') }} {{ names[lastPartyIndex] }}
</span>
</a>
</li>
@@ -321,6 +321,15 @@ export default {
this.t('twentieth_party')
]
},
lastPartyIndex () {
const index = Math.max(...this.submitters.map((s) => this.names.indexOf(s.name)))
if (index !== -1) {
return index + 1
} else {
return this.submitters.length
}
},
selectedSubmitter () {
return this.submitters.find((e) => e.uuid === this.modelValue)
}
@@ -353,7 +362,7 @@ export default {
},
addSubmitter () {
const newSubmitter = {
name: this.names[this.submitters.length],
name: this.names[this.lastPartyIndex],
uuid: v4()
}
+1 -1
View File
@@ -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>
+5 -22
View File
@@ -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,33 +5,32 @@ 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,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error
nil
end
@@ -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,32 +5,28 @@ 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,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error
nil
end
@@ -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
})
+5 -3
View File
@@ -138,9 +138,11 @@ class SubmitterMailer < ApplicationMailer
private
def build_submitter_reply_to(submitter)
reply_to =
submitter.preferences['reply_to'].presence ||
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
reply_to = submitter.preferences['reply_to'].presence
if reply_to.blank? && (submitter.submission.created_by_user || submitter.template.author)&.email != submitter.email
reply_to = (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
end
return nil if reply_to.to_s.match?(NO_REPLY_REGEXP)
-1
View File
@@ -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'
+1 -3
View File
@@ -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
+1
View File
@@ -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
+16 -2
View File
@@ -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
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" x-bind:width="size" x-bind:height="size" viewBox="0 0 24 24" fill="none" stroke="currentColor" x-bind:stroke-width="stroke" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6z"></path>
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path>
<path d="M8 11v-4a4 4 0 1 1 8 0v4"></path>
</svg>

After

Width:  |  Height:  |  Size: 496 B

+3
View File
@@ -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'] %>
+1 -1
View File
@@ -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 -1
View File
@@ -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>
+3 -3
View File
@@ -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>
+2 -1
View File
@@ -1,5 +1,6 @@
<% align = field.dig('preferences', 'align') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size']}pt); line-height: `clamp(6pt, 2.0vw, #{field['preferences']['font_size'].to_i + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% color = field.dig('preferences', 'color') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
+7 -3
View File
@@ -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>
@@ -15,9 +15,10 @@
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head>
<body>
<draw-signature data-slug="<%= params[:slug] %>" class="flex items-center h-screen p-2 justify-center">
<% field = (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) } %>
<draw-signature data-slug="<%= params[:slug] %>" data-color="<%= field.dig('preferences', 'color') %>" class="flex items-center h-screen p-2 justify-center">
<%= form_for '', url: submit_form_path(params[:slug]), html: { style: 'max-width: 900px; width: 100%; margin-bottom: 120px' }, method: :put do |f| %>
<input value="" type="hidden" name="values[<%= (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) }['uuid'] %>]">
<input value="" type="hidden" name="values[<%= field['uuid'] %>]">
<div class="font-semibold text-4xl text-center w-full mb-2">
<%= t('draw_signature') %>
</div>
+2 -2
View File
@@ -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 %>
+34
View File
@@ -0,0 +1,34 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="flex-grow min-w-0">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
<h1 class="text-4xl font-bold">Webhooks</h1>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<%= render 'shared/test_mode_toggle' %>
<% if @webhook_url.persisted? %>
<%= link_to new_settings_webhook_path, class: 'md:ml-3 btn bg-white btn-outline btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_webhook') %></span>
<% end %>
<% end %>
</div>
</div>
<div class="space-y-4">
<% @webhook_urls.each do |webhook_url| %>
<%= link_to settings_webhook_path(webhook_url), class: 'card bg-base-200' do %>
<div class="card-body p-6 min-w-0">
<p class="flex items-center space-x-1">
<%= svg_icon('world', class: 'w-6 h-6 shrink-0') %>
<span class="text-xl font-semibold truncate"><%= webhook_url.url %></span>
</p>
<div class="flex flex-wrap gap-2 mt-2">
<% webhook_url.events.each do |event| %>
<span class="badge badge-outline"><%= event %></span>
<% end %>
</div>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
+29
View File
@@ -0,0 +1,29 @@
<%= render 'shared/turbo_modal', title: t('new_webhook') do %>
<%= form_for @webhook_url, url: settings_webhooks_path, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
<div class="space-y-4">
<div class="form-control">
<%= f.label :url, 'Webhook URL', class: 'label' %>
<%= f.url_field :url, class: 'base-input', placeholder: 'https://example.com/hook', required: true %>
</div>
<div class="space-y-4">
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-2">
<%= f.collection_check_boxes(:events, events, :to_s, :to_s, include_hidden: false) do |b| %>
<div class="flex">
<label class="flex items-center space-x-2">
<%= b.check_box class: 'base-checkbox', checked: @webhook_url.events.include?(b.value) %>
<span>
<%= b.label %>
</span>
</label>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
</div>
<% end %>
<% end %>
+71 -30
View File
@@ -1,40 +1,81 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="flex-grow">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
<h1 class="text-4xl font-bold">Webhooks</h1>
<%= render 'shared/test_mode_toggle' %>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
<h1 class="text-4xl font-bold">Webhook</h1>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<% if params[:action] == 'index' %>
<%= render 'shared/test_mode_toggle' %>
<% end %>
<% if @webhook_url.persisted? && params[:action] == 'index' %>
<%= link_to new_settings_webhook_path, class: 'md:ml-3 btn bg-white btn-outline btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_webhook') %></span>
<% end %>
<% end %>
</div>
</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' %>
<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' %>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end md:relative">
<%= label_tag :url, 'Webhook URL', class: 'text-sm font-semibold' %>
<% if @webhook_url.persisted? %>
<div class="flex items-center space-x-2 md:absolute md:right-0">
<%= link_to webhook_secret_path(@webhook_url), class: 'btn btn-outline btn-sm bg-white', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('lock', class: 'w-4 h-4') %>
<span><%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %></span>
<% end %>
<div class="tooltip tooltip-left md:tooltip-top" data-tip="<%= t('delete_webhook') %>">
<%= button_to settings_webhook_path(@webhook_url), class: 'btn btn-warning btn-sm', method: :delete, data: { turbo_confirm: t('are_you_sure_') } do %>
<span><%= t('delete') %></span>
<% end %>
</div>
</div>
<% end %>
</div>
<%= form_for @webhook_url, url: @webhook_url.persisted? ? settings_webhook_path(@webhook_url) : settings_webhooks_path, html: { autocomplete: 'off' } do |f| %>
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-2">
<%= f.url_field :url, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook', required: true %>
<%= 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>
<% unless @webhook_url.persisted? %>
<div class="space-y-4 mt-4">
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-2">
<%= f.collection_check_boxes(:events, events, :to_s, :to_s, include_hidden: false) do |b| %>
<div class="flex">
<label class="flex items-center space-x-2">
<%= b.check_box class: 'base-checkbox', checked: @webhook_url.events.include?(b.value) %>
<span>
<%= b.label %>
</span>
</label>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
</div>
<% end %>
<% end %>
<% if @webhook_url.persisted? %>
<%= form_for @webhook_url, url: webhook_preference_path(@webhook_url), method: :put, html: { autocomplete: 'off', class: 'mt-2' } do |f| %>
<div class="space-y-4">
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-4">
<% events.each do |event| %>
<%= f.fields_for :events do |ff| %>
<div class="flex">
<label class="flex items-center cursor-pointer">
<%= ff.check_box event, class: 'base-checkbox', checked: @webhook_url.events.include?(event), onchange: 'this.form.requestSubmit()' %>
<span class="ml-2"><%= event %></span>
</label>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
</div>
@@ -43,12 +84,12 @@
<div class="space-y-4 mt-4">
<div class="collapse collapse-open bg-base-200 px-1">
<div class="p-4 text-xl font-medium">
<div class="flex items-center justify-between gap-2">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center md:h-8">
<span>
<%= t('submission_example_payload') %>
</span>
<% if @encrypted_config.value.present? %>
<%= 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 %>
<% 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_webhook_resend_path(@webhook_url), class: 'btn btn-neutral btn-outline btn-sm', method: :post %>
<% end %>
</div>
</div>
+80 -21
View File
@@ -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.
@@ -508,13 +511,17 @@ en: &en
webhook_secret_has_been_saved: Webhook Secret has been saved.
webhook_url_has_been_saved: Webhook URL has been saved.
webhook_request_has_been_sent: Webhook request has been sent.
webhook_url_has_been_updated: Webhook URL has been updated.
webhook_url_has_been_deleted: Webhook URL has been deleted.
unable_to_resend_webhook_request: Unable to resend webhook request.
new_webhook: New Webhook
delete_webhook: Delete webhook
count_submissions_have_been_created: '%{count} submissions have been created.'
gmail_has_been_connected: Gmail has been connected
microsoft_account_has_been_connected: Microsoft Account has been connected
sms_length_cant_be_longer_than_120_bytes: SMS length can't be longer than 120 bytes
connected_successfully: Connected successfully.
user_nameid_not_found: 'User %{nameid} not found.'
webhook_request_has_been_sent: Webhook request has been sent.
sso_settings_have_been_updated: SSO settings have been updated.
sms_has_been_sent: SMS has been sent.
account_has_been_created: Account has been created.
@@ -621,7 +628,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 +667,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 +769,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 +1123,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 +1149,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.
@@ -1148,13 +1160,17 @@ es: &es
webhook_secret_has_been_saved: El secreto del Webhook ha sido guardado.
webhook_url_has_been_saved: La URL del Webhook ha sido guardada.
webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada.
webhook_url_has_been_updated: La URL del Webhook ha sido actualizada.
webhook_url_has_been_deleted: La URL del Webhook ha sido eliminada.
unable_to_resend_webhook_request: No se pudo reenviar la solicitud del webhook.
new_webhook: Nuevo Webhook
delete_webhook: Eliminar webhook
count_submissions_have_been_created: '%{count} envíos han sido creados.'
gmail_has_been_connected: Gmail ha sido conectado.
microsoft_account_has_been_connected: La cuenta de Microsoft ha sido conectada.
sms_length_cant_be_longer_than_120_bytes: La longitud del SMS no puede ser mayor a 120 bytes.
connected_successfully: Conectado con éxito.
user_nameid_not_found: 'Usuario %{nameid} no encontrado.'
webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada.
sso_settings_have_been_updated: La configuración de SSO ha sido actualizada.
sms_has_been_sent: El SMS ha sido enviado.
account_has_been_created: La cuenta ha sido creada.
@@ -1261,6 +1277,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 +1316,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 +1418,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 +1772,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 +1798,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.
@@ -1787,13 +1809,17 @@ it: &it
webhook_secret_has_been_saved: Il segreto del Webhook è stato salvato.
webhook_url_has_been_saved: "L'URL del Webhook è stato salvato."
webhook_request_has_been_sent: La richiesta del Webhook è stata inviata.
webhook_url_has_been_updated: "L'URL del Webhook è stata aggiornata."
webhook_url_has_been_deleted: "L'URL del Webhook è stata eliminata."
unable_to_resend_webhook_request: Impossibile reinviare la richiesta del webhook.
new_webhook: Nuovo Webhook
delete_webhook: Elimina webhook
count_submissions_have_been_created: '%{count} invii sono stati creati.'
gmail_has_been_connected: Gmail è stato connesso.
microsoft_account_has_been_connected: "L'account Microsoft è stato connesso."
sms_length_cant_be_longer_than_120_bytes: "La lunghezza dell'SMS non può superare i 120 byte."
connected_successfully: Collegamento avvenuto con successo.
user_nameid_not_found: 'Utente %{nameid} non trovato.'
webhook_request_has_been_sent: La richiesta del Webhook è stata inviata.
sso_settings_have_been_updated: Le impostazioni SSO sono state aggiornate.
sms_has_been_sent: "L'SMS è stato inviato."
account_has_been_created: "L'account è stato creato."
@@ -1900,6 +1926,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 +1965,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 +2068,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 +2422,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 +2448,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.
@@ -2427,13 +2459,17 @@ fr: &fr
webhook_secret_has_been_saved: Le secret du Webhook a été enregistré.
webhook_url_has_been_saved: "L'URL du Webhook a été enregistrée."
webhook_request_has_been_sent: La demande du Webhook a été envoyée.
webhook_url_has_been_updated: "L'URL du Webhook a été mise à jour."
webhook_url_has_been_deleted: "L'URL du Webhook a été supprimée."
unable_to_resend_webhook_request: Impossible de renvoyer la requête du webhook.
new_webhook: Nouveau Webhook
delete_webhook: Supprimer le webhook
count_submissions_have_been_created: '%{count} soumissions ont été créées.'
gmail_has_been_connected: Gmail a été connecté.
microsoft_account_has_been_connected: Le compte Microsoft a été connecté.
sms_length_cant_be_longer_than_120_bytes: La longueur du SMS ne peut pas dépasser 120 octets.
connected_successfully: Connecté avec succès.
user_nameid_not_found: 'Utilisateur %{nameid} introuvable.'
webhook_request_has_been_sent: La demande du Webhook a été envoyée.
sso_settings_have_been_updated: Les paramètres de SSO ont été mis à jour.
sms_has_been_sent: Le SMS a été envoyé.
account_has_been_created: Le compte a été créé.
@@ -2540,6 +2576,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 +2615,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 +2717,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 +3071,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 +3097,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.
@@ -3066,13 +3108,17 @@ pt: &pt
webhook_secret_has_been_saved: O segredo do Webhook foi salvo.
webhook_url_has_been_saved: A URL do Webhook foi salva.
webhook_request_has_been_sent: A solicitação do Webhook foi enviada.
webhook_url_has_been_updated: URL do Webhook foi atualizada.
webhook_url_has_been_deleted: URL do Webhook foi excluída.
unable_to_resend_webhook_request: Não foi possível reenviar a solicitação do webhook.
new_webhook: Novo Webhook
delete_webhook: Excluir webhook
count_submissions_have_been_created: '%{count} submissões foram criadas.'
gmail_has_been_connected: O Gmail foi conectado
microsoft_account_has_been_connected: A conta da Microsoft foi conectada
sms_length_cant_be_longer_than_120_bytes: O comprimento do SMS não pode ultrapassar 120 bytes
connected_successfully: Conectado com sucesso.
user_nameid_not_found: 'Usuário %{nameid} não encontrado.'
webhook_request_has_been_sent: A solicitação do Webhook foi enviada.
sso_settings_have_been_updated: As configurações de SSO foram atualizadas.
sms_has_been_sent: O SMS foi enviado.
account_has_been_created: A conta foi criada.
@@ -3179,6 +3225,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 +3264,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 +3366,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
@@ -3669,7 +3719,8 @@ de: &de
api_key: API-Schlüssel
logo: Logo
back: Zurück
add_secret: Geheimnis hinzufügen
add_secret: Geheimnis hinzuf
edit_secret: Geheimnis bearb
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 +3746,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.
@@ -3705,13 +3757,17 @@ de: &de
webhook_secret_has_been_saved: Das Webhook-Geheimnis wurde gespeichert.
webhook_url_has_been_saved: Die Webhook-URL wurde gespeichert.
webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet.
webhook_url_has_been_updated: Webhook-URL wurde aktualisiert.
webhook_url_has_been_deleted: Webhook-URL wurde gelöscht.
unable_to_resend_webhook_request: Webhook-Anfrage konnte nicht erneut gesendet werden.
new_webhook: Neuer Webhook
delete_webhook: Webhook löschen
count_submissions_have_been_created: '%{count} Einreichungen wurden erstellt.'
gmail_has_been_connected: Gmail wurde verbunden.
microsoft_account_has_been_connected: Microsoft-Konto wurde verbunden.
sms_length_cant_be_longer_than_120_bytes: Die SMS-Länge darf 120 Bytes nicht überschreiten.
connected_successfully: Erfolgreich verbunden.
user_nameid_not_found: 'Benutzer %{nameid} nicht gefunden.'
webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet.
sso_settings_have_been_updated: Die SSO-Einstellungen wurden aktualisiert.
sms_has_been_sent: Die SMS wurde gesendet.
account_has_been_created: Das Konto wurde erstellt.
@@ -3818,6 +3874,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 +3963,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 +4017,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 +4071,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 +4125,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 +4179,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 +4233,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 +4287,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:
+8 -4
View File
@@ -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'
@@ -166,7 +168,9 @@ Rails.application.routes.draw do
defaults: { status: :integration }
resource :personalization, only: %i[show create], controller: 'personalization_settings'
resources :api, only: %i[index create], controller: 'api_settings'
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
post :resend
end
resource :account, only: %i[show update destroy]
resources :profile, only: %i[index] do
collection do
@@ -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
View File
@@ -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
+1
View File
@@ -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
-24
View File
@@ -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
View File
@@ -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'
+10 -1
View File
@@ -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
+4
View File
@@ -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
+22 -5
View File
@@ -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'
@@ -69,7 +68,7 @@ module Submissions
name: item['name'])
end
return result_attachments.map { |e| e.tap(&:save!) } if image_pdfs.size < 2
return ApplicationRecord.no_touching { result_attachments.map { |e| e.tap(&:save!) } } if image_pdfs.size < 2
images_pdf =
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
@@ -88,7 +87,9 @@ module Submissions
name: template.name
)
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
ApplicationRecord.no_touching do
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
end
end
def generate_pdfs(submitter)
@@ -178,6 +179,8 @@ module Submissions
font_size = preferences_font_size
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
fill_color = field.dig('preferences', 'color').presence
font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME)
value = submitter.values[field['uuid']]
@@ -273,7 +276,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
@@ -371,6 +382,7 @@ module Submissions
next if char.blank?
text = HexaPDF::Layout::TextFragment.create(char, font:,
fill_color:,
font_size:)
line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -378,6 +390,7 @@ module Submissions
if preferences_font_size.blank? && line_height > (area['h'] * height)
text = HexaPDF::Layout::TextFragment.create(char,
font:,
fill_color:,
font_size: (font_size / 1.4).to_i)
line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -386,6 +399,7 @@ module Submissions
if preferences_font_size.blank? && line_height > (area['h'] * height)
text = HexaPDF::Layout::TextFragment.create(char,
font:,
fill_color:,
font_size: (font_size / 1.9).to_i)
line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -405,6 +419,7 @@ module Submissions
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
text = HexaPDF::Layout::TextFragment.create(value, font:,
fill_color:,
font_size:)
lines = layouter.fit([text], area['w'] * width, height).lines
@@ -413,6 +428,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(value,
font:,
fill_color:,
font_size: (font_size / 1.4).to_i)
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
@@ -423,6 +439,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(value,
font:,
fill_color:,
font_size: (font_size / 1.9).to_i)
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
+12 -3
View File
@@ -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
+5 -2
View File
@@ -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
+20 -3
View File
@@ -6,6 +6,20 @@ module Templates
FIELD_NAME_REGEXP = /\A(?=.*\p{L})[\p{L}\d\s-]+\z/
SKIP_FIELD_DESCRIPTION = %w[undefined].freeze
SELECT_PLACEHOLDER_REGEXP = /\b(
Select |
Choose |
Wählen |
Auswählen |
Sélectionner|
Choisir |
Seleccionar |
Elegir |
Seleziona |
Scegliere |
Selecionar |
Escolher
)\b/ix
module_function
@@ -143,8 +157,8 @@ module Templates
{
**attrs,
type: 'select',
options: build_options(field[:Opt]),
default_value: field.field_value
options: build_options(field[:Opt], 'select'),
default_value: field.field_value.to_s.match?(SELECT_PLACEHOLDER_REGEXP) ? nil : field.field_value
}
elsif field.field_type == :Ch && field.concrete_field_type == :multi_select && field[:Opt].present?
{
@@ -178,11 +192,14 @@ module Templates
def build_options(values, type = nil)
is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1
values.map do |option|
values.filter_map do |option|
is_option_number = option.is_a?(Symbol) && option.to_s.match?(/\A\d+\z/)
option = option[1] if option.is_a?(Array) && option.size == 2
option = option.encode('utf-8', invalid: :replace, undef: :replace, replace: '') if option.is_a?(String)
next if type == 'select' && option.to_s.match?(SELECT_PLACEHOLDER_REGEXP)
{
uuid: SecureRandom.uuid,
value: is_option_number || is_skip_single_value ? '' : option
+9
View File
@@ -122,6 +122,15 @@ module Templates
io = StringIO.new
pdf.acro_form.each_field do |field|
next if field.field_type != :Ch ||
field[:Opt].blank? ||
%i[combo_box editable_combo_box].exclude?(field.concrete_field_type) ||
!field.field_value.to_s.match?(FindAcroFields::SELECT_PLACEHOLDER_REGEXP)
field[:V] = ''
end
pdf.acro_form.create_appearances(force: true) if pdf.acro_form[:NeedAppearances]
pdf.acro_form.flatten
+7 -1
View File
@@ -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
+27
View File
@@ -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? || account_id == 1
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
+8
View File
@@ -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: {
'event_type' => 'form.completed',
'timestamp' => /.*/,
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
},
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: {
'event_type' => 'form.completed',
'timestamp' => /.*/,
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
},
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: {
'event_type' => 'form.declined',
'timestamp' => /.*/,
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
},
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: {
'event_type' => 'form.declined',
'timestamp' => /.*/,
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
},
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: {
'event_type' => 'form.started',
'timestamp' => /.*/,
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
},
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: {
'event_type' => 'form.started',
'timestamp' => /.*/,
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
},
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: {
'event_type' => 'form.viewed',
'timestamp' => /.*/,
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
},
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: {
'event_type' => 'form.viewed',
'timestamp' => /.*/,
'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
},
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: {
'event_type' => 'submission.archived',
'timestamp' => /.*/,
'data' => JSON.parse(submission.reload.as_json(only: %i[id archived_at]).to_json)
},
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: {
'event_type' => 'submission.archived',
'timestamp' => /.*/,
'data' => JSON.parse(submission.reload.as_json(only: %i[id archived_at]).to_json)
},
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: {
'event_type' => 'submission.completed',
'timestamp' => /.*/,
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
},
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: {
'event_type' => 'submission.completed',
'timestamp' => /.*/,
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
},
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: {
'event_type' => 'submission.created',
'timestamp' => /.*/,
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
},
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: {
'event_type' => 'submission.created',
'timestamp' => /.*/,
'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
},
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: {
'event_type' => 'template.created',
'timestamp' => /.*/,
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
},
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: {
'event_type' => 'template.created',
'timestamp' => /.*/,
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
},
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: {
'event_type' => 'template.updated',
'timestamp' => /.*/,
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
},
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: {
'event_type' => 'template.updated',
'timestamp' => /.*/,
'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
},
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
+3 -1
View File
@@ -44,7 +44,9 @@ RSpec.describe 'Template' do
it 'archives a template' do
expect do
click_button 'Archive'
accept_confirm('Are you sure?') do
click_button 'Archive'
end
end.to change { Template.active.count }.by(-1)
expect(page).to have_content('Template has been archived')
+167 -9
View File
@@ -8,24 +8,182 @@ RSpec.describe 'Webhook Settings' do
before do
sign_in(user)
end
it 'shows webhook settings page with empty form when there are no webhooks' do
visit settings_webhooks_path
end
it 'shows webhook settings page' do
expect(page).to have_content('Webhooks')
expect(page).to have_field('Webhook URL')
expect(page).to have_content('Webhook')
expect(page).to have_content('Webhook URL')
expect(page).to have_field('webhook_url[url]', type: 'url')
expect(page).to have_button('Save')
WebhookUrl::EVENTS.each do |event|
expect(page).to have_field(event, type: 'checkbox')
end
end
it 'updates the webhook URL' do
fill_in 'Webhook URL', with: 'https://example.com'
it 'shows list of webhooks when there are more than one' do
webhook_urls = create_list(:webhook_url, 2, account:)
visit settings_webhooks_path
expect(page).to have_content('Webhooks')
expect(page).to have_link('New Webhook')
webhook_urls.each do |webhook_url|
expect(page).to have_content(webhook_url.url)
within("a[href='#{settings_webhook_path(webhook_url)}']") do
webhook_url.events.each do |event|
expect(page).to have_content(event)
end
end
end
end
it 'shows webhook settings page with pre-filled form when there is one webhook' do
webhook_url = create(:webhook_url, account:)
visit settings_webhooks_path
expect(page).to have_content('Webhook')
expect(page).to have_field('webhook_url[url]', type: 'url', with: webhook_url.url)
expect(page).to have_button('Save')
expect(page).to have_button('Delete')
expect(page).to have_link('Add Secret')
WebhookUrl::EVENTS.each do |event|
expect(page).to have_field(event, type: 'checkbox', checked: webhook_url.events.include?(event))
end
end
it 'creates the webhook' do
visit settings_webhooks_path
fill_in 'webhook_url[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')
expect(page).to have_content('Webhook URL has been saved.')
expect(page.current_path).to eq(settings_webhooks_path)
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[url]', with: 'https://example.org/webhook'
click_button 'Save'
webhook_url.reload
expect(webhook_url.url).to eq('https://example.org/webhook')
expect(page).to have_content('Webhook URL has been updated.')
expect(page.current_path).to eq(settings_webhooks_path)
end
it 'deletes the webhook' do
create(:webhook_url, account:)
visit settings_webhooks_path
expect do
accept_confirm('Are you sure?') do
click_button 'Delete'
end
end.to change(WebhookUrl, :count).by(-1)
expect(page).to have_content('Webhook URL has been deleted.')
expect(page.current_path).to eq(settings_webhooks_path)
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
expect(page).to have_link('Edit Secret')
expect(page).to have_content('Webhook Secret has been saved.')
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
expect(page).to have_link('Add Secret')
expect(page).to have_content('Webhook Secret has been saved.')
end
context 'when testing the webhook' do
let!(:webhook_url) { create(:webhook_url, 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
it 'sends the webhook request' do
visit settings_webhooks_path
expect do
click_button 'Test Webhook'
end.to change(SendFormCompletedWebhookRequestJob.jobs, :size).by(1)
args = SendFormCompletedWebhookRequestJob.jobs.last['args'].first
expect(args['webhook_url_id']).to eq(webhook_url.id)
expect(args['submitter_id']).to eq(submitter.id)
expect(page).to have_content('Webhook request has been sent.')
end
end
end