prefill signature

pull/349/head
Pete Matsyburka 1 year ago
parent 9e66f8412c
commit f9d52e56a2

@ -8,6 +8,7 @@ class AccountConfigsController < ApplicationController
AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,

@ -1,16 +1,37 @@
# frozen_string_literal: true
module Api
class AttachmentsController < ApiBaseController
skip_before_action :authenticate_user!
skip_authorization_check
class AttachmentsController < ActionController::API
include ActionController::Cookies
include ActiveStorage::SetCurrent
COOKIE_STORE_LIMIT = 10
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
attachment = Submitters.create_attachment!(submitter, params)
if params[:remember_signature] == 'true' && submitter.email.present?
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(submitter, attachment)
end
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
end
def build_new_cookie_signatures_json(submitter, attachment)
values =
begin
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
rescue JSON::ParserError
{}
end
values[submitter.email] = attachment.uuid
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
values.to_json
end
end
end

@ -7,12 +7,14 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
CONFIG_KEYS = [].freeze
PRELOAD_ALL_PAGES_AMOUNT = 200
def show
@submitter = Submitter.find_by!(slug: params[:slug])
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
ActiveRecord::Associations::Preloader.new(
records: [@submitter],
@ -34,11 +36,14 @@ class SubmitFormController < ApplicationController
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
unless Docuseal.multitenant?
@signature_attachment = Submitters::MaybeAssignDefaultSignature.call(@submitter, params, @attachments_index)
end
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return unless @form_configs[:prefill_signature]
@signature_attachment =
Submitters::MaybeAssignDefaultBrowserSignature.call(@submitter, params, cookies, @attachments_index.values)
render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
@attachments_index[@signature_attachment.uuid] = @signature_attachment if @signature_attachment
end
def update

@ -70,6 +70,7 @@ window.customElements.define('draw-signature', class extends HTMLElement {
formData.append('file', file)
formData.append('submitter_slug', this.dataset.slug)
formData.append('name', 'attachments')
formData.append('remember_signature', 'true')
return fetch('/api/attachments', {
method: 'POST',

@ -23,6 +23,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
withDisclosure: this.dataset.withDisclosure === 'true',
withTypedSignature: this.dataset.withTypedSignature !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
rememberSignature: this.dataset.rememberSignature === 'true',
values: reactive(JSON.parse(this.dataset.values)),
completedButton: JSON.parse(this.dataset.completedButton || '{}'),
withQrButton: true,

@ -323,11 +323,12 @@
:field="currentField"
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
:with-typed-signature="withTypedSignature"
:remember-signature="rememberSignature"
:attachments-index="attachmentsIndex"
:button-text="buttonText"
:with-disclosure="withDisclosure"
:with-qr-button="withQrButton"
:submitter-slug="submitterSlug"
:submitter="submitter"
:show-field-names="showFieldNames"
@attached="attachments.push($event)"
@start="scrollIntoField(currentField)"
@ -549,6 +550,11 @@ export default {
required: false,
default: null
},
rememberSignature: {
type: Boolean,
required: false,
default: false
},
minimize: {
type: Boolean,
required: false,

@ -255,8 +255,8 @@ export default {
type: Object,
required: true
},
submitterSlug: {
type: String,
submitter: {
type: Object,
required: true
},
showFieldNames: {
@ -284,6 +284,11 @@ export default {
required: false,
default: true
},
rememberSignature: {
type: Boolean,
required: false,
default: false
},
attachmentsIndex: {
type: Object,
required: false,
@ -311,6 +316,9 @@ export default {
}
},
computed: {
submitterSlug () {
return this.submitter.slug
},
computedPreviousValue () {
if (this.isUsePreviousValue) {
return this.previousValue
@ -521,6 +529,27 @@ export default {
this.uploadImageInputKey = Math.random().toString()
}
},
maybeSetSignedUuid (signedUuid) {
try {
if (window.localStorage && signedUuid && this.rememberSignature) {
const values = window.localStorage.getItem('signed_signature_uuids')
let data
if (values) {
data = JSON.parse(values)
} else {
data = {}
}
data[this.submitter.email] = signedUuid
window.localStorage.setItem('signed_signature_uuids', JSON.stringify(data))
}
} catch (e) {
console.error(e)
}
},
async submit () {
if (this.modelValue || this.computedPreviousValue) {
if (this.computedPreviousValue) {
@ -539,6 +568,7 @@ export default {
formData.append('file', file)
formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments')
formData.append('remember_signature', this.rememberSignature)
return fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
@ -547,6 +577,8 @@ export default {
this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid)
this.maybeSetSignedUuid(attachment.signed_uuid)
return resolve(attachment)
})
}).catch((error) => {

@ -32,6 +32,7 @@ class AccountConfig < ApplicationRecord
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
WEBHOOK_PREFERENCES_KEY = 'webhook_preferences'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'

@ -75,6 +75,18 @@
</div>
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::FORM_PREFILL_SIGNATURE_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
Remember and pre-fill signatures
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>

@ -1,4 +1,3 @@
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
<% configs = Submitters::FormConfigs.call(submitter) %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-message="<%= configs[:completed_message].to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>"></submission-form>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'] %>" data-completed-message="<%= configs[:completed_message].to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>"></submission-form>

@ -41,7 +41,7 @@
<div class="fixed bottom-0 w-full h-0 z-20">
<div class="mx-auto" style="max-width: 1000px">
<div class="relative md:mx-32">
<%= render 'submission_form', attachments_index: @attachments_index, submitter: @submitter, signature_attachment: @signature_attachment %>
<%= render 'submission_form', attachments_index: @attachments_index, submitter: @submitter, signature_attachment: @signature_attachment, configs: @form_configs %>
</div>
</div>
</div>

@ -5,20 +5,21 @@ module Submitters
DEFAULT_KEYS = [AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE].freeze
module_function
def call(submitter, keys = [])
configs = submitter.submission.account.account_configs
.where(key: DEFAULT_KEYS + keys)
configs = submitter.submission.account.account_configs.where(key: DEFAULT_KEYS + keys)
completed_button = configs.find { |e| e.key == AccountConfig::FORM_COMPLETED_BUTTON_KEY }&.value || {}
completed_message = configs.find { |e| e.key == AccountConfig::FORM_COMPLETED_MESSAGE_KEY }&.value || {}
with_typed_signature = configs.find { |e| e.key == AccountConfig::ALLOW_TYPED_SIGNATURE }&.value != false
with_confetti = configs.find { |e| e.key == AccountConfig::FORM_WITH_CONFETTI_KEY }&.value != false
completed_button = find_safe_value(configs, AccountConfig::FORM_COMPLETED_BUTTON_KEY) || {}
completed_message = find_safe_value(configs, AccountConfig::FORM_COMPLETED_MESSAGE_KEY) || {}
with_typed_signature = find_safe_value(configs, AccountConfig::ALLOW_TYPED_SIGNATURE) != false
with_confetti = find_safe_value(configs, AccountConfig::FORM_WITH_CONFETTI_KEY) != false
prefill_signature = find_safe_value(configs, AccountConfig::FORM_PREFILL_SIGNATURE_KEY) != false
attrs = { completed_button:, with_typed_signature:, with_confetti:, completed_message: }
attrs = { completed_button:, with_typed_signature:, with_confetti:, completed_message:, prefill_signature: }
keys.each do |key|
attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value
@ -26,5 +27,9 @@ module Submitters
attrs
end
def find_safe_value(configs, key)
configs.find { |e| e.key == key }&.value
end
end
end

@ -0,0 +1,86 @@
# frozen_string_literal: true
module Submitters
module MaybeAssignDefaultBrowserSignature
SIGNED_UUID_PURPPOSE = 'signature'
module_function
def call(submitter, params, cookies = nil, attachments = [])
if (value = params[:signature_src].presence || params[:signature].presence)
find_or_create_signature_from_value(submitter, value, attachments)
elsif params[:signed_signature_uuids].present?
find_storage_signature(submitter, params[:signed_signature_uuids], attachments)
elsif cookies
find_session_signature(submitter, cookies, attachments)
end
end
def find_or_create_signature_from_value(submitter, value, attachments)
_, attachment = Submitters::NormalizeValues.normalize_attachment_value(value,
'signature',
submitter.account,
attachments,
submitter)
attachment.record ||= submitter
attachment.save!
attachment
end
def sign_signature_uuid(uuid)
ApplicationRecord.signed_id_verifier.generate(uuid, purpose: SIGNED_UUID_PURPPOSE)
end
def verify_signature_uuid(signed_uuid)
ApplicationRecord.signed_id_verifier.verified(signed_uuid, purpose: SIGNED_UUID_PURPPOSE)
end
def find_storage_signature(submitter, signed_uuids, attachments)
signed_uuid = signed_uuids[submitter.email]
return if signed_uuid.blank?
uuid = verify_signature_uuid(signed_uuid)
return if uuid.blank?
find_signature_from_uuid(submitter, uuid, attachments)
end
def find_session_signature(submitter, cookies, attachments)
values =
begin
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
rescue JSON::ParserError
{}
end
return if values.blank?
uuid = values[submitter.email]
return if uuid.blank?
find_signature_from_uuid(submitter, uuid, attachments)
end
def find_signature_from_uuid(submitter, uuid, attachments)
signature_attachment = ActiveStorage::Attachment.find_by(uuid:)
return unless signature_attachment
return if signature_attachment.record.email != submitter.email
existing_attachment = attachments.find do |a|
a.blob_id == signature_attachment.blob_id && submitter.id == a.record_id
end
return existing_attachment if existing_attachment
submitter.attachments_attachments.create_or_find_by!(blob_id: signature_attachment.blob_id)
end
end
end

@ -1,10 +1,10 @@
# frozen_string_literal: true
module Submitters
module MaybeAssignDefaultSignature
module MaybeAssignDefaultEmailSignature
module_function
def call(submitter, params, attachments_index)
def call(submitter, params, attachments)
return if params[:t].present? && params[:t] != SubmissionEvents.build_tracking_param(submitter, 'click_email')
return if params[:t].blank? && !submitter.submission_events.exists?(event_type: :click_email)
@ -12,18 +12,13 @@ module Submitters
return if signature_attachment.blank?
existing_attachment = attachments_index.values.find do |a|
a.blob_id == signature_attachment.blob_id && submitter.id == signature_attachment.record_id
existing_attachment = attachments.find do |a|
a.blob_id == signature_attachment.blob_id && submitter.id == a.record_id
end
return existing_attachment if existing_attachment
attachment =
submitter.attachments_attachments.create_or_find_by!(blob_id: signature_attachment.blob_id)
attachments_index[attachment.uuid] = attachment
attachment
submitter.attachments_attachments.create_or_find_by!(blob_id: signature_attachment.blob_id)
end
def find_previous_signature(submitter)
Loading…
Cancel
Save