pull/606/head
Usman Sarwar 3 weeks ago
parent 10ecb1d417
commit 2c802b3a5a

@ -10,11 +10,12 @@ class SubmissionsDownloadController < ApplicationController
def index def index
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present? @submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid = @signature_valid =
if @submitter&.slug == params[:submitter_slug] if @submitter&.slug == params[:submitter_slug]
true true
else else
@submitter = nil @submitter = nil
false
end end
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug]) @submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
@ -27,7 +28,7 @@ class SubmissionsDownloadController < ApplicationController
Submissions::EnsureResultGenerated.call(last_submitter) Submissions::EnsureResultGenerated.call(last_submitter)
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter) if last_submitter.completed_at < TTL.ago && !@signature_valid && !current_user_submitter?(last_submitter)
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found return head :not_found
@ -46,8 +47,32 @@ class SubmissionsDownloadController < ApplicationController
end end
end end
def signed_download_url
@submitter = Submitter.find_by!(slug: params[:slug])
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
return head :not_found unless last_submitter
Submissions::EnsureResultGenerated.call(last_submitter)
if last_submitter.completed_at < TTL.ago && !current_user_submitter?(last_submitter)
return head :not_found
end
url = submitter_download_index_url(
@submitter.slug,
sig: @submitter.signed_id(expires_in: TTL, purpose: :download_completed)
)
render json: { url: url }
end
private private
def admin_download?(last_submitter)
# No valid signature link = download from app (e.g. submissions page) → serve unredacted
!@signature_valid
end
def current_user_submitter?(submitter) def current_user_submitter?(submitter)
current_user && current_user.account.submitters.exists?(id: submitter.id) current_user && current_user.account.submitters.exists?(id: submitter.id)
end end
@ -56,7 +81,14 @@ class SubmissionsDownloadController < ApplicationController
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
Submitters.select_attachments_for_download(submitter).map do |attachment| attachments = if admin_download?(submitter)
Submissions::GenerateResultAttachments.call(submitter, for_admin: true)
Submitters.select_admin_attachments_for_download(submitter)
else
Submitters.select_attachments_for_download(submitter)
end
attachments.map do |attachment|
ActiveStorage::Blob.proxy_url( ActiveStorage::Blob.proxy_url(
attachment.blob, attachment.blob,
expires_at: FILES_TTL.from_now.to_i, expires_at: FILES_TTL.from_now.to_i,

@ -9,7 +9,7 @@ class SubmitFormDownloadController < ApplicationController
def index def index
@submitter = Submitter.find_by!(slug: params[:submit_form_slug]) @submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at? return redirect_to submitter_download_index_path(@submitter.slug, sig: @submitter.signed_id(expires_in: 40.minutes, purpose: :download_completed)) if @submitter.completed_at?
return head :unprocessable_content if @submitter.declined_at? || return head :unprocessable_content if @submitter.declined_at? ||
@submitter.submission.archived_at? || @submitter.submission.archived_at? ||

@ -214,21 +214,33 @@ export default {
download () { download () {
this.isDownloading = true this.isDownloading = true
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => { fetch(this.baseUrl + `/submitters/${this.submitterSlug}/signed_download_url`)
if (response.ok) { .then(async (response) => {
const urls = await response.json() if (!response.ok) {
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent) throw new Error('failed')
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent) }
const { url } = await response.json()
return fetch(url)
})
.then(async (response) => {
if (response.ok) {
const urls = await response.json()
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
if (isSafariIos && urls.length > 1) { if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls) this.downloadSafariIos(urls)
} else {
this.downloadUrls(urls)
}
} else { } else {
this.downloadUrls(urls) alert(this.t('failed_to_download_files'))
} }
} else { })
.catch(() => {
alert(this.t('failed_to_download_files')) alert(this.t('failed_to_download_files'))
} this.isDownloading = false
}) })
}, },
downloadUrls (urls) { downloadUrls (urls) {
const fileRequests = urls.map((url) => { const fileRequests = urls.map((url) => {

@ -21,7 +21,13 @@ class SendSubmitterInvitationEmailJob
Submitters::ValidateSending.call(submitter, mail) Submitters::ValidateSending.call(submitter, mail)
mail.deliver_now! begin
mail.deliver_now!
rescue StandardError => e
Rollbar.error(e, submitter_id: submitter.id) if defined?(Rollbar)
raise
end
SubmissionEvent.create!(submitter:, event_type: 'send_email') SubmissionEvent.create!(submitter:, event_type: 'send_email')

@ -68,6 +68,7 @@ class Submission < ApplicationRecord
has_many_attached :preview_documents has_many_attached :preview_documents
has_many_attached :documents has_many_attached :documents
has_many_attached :admin_result_documents
has_many :template_accesses, primary_key: :template_id, foreign_key: :template_id, dependent: nil, inverse_of: false has_many :template_accesses, primary_key: :template_id, foreign_key: :template_id, dependent: nil, inverse_of: false

@ -29,8 +29,8 @@
<div class="py-2"></div> <div class="py-2"></div>
<% end %> <% end %>
<% end %> <% end %>
<% if @submitter.completed_at > 30.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %> <% if @submitter.completed_at > 30.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %>
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full"> <download-button data-src="<%= submitter_download_index_path(@submitter.slug, sig: @submitter.signed_id(expires_in: 40.minutes, purpose: :download_completed)) %>" class="base-button w-full">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-6 h-6') %> <%= svg_icon('download', class: 'w-6 h-6') %>
<span><%= t('download_documents') %></span> <span><%= t('download_documents') %></span>

@ -70,6 +70,24 @@ if ENV['RAILS_ENV'] == 'production'
end end
end end
# In non-production environments (e.g. development), also load `docuseal.env`
# so that SMTP and other settings defined there are available via ENV.
if ENV['RAILS_ENV'] != 'production'
dotenv_path = "#{ENV.fetch('WORKDIR', '.')}/docuseal.env"
if File.exist?(dotenv_path)
File.foreach(dotenv_path) do |line|
line = line.strip
next if line.empty? || line.start_with?('#')
key, value = line.split('=', 2)
next if key.to_s.empty?
ENV[key] = value.to_s
end
end
end
if ENV['DATABASE_URL'].to_s.split('@').last.to_s.split('/').first.to_s.include?('_') if ENV['DATABASE_URL'].to_s.split('@').last.to_s.split('/').first.to_s.include?('_')
require 'addressable' require 'addressable'

@ -53,10 +53,21 @@ Rails.application.configure do
config.active_storage.service = :disk config.active_storage.service = :disk
config.active_storage.resolve_model_to_route = :rails_storage_proxy config.active_storage.resolve_model_to_route = :rails_storage_proxy
# Don't care if the mailer can't send. # Mailer: send real emails in development using SMTP (e.g. Gmail).
config.action_mailer.raise_delivery_errors = true config.action_mailer.raise_delivery_errors = true
config.action_mailer.perform_deliveries = true config.action_mailer.perform_deliveries = true
config.action_mailer.delivery_method = :letter_opener_web config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV.fetch('SMTP_ADDRESS', 'smtp.gmail.com'),
port: ENV.fetch('SMTP_PORT', 587),
domain: ENV.fetch('SMTP_DOMAIN', 'gmail.com'),
user_name: ENV.fetch('SMTP_USERNAME', nil),
password: ENV.fetch('SMTP_PASSWORD', nil),
authentication: (ENV['SMTP_PASSWORD'].present? ? ENV.fetch('SMTP_AUTHENTICATION', 'plain') : nil),
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] != 'false',
openssl_verify_mode: OpenSSL::SSL::VERIFY_NONE
}.compact
config.action_mailer.perform_caching = false config.action_mailer.perform_caching = false

@ -84,7 +84,7 @@ Rails.application.configure do
port: ENV.fetch('SMTP_PORT', 587), port: ENV.fetch('SMTP_PORT', 587),
domain: ENV.fetch('SMTP_DOMAIN', nil), domain: ENV.fetch('SMTP_DOMAIN', nil),
user_name: ENV.fetch('SMTP_USERNAME', nil), user_name: ENV.fetch('SMTP_USERNAME', nil),
password: ENV.fetch('SMTP_PASSWORD', nil), password: ENV.fetch('SMTP_PASSWORD', uqqq vkuy qivk lntt),
authentication: ENV.fetch('SMTP_PASSWORD', nil).present? ? ENV.fetch('SMTP_AUTHENTICATION', 'plain') : nil, authentication: ENV.fetch('SMTP_PASSWORD', nil).present? ? ENV.fetch('SMTP_AUTHENTICATION', 'plain') : nil,
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] != 'false' enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] != 'false'
}.compact }.compact

@ -159,6 +159,7 @@ Rails.application.routes.draw do
resources :send_submission_email, only: %i[create] resources :send_submission_email, only: %i[create]
resources :submitters, only: %i[], param: 'slug' do resources :submitters, only: %i[], param: 'slug' do
get :signed_download_url, on: :member, to: 'submissions_download#signed_download_url'
resources :download, only: %i[index], controller: 'submissions_download' resources :download, only: %i[index], controller: 'submissions_download'
resources :send_email, only: %i[create], controller: 'submitters_send_email' resources :send_email, only: %i[create], controller: 'submitters_send_email'
resources :debug, only: %i[index], controller: 'submissions_debug' if Rails.env.development? resources :debug, only: %i[index], controller: 'submissions_debug' if Rails.env.development?

@ -1,2 +1,10 @@
DATABASE_URL= # keep empty to use sqlite or specify postgresql database URL #DATABASE_URL= # keep empty to use sqlite or specify postgresql database URL
SECRET_KEY_BASE=001f0f350ae2b0d7898d11cd9fd25cc2f193990470d9fd4b7feafa7d363794c55902d56ebd75a079f75e57b9706388ce9279f1db2153a0a63588bb867cb7371c SECRET_KEY_BASE=001f0f350ae2b0d7898d11cd9fd25cc2f193990470d9fd4b7feafa7d363794c55902d56ebd75a079f75e57b9706388ce9279f1db2153a0a63588bb867cb7371c
#SMTP_ADDRESS=smtp.gmail.com
SMTP_PORT=587
SMTP_DOMAIN=gmail.com
SMTP_USERNAME=huzaifahafeez10@gmail.com
SMTP_PASSWORD=uqqq vkuy qivk lntt
SMTP_ENABLE_STARTTLS_AUTO=true
SMTP_AUTHENTICATION=plain

@ -16,11 +16,12 @@ module ActionMailerConfigsInterceptor
end end
if Rails.env.production? && Rails.application.config.action_mailer.delivery_method if Rails.env.production? && Rails.application.config.action_mailer.delivery_method
from = ENV.fetch('SMTP_FROM').to_s.split(',').sample from_candidates = ENV['SMTP_FROM'].to_s.split(',').map(&:strip).reject(&:blank?)
from = from_candidates.sample.presence || Array(message.from).first
if from.match?(User::FULL_EMAIL_REGEXP) if from.present? && from.match?(User::FULL_EMAIL_REGEXP)
message[:from] = message[:from].to_s.sub(User::EMAIL_REGEXP, from) message[:from] = message[:from].to_s.sub(User::EMAIL_REGEXP, from)
else elsif from.present?
message.from = from message.from = from
end end

@ -81,10 +81,10 @@ module Submissions
module_function module_function
# rubocop:disable Metrics # rubocop:disable Metrics
def call(submitter) def call(submitter, for_admin: false)
return generate_detached_signature_attachments(submitter) if detached_signature?(submitter) return generate_detached_signature_attachments(submitter) if detached_signature?(submitter)
pdfs_index = generate_pdfs(submitter) pdfs_index = generate_pdfs(submitter, for_admin:)
account = submitter.account account = submitter.account
submission = submitter.submission submission = submitter.submission
@ -97,6 +97,8 @@ module Submissions
result_attachments = result_attachments =
submission.template_schema.filter_map do |item| submission.template_schema.filter_map do |item|
next if item.blank? || !item.is_a?(Hash)
pdf = pdfs_index[item['attachment_uuid']] pdf = pdfs_index[item['attachment_uuid']]
next if pdf.nil? next if pdf.nil?
@ -109,9 +111,11 @@ module Submissions
build_pdf_attachment(pdf:, submitter:, pkcs:, tsa_url:, build_pdf_attachment(pdf:, submitter:, pkcs:, tsa_url:,
uuid: item['attachment_uuid'], uuid: item['attachment_uuid'],
name: item['name']) name: item['name'],
for_admin:, submission:)
end end
submission.admin_result_documents.purge if for_admin
return ApplicationRecord.no_touching { result_attachments.map { |e| e.tap(&:save!) } } if image_pdfs.size < 2 return ApplicationRecord.no_touching { result_attachments.map { |e| e.tap(&:save!) } } if image_pdfs.size < 2
images_pdf = images_pdf =
@ -128,15 +132,18 @@ module Submissions
tsa_url:, tsa_url:,
pkcs:, pkcs:,
uuid: images_pdf_uuid(original_documents.select(&:image?)), uuid: images_pdf_uuid(original_documents.select(&:image?)),
name: submission.name || submission.template.name name: submission.name || submission.template.name,
for_admin:,
submission:
) )
submission.admin_result_documents.purge if for_admin
ApplicationRecord.no_touching do ApplicationRecord.no_touching do
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) } (result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
end end
end end
def generate_pdfs(submitter) def generate_pdfs(submitter, for_admin: false)
configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY, configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::WITH_FILE_LINKS_KEY, AccountConfig::WITH_FILE_LINKS_KEY,
@ -195,13 +202,17 @@ module Submissions
fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:, fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:,
with_submitter_timezone:, with_submitter_timezone:,
with_file_links:, with_file_links:,
with_signature_id_reason:) with_signature_id_reason:,
for_admin:)
rasterize_redacted_pages(submitter.submission, pdfs_index) unless for_admin
rasterize_redacted_pages(submitter.submission, pdfs_index) pdfs_index
end end
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil, def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
with_submitter_timezone: false, with_signature_id_reason: true, with_file_links: nil) with_submitter_timezone: false, with_signature_id_reason: true, with_file_links: nil,
for_admin: false)
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center) cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
attachments_data_cache = {} attachments_data_cache = {}
@ -635,6 +646,8 @@ module Submissions
end end
end end
when 'redact' when 'redact'
next if for_admin
area_x = area['x'] * width area_x = area['x'] * width
area_y = area['y'] * height area_y = area['y'] * height
area_w = area['w'] * width area_w = area['w'] * width
@ -715,7 +728,7 @@ module Submissions
pdfs_index pdfs_index
end end
def build_pdf_attachment(pdf:, submitter:, pkcs:, tsa_url:, uuid:, name:) def build_pdf_attachment(pdf:, submitter:, pkcs:, tsa_url:, uuid:, name:, for_admin: false, submission: nil)
io = StringIO.new io = StringIO.new
pdf.trailer.info[:Creator] = info_creator pdf.trailer.info[:Creator] = info_creator
@ -764,13 +777,16 @@ module Submissions
end end
end end
record = for_admin ? submission : submitter
attachment_name = for_admin ? 'admin_result_documents' : 'documents'
ActiveStorage::Attachment.new( ActiveStorage::Attachment.new(
blob: ActiveStorage::Blob.create_and_upload!(io: io.tap(&:rewind), filename: "#{name}.pdf"), blob: ActiveStorage::Blob.create_and_upload!(io: io.tap(&:rewind), filename: "#{name}.pdf"),
metadata: { original_uuid: uuid, metadata: { original_uuid: uuid,
analyzed: true, analyzed: true,
sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) }, sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) },
name: 'documents', name: attachment_name,
record: submitter record:
) )
end end
# rubocop:enable Metrics # rubocop:enable Metrics
@ -806,12 +822,21 @@ module Submissions
documents = latest_submitter&.documents&.preload(:blob).to_a.presence documents = latest_submitter&.documents&.preload(:blob).to_a.presence
documents ||= submission.schema_documents.preload(:blob) documents ||= submission.schema_documents.preload(:blob)
attachment_uuids = Submissions.filtered_conditions_schema(submission).pluck('attachment_uuid') schema_documents = submission.schema_documents.preload(:blob)
schema_items = submission.template_schema || submission.template.schema
attachment_uuids =
schema_items.filter_map do |item|
next unless item.is_a?(Hash)
item['attachment_uuid']
end.uniq
attachments_index = documents.index_by { |a| a.metadata['original_uuid'] || a.uuid } attachments_index = documents.index_by { |a| a.metadata['original_uuid'] || a.uuid }
attachment_uuids.each_with_object({}) do |uuid, acc| attachment_uuids.each_with_object({}) do |uuid, acc|
attachment = attachments_index[uuid] attachment = attachments_index[uuid]
attachment ||= submission.schema_documents.preload(:blob).find { |a| a.uuid == uuid } attachment ||= schema_documents.find { |a| a.uuid == uuid || a.metadata['original_uuid'] == uuid }
next unless attachment next unless attachment

@ -103,10 +103,7 @@ module Submitters
end end
def select_attachments_for_download(submitter) def select_attachments_for_download(submitter)
if AccountConfig.exists?(account_id: submitter.submission.account_id, if submitter.submission.submitters.all?(&:completed_at?) &&
key: AccountConfig::COMBINE_PDF_RESULT_KEY,
value: true) &&
submitter.submission.submitters.all?(&:completed_at?) &&
submitter.submission.template_fields.none? { |f| f['type'] == 'verification' } submitter.submission.template_fields.none? { |f| f['type'] == 'verification' }
return [submitter.submission.combined_document_attachment || Submissions::EnsureCombinedGenerated.call(submitter)] return [submitter.submission.combined_document_attachment || Submissions::EnsureCombinedGenerated.call(submitter)]
end end
@ -120,6 +117,19 @@ module Submitters
end end
end end
def select_admin_attachments_for_download(submitter)
submission = submitter.submission
return [] unless submission.admin_result_documents.attached?
original_documents = submission.schema_documents.preload(:blob)
is_more_than_two_images = original_documents.many?(&:image?)
submission.admin_result_documents.reject do |attachment|
is_more_than_two_images &&
original_documents.find { |a| a.uuid == (attachment.metadata['original_uuid'] || attachment.uuid) }&.image?
end
end
def create_attachment!(submitter, params) def create_attachment!(submitter, params)
blob = blob =
if (file = params[:file]) if (file = params[:file])

@ -142,9 +142,11 @@ module Templates
# Use LibreOffice headless mode to convert to PDF # Use LibreOffice headless mode to convert to PDF
success = system(libreoffice_path, '--headless', '--convert-to', 'pdf', '--outdir', output_dir, input_temp.path, out: File::NULL, err: File::NULL) success = system(libreoffice_path, '--headless', '--convert-to', 'pdf', '--outdir', output_dir, input_temp.path, out: File::NULL, err: File::NULL)
if success && File.exist?(output_file) if success
pdf_data = File.binread(output_file) generated_pdf = Dir.glob(File.join(output_dir, '*.pdf')).first
return pdf_data if generated_pdf && File.exist?(generated_pdf)
return File.binread(generated_pdf)
end
end end
rescue StandardError => e rescue StandardError => e
Rails.logger.warn("Document conversion failed: #{e.message}") Rails.logger.warn("Document conversion failed: #{e.message}")

Loading…
Cancel
Save