From cf6cacdc92258f6b344b7acb44b7c5c6790f6c23 Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Sun, 10 Sep 2023 19:25:57 +0300 Subject: [PATCH] add audit trail --- Dockerfile | 2 +- .../submissions_debug_controller.rb | 8 +- app/jobs/process_submitter_completion_job.rb | 2 + app/mailers/submitter_mailer.rb | 26 +- app/models/submission.rb | 2 + lib/pdf_icons.rb | 4 + lib/pdf_icons/logo.png | Bin 0 -> 15666 bytes lib/submission_events.rb | 11 + lib/submissions/generate_audit_trail.rb | 248 ++++++++++++++++++ .../generate_result_attachments.rb | 6 +- lib/templates/create_attachments.rb | 2 + 11 files changed, 296 insertions(+), 15 deletions(-) create mode 100644 lib/pdf_icons/logo.png create mode 100644 lib/submissions/generate_audit_trail.rb diff --git a/Dockerfile b/Dockerfile index 14e596bb..acf9dfbf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ ENV BUNDLE_WITHOUT="development:test" WORKDIR /app -RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /LiberationSans-Regular.ttf && apk del ttf-liberation +RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf / && apk del ttf-liberation COPY ./Gemfile ./Gemfile.lock ./ diff --git a/app/controllers/submissions_debug_controller.rb b/app/controllers/submissions_debug_controller.rb index 952c651f..2b3bab97 100644 --- a/app/controllers/submissions_debug_controller.rb +++ b/app/controllers/submissions_debug_controller.rb @@ -15,9 +15,13 @@ class SubmissionsDebugController < ApplicationController render 'submit_form/show' end f.pdf do - Submissions::GenerateResultAttachments.call(@submitter) + if params[:audit] + Submissions::GenerateAuditTrail.call(@submitter.submission) + else + Submissions::GenerateResultAttachments.call(@submitter) + end - send_data ActiveStorage::Attachment.where(name: :documents).last.download, + send_data ActiveStorage::Attachment.where(name: params[:audit] ? :audit_trail : :documents).last.download, filename: 'debug.pdf', disposition: 'inline', type: 'application/pdf' diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index 89178615..513f2a6a 100644 --- a/app/jobs/process_submitter_completion_job.rb +++ b/app/jobs/process_submitter_completion_job.rb @@ -17,6 +17,8 @@ class ProcessSubmitterCompletionJob < ApplicationJob return unless is_all_completed return if submitter.completed_at != submitter.submission.submitters.maximum(:completed_at) + Submissions::GenerateAuditTrail.call(submitter.submission) + enqueue_emails(submitter) end diff --git a/app/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb index 490d22f2..7c0eb1e9 100644 --- a/app/mailers/submitter_mailer.rb +++ b/app/mailers/submitter_mailer.rb @@ -33,11 +33,7 @@ class SubmitterMailer < ApplicationMailer @email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY) - documents = Submitters.select_attachments_for_download(submitter) - - documents.each do |attachment| - attachments[attachment.filename.to_s] = attachment.download - end + add_completed_email_attachments!(submitter) subject = if @email_config @@ -60,11 +56,7 @@ class SubmitterMailer < ApplicationMailer Submissions::EnsureResultGenerated.call(@submitter) - @documents = Submitters.select_attachments_for_download(submitter) - - @documents.each do |attachment| - attachments[attachment.filename.to_s] = attachment.download - end + @documents = add_completed_email_attachments!(submitter) @email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY) @@ -82,6 +74,20 @@ class SubmitterMailer < ApplicationMailer private + def add_completed_email_attachments!(submitter) + documents = Submitters.select_attachments_for_download(submitter) + + documents.each do |attachment| + attachments[attachment.filename.to_s] = attachment.download + end + + if submitter.submission.audit_trail.present? + attachments[submitter.submission.audit_trail.filename.to_s] = submitter.submission.audit_trail.download + end + + documents + end + def from_address_for_submitter(submitter) submitter.submission.created_by_user&.friendly_name || submitter.submission.template.author.friendly_name end diff --git a/app/models/submission.rb b/app/models/submission.rb index 830a1343..d27ce2ed 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -41,6 +41,8 @@ class Submission < ApplicationRecord attribute :source, :string, default: 'link' attribute :submitters_order, :string, default: 'random' + has_one_attached :audit_trail + has_many :template_schema_documents, ->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) }, through: :template, source: :documents_attachments diff --git a/lib/pdf_icons.rb b/lib/pdf_icons.rb index 424e2aab..6dac0b1c 100644 --- a/lib/pdf_icons.rb +++ b/lib/pdf_icons.rb @@ -15,4 +15,8 @@ module PdfIcons def paperclip_io @paperclip_io ||= StringIO.new(PATH.join('paperclip.png').read) end + + def logo_io + @logo_io ||= StringIO.new(PATH.join('logo.png').read) + end end diff --git a/lib/pdf_icons/logo.png b/lib/pdf_icons/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ee77f2ad3c23f462afb9deaa0cd2e094fa7eb091 GIT binary patch literal 15666 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 fOaK4!@+#g`HJb`nI5>pWI|CGD)nsa 1 && { text: "#{item['name']}\n" }, + submitter.email && { text: "#{submitter.email}\n", font: [FONT_BOLD_NAME, { variant: :bold }] }, + submitter.name && { text: "#{submitter.name}\n" }, + submitter.phone && { text: "#{submitter.phone}\n" } + ].compact_blank, line_spacing: 1.8, padding: [0, 20, 0, 0] + ), + composer.document.layout.formatted_text_box( + [ + submitter.email && { + text: "Email verification: #{click_email_event ? 'Verified by DocuSeal' : 'Unverififed'}\n" + }, + submitter.phone && { + text: "Phone verification: #{is_phone_verified ? 'Verified by DocuSeal' : 'Unverififed'}\n" + }, + completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" }, + completed_event.data['sid'] && { text: "Session ID: #{completed_event.data['sid']}\n" }, + completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" }, + "\n" + ].compact_blank, line_spacing: 1.8, padding: [10, 20, 0, 0] + ) + ], + submission.template_fields.filter_map do |field| + next if field['submitter_uuid'] != submitter.uuid + + submitter_field_counters[field['type']] += 1 + + value = submitter.values[field['uuid']] + + [ + composer.document.layout.formatted_text_box( + [ + { + text: field['name'].presence || + "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}\n".upcase, + font_size: 6 + } + ].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0] + ), + if field['type'].in?(%w[image signature]) + attachment = submitter.attachments.find { |a| a.uuid == value } + image = Vips::Image.new_from_buffer(attachment.download, '').autorot + + scale = [300.0 / image.width, 300.0 / image.height].min + + io = StringIO.new(image.resize([scale, 1].min).write_to_buffer('.png')) + + composer.document.layout.image(io, padding: [0, 100, 10, 0]) + elsif field['type'] == 'file' + composer.document.layout.formatted_text_box( + Array.wrap(value).map do |uuid| + attachment = submitter.attachments.find { |a| a.uuid == uuid } + link = + Rails.application.routes.url_helpers.rails_blob_url(attachment, **Docuseal.default_url_options) + + { link:, text: "#{attachment.filename}\n", style: :link } + end, + padding: [0, 0, 10, 0] + ) + elsif field['type'] == 'checkbox' + composer.document.layout.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0]) + else + value = I18n.l(Date.parse(value), format: :long, locale: account.locale) if field['type'] == 'date' + value = value.join(', ') if value.is_a?(Array) + + composer.document.layout.formatted_text_box([{ text: value.to_s.presence || 'n/a' }], + padding: [0, 0, 10, 0]) + end + ] + end.flatten + ] + end + + composer.table(submitters_data, cell_style: { padding: [0, 0, 25, 0], border: { width: 0 } }) + + composer.draw_box(divider) + + composer.text('Event Log', font_size: 12, padding: [20, 0, 20, 0]) + + events_data = submission.submission_events.sort_by(&:event_timestamp).map do |event| + submitter = submission.submitters.find { |e| e.id == event.submitter_id } + [ + I18n.l(event.event_timestamp, format: :long, locale: account.locale), + composer.document.layout.formatted_text_box( + [ + { text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym], + font: [FONT_BOLD_NAME, { variant: :bold }] }, + event.event_type.include?('send_') ? ' to ' : ' by ', + if event.event_type.include?('sms') + submitter.phone + else + (submitter.email || submitter.name || submitter.phone) + end + ] + ) + ] + end + + composer.table(events_data, cell_style: { padding: [0, 0, 20, 0], border: { width: 0 } }) + + io = StringIO.new + + composer.document.trailer.info[:Creator] = INFO_CREATOR + + composer.document.sign(io, reason: SIGN_REASON, + certificate: pkcs.certificate, + key: pkcs.key, + certificate_chain: pkcs.ca_certs || []) + + ActiveStorage::Attachment.create!( + blob: ActiveStorage::Blob.create_and_upload!( + io: StringIO.new(io.string), filename: "Audit Log - #{submission.template.name}.pdf" + ), + name: 'audit_trail', + record: submission + ) + end + # rubocop:enable Metrics + end +end diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 14d73f9e..83fe5831 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -28,7 +28,8 @@ module Submissions template = submitter.submission.template - pkcs = Accounts.load_signing_pkcs(submitter.submission.template.account) + account = submitter.submission.template.account + pkcs = Accounts.load_signing_pkcs(account) pdfs_index = build_pdfs_index(submitter) @@ -149,7 +150,7 @@ module Submissions height - (area['y'] * height)) end else - value = I18n.l(Date.parse(value), format: :long) if field['type'] == 'date' + value = I18n.l(Date.parse(value), format: :long, locale: account.locale) if field['type'] == 'date' text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), font: pdf.fonts.add(FONT_NAME), font_size:) @@ -214,6 +215,7 @@ module Submissions blob: ActiveStorage::Blob.create_and_upload!( io: StringIO.new(io.string), filename: "#{name}.pdf" ), + metadata: { sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) }, name: 'documents', record: submitter ) diff --git a/lib/templates/create_attachments.rb b/lib/templates/create_attachments.rb index 74e0da2d..5301c7ec 100644 --- a/lib/templates/create_attachments.rb +++ b/lib/templates/create_attachments.rb @@ -19,6 +19,7 @@ module Templates if blob.content_type == PDF_CONTENT_TYPE && blob.metadata['pdf'].nil? blob.metadata['pdf'] = { 'annotations' => Templates::BuildAnnotations.call(document_data) } + blob.metadata['sha256'] = Base64.urlsafe_encode64(Digest::SHA256.digest(document_data)) end blob.save! @@ -37,6 +38,7 @@ module Templates if file.content_type == PDF_CONTENT_TYPE metadata = { 'identified' => true, 'analyzed' => true, + 'sha256' => Base64.urlsafe_encode64(Digest::SHA256.digest(data)), 'pdf' => { 'annotations' => Templates::BuildAnnotations.call(data) } } end