Compare commits

...
21 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
72 changed files with 1696 additions and 334 deletions

No files matched your search

+2 -2
View File
@@ -51,10 +51,10 @@ Metrics/AbcSize:
- spec/**/* - spec/**/*
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 500 Max: 1000
Metrics/ClassLength: Metrics/ClassLength:
Max: 500 Max: 1000
RSpec/NestedGroups: RSpec/NestedGroups:
Max: 6 Max: 6
+10 -4
View File
@@ -67,15 +67,18 @@ module Api
submissions = create_submissions(@template, params) submissions = create_submissions(@template, params)
submissions.each do |submission| WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id }) submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
'webhook_url_id' => webhook_url.id)
end
end end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
submissions.each do |submission| submissions.each do |submission|
submission.submitters.each do |submitter| 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
end end
@@ -93,7 +96,10 @@ module Api
else else
@submission.update!(archived_at: Time.current) @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 end
render json: @submission.as_json(only: %i[id archived_at]) 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) 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: {} render json: {}
end end
+1 -1
View File
@@ -68,7 +68,7 @@ module Api
end end
if @submitter.completed_at? 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] elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter]) Submitters.send_signature_requests([@submitter])
end end
@@ -25,7 +25,10 @@ module Api
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template) 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) render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
end end
+4 -1
View File
@@ -65,7 +65,10 @@ module Api
@template.update!(template_params) @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]) render json: @template.as_json(only: %i[id updated_at])
end end
+4 -1
View File
@@ -38,7 +38,10 @@ class StartFormController < ApplicationController
if @submitter.save if @submitter.save
if is_new_record 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 end
redirect_to submit_form_path(@submitter.slug) 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)) params: params.merge('send_completed_email' => true))
end end
submissions.each do |submission| enqueue_submission_created_webhooks(@template, submissions)
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
@@ -68,7 +66,10 @@ class SubmissionsController < ApplicationController
else else
@submission.update!(archived_at: Time.current) @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') I18n.t('submission_has_been_archived')
end end
@@ -85,6 +86,15 @@ class SubmissionsController < ApplicationController
template.save! template.save!
end 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 def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] }) params.permit(submission: { submitters: [%i[uuid email phone name]] })
end end
@@ -25,7 +25,10 @@ class SubmitFormDeclineController < ApplicationController
SubmitterMailer.declined_email(submitter, user).deliver_later! SubmitterMailer.declined_email(submitter, user).deliver_later!
end end
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id) WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id)
end
redirect_to submit_form_path(submitter.slug) redirect_to submit_form_path(submitter.slug)
end end
+16 -2
View File
@@ -66,7 +66,7 @@ class TemplatesController < ApplicationController
if @template.save if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template 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) maybe_redirect_to_template(@template)
else else
@@ -77,7 +77,7 @@ class TemplatesController < ApplicationController
def update def update
@template.update!(template_params) @template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id) enqueue_template_updated_webhooks(@template)
head :ok head :ok
end end
@@ -128,6 +128,20 @@ class TemplatesController < ApplicationController
end end
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 def load_base_template
return if params[:base_template_id].blank? return if params[:base_template_id].blank?
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
@template.update!(schema:) @template.update!(schema:)
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id) enqueue_template_created_webhooks(@template)
redirect_to edit_template_path(@template) redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
@@ -65,4 +65,11 @@ class TemplatesUploadsController < ApplicationController
{ files: [file] } { files: [file] }
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
end end
@@ -4,9 +4,8 @@ class TestingApiSettingsController < ApplicationController
def index def index
authorize!(:manage, current_user.access_token) authorize!(:manage, current_user.access_token)
@webhook_config = @webhook_url = current_account.webhook_urls.first_or_initialize
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
authorize!(:manage, @webhook_config) authorize!(:manage, @webhook_url)
end end
end end
@@ -1,37 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookPreferencesController < ApplicationController class WebhookPreferencesController < ApplicationController
EVENTS = %w[ load_and_authorize_resource :webhook_url, parent: false
form.viewed
form.started
form.completed
form.declined
template.created
template.updated
submission.created
submission.archived
].freeze
before_action :load_account_config def update
authorize_resource :account_config, parent: false 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 @webhook_url.save!
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
@account_config.save!
head :ok head :ok
end end
private private
def load_account_config def webhook_preferences_params
@account_config = params.require(:webhook_url).permit(events: {})
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
@account_config.value ||= {}
end
def account_config_params
params.permit(:event, :value)
end end
end end
+9 -16
View File
@@ -1,29 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSecretController < ApplicationController class WebhookSecretController < ApplicationController
before_action :load_encrypted_config load_and_authorize_resource :webhook_url, parent: false
authorize_resource :encrypted_config, parent: false
def index; end def show; end
def create def update
@encrypted_config.assign_attributes(value: { @webhook_url.update!(secret: {
encrypted_config_params[:key] => encrypted_config_params[:value] webhook_secret_params[:key] => webhook_secret_params[:value]
}.compact_blank) }.compact_blank)
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete redirect_back(fallback_location: settings_webhook_path(@webhook_url),
notice: I18n.t('webhook_secret_has_been_saved'))
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved'))
end end
private private
def load_encrypted_config def webhook_secret_params
@encrypted_config = params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {})
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, {})
end end
end end
+37 -13
View File
@@ -1,36 +1,60 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSettingsController < ApplicationController class WebhookSettingsController < ApplicationController
before_action :load_encrypted_config load_and_authorize_resource :webhook_url, parent: false, only: %i[index show new create update destroy]
authorize_resource :encrypted_config, parent: false 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 show; end
def new; end
def create def create
@encrypted_config.assign_attributes(encrypted_config_params) @webhook_url.save!
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
end end
def update 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 submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, if submitter.blank? || @webhook_url.blank?
'encrypted_config_id' => @encrypted_config.id }) 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')) redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
end end
private private
def load_encrypted_config def create_params
@encrypted_config = params.require(:webhook_url).permit(:url, events: []).reverse_merge(events: [])
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end end
def encrypted_config_params def update_params
params.require(:encrypted_config).permit(:value) params.require(:webhook_url).permit(:url)
end end
end end
+4
View File
@@ -12,6 +12,10 @@ window.customElements.define('draw-signature', class extends HTMLElement {
this.pad = new SignaturePad(this.canvas) this.pad = new SignaturePad(this.canvas)
if (this.dataset.color) {
this.pad.penColor = this.dataset.color
}
this.pad.addEventListener('endStroke', () => { this.pad.addEventListener('endStroke', () => {
this.updateSubmitButtonVisibility() this.updateSubmitButtonVisibility()
}) })
+6 -3
View File
@@ -39,7 +39,6 @@
</template> </template>
</div> </div>
<div <div
v-if="isActive"
ref="scrollToElem" ref="scrollToElem"
class="absolute" class="absolute"
:style="{ top: scrollPadding }" :style="{ top: scrollPadding }"
@@ -405,8 +404,12 @@ export default {
} }
if (this.field.preferences?.font_size) { if (this.field.preferences?.font_size) {
style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}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) + 3}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 return style
+64 -4
View File
@@ -24,6 +24,44 @@
:fields="formulaFields" :fields="formulaFields"
:values="values" :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 <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button" id="expand_form_button"
@@ -74,6 +112,7 @@
> >
<form <form
v-if="!isCompleted && !isInvite" v-if="!isCompleted && !isInvite"
id="steps_form"
ref="form" ref="form"
:action="submitPath" :action="submitPath"
method="post" method="post"
@@ -592,6 +631,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
completeButtonToRef: {
type: Object,
required: false,
default: null
},
attachments: { attachments: {
type: Array, type: Array,
required: false, required: false,
@@ -772,6 +816,7 @@ export default {
phoneVerifiedValues: {}, phoneVerifiedValues: {},
orientation: screen?.orientation?.type, orientation: screen?.orientation?.type,
isSubmitting: false, isSubmitting: false,
isSubmittingComplete: false,
submittedValues: {}, submittedValues: {},
recalculateButtonDisabledKey: '' recalculateButtonDisabledKey: ''
} }
@@ -780,6 +825,13 @@ export default {
isMobile () { isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent) 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 () { submitButtonText () {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
return this.t('submit') return this.t('submit')
@@ -1146,9 +1198,15 @@ export default {
scrollIntoArea (area) { scrollIntoArea (area) {
return this.$refs.areas.scrollIntoArea(area) return this.$refs.areas.scrollIntoArea(area)
}, },
async submitStep () { async submitStep (e) {
this.isSubmitting = true this.isSubmitting = true
const forceComplete = e?.submitter?.getAttribute('name') === 'completed'
if (forceComplete) {
this.isSubmittingComplete = true
}
const submitStep = this.currentStep const submitStep = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type) const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
@@ -1157,7 +1215,7 @@ export default {
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
if (index >= submitStep) { if (forceComplete ? index === submitStep : index >= submitStep) {
return false return false
} }
@@ -1167,7 +1225,7 @@ export default {
}) })
const formData = new FormData(this.$refs.form) 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) { if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
formData.append('completed', 'true') formData.append('completed', 'true')
@@ -1192,7 +1250,7 @@ export default {
return Promise.reject(new Error(data.error)) 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 (nextStep) {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
@@ -1213,6 +1271,7 @@ export default {
console.error(error) console.error(error)
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
this.isSubmittingComplete = false
}) })
}).catch(error => { }).catch(error => {
if (error?.message === 'Image too small') { if (error?.message === 'Image too small') {
@@ -1222,6 +1281,7 @@ export default {
} }
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
this.isSubmittingComplete = false
}) })
}, },
minimizeForm () { minimizeForm () {
+13
View File
@@ -1,5 +1,6 @@
const en = { const en = {
complete: 'Complete', complete: 'Complete',
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
sign_and_complete: 'Sign and Complete', sign_and_complete: 'Sign and Complete',
text: 'Text', text: 'Text',
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the', by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
@@ -93,6 +94,7 @@ const en = {
const es = { const es = {
complete: 'Completar', complete: 'Completar',
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
sign_and_complete: 'Firmar y Completar', sign_and_complete: 'Firmar y Completar',
invite: 'Invitar', invite: 'Invitar',
email: 'Correo electrónico', email: 'Correo electrónico',
@@ -185,6 +187,7 @@ const es = {
const it = { const it = {
complete: 'Completa', complete: 'Completa',
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
sign_and_complete: 'Firma e Completa', sign_and_complete: 'Firma e Completa',
invite: 'Invita', invite: 'Invita',
email: 'Email', email: 'Email',
@@ -277,6 +280,7 @@ const it = {
const de = { const de = {
complete: 'Abschließen', complete: 'Abschließen',
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
sign_and_complete: 'Signieren und Abschließen', sign_and_complete: 'Signieren und Abschließen',
invite: 'Einladen', invite: 'Einladen',
email: 'E-Mail', email: 'E-Mail',
@@ -369,6 +373,7 @@ const de = {
const fr = { const fr = {
complete: 'Terminer', complete: 'Terminer',
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
sign_and_complete: 'Signer et Terminer', sign_and_complete: 'Signer et Terminer',
invite: 'Inviter', invite: 'Inviter',
email: 'Courriel', email: 'Courriel',
@@ -461,6 +466,7 @@ const fr = {
const pl = { const pl = {
complete: 'Zakończ', complete: 'Zakończ',
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
sign_and_complete: 'Podpisz i zakończ', sign_and_complete: 'Podpisz i zakończ',
invite: 'Zaproś', invite: 'Zaproś',
email: 'E-mail', email: 'E-mail',
@@ -553,6 +559,7 @@ const pl = {
const uk = { const uk = {
complete: 'Завершити', complete: 'Завершити',
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
sign_and_complete: 'Підписати і завершити', sign_and_complete: 'Підписати і завершити',
invite: 'Запросити', invite: 'Запросити',
email: 'Електронна пошта', email: 'Електронна пошта',
@@ -645,6 +652,7 @@ const uk = {
const cs = { const cs = {
complete: 'Dokončit', complete: 'Dokončit',
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
sign_and_complete: 'Podepsat a dokončit', sign_and_complete: 'Podepsat a dokončit',
invite: 'Pozvat', invite: 'Pozvat',
email: 'E-mail', email: 'E-mail',
@@ -737,6 +745,7 @@ const cs = {
const pt = { const pt = {
complete: 'Completar', complete: 'Completar',
preencher_todos_os_campos_obrigatórios_para_concluir: 'Preencher todos os campos obrigatórios para concluir',
sign_and_complete: 'Assinar e Completar', sign_and_complete: 'Assinar e Completar',
invite: 'Convidar', invite: 'Convidar',
email: 'E-mail', email: 'E-mail',
@@ -829,6 +838,7 @@ const pt = {
const he = { const he = {
complete: 'השלם', complete: 'השלם',
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
sign_and_complete: 'חתום והשלם', sign_and_complete: 'חתום והשלם',
invite: 'הזמן', invite: 'הזמן',
email: 'דוא"ל', email: 'דוא"ל',
@@ -922,6 +932,7 @@ const he = {
const nl = { const nl = {
complete: 'Voltooien', complete: 'Voltooien',
vul_alle_verplichte_velden_in_om_te_voltooien: 'Vul alle verplichte velden in om te voltooien',
sign_and_complete: 'Ondertekenen en voltooien', sign_and_complete: 'Ondertekenen en voltooien',
invite: 'Uitnodigen', invite: 'Uitnodigen',
email: 'E-mail', email: 'E-mail',
@@ -1015,6 +1026,7 @@ const nl = {
const ar = { const ar = {
complete: 'اكتمال', complete: 'اكتمال',
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
sign_and_complete: 'التوقيع والاكتمال', sign_and_complete: 'التوقيع والاكتمال',
invite: 'دعوة', invite: 'دعوة',
email: 'البريد الإلكتروني', email: 'البريد الإلكتروني',
@@ -1107,6 +1119,7 @@ const ar = {
const ko = { const ko = {
complete: '완료', complete: '완료',
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
sign_and_complete: '서명하고 완료하기', sign_and_complete: '서명하고 완료하기',
invite: '초대하기', invite: '초대하기',
email: '이메일', email: '이메일',
@@ -226,6 +226,10 @@ export default {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(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.pad.addEventListener('beginStroke', () => {
this.isInitialsStarted = true this.isInitialsStarted = true
@@ -423,6 +423,10 @@ export default {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(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.pad.addEventListener('endStroke', () => {
this.isSignatureStarted = true this.isSignatureStarted = true
+1
View File
@@ -175,6 +175,7 @@
<div <div
ref="textContainer" ref="textContainer"
class="flex items-center px-0.5" class="flex items-center px-0.5"
:style="{ color: field.preferences?.color }"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }" :class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
> >
<IconCheck <IconCheck
@@ -79,7 +79,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
{{ t('add') }} {{ names[submitters.length] }} {{ t('add') }} {{ names[lastPartyIndex] }}
</span> </span>
</a> </a>
</li> </li>
@@ -202,7 +202,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
{{ t('add') }} {{ names[submitters.length] }} {{ t('add') }} {{ names[lastPartyIndex] }}
</span> </span>
</a> </a>
</li> </li>
@@ -321,6 +321,15 @@ export default {
this.t('twentieth_party') 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 () { selectedSubmitter () {
return this.submitters.find((e) => e.uuid === this.modelValue) return this.submitters.find((e) => e.uuid === this.modelValue)
} }
@@ -353,7 +362,7 @@ export default {
}, },
addSubmitter () { addSubmitter () {
const newSubmitter = { const newSubmitter = {
name: this.names[this.submitters.length], name: this.names[this.lastPartyIndex],
uuid: v4() uuid: v4()
} }
+5 -22
View File
@@ -63,32 +63,15 @@ class ProcessSubmitterCompletionJob
end end
def enqueue_completed_webhooks(submitter, is_all_completed: false) def enqueue_completed_webhooks(submitter, is_all_completed: false)
webhook_config = Accounts.load_webhook_config(submitter.account) WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
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|
if webhook.events.include?('form.completed') if webhook.events.include?('form.completed')
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook.id }) 'webhook_url_id' => webhook.id)
end end
if webhook.events.include?('submission.completed') && is_all_completed if webhook.events.include?('submission.completed') && is_all_completed
SendSubmissionCompletedWebhookRequestJob.perform_async({ 'submission_id' => submitter.submission_id, SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
'webhook_url_id' => webhook.id }) 'webhook_url_id' => webhook.id)
end end
end end
end end
@@ -5,33 +5,32 @@ class SendFormCompletedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
url, secret = load_url_and_secret(submitter, params) return if webhook_url.url.blank? || webhook_url.events.exclude?('form.completed')
return if url.blank?
Submissions::EnsureResultGenerated.call(submitter) Submissions::EnsureResultGenerated.call(submitter)
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'form.completed', event_type: 'form.completed',
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**secret.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error rescue Faraday::Error
nil nil
end end
@@ -45,27 +44,4 @@ class SendFormCompletedWebhookRequestJob
}) })
end end
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 end
@@ -5,34 +5,28 @@ class SendFormDeclinedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank? return if webhook_url.url.blank? || webhook_url.events.exclude?('form.declined')
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.declined'] == false
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'form.declined', event_type: 'form.declined',
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -43,6 +37,7 @@ class SendFormDeclinedWebhookRequestJob
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, { SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id, 'submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,34 +5,28 @@ class SendFormStartedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank? return if webhook_url.url.blank? || webhook_url.events.exclude?('form.started')
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.started'] == false
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'form.started', event_type: 'form.started',
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -43,6 +37,7 @@ class SendFormStartedWebhookRequestJob
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, { SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id, 'submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,34 +5,28 @@ class SendFormViewedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank? return if webhook_url.url.blank? || webhook_url.events.exclude?('form.viewed')
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.viewed'] == false
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'form.viewed', event_type: 'form.viewed',
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -43,6 +37,7 @@ class SendFormViewedWebhookRequestJob
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, { SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id, 'submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,33 +5,26 @@ class SendSubmissionArchivedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submission = Submission.find(params['submission_id']) submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submission.account) return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived')
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(submission.account)
return if preferences['submission.archived'].blank?
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'submission.archived', event_type: 'submission.archived',
timestamp: Time.current, timestamp: Time.current,
data: submission.as_json(only: %i[id archived_at]) data: submission.as_json(only: %i[id archived_at])
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendSubmissionArchivedWebhookRequestJob
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, { SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submission_id' => submission.id, 'submission_id' => submission.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,32 +5,28 @@ class SendSubmissionCompletedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submission = Submission.find(params['submission_id']) submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
webhook_url = submission.account.webhook_urls.find(params['webhook_url_id']) return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed')
url = webhook_url.url if webhook_url.events.include?('submission.completed')
return if url.blank?
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'submission.completed', event_type: 'submission.completed',
timestamp: Time.current, timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission) data: Submissions::SerializeForApi.call(submission)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: submission.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error rescue Faraday::Error
nil nil
end end
@@ -5,33 +5,26 @@ class SendSubmissionCreatedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submission = Submission.find(params['submission_id']) submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submission.account) return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created')
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(submission.account)
return if preferences['submission.created'].blank?
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'submission.created', event_type: 'submission.created',
timestamp: Time.current, timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission) data: Submissions::SerializeForApi.call(submission)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendSubmissionCreatedWebhookRequestJob
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, { SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submission_id' => submission.id, 'submission_id' => submission.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,33 +5,26 @@ class SendTemplateCreatedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
template = Template.find(params['template_id']) template = Template.find(params['template_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(template.account) return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created')
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(template.account)
return if preferences['template.created'].blank?
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'template.created', event_type: 'template.created',
timestamp: Time.current, timestamp: Time.current,
data: Templates::SerializeForApi.call(template) data: Templates::SerializeForApi.call(template)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendTemplateCreatedWebhookRequestJob
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, { SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'template_id' => template.id, 'template_id' => template.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,33 +5,26 @@ class SendTemplateUpdatedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
template = Template.find(params['template_id']) template = Template.find(params['template_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(template.account) return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated')
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(template.account)
return if preferences['template.updated'].blank?
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'template.updated', event_type: 'template.updated',
timestamp: Time.current, timestamp: Time.current,
data: Templates::SerializeForApi.call(template) data: Templates::SerializeForApi.call(template)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendTemplateUpdatedWebhookRequestJob
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, { SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'template_id' => template.id, 'template_id' => template.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
+5 -3
View File
@@ -138,9 +138,11 @@ class SubmitterMailer < ApplicationMailer
private private
def build_submitter_reply_to(submitter) def build_submitter_reply_to(submitter)
reply_to = reply_to = submitter.preferences['reply_to'].presence
submitter.preferences['reply_to'].presence ||
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@') 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) 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_WITH_CONFETTI_KEY = 'form_with_confetti'
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature' FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
ESIGNING_PREFERENCE_KEY = 'esigning_preference' ESIGNING_PREFERENCE_KEY = 'esigning_preference'
WEBHOOK_PREFERENCES_KEY = 'webhook_preferences'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth' DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
FORCE_SSO_AUTH_KEY = 'force_sso_auth' FORCE_SSO_AUTH_KEY = 'force_sso_auth'
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf' FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
+1 -3
View File
@@ -26,9 +26,7 @@ class EncryptedConfig < ApplicationRecord
EMAIL_SMTP_KEY = 'action_mailer_smtp', EMAIL_SMTP_KEY = 'action_mailer_smtp',
ESIGN_CERTS_KEY = 'esign_certs', ESIGN_CERTS_KEY = 'esign_certs',
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url', TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
APP_URL_KEY = 'app_url', APP_URL_KEY = 'app_url'
WEBHOOK_URL_KEY = 'webhook_url',
WEBHOOK_SECRET_KEY = 'webhook_secret'
].freeze ].freeze
belongs_to :account belongs_to :account
+16 -2
View File
@@ -6,6 +6,7 @@
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# events :text not null # events :text not null
# secret :text not null
# sha1 :string not null # sha1 :string not null
# url :text not null # url :text not null
# created_at :datetime not null # created_at :datetime not null
@@ -22,15 +23,28 @@
# fk_rails_... (account_id => accounts.id) # fk_rails_... (account_id => accounts.id)
# #
class WebhookUrl < ApplicationRecord 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 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 :events, coder: JSON
serialize :secret, coder: JSON
before_validation :set_sha1 before_validation :set_sha1
encrypts :url encrypts :url, :secret
def set_sha1 def set_sha1
self.sha1 = Digest::SHA1.hexdigest(url) 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"> <div class="max-w-lg mx-auto px-2">
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %> <%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
<h1 class="text-4xl font-bold text-center mt-8"><%= t('sign_in') %></h1> <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 %> <% else %>
<%= javascript_pack_tag 'application', defer: true %> <%= javascript_pack_tag 'application', defer: true %>
<% end %> <% end %>
<% if canonical_url = content_for(:canonical_url) %>
<link href="<%= canonical_url %>" rel="canonical">
<% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %> <%= stylesheet_pack_tag 'application', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %> <%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %> <%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
+3 -3
View File
@@ -56,7 +56,7 @@
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::WEBHOOK_URL_KEY, account: current_account)) %> <% if can?(:read, WebhookUrl) %>
<li> <li>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
@@ -72,13 +72,13 @@
<% end %> <% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
<li> <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 %> <% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %> <% end %>
</li> </li>
<% if Docuseal.multitenant? %> <% if Docuseal.multitenant? %>
<li> <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') %> <%= t('embedding') %>
<% end %> <% end %>
</li> </li>
+2 -1
View File
@@ -1,5 +1,6 @@
<% align = field.dig('preferences', 'align') %> <% 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' %> <% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden"> <div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%"> <div class="flex-grow flex overflow-hidden" style="min-height: 50%">
@@ -15,9 +15,10 @@
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %> <%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head> </head>
<body> <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| %> <%= 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"> <div class="font-semibold text-4xl text-center w-full mb-2">
<%= t('draw_signature') %> <%= t('draw_signature') %>
</div> </div>
@@ -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') %> <%= 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>
</div> </div>
<%= form_for @webhook_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> <%= form_for @webhook_url, 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' %> <%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
<div class="space-y-2 md:flex-nowrap mt-2"> <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' %> <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full' %>
</div> </div>
<% end %> <% end %>
@@ -1,13 +1,13 @@
<%= render 'shared/turbo_modal', title: 'Webhook Secret' do %> <%= render 'shared/turbo_modal', title: t('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| %> <%= 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"> <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"> <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' %> <%= ff.text_field :key, class: 'base-input', placeholder: 'X-Example-Header' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :value, class: 'label' %> <%= ff.label :value, t('value'), class: 'label' %>
<%= ff.text_field :value, class: 'base-input' %> <%= ff.text_field :value, class: 'base-input' %>
</div> </div>
<% end %> <% 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"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow"> <div class="flex-grow">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4"> <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> <h1 class="text-4xl font-bold">Webhook</h1>
<%= render 'shared/test_mode_toggle' %> <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>
<div class="card bg-base-200"> <div class="card bg-base-200">
<div class="card-body p-6"> <div class="card-body p-6">
<%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end md:relative">
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %> <%= label_tag :url, 'Webhook URL', class: 'text-sm font-semibold' %>
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-2 mt-2"> <% if @webhook_url.persisted? %>
<%= f.url_field :value, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %> <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' %> <%= 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> </div>
<% end %> <% unless @webhook_url.persisted? %>
<% preference = current_account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)&.value || {} %> <div class="space-y-4 mt-4">
<% WebhookPreferencesController::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %> <% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 mt-2 gap-y-2"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-2">
<% events.each do |event| %> <%= f.collection_check_boxes(:events, events, :to_s, :to_s, include_hidden: false) do |b| %>
<%= form_for '', url: webhook_preferences_path, method: :post do |f| %> <div class="flex">
<%= f.hidden_field :event, value: event %> <label class="flex items-center space-x-2">
<% uuid = SecureRandom.uuid %> <%= b.check_box class: 'base-checkbox', checked: @webhook_url.events.include?(b.value) %>
<div class="flex"> <span>
<label for="<%= uuid %>" class="flex items-center space-x-2"> <%= b.label %>
<%= f.check_box :value, class: 'base-checkbox', checked: preference[event] || (!preference.key?(event) && event.starts_with?('form.')), onchange: 'this.form.requestSubmit()', id: uuid %> </span>
<span> </label>
<%= event %> </div>
</span> <% end %>
</label>
</div> </div>
<% end %> <% 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 %> <% end %>
</div> </div>
</div> </div>
@@ -43,12 +84,12 @@
<div class="space-y-4 mt-4"> <div class="space-y-4 mt-4">
<div class="collapse collapse-open bg-base-200 px-1"> <div class="collapse collapse-open bg-base-200 px-1">
<div class="p-4 text-xl font-medium"> <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> <span>
<%= t('submission_example_payload') %> <%= t('submission_example_payload') %>
</span> </span>
<% if @encrypted_config.value.present? %> <% if @webhook_url.url.present? && @webhook_url.events.include?('form.completed') %>
<%= button_to button_title(title: 'Test Webhook', disabled_with: t('sending'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %> <%= 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 %> <% end %>
</div> </div>
</div> </div>
+55 -8
View File
@@ -474,6 +474,7 @@ en: &en
logo: Logo logo: Logo
back: Back back: Back
add_secret: Add Secret add_secret: Add Secret
edit_secret: Edit Secret
submission_example_payload: Submission example payload submission_example_payload: Submission example payload
there_are_no_signatures: There are no signatures there_are_no_signatures: There are no signatures
signed_with_trusted_certificate: Signed with trusted certificate signed_with_trusted_certificate: Signed with trusted certificate
@@ -510,13 +511,17 @@ en: &en
webhook_secret_has_been_saved: Webhook Secret has been saved. webhook_secret_has_been_saved: Webhook Secret has been saved.
webhook_url_has_been_saved: Webhook URL has been saved. webhook_url_has_been_saved: Webhook URL has been saved.
webhook_request_has_been_sent: Webhook request has been sent. 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.' count_submissions_have_been_created: '%{count} submissions have been created.'
gmail_has_been_connected: Gmail has been connected gmail_has_been_connected: Gmail has been connected
microsoft_account_has_been_connected: Microsoft Account 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 sms_length_cant_be_longer_than_120_bytes: SMS length can't be longer than 120 bytes
connected_successfully: Connected successfully. connected_successfully: Connected successfully.
user_nameid_not_found: 'User %{nameid} not found.' 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. sso_settings_have_been_updated: SSO settings have been updated.
sms_has_been_sent: SMS has been sent. sms_has_been_sent: SMS has been sent.
account_has_been_created: Account has been created. account_has_been_created: Account has been created.
@@ -623,7 +628,9 @@ en: &en
archived_users: Archived Users archived_users: Archived Users
embedding_users: Embedding Users embedding_users: Embedding Users
view_embedding_users: View Embedding Users view_embedding_users: View Embedding Users
view_users: View Users key: Key
value: Value
webhook_secret: Webhook Secret
submission_event_names: submission_event_names:
send_email_to_html: '<b>Email sent</b> to %{submitter_name}' send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}' send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
@@ -1116,6 +1123,7 @@ es: &es
logo: Logotipo logo: Logotipo
back: Atrás back: Atrás
add_secret: Agregar secreto add_secret: Agregar secreto
edit_secret: Editar secreto
submission_example_payload: Ejemplo de payload de envío submission_example_payload: Ejemplo de payload de envío
there_are_no_signatures: No hay firmas there_are_no_signatures: No hay firmas
signed_with_trusted_certificate: Firmado con certificado de confianza signed_with_trusted_certificate: Firmado con certificado de confianza
@@ -1152,13 +1160,17 @@ es: &es
webhook_secret_has_been_saved: El secreto del Webhook ha sido guardado. 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_url_has_been_saved: La URL del Webhook ha sido guardada.
webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada. 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.' count_submissions_have_been_created: '%{count} envíos han sido creados.'
gmail_has_been_connected: Gmail ha sido conectado. gmail_has_been_connected: Gmail ha sido conectado.
microsoft_account_has_been_connected: La cuenta de Microsoft ha sido conectada. 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. sms_length_cant_be_longer_than_120_bytes: La longitud del SMS no puede ser mayor a 120 bytes.
connected_successfully: Conectado con éxito. connected_successfully: Conectado con éxito.
user_nameid_not_found: 'Usuario %{nameid} no encontrado.' 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. sso_settings_have_been_updated: La configuración de SSO ha sido actualizada.
sms_has_been_sent: El SMS ha sido enviado. sms_has_been_sent: El SMS ha sido enviado.
account_has_been_created: La cuenta ha sido creada. account_has_been_created: La cuenta ha sido creada.
@@ -1265,6 +1277,9 @@ es: &es
archived_users: Usuarios Archivados archived_users: Usuarios Archivados
embedding_users: Usuarios Integrados embedding_users: Usuarios Integrados
view_embedding_users: Ver Usuarios Integrado view_embedding_users: Ver Usuarios Integrado
key: Clave
value: Valor
webhook_secret: Secreto del Webhook
submission_event_names: submission_event_names:
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}' 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}' send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
@@ -1757,6 +1772,7 @@ it: &it
logo: Logo logo: Logo
back: Indietro back: Indietro
add_secret: Aggiungi segreto add_secret: Aggiungi segreto
edit_secret: Modifica segreto
submission_example_payload: Esempio di payload di invio submission_example_payload: Esempio di payload di invio
there_are_no_signatures: Non ci sono firme there_are_no_signatures: Non ci sono firme
signed_with_trusted_certificate: Firmato con certificato affidabile signed_with_trusted_certificate: Firmato con certificato affidabile
@@ -1793,13 +1809,17 @@ it: &it
webhook_secret_has_been_saved: Il segreto del Webhook è stato salvato. webhook_secret_has_been_saved: Il segreto del Webhook è stato salvato.
webhook_url_has_been_saved: "L'URL 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_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.' count_submissions_have_been_created: '%{count} invii sono stati creati.'
gmail_has_been_connected: Gmail è stato connesso. gmail_has_been_connected: Gmail è stato connesso.
microsoft_account_has_been_connected: "L'account Microsoft è 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." sms_length_cant_be_longer_than_120_bytes: "La lunghezza dell'SMS non può superare i 120 byte."
connected_successfully: Collegamento avvenuto con successo. connected_successfully: Collegamento avvenuto con successo.
user_nameid_not_found: 'Utente %{nameid} non trovato.' 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. sso_settings_have_been_updated: Le impostazioni SSO sono state aggiornate.
sms_has_been_sent: "L'SMS è stato inviato." sms_has_been_sent: "L'SMS è stato inviato."
account_has_been_created: "L'account è stato creato." account_has_been_created: "L'account è stato creato."
@@ -1906,6 +1926,9 @@ it: &it
archived_users: Utenti Archiviati archived_users: Utenti Archiviati
embedding_users: Utenti Incorporati embedding_users: Utenti Incorporati
view_embedding_users: Visualizza Utenti Incorporati view_embedding_users: Visualizza Utenti Incorporati
key: Chiave
value: Valore
webhook_secret: Segreto del Webhook
submission_event_names: submission_event_names:
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}' 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}' send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
@@ -2399,6 +2422,7 @@ fr: &fr
logo: Logo logo: Logo
back: Retour back: Retour
add_secret: Ajouter un secret add_secret: Ajouter un secret
edit_secret: Modifier le Secret
submission_example_payload: Exemple de payload de soumission submission_example_payload: Exemple de payload de soumission
there_are_no_signatures: "Il n'y a pas de signatures" there_are_no_signatures: "Il n'y a pas de signatures"
signed_with_trusted_certificate: Signé avec un certificat de confiance signed_with_trusted_certificate: Signé avec un certificat de confiance
@@ -2435,13 +2459,17 @@ fr: &fr
webhook_secret_has_been_saved: Le secret du Webhook a été enregistré. 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_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_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.' count_submissions_have_been_created: '%{count} soumissions ont été créées.'
gmail_has_been_connected: Gmail a été connecté. gmail_has_been_connected: Gmail a été connecté.
microsoft_account_has_been_connected: Le compte Microsoft 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. 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. connected_successfully: Connecté avec succès.
user_nameid_not_found: 'Utilisateur %{nameid} introuvable.' 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. sso_settings_have_been_updated: Les paramètres de SSO ont été mis à jour.
sms_has_been_sent: Le SMS a été envoyé. sms_has_been_sent: Le SMS a été envoyé.
account_has_been_created: Le compte a été créé. account_has_been_created: Le compte a été créé.
@@ -2548,6 +2576,9 @@ fr: &fr
archived_users: Utilisateurs Archivés archived_users: Utilisateurs Archivés
embedding_users: Utilisateurs Intégrés embedding_users: Utilisateurs Intégrés
view_embedding_users: Voir les 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: submission_event_names:
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}' 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}' send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
@@ -3040,6 +3071,7 @@ pt: &pt
logo: Logotipo logo: Logotipo
back: Voltar back: Voltar
add_secret: Adicionar segredo add_secret: Adicionar segredo
edit_secret: Editar Segredo
submission_example_payload: Exemplo de payload de submissão submission_example_payload: Exemplo de payload de submissão
there_are_no_signatures: Não há assinaturas there_are_no_signatures: Não há assinaturas
signed_with_trusted_certificate: Assinado com certificado confiável signed_with_trusted_certificate: Assinado com certificado confiável
@@ -3076,13 +3108,17 @@ pt: &pt
webhook_secret_has_been_saved: O segredo do Webhook foi salvo. webhook_secret_has_been_saved: O segredo do Webhook foi salvo.
webhook_url_has_been_saved: A URL do Webhook foi salva. webhook_url_has_been_saved: A URL do Webhook foi salva.
webhook_request_has_been_sent: A solicitação do Webhook foi enviada. 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.' count_submissions_have_been_created: '%{count} submissões foram criadas.'
gmail_has_been_connected: O Gmail foi conectado gmail_has_been_connected: O Gmail foi conectado
microsoft_account_has_been_connected: A conta da Microsoft foi conectada 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 sms_length_cant_be_longer_than_120_bytes: O comprimento do SMS não pode ultrapassar 120 bytes
connected_successfully: Conectado com sucesso. connected_successfully: Conectado com sucesso.
user_nameid_not_found: 'Usuário %{nameid} não encontrado.' 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. sso_settings_have_been_updated: As configurações de SSO foram atualizadas.
sms_has_been_sent: O SMS foi enviado. sms_has_been_sent: O SMS foi enviado.
account_has_been_created: A conta foi criada. account_has_been_created: A conta foi criada.
@@ -3189,6 +3225,9 @@ pt: &pt
archived_users: Usuários Arquivados archived_users: Usuários Arquivados
embedding_users: Usuários Incorporados embedding_users: Usuários Incorporados
view_embedding_users: Ver Usuários Incorporados view_embedding_users: Ver Usuários Incorporados
key: Chave
value: Valor
webhook_secret: Segredo do Webhook
submission_event_names: submission_event_names:
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}' 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}' send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
@@ -3680,7 +3719,8 @@ de: &de
api_key: API-Schlüssel api_key: API-Schlüssel
logo: Logo logo: Logo
back: Zurück back: Zurück
add_secret: Geheimnis hinzufügen add_secret: Geheimnis hinzuf
edit_secret: Geheimnis bearb
submission_example_payload: Beispiel-Payload für Einreichung submission_example_payload: Beispiel-Payload für Einreichung
there_are_no_signatures: Es gibt keine Unterschriften there_are_no_signatures: Es gibt keine Unterschriften
signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
@@ -3717,13 +3757,17 @@ de: &de
webhook_secret_has_been_saved: Das Webhook-Geheimnis wurde gespeichert. webhook_secret_has_been_saved: Das Webhook-Geheimnis wurde gespeichert.
webhook_url_has_been_saved: Die Webhook-URL wurde gespeichert. webhook_url_has_been_saved: Die Webhook-URL wurde gespeichert.
webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet. 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.' count_submissions_have_been_created: '%{count} Einreichungen wurden erstellt.'
gmail_has_been_connected: Gmail wurde verbunden. gmail_has_been_connected: Gmail wurde verbunden.
microsoft_account_has_been_connected: Microsoft-Konto 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. sms_length_cant_be_longer_than_120_bytes: Die SMS-Länge darf 120 Bytes nicht überschreiten.
connected_successfully: Erfolgreich verbunden. connected_successfully: Erfolgreich verbunden.
user_nameid_not_found: 'Benutzer %{nameid} nicht gefunden.' 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. sso_settings_have_been_updated: Die SSO-Einstellungen wurden aktualisiert.
sms_has_been_sent: Die SMS wurde gesendet. sms_has_been_sent: Die SMS wurde gesendet.
account_has_been_created: Das Konto wurde erstellt. account_has_been_created: Das Konto wurde erstellt.
@@ -3830,6 +3874,9 @@ de: &de
archived_users: Archivierte Benutzer archived_users: Archivierte Benutzer
embedding_users: Einbettende Benutzer embedding_users: Einbettende Benutzer
view_embedding_users: Einbettende Benutzer anzeigen view_embedding_users: Einbettende Benutzer anzeigen
key: Schlüssel
value: Wert
webhook_secret: Webhook-Geheimnis
submission_event_names: submission_event_names:
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}' 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}' send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
+5 -3
View File
@@ -80,8 +80,8 @@ Rails.application.routes.draw do
resources :testing_api_settings, only: %i[index] resources :testing_api_settings, only: %i[index]
resources :submitters_autocomplete, only: %i[index] resources :submitters_autocomplete, only: %i[index]
resources :template_folders_autocomplete, only: %i[index] resources :template_folders_autocomplete, only: %i[index]
resources :webhook_preferences, only: %i[create] resources :webhook_secret, only: %i[show update]
resources :webhook_secret, only: %i[index create] resources :webhook_preferences, only: %i[update]
resource :templates_upload, only: %i[create] resource :templates_upload, only: %i[create]
authenticated do authenticated do
resource :templates_upload, only: %i[show], path: 'new' resource :templates_upload, only: %i[show], path: 'new'
@@ -168,7 +168,9 @@ Rails.application.routes.draw do
defaults: { status: :integration } defaults: { status: :integration }
resource :personalization, only: %i[show create], controller: 'personalization_settings' resource :personalization, only: %i[show create], controller: 'personalization_settings'
resources :api, only: %i[index create], controller: 'api_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] resource :account, only: %i[show update destroy]
resources :profile, only: %i[index] do resources :profile, only: %i[index] do
collection 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. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -367,6 +367,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_26_161207) do
t.string "sha1", null: false t.string "sha1", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_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 ["account_id"], name: "index_webhook_urls_on_account_id"
t.index ["sha1"], name: "index_webhook_urls_on_sha1" t.index ["sha1"], name: "index_webhook_urls_on_sha1"
end end
+1
View File
@@ -22,5 +22,6 @@ class Ability
can :manage, UserConfig, user_id: user.id can :manage, UserConfig, user_id: user.id
can :manage, Account, id: user.account_id can :manage, Account, id: user.account_id
can :manage, AccessToken, user_id: user.id can :manage, AccessToken, user_id: user.id
can :manage, WebhookUrl, account_id: user.account_id
end end
end end
-24
View File
@@ -78,30 +78,6 @@ module Accounts
new_template new_template
end 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) def load_signing_pkcs(account)
cert_data = cert_data =
if Docuseal.multitenant? if Docuseal.multitenant?
+12 -2
View File
@@ -68,7 +68,7 @@ module Submissions
name: item['name']) name: item['name'])
end 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 = images_pdf =
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc| image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
@@ -87,7 +87,9 @@ module Submissions
name: template.name 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 end
def generate_pdfs(submitter) def generate_pdfs(submitter)
@@ -177,6 +179,8 @@ module Submissions
font_size = preferences_font_size font_size = preferences_font_size
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i 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) font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME)
value = submitter.values[field['uuid']] value = submitter.values[field['uuid']]
@@ -378,6 +382,7 @@ module Submissions
next if char.blank? next if char.blank?
text = HexaPDF::Layout::TextFragment.create(char, font:, text = HexaPDF::Layout::TextFragment.create(char, font:,
fill_color:,
font_size:) font_size:)
line_height = layouter.fit([text], cell_width, height).lines.first.height line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -385,6 +390,7 @@ module Submissions
if preferences_font_size.blank? && line_height > (area['h'] * height) if preferences_font_size.blank? && line_height > (area['h'] * height)
text = HexaPDF::Layout::TextFragment.create(char, text = HexaPDF::Layout::TextFragment.create(char,
font:, font:,
fill_color:,
font_size: (font_size / 1.4).to_i) font_size: (font_size / 1.4).to_i)
line_height = layouter.fit([text], cell_width, height).lines.first.height line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -393,6 +399,7 @@ module Submissions
if preferences_font_size.blank? && line_height > (area['h'] * height) if preferences_font_size.blank? && line_height > (area['h'] * height)
text = HexaPDF::Layout::TextFragment.create(char, text = HexaPDF::Layout::TextFragment.create(char,
font:, font:,
fill_color:,
font_size: (font_size / 1.9).to_i) font_size: (font_size / 1.9).to_i)
line_height = layouter.fit([text], cell_width, height).lines.first.height line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -412,6 +419,7 @@ module Submissions
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', ')) value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
text = HexaPDF::Layout::TextFragment.create(value, font:, text = HexaPDF::Layout::TextFragment.create(value, font:,
fill_color:,
font_size:) font_size:)
lines = layouter.fit([text], area['w'] * width, height).lines lines = layouter.fit([text], area['w'] * width, height).lines
@@ -420,6 +428,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1 if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(value, text = HexaPDF::Layout::TextFragment.create(value,
font:, font:,
fill_color:,
font_size: (font_size / 1.4).to_i) font_size: (font_size / 1.4).to_i)
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
@@ -430,6 +439,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1 if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(value, text = HexaPDF::Layout::TextFragment.create(value,
font:, font:,
fill_color:,
font_size: (font_size / 1.9).to_i) font_size: (font_size / 1.9).to_i)
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
+5 -2
View File
@@ -14,14 +14,17 @@ module Submitters
unless submitter.submission_events.exists?(event_type: 'start_form') unless submitter.submission_events.exists?(event_type: 'start_form')
SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request) 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 end
update_submitter!(submitter, params, request) update_submitter!(submitter, params, request)
submitter.submission.save! 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 submitter
end end
+20 -3
View File
@@ -6,6 +6,20 @@ module Templates
FIELD_NAME_REGEXP = /\A(?=.*\p{L})[\p{L}\d\s-]+\z/ FIELD_NAME_REGEXP = /\A(?=.*\p{L})[\p{L}\d\s-]+\z/
SKIP_FIELD_DESCRIPTION = %w[undefined].freeze 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 module_function
@@ -143,8 +157,8 @@ module Templates
{ {
**attrs, **attrs,
type: 'select', type: 'select',
options: build_options(field[:Opt]), options: build_options(field[:Opt], 'select'),
default_value: field.field_value 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? 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) def build_options(values, type = nil)
is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1 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/) 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) 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, uuid: SecureRandom.uuid,
value: is_option_number || is_skip_single_value ? '' : option value: is_option_number || is_skip_single_value ? '' : option
+9
View File
@@ -122,6 +122,15 @@ module Templates
io = StringIO.new 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.create_appearances(force: true) if pdf.acro_form[:NeedAppearances]
pdf.acro_form.flatten pdf.acro_form.flatten
+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 it 'archives a template' do
expect do expect do
click_button 'Archive' accept_confirm('Are you sure?') do
click_button 'Archive'
end
end.to change { Template.active.count }.by(-1) end.to change { Template.active.count }.by(-1)
expect(page).to have_content('Template has been archived') expect(page).to have_content('Template has been archived')
+167 -9
View File
@@ -8,24 +8,182 @@ RSpec.describe 'Webhook Settings' do
before do before do
sign_in(user) sign_in(user)
end
it 'shows webhook settings page with empty form when there are no webhooks' do
visit settings_webhooks_path visit settings_webhooks_path
end
it 'shows webhook settings page' do expect(page).to have_content('Webhook')
expect(page).to have_content('Webhooks') expect(page).to have_content('Webhook URL')
expect(page).to have_field('Webhook URL') expect(page).to have_field('webhook_url[url]', type: 'url')
expect(page).to have_button('Save') expect(page).to have_button('Save')
WebhookUrl::EVENTS.each do |event|
expect(page).to have_field(event, type: 'checkbox')
end
end end
it 'updates the webhook URL' do it 'shows list of webhooks when there are more than one' do
fill_in 'Webhook URL', with: 'https://example.com' 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 expect do
click_button 'Save' 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
end end