add webhook settings
This commit is contained in:
@@ -34,8 +34,8 @@ class ApplicationController < ActionController::Base
|
|||||||
redirect_to setup_index_path unless User.exists?
|
redirect_to setup_index_path unless User.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil)
|
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil)
|
||||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon: })
|
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
|
||||||
end
|
end
|
||||||
|
|
||||||
def svg_icon(icon_name, class: '')
|
def svg_icon(icon_name, class: '')
|
||||||
|
|||||||
@@ -17,11 +17,8 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
submitter = Submitter.find_by!(slug: params[:slug])
|
submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
submitter.values.merge!(normalized_values)
|
|
||||||
submitter.completed_at = Time.current if params[:completed] == 'true'
|
|
||||||
submitter.opened_at ||= Time.current
|
|
||||||
|
|
||||||
submitter.save!
|
update_submitter!(submitter)
|
||||||
|
|
||||||
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
|
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
|
||||||
|
|
||||||
@@ -30,6 +27,10 @@ class SubmitFormController < ApplicationController
|
|||||||
if submitter.completed_at?
|
if submitter.completed_at?
|
||||||
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
|
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
|
||||||
|
|
||||||
|
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
SendWebhookRequestJob.perform_later(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
submitter.submission.template.account.users.active.each do |user|
|
submitter.submission.template.account.users.active.each do |user|
|
||||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
end
|
end
|
||||||
@@ -44,6 +45,16 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def update_submitter!(submitter)
|
||||||
|
submitter.values.merge!(normalized_values)
|
||||||
|
submitter.completed_at = Time.current if params[:completed] == 'true'
|
||||||
|
submitter.opened_at ||= Time.current
|
||||||
|
|
||||||
|
submitter.save!
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
def normalized_values
|
def normalized_values
|
||||||
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
||||||
if params[:cast_boolean] == 'true'
|
if params[:cast_boolean] == 'true'
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WebhookSettingsController < ApplicationController
|
||||||
|
def show
|
||||||
|
@encrypted_config =
|
||||||
|
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@encrypted_config =
|
||||||
|
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
|
||||||
|
@encrypted_config.update!(encrypted_config_params)
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||||
|
|
||||||
|
SendWebhookRequestJob.perform_later(submitter)
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def encrypted_config_params
|
||||||
|
params.require(:encrypted_config).permit(:value)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SendWebhookRequestJob < ApplicationJob
|
||||||
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
|
def perform(submitter)
|
||||||
|
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
|
||||||
|
return if config.blank? || config.value.blank?
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
|
Faraday.post(config.value,
|
||||||
|
{
|
||||||
|
event_type: 'form.submitted',
|
||||||
|
timestamp: Time.current.iso8601,
|
||||||
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
|
}.to_json,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'User-Agent' => USER_AGENT)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -15,6 +15,8 @@ class Account < ApplicationRecord
|
|||||||
has_many :users, dependent: :destroy
|
has_many :users, dependent: :destroy
|
||||||
has_many :encrypted_configs, dependent: :destroy
|
has_many :encrypted_configs, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy
|
has_many :templates, dependent: :destroy
|
||||||
|
has_many :submissions, through: :templates
|
||||||
|
has_many :submitters, through: :submissions
|
||||||
has_many :active_users, -> { active }, dependent: :destroy,
|
has_many :active_users, -> { active }, dependent: :destroy,
|
||||||
inverse_of: :account, class_name: 'User'
|
inverse_of: :account, class_name: 'User'
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,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'
|
||||||
APP_URL_KEY = 'app_url'
|
APP_URL_KEY = 'app_url'
|
||||||
|
WEBHOOK_URL_KEY = 'webhook_url'
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#
|
#
|
||||||
class Submission < ApplicationRecord
|
class Submission < ApplicationRecord
|
||||||
belongs_to :template
|
belongs_to :template
|
||||||
|
has_one :account, through: :template
|
||||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||||
|
|
||||||
has_many :submitters, dependent: :destroy
|
has_many :submitters, dependent: :destroy
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
#
|
#
|
||||||
class Submitter < ApplicationRecord
|
class Submitter < ApplicationRecord
|
||||||
belongs_to :submission
|
belongs_to :submission
|
||||||
|
has_one :template, through: :submission
|
||||||
|
has_one :account, through: :template
|
||||||
|
|
||||||
attribute :values, :string, default: -> { {} }
|
attribute :values, :string, default: -> { {} }
|
||||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
<div class="card bg-base-200">
|
<div class="card bg-base-200">
|
||||||
<div class="card-body p-6">
|
<div class="card-body p-6">
|
||||||
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
|
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
|
||||||
<div class=" flex flex-row space-x-4">
|
<div class="flex flex-row space-x-4">
|
||||||
<input id="api_key" type="text" value="<%= jwt = JsonWebToken.encode(uuid: current_user.uuid, scope: :api) %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
<input id="api_key" type="text" value="<%= jwt = JsonWebToken.encode(uuid: current_user.uuid, scope: :api) %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||||
<%= render 'shared/clipboard_copy', text: jwt, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: jwt, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,12 +25,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="collapse-content" style="display: inherit">
|
<div class="collapse-content" style="display: inherit">
|
||||||
<div class="mockup-code overflow-hidden">
|
<div class="mockup-code overflow-hidden">
|
||||||
<pre data-prefix="$"><code class="overflow-hidden w-full">curl --location '<%= api_submissions_url %>' \
|
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
|
||||||
--header 'X-Auth-Token: <%= jwt %>' \
|
--header 'X-Auth-Token: <%= jwt %>' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"template_id": <%= current_account.templates.last.id %>,
|
"template_id": <%= current_account.templates.last&.id || 1 %>,
|
||||||
"emails": "<%= current_user.email.sub('@', '+test@') %>, <%= current_user.email.sub('@', '+test2@') %>"
|
"emails": "<%= current_user.email.sub('@', '+test@') %>, <%= current_user.email.sub('@', '+test2@') %>"
|
||||||
}'</code></pre>
|
}'<% end.to_str %>
|
||||||
|
<span class="top-0 right-0 absolute">
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||||
|
</span>
|
||||||
|
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,19 +51,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="collapse-content" style="display: inherit">
|
<div class="collapse-content" style="display: inherit">
|
||||||
<div class="mockup-code overflow-hidden">
|
<div class="mockup-code overflow-hidden">
|
||||||
<pre data-prefix="$"><code class="overflow-hidden w-full">curl --location '<%= api_submissions_url %>' \
|
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
|
||||||
--header 'X-Auth-Token: <%= jwt %>' \
|
--header 'X-Auth-Token: <%= jwt %>' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"template_id": <%= current_account.templates.last.id %>,
|
"template_id": <%= current_account.templates.last&.id || 1 %>,
|
||||||
"submission": [
|
"submission": [
|
||||||
{
|
{
|
||||||
"submitters": [
|
"submitters": [
|
||||||
{ "name": "<%= current_account.templates.last.submitters.first['name'] %>", "email": "<%= current_user.email.sub('@', '+test@') %>" },
|
{ "name": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>", "email": "<%= current_user.email.sub('@', '+test@') %>" },
|
||||||
{ "name": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
|
{ "name": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}'</code></pre>
|
}'<% end.to_str %>
|
||||||
|
<span class="top-0 right-0 absolute">
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||||
|
</span>
|
||||||
|
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="disabled">
|
<span class="disabled">
|
||||||
<span class="flex items-center justify-center space-x-2">
|
<span class="flex items-center justify-center space-x-2">
|
||||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||||
<span><%= disabled_with %>...</span>
|
<span><%= disabled_with %>...</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<label class="<%= local_assigns[:class] %>">
|
<label class="<%= local_assigns[:class] %>">
|
||||||
<input type="radio" class="peer hidden">
|
<input type="radio" class="peer hidden">
|
||||||
<span class="peer-checked:hidden flex items-center space-x-2">
|
<span class="peer-checked:hidden flex items-center space-x-2">
|
||||||
<%= svg_icon('link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
<%= svg_icon(local_assigns[:icon] || 'link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||||
<span class="hidden md:inline">
|
<span class="hidden md:inline">
|
||||||
<%= local_assigns[:copy_title] || 'Copy' %>
|
<%= local_assigns[:copy_title] || 'Copy' %>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="hidden peer-checked:flex items-center space-x-2">
|
<span class="hidden peer-checked:flex items-center space-x-2">
|
||||||
<%= svg_icon('clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
<%= svg_icon(local_assigns[:copied_icon] || 'clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||||
<span class="hidden md:inline">
|
<span class="hidden md:inline">
|
||||||
<%= local_assigns[:copied_title] || 'Copied' %>
|
<%= local_assigns[:copied_title] || 'Copied' %>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
<li>
|
<li>
|
||||||
<%= 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>
|
||||||
|
<li>
|
||||||
|
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to 'Console', console_redirect_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Console', console_redirect_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<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">
|
||||||
|
<h1 class="text-4xl font-bold mb-4">Webhooks</h1>
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
|
||||||
|
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
|
||||||
|
<div class="flex flex-row space-x-4 mt-2">
|
||||||
|
<%= f.text_field :value, required: true, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %>
|
||||||
|
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-32' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %>
|
||||||
|
<% if submitter %>
|
||||||
|
<div class="space-y-4 mt-4">
|
||||||
|
<div class="collapse collapse-open bg-base-200 px-1">
|
||||||
|
<div class="p-4 text-xl font-medium">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>
|
||||||
|
Submission example payload
|
||||||
|
</span>
|
||||||
|
<% if @encrypted_config.value.present? %>
|
||||||
|
<%= button_to button_title(title: 'Test Webhook', disabled_with: 'Sending', icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content" style="display: inherit">
|
||||||
|
<div class="mockup-code overflow-hidden relative">
|
||||||
|
<span class="top-0 right-0 absolute">
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: code = JSON.pretty_generate({ event_type: 'form.submitted', timestamp: Time.current.iso8601, data: Submitters::SerializeForWebhook.call(submitter) }).gsub(/^/, ' ').sub(/^\s+/, ''), class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||||
|
</span>
|
||||||
|
<pre><code class="overflow-hidden w-full"><%= code %></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -71,6 +71,7 @@ Rails.application.routes.draw do
|
|||||||
resources :esign, only: %i[index create], controller: 'esign_settings'
|
resources :esign, only: %i[index create], controller: 'esign_settings'
|
||||||
resources :users, only: %i[index]
|
resources :users, only: %i[index]
|
||||||
resources :api, only: %i[index], controller: 'api_settings' unless Docuseal.multitenant?
|
resources :api, only: %i[index], controller: 'api_settings' unless Docuseal.multitenant?
|
||||||
|
resource :webhooks, only: %i[show create update], controller: 'webhook_settings' unless Docuseal.multitenant?
|
||||||
resource :account, only: %i[show update]
|
resource :account, only: %i[show update]
|
||||||
resources :profile, only: %i[index] do
|
resources :profile, only: %i[index] do
|
||||||
collection do
|
collection do
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submitters
|
||||||
|
module SerializeForWebhook
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(submitter)
|
||||||
|
values = build_values_array(submitter)
|
||||||
|
documents = build_documents_array(submitter)
|
||||||
|
|
||||||
|
submitter_name = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }['name']
|
||||||
|
|
||||||
|
submitter.as_json(include: [template: { only: %i[id name created_at updated_at] }])
|
||||||
|
.except('uuid', 'values', 'slug')
|
||||||
|
.merge('values' => values,
|
||||||
|
'documents' => documents,
|
||||||
|
'submitter_name' => submitter_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_values_array(submitter)
|
||||||
|
fields_index = submitter.submission.template_fields.index_by { |e| e['uuid'] }
|
||||||
|
attachments_index = submitter.attachments.preload(:blob).index_by(&:uuid)
|
||||||
|
submitter_field_counters = Hash.new { 0 }
|
||||||
|
|
||||||
|
submitter.values.map do |uuid, value|
|
||||||
|
field = fields_index[uuid]
|
||||||
|
submitter_field_counters[field['type']] += 1
|
||||||
|
|
||||||
|
field_name =
|
||||||
|
field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}"
|
||||||
|
|
||||||
|
value = fetch_field_value(field, value, attachments_index)
|
||||||
|
|
||||||
|
{ field: field_name, value: }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_documents_array(submitter)
|
||||||
|
submitter.documents.preload(:blob).map do |attachment|
|
||||||
|
{ name: attachment.filename.base, url: attachment.url }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_field_value(field, value, attachments_index)
|
||||||
|
if field['type'].in?(%w[image signature])
|
||||||
|
attachments_index[value]&.url
|
||||||
|
elsif field['type'] == 'file'
|
||||||
|
Array.wrap(value).compact_blank.filter_map { |e| attachments_index[e]&.url }
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user