Merge from docusealco/wip

pull/381/merge 2.1.3
Alex Turchyn 2 months ago committed by GitHub
commit 9b935a8180
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -15,6 +15,7 @@ class AccountConfigsController < ApplicationController
AccountConfig::ESIGNING_PREFERENCE_KEY, AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY, AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY, AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY, AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY, AccountConfig::ENFORCE_SIGNING_ORDER_KEY,

@ -13,7 +13,7 @@ module Api
def show def show
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid]) blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i) if blob_uuid.blank? || purp != 'blob'
Rollbar.error('Blob not found') if defined?(Rollbar) Rollbar.error('Blob not found') if defined?(Rollbar)
return head :not_found return head :not_found
@ -24,8 +24,9 @@ module Api
attachment = blob.attachments.take attachment = blob.attachments.take
@record = attachment.record @record = attachment.record
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
authorization_check!(attachment) if exp.blank? authorization_check!(attachment, @record, exp)
if request.headers['Range'].present? if request.headers['Range'].present?
send_blob_byte_range_data blob, request.headers['Range'] send_blob_byte_range_data blob, request.headers['Range']
@ -41,14 +42,19 @@ module Api
private private
def authorization_check!(attachment) def authorization_check!(attachment, record, exp)
is_authorized = attachment.name.in?(%w[logo preview_images]) || return if attachment.name == 'logo'
(current_user && attachment.record.account.id == current_user.account_id) || return if exp.to_i >= Time.current.to_i
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
!attachment.record.account.account_configs
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
return if is_authorized return if current_user && current_ability.can?(:read, record)
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
require_auth = configs.any? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_AUTH_KEY && c.value }
require_ttl = configs.none? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY && c.value == false }
return if !require_ttl && !require_auth
Rollbar.error('Blob aunauthorized') if defined?(Rollbar) Rollbar.error('Blob aunauthorized') if defined?(Rollbar)

@ -18,12 +18,14 @@ module Api
field: :completed_at field: :completed_at
) )
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
data: submitters.map do |s| data: submitters.map do |s|
{ {
event_type: 'form.completed', event_type: 'form.completed',
timestamp: s.completed_at, timestamp: s.completed_at,
data: Submitters::SerializeForWebhook.call(s) data: Submitters::SerializeForWebhook.call(s, expires_at:)
} }
end, end,
pagination: { pagination: {

@ -34,10 +34,12 @@ module Api
associations: [:blob] associations: [:blob]
).call ).call
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
id: @submission.id, id: @submission.id,
documents: documents.map do |attachment| documents: documents.map do |attachment|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) } { name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
end end
} }
end end

@ -14,16 +14,19 @@ module Api
:created_by_user, :submission_events, :created_by_user, :submission_events,
template: :folder, template: :folder,
submitters: { documents_attachments: :blob, attachments_attachments: :blob }, submitters: { documents_attachments: :blob, attachments_attachments: :blob },
audit_trail_attachment: :blob audit_trail_attachment: :blob,
combined_document_attachment: :blob
), ),
field: :completed_at) field: :completed_at)
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
data: submissions.map do |s| data: submissions.map do |s|
{ {
event_type: 'submission.completed', event_type: 'submission.completed',
timestamp: s.completed_at, timestamp: s.completed_at,
data: Submissions::SerializeForApi.call(s, s.submitters) data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
} }
end, end,
pagination: { pagination: {

@ -18,10 +18,12 @@ module Api
combined_document_attachment: :blob, combined_document_attachment: :blob,
audit_trail_attachment: :blob)) audit_trail_attachment: :blob))
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
data: submissions.map do |s| data: submissions.map do |s|
Submissions::SerializeForApi.call(s, s.submitters, params, Submissions::SerializeForApi.call(s, s.submitters, params,
with_events: false, with_documents: false, with_values: false) with_events: false, with_documents: false, with_values: false, expires_at:)
end, end,
pagination: { pagination: {
count: submissions.size, count: submissions.size,

@ -14,9 +14,11 @@ module Api
documents_attachments: :blob, attachments_attachments: :blob) documents_attachments: :blob, attachments_attachments: :blob)
) )
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
data: submitters.map do |s| data: submitters.map do |s|
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:) Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:, expires_at:)
end, end,
pagination: { pagination: {
count: submitters.size, count: submitters.size,

@ -26,13 +26,15 @@ module Api
original_template: @template, original_template: @template,
documents: params[:documents]) documents: params[:documents])
Templates.maybe_assign_access(cloned_template)
cloned_template.save! cloned_template.save!
WebhookUrls.enqueue_events(cloned_template, 'template.created') WebhookUrls.enqueue_events(cloned_template, 'template.created')
SearchEntries.enqueue_reindex(cloned_template) SearchEntries.enqueue_reindex(cloned_template)
render json: Templates::SerializeForApi.call(cloned_template, schema_documents) render json: Templates::SerializeForApi.call(cloned_template, schema_documents:)
end end
end end
end end

@ -24,13 +24,14 @@ module Api
name: :preview_images) name: :preview_images)
.preload(:blob) .preload(:blob)
expires_at = Accounts.link_expires_at(current_account)
render json: { render json: {
data: templates.map do |t| data: templates.map do |t|
Templates::SerializeForApi.call( Templates::SerializeForApi.call(t,
t, schema_documents: schema_documents.select { |e| e.record_id == t.id },
schema_documents.select { |e| e.record_id == t.id }, preview_image_attachments:,
preview_image_attachments expires_at:)
)
end, end,
pagination: { pagination: {
count: templates.size, count: templates.size,

@ -10,11 +10,13 @@ class SubmissionsExportController < ApplicationController
attachments_attachments: :blob }) attachments_attachments: :blob })
.order(id: :asc) .order(id: :asc)
expires_at = Accounts.link_expires_at(current_account)
if params[:format] == 'csv' if params[:format] == 'csv'
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]), send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
filename: "#{@template.name}.csv" filename: "#{@template.name}.csv"
elsif params[:format] == 'xlsx' elsif params[:format] == 'xlsx'
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]), send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
filename: "#{@template.name}.xlsx" filename: "#{@template.name}.xlsx"
end end
end end

@ -17,6 +17,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true) documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
Templates.maybe_assign_access(cloned_template)
cloned_template.save! cloned_template.save!
Templates::CloneAttachments.call(template: cloned_template, original_template: @template, Templates::CloneAttachments.call(template: cloned_template, original_template: @template,

@ -69,6 +69,8 @@ class TemplatesController < ApplicationController
@template.account = current_account @template.account = current_account
end end
Templates.maybe_assign_access(@template)
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

@ -46,6 +46,8 @@ class TemplatesUploadsController < ApplicationController
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*') template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
Templates.maybe_assign_access(template)
template.save! template.save!
template template

@ -60,7 +60,15 @@ export default {
return ['UL', 'I', 'EM', 'B', 'STRONG', 'P'] return ['UL', 'I', 'EM', 'B', 'STRONG', 'P']
}, },
dom () { dom () {
const text = this.string.replace(/(?<!\(\s*)(https?:\/\/[^\s)]+)(?!\s*\))/g, (url) => `[${url}](${url})`) const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
const text = linkParts.map((part, index) => {
if (part.match(/^https?:\/\//) && !linkParts[index - 1]?.match(/\(\s*$/) && !linkParts[index + 1]?.match(/^\s*\)/)) {
return `[${part}](${part})`
} else {
return part
}
}).join('')
return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html') return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html')
} }

@ -1265,24 +1265,31 @@ export default {
if (!field.areas.length) { if (!field.areas.length) {
this.template.fields.splice(this.template.fields.indexOf(field), 1) this.template.fields.splice(this.template.fields.indexOf(field), 1)
this.template.fields.forEach((f) => { this.removeFieldConditions(field)
(f.conditions || []).forEach((c) => { }
this.save()
},
removeFieldConditions (field) {
this.template.fields.forEach((f) => {
if (f.conditions) {
f.conditions.forEach((c) => {
if (c.field_uuid === field.uuid) { if (c.field_uuid === field.uuid) {
f.conditions.splice(f.conditions.indexOf(c), 1) f.conditions.splice(f.conditions.indexOf(c), 1)
} }
}) })
}) }
})
this.template.schema.forEach((item) => { this.template.schema.forEach((item) => {
(item.conditions || []).forEach((c) => { if (item.conditions) {
item.conditions.forEach((c) => {
if (c.field_uuid === field.uuid) { if (c.field_uuid === field.uuid) {
item.conditions.splice(item.conditions.indexOf(c), 1) item.conditions.splice(item.conditions.indexOf(c), 1)
} }
}) })
}) }
} })
this.save()
}, },
pasteField () { pasteField () {
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea)) const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
@ -1715,8 +1722,15 @@ export default {
}) })
}) })
this.template.fields = this.template.fields = this.template.fields.reduce((acc, f) => {
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length) if (removedFieldUuids.includes(f.uuid) && !f.areas?.length) {
this.removeFieldConditions(f)
} else {
acc.push(f)
}
return acc
}, [])
this.save() this.save()
} }

@ -38,6 +38,7 @@ class AccountConfig < ApplicationRecord
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature' FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
ESIGNING_PREFERENCE_KEY = 'esigning_preference' ESIGNING_PREFERENCE_KEY = 'esigning_preference'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth' DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
DOWNLOAD_LINKS_EXPIRE_KEY = 'download_links_expire'
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'
WITH_SIGNATURE_ID = 'with_signature_id' WITH_SIGNATURE_ID = 'with_signature_id'

@ -114,16 +114,16 @@ class Submission < ApplicationRecord
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] } @fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
end end
def audit_trail_url def audit_trail_url(expires_at: nil)
return if audit_trail.blank? return if audit_trail.blank?
ActiveStorage::Blob.proxy_url(audit_trail.blob) ActiveStorage::Blob.proxy_url(audit_trail.blob, expires_at:)
end end
alias audit_log_url audit_trail_url alias audit_log_url audit_trail_url
def combined_document_url def combined_document_url(expires_at: nil)
return if combined_document.blank? return if combined_document.blank?
ActiveStorage::Blob.proxy_url(combined_document.blob) ActiveStorage::Blob.proxy_url(combined_document.blob, expires_at:)
end end
end end

@ -130,6 +130,18 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_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>
<%= t('expirable_file_download_links') %>
</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) %> <% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY) %>
<% if can?(:manage, account_config) %> <% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %>

@ -26,6 +26,7 @@ en: &en
select: Select select: Select
party: Party party: Party
edit_order: Edit Order edit_order: Edit Order
expirable_file_download_links: Expirable file download links
invite_form_fields: Invite form fields invite_form_fields: Invite form fields
default_parties: Default parties default_parties: Default parties
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
@ -36,6 +37,7 @@ en: &en
bcc_recipients: BCC recipients bcc_recipients: BCC recipients
resend_pending: Re-send pending resend_pending: Re-send pending
always_enforce_signing_order: Always enforce the signing order always_enforce_signing_order: Always enforce the signing order
create_templates_with_private_access_by_default: Create templates with private access by default
ensure_unique_recipients: Ensure unique recipients ensure_unique_recipients: Ensure unique recipients
edit_per_party: Edit per party edit_per_party: Edit per party
reply_to: Reply to reply_to: Reply to
@ -900,6 +902,8 @@ en: &en
range_without_total: "%{from}-%{to} events" range_without_total: "%{from}-%{to} events"
es: &es es: &es
expirable_file_download_links: Enlaces de descarga de archivos con vencimiento
create_templates_with_private_access_by_default: Crear plantillas con acceso privado por defecto
party: Parte party: Parte
edit_order: Edita Pedido edit_order: Edita Pedido
select: Seleccionar select: Seleccionar
@ -1780,6 +1784,8 @@ es: &es
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
it: &it it: &it
expirable_file_download_links: Link di download di file con scadenza
create_templates_with_private_access_by_default: Crea modelli con accesso privato per impostazione predefinita
party: Parte party: Parte
edit_order: Modifica Ordine edit_order: Modifica Ordine
select: Seleziona select: Seleziona
@ -2660,6 +2666,8 @@ it: &it
range_without_total: "%{from}-%{to} eventi" range_without_total: "%{from}-%{to} eventi"
fr: &fr fr: &fr
expirable_file_download_links: Liens de téléchargement de fichiers expirables
create_templates_with_private_access_by_default: Créer des modèles avec un accès privé par défaut
party: Partie party: Partie
edit_order: Modifier la commande edit_order: Modifier la commande
select: Sélectionner select: Sélectionner
@ -3543,6 +3551,8 @@ fr: &fr
range_without_total: "%{from} à %{to} événements" range_without_total: "%{from} à %{to} événements"
pt: &pt pt: &pt
expirable_file_download_links: Links de download de arquivos com expiração
create_templates_with_private_access_by_default: Criar modelos com acesso privado por padrão
party: Parte party: Parte
edit_order: Edita Pedido edit_order: Edita Pedido
select: Selecionar select: Selecionar
@ -4424,6 +4434,8 @@ pt: &pt
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
de: &de de: &de
expirable_file_download_links: Ablaufbare Datei-Download-Links
create_templates_with_private_access_by_default: Vorlagen standardmäßig mit privatem Zugriff erstellen
party: Partei party: Partei
edit_order: Bestellung bearbeiten edit_order: Bestellung bearbeiten
select: Auswählen select: Auswählen

@ -0,0 +1,30 @@
# frozen_string_literal: true
class PopulateExpireLinkConfigs < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
class MigrationAccount < ActiveRecord::Base
self.table_name = 'accounts'
end
class MigrationAccountConfig < ActiveRecord::Base
self.table_name = 'account_configs'
serialize :value, coder: JSON
end
def up
MigrationAccount.find_each do |account|
config = MigrationAccountConfig.find_or_initialize_by(key: 'download_links_expire', account_id: account.id)
next if config.persisted?
config.value = false
config.save!
end
end
def down
nil
end
end

@ -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[8.0].define(version: 2025_07_18_121133) do ActiveRecord::Schema[8.0].define(version: 2025_08_31_125322) 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 "btree_gin" enable_extension "btree_gin"
enable_extension "plpgsql" enable_extension "plpgsql"

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module Accounts module Accounts
LINK_EXPIRES_AT = 40.minutes
module_function module_function
def create_duplicate(account) def create_duplicate(account)
@ -185,4 +187,11 @@ module Accounts
rescue TZInfo::InvalidTimezoneIdentifier rescue TZInfo::InvalidTimezoneIdentifier
'UTC' 'UTC'
end end
def link_expires_at(account)
return if AccountConfig.find_or_initialize_by(account: account,
key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY).value == false
LINK_EXPIRES_AT.from_now
end
end end

@ -102,6 +102,7 @@ module ReplaceEmailVariables
return unless submitter return unless submitter
value = submitter.try(field_name) value = submitter.try(field_name)
expires_at = nil
if value_name if value_name
field = (submission.template_fields || submission.template.fields).find { |e| e['name'] == value_name } field = (submission.template_fields || submission.template.fields).find { |e| e['name'] == value_name }
@ -114,7 +115,11 @@ module ReplaceEmailVariables
attachment = submitter.attachments.find { |e| e.uuid == attachment_uuid } attachment = submitter.attachments.find { |e| e.uuid == attachment_uuid }
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment if attachment
expires_at ||= Accounts.link_expires_at(Account.new(id: submission.account_id))
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
end
else else
value[field&.dig('uuid')] value[field&.dig('uuid')]
end end

@ -239,7 +239,7 @@ module Submissions
item['conditions'].each_with_object([]) do |condition, acc| item['conditions'].each_with_object([]) do |condition, acc|
result = result =
if fields_index[condition['field_uuid']]['submitter_uuid'] == include_submitter_uuid if fields_index.dig(condition['field_uuid'], 'submitter_uuid') == include_submitter_uuid
submitter_conditions_acc << condition if submitter_conditions_acc submitter_conditions_acc << condition if submitter_conditions_acc
true true

@ -391,8 +391,8 @@ module Submissions
composer.formatted_text_box( composer.formatted_text_box(
Array.wrap(value).map do |uuid| Array.wrap(value).map do |uuid|
attachment = submitter.attachments.find { |a| a.uuid == uuid } attachment = submitter.attachments.find { |a| a.uuid == uuid }
link =
ActiveStorage::Blob.proxy_url(attachment.blob) link = r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
{ link:, text: "#{attachment.filename}\n", style: :link } { link:, text: "#{attachment.filename}\n", style: :link }
end, end,
@ -489,6 +489,10 @@ module Submissions
padding: [5, 0, 0, 8], padding: [5, 0, 0, 8],
position: :float, text_align: :left) position: :float, text_align: :left)
end end
def r
Rails.application.routes.url_helpers
end
# rubocop:enable Metrics # rubocop:enable Metrics
end end
end end

@ -6,8 +6,8 @@ module Submissions
module_function module_function
def call(submissions, format: :csv) def call(submissions, format: :csv, expires_at: nil)
rows = build_table_rows(submissions) rows = build_table_rows(submissions, expires_at:)
if format.to_sym == :csv if format.to_sym == :csv
rows_to_csv(rows) rows_to_csv(rows)
@ -57,7 +57,7 @@ module Submissions
headers.map { |key| row.find { |e| e[:name] == key }&.dig(:value) } headers.map { |key| row.find { |e| e[:name] == key }&.dig(:value) }
end end
def build_table_rows(submissions) def build_table_rows(submissions, expires_at: nil)
submissions.preload(submitters: [attachments_attachments: :blob, documents_attachments: :blob]) submissions.preload(submitters: [attachments_attachments: :blob, documents_attachments: :blob])
.find_each.map do |submission| .find_each.map do |submission|
submission_data = [] submission_data = []
@ -69,7 +69,7 @@ module Submissions
submission_data += build_submission_data(submitter, submitter_name, submitters_count) submission_data += build_submission_data(submitter, submitter_name, submitters_count)
submission_data += submitter_formatted_fields(submitter).map do |field| submission_data += submitter_formatted_fields(submitter, expires_at:).map do |field|
{ {
name: column_name(field[:name], submitter_name, submitters_count), name: column_name(field[:name], submitter_name, submitters_count),
value: field[:value] value: field[:value]
@ -81,7 +81,7 @@ module Submissions
submission_data += submitter.documents.map.with_index(1) do |attachment, index| submission_data += submitter.documents.map.with_index(1) do |attachment, index|
{ {
name: "#{I18n.t('document')} #{index}", name: "#{I18n.t('document')} #{index}",
value: ActiveStorage::Blob.proxy_url(attachment.blob) value: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
} }
end end
end end
@ -123,7 +123,7 @@ module Submissions
submitters_count > 1 ? "#{submitter_name} - #{name}" : name submitters_count > 1 ? "#{submitter_name} - #{name}" : name
end end
def submitter_formatted_fields(submitter) def submitter_formatted_fields(submitter, expires_at: nil)
fields = submitter.submission.template_fields || submitter.submission.template.fields fields = submitter.submission.template_fields || submitter.submission.template.fields
template_fields = fields.select { |f| f['submitter_uuid'] == submitter.uuid } template_fields = fields.select { |f| f['submitter_uuid'] == submitter.uuid }
@ -142,11 +142,13 @@ module Submissions
value = value =
if template_field_type.in?(%w[image signature]) if template_field_type.in?(%w[image signature])
attachment = attachments_index[submitter_value] attachment = attachments_index[submitter_value]
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
elsif template_field_type == 'file' elsif template_field_type == 'file'
Array.wrap(submitter_value).compact_blank.filter_map do |e| Array.wrap(submitter_value).compact_blank.filter_map do |e|
attachment = attachments_index[e] attachment = attachments_index[e]
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
end end
else else
submitter_value submitter_value

@ -201,13 +201,15 @@ module Submissions
attachments_data_cache = {} attachments_data_cache = {}
return pdfs_index if submitter.submission.template_fields.blank? submission = submitter.submission
return pdfs_index if submission.template_fields.blank?
with_headings = find_last_submitter(submitter.submission, submitter:).blank? if with_headings.nil? with_headings = find_last_submitter(submission, submitter:).blank? if with_headings.nil?
locale = submitter.metadata.fetch('lang', account.locale) locale = submitter.metadata.fetch('lang', account.locale)
submitter.submission.template_fields.each do |field| submission.template_fields.each do |field|
next if field['type'] == 'heading' && !with_headings next if field['type'] == 'heading' && !with_headings
next if field['submitter_uuid'] != submitter.uuid && field['type'] != 'heading' next if field['submitter_uuid'] != submitter.uuid && field['type'] != 'heading'
@ -476,7 +478,7 @@ module Submissions
height_diff - (height_diff.zero? ? diff : 0) height_diff - (height_diff.zero? ? diff : 0)
], ],
A: { Type: :Action, S: :URI, A: { Type: :Action, S: :URI,
URI: ActiveStorage::Blob.proxy_url(attachment.blob) } URI: r.submissions_preview_url(submission.slug, **Docuseal.default_url_options) }
} }
) )
@ -747,11 +749,15 @@ module Submissions
def maybe_rotate_pdf(pdf) def maybe_rotate_pdf(pdf)
return pdf if pdf.pages.size > MAX_PAGE_ROTATE return pdf if pdf.pages.size > MAX_PAGE_ROTATE
is_pages_rotated = pdf.pages.root[:Rotate].present? && pdf.pages.root[:Rotate] != 0
pdf.pages.root[:Rotate] = 0 if is_pages_rotated
is_rotated = pdf.pages.filter_map do |page| is_rotated = pdf.pages.filter_map do |page|
page.rotate(0, flatten: true) if page[:Rotate] != 0 page.rotate(0, flatten: true) if page[:Rotate] != 0
end.present? end.present?
return pdf unless is_rotated return pdf if !is_rotated && !is_pages_rotated
io = StringIO.new io = StringIO.new
@ -873,7 +879,7 @@ module Submissions
end end
end end
def h def r
Rails.application.routes.url_helpers Rails.application.routes.url_helpers
end end
end end

@ -4,7 +4,6 @@ module Submissions
module SerializeForApi module SerializeForApi
SERIALIZE_PARAMS = { SERIALIZE_PARAMS = {
only: %i[id name slug source submitters_order expire_at created_at updated_at archived_at], only: %i[id name slug source submitters_order expire_at created_at updated_at archived_at],
methods: %i[audit_log_url combined_document_url],
include: { include: {
submitters: { only: %i[id] }, submitters: { only: %i[id] },
template: { only: %i[id name external_id created_at updated_at], template: { only: %i[id name external_id created_at updated_at],
@ -15,11 +14,13 @@ module Submissions
module_function module_function
def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true) def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true,
expires_at: Accounts.link_expires_at(Account.new(id: submission.account_id)))
submitters ||= submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob) submitters ||= submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
serialized_submitters = submitters.map do |submitter| serialized_submitters = submitters.map do |submitter|
Submitters::SerializeForApi.call(submitter, with_documents:, with_events: false, with_values:, params:) Submitters::SerializeForApi.call(submitter, with_documents:, with_events: false, with_values:, params:,
expires_at:)
end end
json = submission.as_json(SERIALIZE_PARAMS) json = submission.as_json(SERIALIZE_PARAMS)
@ -30,8 +31,6 @@ module Submissions
json['submission_events'] = Submitters::SerializeForApi.serialize_events(submission.submission_events) json['submission_events'] = Submitters::SerializeForApi.serialize_events(submission.submission_events)
end end
json['combined_document_url'] ||= maybe_build_combined_url(submitters, submission, params)
if submitters.all?(&:completed_at?) if submitters.all?(&:completed_at?)
last_submitter = submitters.max_by(&:completed_at) last_submitter = submitters.max_by(&:completed_at)
@ -39,10 +38,16 @@ module Submissions
json['documents'] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents'] json['documents'] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
end end
json['audit_log_url'] = submission.audit_log_url(expires_at:)
json['combined_document_url'] = submission.combined_document_url(expires_at:)
json['combined_document_url'] ||= maybe_build_combined_url(submitters, submission, params, expires_at:)
json['status'] = 'completed' json['status'] = 'completed'
json['completed_at'] = last_submitter.completed_at.as_json json['completed_at'] = last_submitter.completed_at.as_json
else else
json['documents'] = [] if with_documents json['documents'] = [] if with_documents
json['audit_log_url'] = nil
json['combined_document_url'] = nil
json['status'] = build_status(submission, submitters) json['status'] = build_status(submission, submitters)
json['completed_at'] = nil json['completed_at'] = nil
end end
@ -60,7 +65,7 @@ module Submissions
end end
end end
def maybe_build_combined_url(submitters, submission, params) def maybe_build_combined_url(submitters, submission, params, expires_at: nil)
return unless submitters.all?(&:completed_at?) return unless submitters.all?(&:completed_at?)
attachment = submission.combined_document_attachment attachment = submission.combined_document_attachment
@ -71,7 +76,7 @@ module Submissions
attachment = Submissions::GenerateCombinedAttachment.call(submitter) attachment = Submissions::GenerateCombinedAttachment.call(submitter)
end end
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
end end
end end
end end

@ -11,7 +11,7 @@ module Submitters
module_function module_function
def call(submitter, with_template: false, with_events: false, with_documents: true, with_urls: false, def call(submitter, with_template: false, with_events: false, with_documents: true, with_urls: false,
with_values: true, params: {}) with_values: true, params: {}, expires_at: Accounts.link_expires_at(Account.new(id: submitter.account_id)))
ActiveRecord::Associations::Preloader.new( ActiveRecord::Associations::Preloader.new(
records: [submitter], records: [submitter],
associations: if with_documents associations: if with_documents
@ -24,15 +24,19 @@ module Submitters
additional_attrs = {} additional_attrs = {}
if params[:include].to_s.include?('fields') if params[:include].to_s.include?('fields')
additional_attrs['fields'] = SerializeForWebhook.build_fields_array(submitter) additional_attrs['fields'] = SerializeForWebhook.build_fields_array(submitter, expires_at:)
end end
if with_template if with_template
additional_attrs['template'] = submitter.submission.template.as_json(only: %i[id name created_at updated_at]) additional_attrs['template'] = submitter.submission.template.as_json(only: %i[id name created_at updated_at])
end end
additional_attrs['values'] = SerializeForWebhook.build_values_array(submitter) if with_values additional_attrs['values'] = SerializeForWebhook.build_values_array(submitter, expires_at:) if with_values
additional_attrs['documents'] = SerializeForWebhook.build_documents_array(submitter) if with_documents
if with_documents
additional_attrs['documents'] = SerializeForWebhook.build_documents_array(submitter, expires_at:)
end
additional_attrs['preferences'] = submitter.preferences.except('default_values') additional_attrs['preferences'] = submitter.preferences.except('default_values')
additional_attrs['submission_events'] = serialize_events(submitter.submission_events) if with_events additional_attrs['submission_events'] = serialize_events(submitter.submission_events) if with_events

@ -10,13 +10,13 @@ module Submitters
module_function module_function
def call(submitter) def call(submitter, expires_at: Accounts.link_expires_at(Account.new(id: submitter.account_id)))
ActiveRecord::Associations::Preloader.new( ActiveRecord::Associations::Preloader.new(
records: [submitter], associations: [documents_attachments: :blob, attachments_attachments: :blob] records: [submitter], associations: [documents_attachments: :blob, attachments_attachments: :blob]
).call ).call
values = build_values_array(submitter) values = build_values_array(submitter, expires_at:)
documents = build_documents_array(submitter) documents = build_documents_array(submitter, expires_at:)
submission = submitter.submission submission = submitter.submission
@ -32,7 +32,7 @@ module Submitters
'preferences' => submitter.preferences.except('default_values'), 'preferences' => submitter.preferences.except('default_values'),
'values' => values, 'values' => values,
'documents' => documents, 'documents' => documents,
'audit_log_url' => submitter.submission.audit_log_url, 'audit_log_url' => submitter.submission.audit_log_url(expires_at:),
'submission_url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options), 'submission_url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options),
'template' => submission.template.as_json( 'template' => submission.template.as_json(
only: %i[id name external_id created_at updated_at], only: %i[id name external_id created_at updated_at],
@ -40,15 +40,15 @@ module Submitters
), ),
'submission' => { 'submission' => {
'id' => submission.id, 'id' => submission.id,
'audit_log_url' => submission.audit_log_url, 'audit_log_url' => submission.audit_log_url(expires_at:),
'combined_document_url' => submission.combined_document_url, 'combined_document_url' => submission.combined_document_url(expires_at:),
'status' => build_submission_status(submission), 'status' => build_submission_status(submission),
'url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options), 'url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options),
'created_at' => submission.created_at.as_json 'created_at' => submission.created_at.as_json
}) })
end end
def build_values_array(submitter) def build_values_array(submitter, expires_at: nil)
fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || [] fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || []
attachments_index = submitter.attachments.index_by(&:uuid) attachments_index = submitter.attachments.index_by(&:uuid)
submitter_field_counters = Hash.new { 0 } submitter_field_counters = Hash.new { 0 }
@ -64,13 +64,13 @@ module Submitters
next if !submitter.values.key?(field['uuid']) && !submitter.completed_at? next if !submitter.values.key?(field['uuid']) && !submitter.completed_at?
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index) value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index, expires_at:)
{ 'field' => field_name, 'value' => value } { 'field' => field_name, 'value' => value }
end end
end end
def build_fields_array(submitter) def build_fields_array(submitter, expires_at: nil)
fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || [] fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || []
attachments_index = submitter.attachments.index_by(&:uuid) attachments_index = submitter.attachments.index_by(&:uuid)
submitter_field_counters = Hash.new { 0 } submitter_field_counters = Hash.new { 0 }
@ -86,7 +86,7 @@ module Submitters
next if !submitter.values.key?(field['uuid']) && !submitter.completed_at? next if !submitter.values.key?(field['uuid']) && !submitter.completed_at?
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index) value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index, expires_at:)
{ 'name' => field_name, 'uuid' => field['uuid'], 'value' => value, 'readonly' => field['readonly'] == true } { 'name' => field_name, 'uuid' => field['uuid'], 'value' => value, 'readonly' => field['readonly'] == true }
end end
@ -104,26 +104,26 @@ module Submitters
end end
end end
def build_documents_array(submitter) def build_documents_array(submitter, expires_at: nil)
submitter.documents.map do |attachment| submitter.documents.map do |attachment|
{ 'name' => attachment.filename.base, 'url' => rails_storage_proxy_url(attachment) } { 'name' => attachment.filename.base, 'url' => rails_storage_proxy_url(attachment, expires_at:) }
end end
end end
def fetch_field_value(field, value, attachments_index) def fetch_field_value(field, value, attachments_index, expires_at: nil)
if field['type'].in?(%w[image signature initials stamp payment]) if field['type'].in?(%w[image signature initials stamp payment])
rails_storage_proxy_url(attachments_index[value]) rails_storage_proxy_url(attachments_index[value], expires_at:)
elsif field['type'] == 'file' elsif field['type'] == 'file'
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e]) } Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e], expires_at:) }
else else
value value
end end
end end
def rails_storage_proxy_url(attachment) def rails_storage_proxy_url(attachment, expires_at: nil)
return if attachment.blank? return if attachment.blank?
ActiveStorage::Blob.proxy_url(attachment.blob) ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
end end
def r def r

@ -37,6 +37,10 @@ module Templates
hash hash
end end
def maybe_assign_access(_template)
nil
end
def search(current_user, templates, keyword) def search(current_user, templates, keyword)
if Docuseal.fulltext_search? if Docuseal.fulltext_search?
fulltext_search(current_user, templates, keyword) fulltext_search(current_user, templates, keyword)

@ -14,7 +14,8 @@ module Templates
module_function module_function
def call(template, schema_documents = template.schema_documents.preload(:blob), preview_image_attachments = nil) def call(template, schema_documents: template.schema_documents.preload(:blob), preview_image_attachments: nil,
expires_at: Accounts.link_expires_at(Account.new(id: template.account_id)))
json = template.as_json(SERIALIZE_PARAMS) json = template.as_json(SERIALIZE_PARAMS)
preview_image_attachments ||= preview_image_attachments ||=
@ -40,8 +41,8 @@ module Templates
{ {
'id' => attachment.id, 'id' => attachment.id,
'uuid' => attachment.uuid, 'uuid' => attachment.uuid,
'url' => ActiveStorage::Blob.proxy_url(attachment.blob), 'url' => ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:),
'preview_image_url' => first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob), 'preview_image_url' => first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob, expires_at:),
'filename' => attachment.filename 'filename' => attachment.filename
} }
end end

@ -8,6 +8,10 @@ describe 'Templates API' do
let(:folder) { create(:template_folder, account:) } let(:folder) { create(:template_folder, account:) }
let(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } } let(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } }
before do
allow(Accounts).to receive(:link_expires_at).and_return(Accounts::LINK_EXPIRES_AT)
end
describe 'GET /api/templates' do describe 'GET /api/templates' do
it 'returns a list of templates' do it 'returns a list of templates' do
templates = [ templates = [
@ -211,8 +215,8 @@ describe 'Templates API' do
{ {
id: template.documents.first.id, id: template.documents.first.id,
uuid: template.documents.first.uuid, uuid: template.documents.first.uuid,
url: ActiveStorage::Blob.proxy_url(attachment.blob), url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: Accounts::LINK_EXPIRES_AT),
preview_image_url: ActiveStorage::Blob.proxy_url(first_page_blob), preview_image_url: ActiveStorage::Blob.proxy_url(first_page_blob, expires_at: Accounts::LINK_EXPIRES_AT),
filename: 'sample-document.pdf' filename: 'sample-document.pdf'
} }
], ],
@ -235,7 +239,7 @@ describe 'Templates API' do
folder_name: folder.name, folder_name: folder.name,
source: 'native', source: 'native',
external_id: template.external_id, external_id: template.external_id,
application_key: template.external_id # Backward compatibility application_key: template.external_id
} }
end end

Loading…
Cancel
Save