diff --git a/app/controllers/account_configs_controller.rb b/app/controllers/account_configs_controller.rb index e868d08a..66683e3f 100644 --- a/app/controllers/account_configs_controller.rb +++ b/app/controllers/account_configs_controller.rb @@ -15,6 +15,7 @@ class AccountConfigsController < ApplicationController AccountConfig::ESIGNING_PREFERENCE_KEY, AccountConfig::FORM_WITH_CONFETTI_KEY, AccountConfig::DOWNLOAD_LINKS_AUTH_KEY, + AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY, AccountConfig::FORCE_SSO_AUTH_KEY, AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::ENFORCE_SIGNING_ORDER_KEY, diff --git a/app/controllers/api/active_storage_blobs_proxy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_controller.rb index 6f505992..8df3b523 100644 --- a/app/controllers/api/active_storage_blobs_proxy_controller.rb +++ b/app/controllers/api/active_storage_blobs_proxy_controller.rb @@ -13,7 +13,7 @@ module Api def show 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) return head :not_found @@ -24,8 +24,9 @@ module Api attachment = blob.attachments.take @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? send_blob_byte_range_data blob, request.headers['Range'] @@ -41,14 +42,19 @@ module Api private - def authorization_check!(attachment) - is_authorized = attachment.name.in?(%w[logo preview_images]) || - (current_user && attachment.record.account.id == current_user.account_id) || - (current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') || - !attachment.record.account.account_configs - .find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value + def authorization_check!(attachment, record, exp) + return if attachment.name == 'logo' + return if exp.to_i >= Time.current.to_i - 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) diff --git a/app/controllers/api/form_events_controller.rb b/app/controllers/api/form_events_controller.rb index 759cd230..4c881594 100644 --- a/app/controllers/api/form_events_controller.rb +++ b/app/controllers/api/form_events_controller.rb @@ -18,12 +18,14 @@ module Api field: :completed_at ) + expires_at = Accounts.link_expires_at(current_account) + render json: { data: submitters.map do |s| { event_type: 'form.completed', timestamp: s.completed_at, - data: Submitters::SerializeForWebhook.call(s) + data: Submitters::SerializeForWebhook.call(s, expires_at:) } end, pagination: { diff --git a/app/controllers/api/submission_documents_controller.rb b/app/controllers/api/submission_documents_controller.rb index 283b55bf..148d499f 100644 --- a/app/controllers/api/submission_documents_controller.rb +++ b/app/controllers/api/submission_documents_controller.rb @@ -34,10 +34,12 @@ module Api associations: [:blob] ).call + expires_at = Accounts.link_expires_at(current_account) + render json: { id: @submission.id, 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 diff --git a/app/controllers/api/submission_events_controller.rb b/app/controllers/api/submission_events_controller.rb index 1a32023b..54d01443 100644 --- a/app/controllers/api/submission_events_controller.rb +++ b/app/controllers/api/submission_events_controller.rb @@ -14,16 +14,19 @@ module Api :created_by_user, :submission_events, template: :folder, submitters: { documents_attachments: :blob, attachments_attachments: :blob }, - audit_trail_attachment: :blob + audit_trail_attachment: :blob, + combined_document_attachment: :blob ), field: :completed_at) + expires_at = Accounts.link_expires_at(current_account) + render json: { data: submissions.map do |s| { event_type: 'submission.completed', timestamp: s.completed_at, - data: Submissions::SerializeForApi.call(s, s.submitters) + data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:) } end, pagination: { diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 82839f05..32b51c17 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -18,10 +18,12 @@ module Api combined_document_attachment: :blob, audit_trail_attachment: :blob)) + expires_at = Accounts.link_expires_at(current_account) + render json: { data: submissions.map do |s| 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, pagination: { count: submissions.size, diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index 990f11ba..e7635591 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -14,9 +14,11 @@ module Api documents_attachments: :blob, attachments_attachments: :blob) ) + expires_at = Accounts.link_expires_at(current_account) + render json: { 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, pagination: { count: submitters.size, diff --git a/app/controllers/api/templates_clone_controller.rb b/app/controllers/api/templates_clone_controller.rb index ec101433..d87a2eb1 100644 --- a/app/controllers/api/templates_clone_controller.rb +++ b/app/controllers/api/templates_clone_controller.rb @@ -34,7 +34,7 @@ module Api 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 diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index a098573d..c8211b7f 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -24,13 +24,14 @@ module Api name: :preview_images) .preload(:blob) + expires_at = Accounts.link_expires_at(current_account) + render json: { data: templates.map do |t| - Templates::SerializeForApi.call( - t, - schema_documents.select { |e| e.record_id == t.id }, - preview_image_attachments - ) + Templates::SerializeForApi.call(t, + schema_documents: schema_documents.select { |e| e.record_id == t.id }, + preview_image_attachments:, + expires_at:) end, pagination: { count: templates.size, diff --git a/app/controllers/submissions_export_controller.rb b/app/controllers/submissions_export_controller.rb index e929c12d..c12a2e6e 100644 --- a/app/controllers/submissions_export_controller.rb +++ b/app/controllers/submissions_export_controller.rb @@ -10,11 +10,13 @@ class SubmissionsExportController < ApplicationController attachments_attachments: :blob }) .order(id: :asc) + expires_at = Accounts.link_expires_at(current_account) + 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" 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" end end diff --git a/app/models/account_config.rb b/app/models/account_config.rb index 66b48577..2ef39cbf 100644 --- a/app/models/account_config.rb +++ b/app/models/account_config.rb @@ -38,6 +38,7 @@ class AccountConfig < ApplicationRecord FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature' ESIGNING_PREFERENCE_KEY = 'esigning_preference' DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth' + DOWNLOAD_LINKS_EXPIRE_KEY = 'download_links_expire' FORCE_SSO_AUTH_KEY = 'force_sso_auth' FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf' WITH_SIGNATURE_ID = 'with_signature_id' diff --git a/app/models/submission.rb b/app/models/submission.rb index 4e61c691..351bcfa5 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -114,16 +114,16 @@ class Submission < ApplicationRecord @fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] } end - def audit_trail_url + def audit_trail_url(expires_at: nil) return if audit_trail.blank? - ActiveStorage::Blob.proxy_url(audit_trail.blob) + ActiveStorage::Blob.proxy_url(audit_trail.blob, expires_at:) end alias audit_log_url audit_trail_url - def combined_document_url + def combined_document_url(expires_at: nil) return if combined_document.blank? - ActiveStorage::Blob.proxy_url(combined_document.blob) + ActiveStorage::Blob.proxy_url(combined_document.blob, expires_at:) end end diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index ac63ff87..d5c5453f 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -130,6 +130,18 @@ <% 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 %> +
+ + <%= t('expirable_file_download_links') %> + + <%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %> +
+ <% 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| %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 0e2805a7..c7a607f9 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -26,6 +26,7 @@ en: &en select: Select party: Party edit_order: Edit Order + expirable_file_download_links: Expirable file download links invite_form_fields: Invite form fields default_parties: Default parties authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token @@ -901,6 +902,7 @@ en: &en range_without_total: "%{from}-%{to} events" 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 edit_order: Edita Pedido @@ -1782,6 +1784,7 @@ es: &es range_without_total: "%{from}-%{to} eventos" 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 edit_order: Modifica Ordine @@ -2663,6 +2666,7 @@ it: &it range_without_total: "%{from}-%{to} eventi" 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 edit_order: Modifier la commande @@ -3547,6 +3551,7 @@ fr: &fr range_without_total: "%{from} à %{to} événements" 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 edit_order: Edita Pedido @@ -4429,6 +4434,7 @@ pt: &pt range_without_total: "%{from}-%{to} eventos" 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 edit_order: Bestellung bearbeiten diff --git a/db/migrate/20250831125322_populate_expire_link_configs.rb b/db/migrate/20250831125322_populate_expire_link_configs.rb new file mode 100644 index 00000000..352f2f77 --- /dev/null +++ b/db/migrate/20250831125322_populate_expire_link_configs.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 136d878a..bb59b018 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # 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 enable_extension "btree_gin" enable_extension "plpgsql" diff --git a/lib/accounts.rb b/lib/accounts.rb index 6153111b..5127d2c7 100644 --- a/lib/accounts.rb +++ b/lib/accounts.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Accounts + LINK_EXPIRES_AT = 40.minutes + module_function def create_duplicate(account) @@ -185,4 +187,11 @@ module Accounts rescue TZInfo::InvalidTimezoneIdentifier 'UTC' 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 diff --git a/lib/replace_email_variables.rb b/lib/replace_email_variables.rb index 3cd2d698..4222e676 100644 --- a/lib/replace_email_variables.rb +++ b/lib/replace_email_variables.rb @@ -102,6 +102,7 @@ module ReplaceEmailVariables return unless submitter value = submitter.try(field_name) + expires_at = nil if 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 } - 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 value[field&.dig('uuid')] end diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb index 81095648..2489c91f 100644 --- a/lib/submissions/generate_audit_trail.rb +++ b/lib/submissions/generate_audit_trail.rb @@ -391,8 +391,8 @@ module Submissions composer.formatted_text_box( Array.wrap(value).map do |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 } end, @@ -489,6 +489,10 @@ module Submissions padding: [5, 0, 0, 8], position: :float, text_align: :left) end + + def r + Rails.application.routes.url_helpers + end # rubocop:enable Metrics end end diff --git a/lib/submissions/generate_export_files.rb b/lib/submissions/generate_export_files.rb index 0742710a..b2b8f076 100644 --- a/lib/submissions/generate_export_files.rb +++ b/lib/submissions/generate_export_files.rb @@ -6,8 +6,8 @@ module Submissions module_function - def call(submissions, format: :csv) - rows = build_table_rows(submissions) + def call(submissions, format: :csv, expires_at: nil) + rows = build_table_rows(submissions, expires_at:) if format.to_sym == :csv rows_to_csv(rows) @@ -57,7 +57,7 @@ module Submissions headers.map { |key| row.find { |e| e[:name] == key }&.dig(:value) } end - def build_table_rows(submissions) + def build_table_rows(submissions, expires_at: nil) submissions.preload(submitters: [attachments_attachments: :blob, documents_attachments: :blob]) .find_each.map do |submission| submission_data = [] @@ -69,7 +69,7 @@ module Submissions 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), value: field[:value] @@ -81,7 +81,7 @@ module Submissions submission_data += submitter.documents.map.with_index(1) do |attachment, index| { name: "#{I18n.t('document')} #{index}", - value: ActiveStorage::Blob.proxy_url(attachment.blob) + value: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) } end end @@ -123,7 +123,7 @@ module Submissions submitters_count > 1 ? "#{submitter_name} - #{name}" : name end - def submitter_formatted_fields(submitter) + def submitter_formatted_fields(submitter, expires_at: nil) fields = submitter.submission.template_fields || submitter.submission.template.fields template_fields = fields.select { |f| f['submitter_uuid'] == submitter.uuid } @@ -142,11 +142,13 @@ module Submissions value = if template_field_type.in?(%w[image signature]) 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' Array.wrap(submitter_value).compact_blank.filter_map do |e| attachment = attachments_index[e] - ActiveStorage::Blob.proxy_url(attachment.blob) if attachment + + ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment end else submitter_value diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 3a6f1b94..4cad84bf 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -201,13 +201,15 @@ module Submissions 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) - submitter.submission.template_fields.each do |field| + submission.template_fields.each do |field| next if field['type'] == 'heading' && !with_headings next if field['submitter_uuid'] != submitter.uuid && field['type'] != 'heading' @@ -476,7 +478,7 @@ module Submissions height_diff - (height_diff.zero? ? diff : 0) ], A: { Type: :Action, S: :URI, - URI: ActiveStorage::Blob.proxy_url(attachment.blob) } + URI: r.submissions_preview_url(submission.slug, **Docuseal.default_url_options) } } ) @@ -877,7 +879,7 @@ module Submissions end end - def h + def r Rails.application.routes.url_helpers end end diff --git a/lib/submissions/serialize_for_api.rb b/lib/submissions/serialize_for_api.rb index 92ad96e1..616803cf 100644 --- a/lib/submissions/serialize_for_api.rb +++ b/lib/submissions/serialize_for_api.rb @@ -4,7 +4,6 @@ module Submissions module SerializeForApi SERIALIZE_PARAMS = { 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: { submitters: { only: %i[id] }, template: { only: %i[id name external_id created_at updated_at], @@ -15,11 +14,13 @@ module Submissions 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) 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 json = submission.as_json(SERIALIZE_PARAMS) @@ -30,8 +31,6 @@ module Submissions json['submission_events'] = Submitters::SerializeForApi.serialize_events(submission.submission_events) end - json['combined_document_url'] ||= maybe_build_combined_url(submitters, submission, params) - if submitters.all?(&: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'] 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['completed_at'] = last_submitter.completed_at.as_json else json['documents'] = [] if with_documents + json['audit_log_url'] = nil + json['combined_document_url'] = nil json['status'] = build_status(submission, submitters) json['completed_at'] = nil end @@ -60,7 +65,7 @@ module Submissions 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?) attachment = submission.combined_document_attachment @@ -71,7 +76,7 @@ module Submissions attachment = Submissions::GenerateCombinedAttachment.call(submitter) end - ActiveStorage::Blob.proxy_url(attachment.blob) if attachment + ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment end end end diff --git a/lib/submitters/serialize_for_api.rb b/lib/submitters/serialize_for_api.rb index ee5046c1..96a85c73 100644 --- a/lib/submitters/serialize_for_api.rb +++ b/lib/submitters/serialize_for_api.rb @@ -11,7 +11,7 @@ module Submitters module_function 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( records: [submitter], associations: if with_documents @@ -24,15 +24,19 @@ module Submitters additional_attrs = {} 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 if with_template additional_attrs['template'] = submitter.submission.template.as_json(only: %i[id name created_at updated_at]) end - additional_attrs['values'] = SerializeForWebhook.build_values_array(submitter) if with_values - additional_attrs['documents'] = SerializeForWebhook.build_documents_array(submitter) if with_documents + additional_attrs['values'] = SerializeForWebhook.build_values_array(submitter, expires_at:) if with_values + + if with_documents + additional_attrs['documents'] = SerializeForWebhook.build_documents_array(submitter, expires_at:) + end + additional_attrs['preferences'] = submitter.preferences.except('default_values') additional_attrs['submission_events'] = serialize_events(submitter.submission_events) if with_events diff --git a/lib/submitters/serialize_for_webhook.rb b/lib/submitters/serialize_for_webhook.rb index 9ef81cbc..0f9cb4a2 100644 --- a/lib/submitters/serialize_for_webhook.rb +++ b/lib/submitters/serialize_for_webhook.rb @@ -10,13 +10,13 @@ module Submitters module_function - def call(submitter) + def call(submitter, expires_at: Accounts.link_expires_at(Account.new(id: submitter.account_id))) ActiveRecord::Associations::Preloader.new( records: [submitter], associations: [documents_attachments: :blob, attachments_attachments: :blob] ).call - values = build_values_array(submitter) - documents = build_documents_array(submitter) + values = build_values_array(submitter, expires_at:) + documents = build_documents_array(submitter, expires_at:) submission = submitter.submission @@ -32,7 +32,7 @@ module Submitters 'preferences' => submitter.preferences.except('default_values'), 'values' => values, '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), 'template' => submission.template.as_json( only: %i[id name external_id created_at updated_at], @@ -40,15 +40,15 @@ module Submitters ), 'submission' => { 'id' => submission.id, - 'audit_log_url' => submission.audit_log_url, - 'combined_document_url' => submission.combined_document_url, + 'audit_log_url' => submission.audit_log_url(expires_at:), + 'combined_document_url' => submission.combined_document_url(expires_at:), 'status' => build_submission_status(submission), 'url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options), 'created_at' => submission.created_at.as_json }) end - def build_values_array(submitter) + def build_values_array(submitter, expires_at: nil) fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || [] attachments_index = submitter.attachments.index_by(&:uuid) submitter_field_counters = Hash.new { 0 } @@ -64,13 +64,13 @@ module Submitters 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 } 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 || [] attachments_index = submitter.attachments.index_by(&:uuid) submitter_field_counters = Hash.new { 0 } @@ -86,7 +86,7 @@ module Submitters 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 } end @@ -104,26 +104,26 @@ module Submitters end end - def build_documents_array(submitter) + def build_documents_array(submitter, expires_at: nil) 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 - 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]) - rails_storage_proxy_url(attachments_index[value]) + rails_storage_proxy_url(attachments_index[value], expires_at:) 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 value end end - def rails_storage_proxy_url(attachment) + def rails_storage_proxy_url(attachment, expires_at: nil) return if attachment.blank? - ActiveStorage::Blob.proxy_url(attachment.blob) + ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) end def r diff --git a/lib/templates/serialize_for_api.rb b/lib/templates/serialize_for_api.rb index 1e1d2dcf..ec9cb628 100644 --- a/lib/templates/serialize_for_api.rb +++ b/lib/templates/serialize_for_api.rb @@ -14,7 +14,8 @@ module Templates 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) preview_image_attachments ||= @@ -40,8 +41,8 @@ module Templates { 'id' => attachment.id, 'uuid' => attachment.uuid, - 'url' => ActiveStorage::Blob.proxy_url(attachment.blob), - 'preview_image_url' => first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob), + 'url' => ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:), + 'preview_image_url' => first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob, expires_at:), 'filename' => attachment.filename } end diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb index 7763d8cd..94c4205b 100644 --- a/spec/requests/templates_spec.rb +++ b/spec/requests/templates_spec.rb @@ -8,6 +8,10 @@ describe 'Templates API' do let(:folder) { create(:template_folder, account:) } 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 it 'returns a list of templates' do templates = [ @@ -211,8 +215,8 @@ describe 'Templates API' do { id: template.documents.first.id, uuid: template.documents.first.uuid, - url: ActiveStorage::Blob.proxy_url(attachment.blob), - preview_image_url: ActiveStorage::Blob.proxy_url(first_page_blob), + url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: Accounts::LINK_EXPIRES_AT), + preview_image_url: ActiveStorage::Blob.proxy_url(first_page_blob, expires_at: Accounts::LINK_EXPIRES_AT), filename: 'sample-document.pdf' } ], @@ -235,7 +239,7 @@ describe 'Templates API' do folder_name: folder.name, source: 'native', external_id: template.external_id, - application_key: template.external_id # Backward compatibility + application_key: template.external_id } end