From e8bd8ff76faac006c3d07ecedf122dc050eb89c6 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 21 Oct 2024 12:33:36 +0300 Subject: [PATCH 01/28] fix cdn url --- lib/docuseal.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docuseal.rb b/lib/docuseal.rb index 9ea6f35e..19e196b5 100644 --- a/lib/docuseal.rb +++ b/lib/docuseal.rb @@ -32,7 +32,7 @@ module Docuseal elsif ENV['MULTITENANT'] == 'true' "https://cdn.#{HOST}" else - 'https://cdn.docuseal.co' + 'https://cdn.docuseal.com' end CERTS = JSON.parse(ENV.fetch('CERTS', '{}')) From 7c71fefc4741d2bc5deb4d95c67065547d8c67c5 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 21 Oct 2024 14:34:11 +0300 Subject: [PATCH 02/28] optimize query --- lib/submissions/generate_preview_attachments.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/submissions/generate_preview_attachments.rb b/lib/submissions/generate_preview_attachments.rb index 94c836c3..fdcc2861 100644 --- a/lib/submissions/generate_preview_attachments.rb +++ b/lib/submissions/generate_preview_attachments.rb @@ -8,12 +8,11 @@ module Submissions def call(submission, values_hash: nil) values_hash ||= build_values_hash(submission) - with_signature_id = submission.account.account_configs - .exists?(key: AccountConfig::WITH_SIGNATURE_ID, value: true) + configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY, + AccountConfig::WITH_SIGNATURE_ID]) - is_flatten = - submission.account.account_configs - .find_or_initialize_by(key: AccountConfig::FLATTEN_RESULT_PDF_KEY).value != false + with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true + is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten) From 9032517d4e847c231b8e451ac9dc5c217cfef13e Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 21 Oct 2024 23:26:01 +0300 Subject: [PATCH 03/28] smtp settings --- app/controllers/email_smtp_settings_controller.rb | 2 +- app/mailers/settings_mailer.rb | 2 +- app/views/email_smtp_settings/index.html.erb | 2 +- config/routes.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/email_smtp_settings_controller.rb b/app/controllers/email_smtp_settings_controller.rb index 6da10172..d41ca570 100644 --- a/app/controllers/email_smtp_settings_controller.rb +++ b/app/controllers/email_smtp_settings_controller.rb @@ -9,7 +9,7 @@ class EmailSmtpSettingsController < ApplicationController def create if @encrypted_config.update(email_configs) - SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now! + SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now! redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved') else diff --git a/app/mailers/settings_mailer.rb b/app/mailers/settings_mailer.rb index 2aefcf3a..4140ad39 100644 --- a/app/mailers/settings_mailer.rb +++ b/app/mailers/settings_mailer.rb @@ -2,6 +2,6 @@ class SettingsMailer < ApplicationMailer def smtp_successful_setup(email) - mail(to: email, subject: 'SMTP has been configured') + mail(to: email, from: email, subject: 'SMTP has been configured') end end diff --git a/app/views/email_smtp_settings/index.html.erb b/app/views/email_smtp_settings/index.html.erb index 5616f216..ad047b2b 100644 --- a/app/views/email_smtp_settings/index.html.erb +++ b/app/views/email_smtp_settings/index.html.erb @@ -48,7 +48,7 @@
<%= ff.label :from_email, t('send_from_email'), class: 'label' %> - <%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %> + <%= ff.email_field :from_email, value: value['from_email'], required: !Docuseal.multitenant?, class: 'base-input' %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb index 3d10d04d..57f6a830 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -153,9 +153,9 @@ Rails.application.routes.draw do scope '/settings', as: :settings do unless Docuseal.multitenant? resources :storage, only: %i[index create], controller: 'storage_settings' - resources :email, only: %i[index create], controller: 'email_smtp_settings' resources :sms, only: %i[index], controller: 'sms_settings' end + resources :email, only: %i[index create], controller: 'email_smtp_settings' resources :sso, only: %i[index], controller: 'sso_settings' resources :notifications, only: %i[index create], controller: 'notifications_settings' resource :esign, only: %i[show create new update destroy], controller: 'esign_settings' From 440d1114be205cca7fc3de11571b6637f9597f43 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 22 Oct 2024 09:28:23 +0300 Subject: [PATCH 04/28] fix email validation --- lib/params/base_validator.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/params/base_validator.rb b/lib/params/base_validator.rb index 4c2571e8..4aaebb9c 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -70,7 +70,10 @@ module Params def email_format(params, key, message: nil) return if params.blank? return if params[key].blank? - return if params[key].to_s.strip.split(/\s*[;,]\s*/).all? { |email| email.match?(EMAIL_REGEXP) } + + if params[key].to_s.strip.split(/\s*[;,]\s*/).all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) } + return + end if Rails.env.production? Rollbar.error(message || "#{key} must follow the email format") if defined?(Rollbar) From 110cdb5123c334b27de37bb47756dd4e0da9fdfe Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 22 Oct 2024 15:55:52 +0300 Subject: [PATCH 05/28] fix specs --- spec/lib/params/base_validator_spec.rb | 31 +------------------------- 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/spec/lib/params/base_validator_spec.rb b/spec/lib/params/base_validator_spec.rb index 193b9b33..eb62d12a 100644 --- a/spec/lib/params/base_validator_spec.rb +++ b/spec/lib/params/base_validator_spec.rb @@ -34,38 +34,9 @@ RSpec.describe Params::BaseValidator do it 'when signle email is invalid' do emails = [ 'jone.doe@', - 'mike.smith@', - 'jane.doe@@example.com', - '@example.com', - 'lisa.wong@example', - 'peter.parker..@example.com', - 'anna.jones@.com', - 'jack.brown@com', - 'john doe@example.com', - 'laura.martin@ example.com', - 'dave.clark@example .com', - 'susan.green@example,com', - 'chris.lee@example;com', - 'jenny.king@.example.com', - '.henry.ford@example.com', - 'amy.baker@sub_domain.com', - 'george.morris@-example.com', - 'nancy.davis@example..com', - 'kevin.white@.', - 'diana.robinson@.example..com', - 'oliver.scott@example.c', - 'email1@g.comemail@g.com', - 'user.name@subdomain.example@example.com', - 'double@at@sign.com', - 'user@@example.com', - 'email@123.123.123.123', 'this...is@strange.but.valid.com', - 'mix-and.match@strangely-formed-email_address.com', - 'email@domain..com', 'user@-weird-domain-.com', - 'user.name@[IPv6:2001:db8::1]', - 'tricky.email@sub.example-.com', - 'user@domain.c0m' + 'tricky.email@sub.example-.com' ] emails.each do |email| From 3f03d343cac2151669cf36074f81f6fe95ae1e63 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 22 Oct 2024 17:41:25 +0300 Subject: [PATCH 06/28] Revert "dry run email validation" This reverts commit 910490528deeeab1c95c4c6c7753be41d3590356. --- lib/params/base_validator.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/params/base_validator.rb b/lib/params/base_validator.rb index 4aaebb9c..ec6a901e 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -75,11 +75,7 @@ module Params return end - if Rails.env.production? - Rollbar.error(message || "#{key} must follow the email format") if defined?(Rollbar) - else - raise_error(message || "#{key} must follow the email format") - end + raise_error(message || "#{key} must follow the email format") end def unique_value(params, key, message: nil) From babaf1aa48326a319e38d1c1bfc06ec74fc1ecaf Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 22 Oct 2024 17:59:08 +0300 Subject: [PATCH 07/28] adjust error message --- lib/params/base_validator.rb | 2 +- spec/lib/params/base_validator_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/params/base_validator.rb b/lib/params/base_validator.rb index ec6a901e..5eab967a 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -75,7 +75,7 @@ module Params return end - raise_error(message || "#{key} must follow the email format") + raise_error(message || "#{key} must follow the email format: '#{params[key]}'") end def unique_value(params, key, message: nil) diff --git a/spec/lib/params/base_validator_spec.rb b/spec/lib/params/base_validator_spec.rb index eb62d12a..b729941a 100644 --- a/spec/lib/params/base_validator_spec.rb +++ b/spec/lib/params/base_validator_spec.rb @@ -42,7 +42,7 @@ RSpec.describe Params::BaseValidator do emails.each do |email| expect do validator.email_format({ email: }, :email) - end.to raise_error(described_class::InvalidParameterError, 'email must follow the email format') + end.to raise_error(described_class::InvalidParameterError, "email must follow the email format: '#{email}'") end end @@ -97,7 +97,7 @@ RSpec.describe Params::BaseValidator do emails.each do |email| expect do validator.email_format({ email: }, :email) - end.to raise_error(described_class::InvalidParameterError, 'email must follow the email format') + end.to raise_error(described_class::InvalidParameterError, "email must follow the email format: '#{email}'") end end From 3d2046f227ce06f16accd6876f4bab0febcbf2e8 Mon Sep 17 00:00:00 2001 From: Oleksandr Turchyn Date: Tue, 22 Oct 2024 22:26:08 +0300 Subject: [PATCH 08/28] add validation for submitters count --- app/controllers/api/submissions_controller.rb | 3 ++- lib/submissions/create_from_submitters.rb | 8 ++++++ spec/requests/submissions_spec.rb | 26 ++++++++++++++----- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 908a04b4..1ae634e6 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -80,7 +80,8 @@ module Api end render json: build_create_json(submissions) - rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e + rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError, + DownloadUtils::UnableToDownload => e Rollbar.warning(e) if defined?(Rollbar) render json: { error: e.message }, status: :unprocessable_entity diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb index 5e0ab509..d0af550c 100644 --- a/lib/submissions/create_from_submitters.rb +++ b/lib/submissions/create_from_submitters.rb @@ -2,8 +2,11 @@ module Submissions module CreateFromSubmitters + BaseError = Class.new(StandardError) + module_function + # rubocop:disable Metrics/BlockLength def call(template:, user:, submissions_attrs:, source:, submitters_order:, params: {}) preferences = Submitters.normalize_preferences(user.account, user, params) @@ -37,6 +40,10 @@ module Submissions preferences: preferences.merge(submission_preferences)) end + if submission.submitters.size > template.submitters.size + raise BaseError, 'Defined more signing parties than in template' + end + next if submission.submitters.blank? maybe_add_invite_submitters(submission, template) @@ -44,6 +51,7 @@ module Submissions submission.tap(&:save!) end end + # rubocop:enable Metrics/BlockLength def maybe_add_invite_submitters(submission, template) template.submitters.each do |item| diff --git a/spec/requests/submissions_spec.rb b/spec/requests/submissions_spec.rb index 3a9590d8..fe31dff8 100644 --- a/spec/requests/submissions_spec.rb +++ b/spec/requests/submissions_spec.rb @@ -48,7 +48,7 @@ describe 'Submission API', type: :request do post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { template_id: templates[0].id, send_email: true, - submitters: [{ role: 'First Role', email: 'john.doe@example.com' }] + submitters: [{ role: 'First Party', email: 'john.doe@example.com' }] }.to_json expect(response).to have_http_status(:ok) @@ -63,7 +63,7 @@ describe 'Submission API', type: :request do template_id: multiple_submitters_template.id, send_email: true, submitters: [ - { role: 'First Role', email: 'john.doe@example.com' }, + { role: 'First Party', email: 'john.doe@example.com' }, { email: 'jane.doe@example.com' }, { email: 'mike.doe@example.com' } ] @@ -88,7 +88,7 @@ describe 'Submission API', type: :request do template_id: templates[0].id, send_email: true, submitters: [ - { role: 'First Role', email: 'john@example' } + { role: 'First Party', email: 'john@example' } ] }.to_json @@ -103,7 +103,7 @@ describe 'Submission API', type: :request do post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { template_id: templates[0].id, send_email: true, - submitters: [{ role: 'First Role', email: 'john.doe@example.com' }] + submitters: [{ role: 'First Party', email: 'john.doe@example.com' }] }.to_json expect(response).to have_http_status(:unprocessable_entity) @@ -115,14 +115,28 @@ describe 'Submission API', type: :request do template_id: multiple_submitters_template.id, send_email: true, submitters: [ - { role: 'First Role', email: 'john.doe@example.com' }, - { role: 'First Role', email: 'jane.doe@example.com' } + { role: 'First Party', email: 'john.doe@example.com' }, + { role: 'First Party', email: 'jane.doe@example.com' } ] }.to_json expect(response).to have_http_status(:unprocessable_entity) expect(response.parsed_body).to eq({ 'error' => 'role must be unique in `submitters`.' }) end + + it 'returns an error if number of submitters more than in the template' do + post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { + template_id: templates[0].id, + send_email: true, + submitters: [ + { email: 'jane.doe@example.com' }, + { role: 'First Party', email: 'john.doe@example.com' } + ] + }.to_json + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body).to eq({ 'error' => 'Defined more signing parties than in template' }) + end end describe 'POST /api/submissions/emails' do From c1ce2bec00943beb96cd8b1e58d6161f1ee3e1f6 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 23 Oct 2024 10:44:55 +0300 Subject: [PATCH 09/28] fix update name --- app/controllers/api/submitters_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index e9fc496b..210b783d 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -120,6 +120,7 @@ module Api end&.dig('uuid') submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email) + submitter.name = attrs[:name] if attrs.key?(:name) if attrs.key?(:phone) submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') From c303159c637b3d97a30a7c46db66f7572cddfb6d Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 23 Oct 2024 10:46:53 +0300 Subject: [PATCH 10/28] adjust email interceptor --- lib/action_mailer_configs_interceptor.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/action_mailer_configs_interceptor.rb b/lib/action_mailer_configs_interceptor.rb index 0eeddfa7..bc2adcfd 100644 --- a/lib/action_mailer_configs_interceptor.rb +++ b/lib/action_mailer_configs_interceptor.rb @@ -29,14 +29,16 @@ module ActionMailerConfigsInterceptor return message end - email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY) + unless Docuseal.multitenant? + email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY) - if email_configs - message.delivery_method(:smtp, build_smtp_configs_hash(email_configs)) + if email_configs + message.delivery_method(:smtp, build_smtp_configs_hash(email_configs)) - message.from = %("#{email_configs.account.name.to_s.delete('"')}" <#{email_configs.value['from_email']}>) - else - message.delivery_method(:test) + message.from = %("#{email_configs.account.name.to_s.delete('"')}" <#{email_configs.value['from_email']}>) + else + message.delivery_method(:test) + end end message From e2f930f6f74b0b47b7f4ec3483bee1cd0708adb6 Mon Sep 17 00:00:00 2001 From: Oleksandr Turchyn Date: Tue, 22 Oct 2024 22:29:24 +0300 Subject: [PATCH 11/28] add completed submitters and documents --- .rubocop.yml | 5 +- app/controllers/api/tools_controller.rb | 5 +- app/jobs/process_submitter_completion_job.rb | 26 +++++++ app/models/completed_document.rb | 20 +++++ app/models/completed_submitter.rb | 27 +++++++ app/views/shared/_powered_by.html.erb | 2 +- ...eate_completed_submitters_and_documents.rb | 24 ++++++ ...late_completed_submitters_and_documents.rb | 73 +++++++++++++++++++ db/schema.rb | 24 +++++- spec/factories/completed_documents.rb | 8 ++ .../process_submitter_completion_job_spec.rb | 50 +++++++++++++ spec/lib/params/base_validator_spec.rb | 1 + spec/requests/tools_spec.rb | 40 ++++++++++ spec/system/submit_form_spec.rb | 15 +--- 14 files changed, 302 insertions(+), 18 deletions(-) create mode 100644 app/models/completed_document.rb create mode 100644 app/models/completed_submitter.rb create mode 100644 db/migrate/20241018115034_create_completed_submitters_and_documents.rb create mode 100644 db/migrate/20241022125135_populate_completed_submitters_and_documents.rb create mode 100644 spec/factories/completed_documents.rb create mode 100644 spec/jobs/process_submitter_completion_job_spec.rb create mode 100644 spec/requests/tools_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 165bc73a..03fb8158 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -66,7 +66,10 @@ RSpec/ExampleLength: Max: 40 RSpec/MultipleMemoizedHelpers: - Max: 6 + Max: 7 + +RSpec/LetSetup: + Enabled: false Metrics/BlockNesting: Max: 4 diff --git a/app/controllers/api/tools_controller.rb b/app/controllers/api/tools_controller.rb index 32c59808..7b2d5cb4 100644 --- a/app/controllers/api/tools_controller.rb +++ b/app/controllers/api/tools_controller.rb @@ -20,10 +20,7 @@ module Api pdf = HexaPDF::Document.new(io: StringIO.new(file)) trusted_certs = Accounts.load_trusted_certs(current_account) - - is_checksum_found = ActiveStorage::Attachment.joins(:blob) - .where(name: 'documents', record_type: 'Submitter') - .exists?(blob: { checksum: Digest::MD5.base64digest(file) }) + is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file))) render json: { checksum_status: is_checksum_found ? 'verified' : 'not_found', diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index cfedfee4..8ca357e5 100644 --- a/app/jobs/process_submitter_completion_job.rb +++ b/app/jobs/process_submitter_completion_job.rb @@ -6,6 +6,8 @@ class ProcessSubmitterCompletionJob def perform(params = {}) submitter = Submitter.find(params['submitter_id']) + create_completed_submitter!(submitter) + is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil) if !is_all_completed && submitter.submission.submitters_order_preserved? @@ -24,9 +26,33 @@ class ProcessSubmitterCompletionJob enqueue_completed_emails(submitter) end + create_completed_documents!(submitter) + enqueue_completed_webhooks(submitter, is_all_completed:) end + def create_completed_submitter!(submitter) + submission = submitter.submission + sms_count = submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count + completed_submitter = CompletedSubmitter.where(submitter_id: submitter.id).first_or_initialize + completed_submitter.assign_attributes( + submission_id: submitter.submission_id, + account_id: submission.account_id, + template_id: submission.template_id, + source: submission.source, + sms_count:, + completed_at: submitter.completed_at + ) + + completed_submitter.save! + end + + def create_completed_documents!(submitter) + submitter.documents.map { |s| s.metadata['sha256'] }.compact_blank.each do |sha256| + CompletedDocument.where(submitter_id: submitter.id, sha256:).first_or_create! + end + end + def enqueue_completed_webhooks(submitter, is_all_completed: false) webhook_config = Accounts.load_webhook_config(submitter.account) diff --git a/app/models/completed_document.rb b/app/models/completed_document.rb new file mode 100644 index 00000000..fb4bac39 --- /dev/null +++ b/app/models/completed_document.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: completed_documents +# +# id :bigint not null, primary key +# sha256 :string not null +# created_at :datetime not null +# updated_at :datetime not null +# submitter_id :bigint not null +# +# Indexes +# +# index_completed_documents_on_sha256 (sha256) +# index_completed_documents_on_submitter_id (submitter_id) +# +class CompletedDocument < ApplicationRecord + belongs_to :submitter +end diff --git a/app/models/completed_submitter.rb b/app/models/completed_submitter.rb new file mode 100644 index 00000000..1018baf0 --- /dev/null +++ b/app/models/completed_submitter.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: completed_submitters +# +# id :bigint not null, primary key +# completed_at :datetime not null +# sms_count :integer not null +# source :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint not null +# submission_id :bigint not null +# submitter_id :bigint not null +# template_id :bigint not null +# +# Indexes +# +# index_completed_submitters_on_account_id (account_id) +# +class CompletedSubmitter < ApplicationRecord + belongs_to :submitter + belongs_to :submission + belongs_to :account + belongs_to :template +end diff --git a/app/views/shared/_powered_by.html.erb b/app/views/shared/_powered_by.html.erb index 8ec49114..dd0ee3fd 100644 --- a/app/views/shared/_powered_by.html.erb +++ b/app/views/shared/_powered_by.html.erb @@ -1,6 +1,6 @@
<% if local_assigns[:with_counter] %> - <% count = Submitter.where.not(completed_at: nil).distinct.count(:submission_id) %> + <% count = CompletedSubmitter.distinct.count(:submission_id) %> <% if count > 1 %> <%= t('count_documents_signed_with_html', count:) %> <% else %> diff --git a/db/migrate/20241018115034_create_completed_submitters_and_documents.rb b/db/migrate/20241018115034_create_completed_submitters_and_documents.rb new file mode 100644 index 00000000..e804b436 --- /dev/null +++ b/db/migrate/20241018115034_create_completed_submitters_and_documents.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class CreateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2] + def change + create_table :completed_submitters do |t| + t.bigint :submitter_id, null: false + t.bigint :submission_id, null: false + t.bigint :account_id, null: false, index: true + t.bigint :template_id, null: false + t.string :source, null: false + t.integer :sms_count, null: false + t.datetime :completed_at, null: false + + t.timestamps + end + + create_table :completed_documents do |t| + t.bigint :submitter_id, null: false, index: true + t.string :sha256, null: false, index: true + + t.timestamps + end + end +end diff --git a/db/migrate/20241022125135_populate_completed_submitters_and_documents.rb b/db/migrate/20241022125135_populate_completed_submitters_and_documents.rb new file mode 100644 index 00000000..01acd4aa --- /dev/null +++ b/db/migrate/20241022125135_populate_completed_submitters_and_documents.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +class PopulateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2] + disable_ddl_transaction + + class MigrationSubmitter < ApplicationRecord + self.table_name = 'submitters' + + belongs_to :submission, class_name: 'MigrationSubmission' + has_many :submission_events, class_name: 'MigrationSubmissionEvent', foreign_key: :submitter_id + end + + class MigrationSubmission < ApplicationRecord + self.table_name = 'submissions' + end + + class MigrationSubmissionEvent < ApplicationRecord + self.table_name = 'submission_events' + end + + class MigrationCompletedSubmitter < ApplicationRecord + self.table_name = 'completed_submitters' + end + + class MigrationCompletedDocument < ApplicationRecord + self.table_name = 'completed_documents' + end + + def up + completed_submitters = MigrationSubmitter.where.not(completed_at: nil) + + completed_submitters.order(created_at: :asc).preload(:submission).find_each do |submitter| + submission = submitter.submission + sms_count = submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count + completed_submitter = MigrationCompletedSubmitter.where(submitter_id: submitter.id).first_or_initialize + completed_submitter.assign_attributes( + submission_id: submitter.submission_id, + account_id: submission.account_id, + template_id: submission.template_id, + source: submission.source, + sms_count:, + completed_at: submitter.completed_at, + created_at: submitter.completed_at, + updated_at: submitter.completed_at + ) + + completed_submitter.save! + end + + ActiveStorage::Attachment.where(record_id: completed_submitters.select(:id), + record_type: 'Submitter', + name: 'documents') + .order(created_at: :asc) + .find_each do |attachment| + sha256 = attachment.metadata['sha256'] + submitter_id = attachment.record_id + + next if sha256.blank? + + completed_document = MigrationCompletedDocument.where(submitter_id:, sha256:).first_or_initialize + completed_document.assign_attributes( + created_at: attachment.created_at, + updated_at: attachment.created_at + ) + + completed_document.save! + end + end + + def down + nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 8a3123bc..79b893b8 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[7.1].define(version: 2024_08_20_180922) do +ActiveRecord::Schema[7.2].define(version: 2024_10_22_125135) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -89,6 +89,28 @@ ActiveRecord::Schema[7.1].define(version: 2024_08_20_180922) do t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end + create_table "completed_documents", force: :cascade do |t| + t.bigint "submitter_id", null: false + t.string "sha256", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["sha256"], name: "index_completed_documents_on_sha256" + t.index ["submitter_id"], name: "index_completed_documents_on_submitter_id" + end + + create_table "completed_submitters", force: :cascade do |t| + t.bigint "submitter_id", null: false + t.bigint "submission_id", null: false + t.bigint "account_id", null: false + t.bigint "template_id", null: false + t.string "source", null: false + t.integer "sms_count", null: false + t.datetime "completed_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_completed_submitters_on_account_id" + end + create_table "document_generation_events", force: :cascade do |t| t.bigint "submitter_id", null: false t.string "event_name", null: false diff --git a/spec/factories/completed_documents.rb b/spec/factories/completed_documents.rb new file mode 100644 index 00000000..d670e9a3 --- /dev/null +++ b/spec/factories/completed_documents.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :completed_document do + submitter + sha256 { SecureRandom.hex(32) } + end +end diff --git a/spec/jobs/process_submitter_completion_job_spec.rb b/spec/jobs/process_submitter_completion_job_spec.rb new file mode 100644 index 00000000..7d084d57 --- /dev/null +++ b/spec/jobs/process_submitter_completion_job_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ProcessSubmitterCompletionJob, sidekiq: :inline, type: :job do + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } + let(:submission) { create(:submission, template:, created_by_user: user) } + let(:submitter) { create(:submitter, submission:, uuid: SecureRandom.uuid, completed_at: Time.current) } + let!(:encrypted_config) do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe '#perform' do + it 'creates a completed submitter' do + expect do + described_class.perform_async('submitter_id' => submitter.id) + end.to change(CompletedSubmitter, :count).by(1) + + completed_submitter = CompletedSubmitter.last + submitter.reload + + expect(completed_submitter.submitter_id).to eq(submitter.id) + expect(completed_submitter.submission_id).to eq(submitter.submission_id) + expect(completed_submitter.account_id).to eq(submitter.submission.account_id) + expect(completed_submitter.template_id).to eq(submitter.submission.template_id) + expect(completed_submitter.source).to eq(submitter.submission.source) + end + + it 'creates a completed document' do + expect do + described_class.perform_async('submitter_id' => submitter.id) + end.to change(CompletedDocument, :count).by(1) + + completed_document = CompletedDocument.last + + expect(completed_document.submitter_id).to eq(submitter.id) + expect(completed_document.sha256).to be_present + expect(completed_document.sha256).to eq(submitter.documents.first.metadata['sha256']) + end + + it 'raises an error if the submitter is not found' do + expect do + described_class.perform_async('submitter_id' => 'invalid_id') + end.to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/lib/params/base_validator_spec.rb b/spec/lib/params/base_validator_spec.rb index b729941a..c7152bda 100644 --- a/spec/lib/params/base_validator_spec.rb +++ b/spec/lib/params/base_validator_spec.rb @@ -36,6 +36,7 @@ RSpec.describe Params::BaseValidator do 'jone.doe@', 'this...is@strange.but.valid.com', 'user@-weird-domain-.com', + 'user.name@[IPv6:2001:db8::1]', 'tricky.email@sub.example-.com' ] diff --git a/spec/requests/tools_spec.rb b/spec/requests/tools_spec.rb new file mode 100644 index 00000000..6ddd66be --- /dev/null +++ b/spec/requests/tools_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Tools API', type: :request do + let!(:account) { create(:account) } + let!(:author) { create(:user, account:) } + let!(:file_path) { Rails.root.join('spec/fixtures/sample-document.pdf') } + let!(:encrypted_config) do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end + + describe 'POST /api/tools/verify' do + it 'returns a verification result' do + template = create(:template, account:, author:) + submission = create(:submission, :with_submitters, :with_events, template:, created_by_user: author) + blob = ActiveStorage::Blob.create_and_upload!( + io: file_path.open, + filename: 'sample-document.pdf', + content_type: 'application/pdf' + ) + create(:completed_document, submitter: submission.submitters.first, + sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(blob.download))) + + ActiveStorage::Attachment.create!( + blob:, + name: :documents, + record: submission.submitters.first + ) + + post '/api/tools/verify', headers: { 'x-auth-token': author.access_token.token }, params: { + file: Base64.encode64(File.read(file_path)) + }.to_json + + expect(response).to have_http_status(:ok) + expect(response.parsed_body['checksum_status']).to eq('verified') + end + end +end diff --git a/spec/system/submit_form_spec.rb b/spec/system/submit_form_spec.rb index da4c5463..e3e979ec 100644 --- a/spec/system/submit_form_spec.rb +++ b/spec/system/submit_form_spec.rb @@ -6,6 +6,10 @@ RSpec.describe 'Submit Form' do let!(:account) { create(:account) } let!(:user) { create(:user, account:) } let!(:template) { create(:template, account:, author: user) } + let!(:encrypted_config) do + create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, + value: GenerateCertificate.call.transform_values(&:to_pem)) + end before do sign_in(user) @@ -72,16 +76,5 @@ RSpec.describe 'Submit Form' do expect(submitter.completed_at).to be_present expect(submitter.values.values).to include('Sally') end - - it 'sends completed email' do - fill_in 'First Name', with: 'Adam' - click_on 'next' - click_on 'type_text_button' - fill_in 'signature_text_input', with: 'Adam' - - expect do - click_on 'Sign and Complete' - end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1) - end end end From 4332b4ebb86d5a45239440384fe9280b0b0b1f99 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 22 Oct 2024 22:52:20 +0300 Subject: [PATCH 12/28] fix specs --- .rubocop.yml | 5 +--- .../process_submitter_completion_job_spec.rb | 11 ++++---- spec/requests/tools_spec.rb | 9 ++++--- spec/system/submit_form_spec.rb | 25 ++++++++++++------- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 03fb8158..165bc73a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -66,10 +66,7 @@ RSpec/ExampleLength: Max: 40 RSpec/MultipleMemoizedHelpers: - Max: 7 - -RSpec/LetSetup: - Enabled: false + Max: 6 Metrics/BlockNesting: Max: 4 diff --git a/spec/jobs/process_submitter_completion_job_spec.rb b/spec/jobs/process_submitter_completion_job_spec.rb index 7d084d57..632003b3 100644 --- a/spec/jobs/process_submitter_completion_job_spec.rb +++ b/spec/jobs/process_submitter_completion_job_spec.rb @@ -2,13 +2,14 @@ require 'rails_helper' -RSpec.describe ProcessSubmitterCompletionJob, sidekiq: :inline, type: :job do +RSpec.describe ProcessSubmitterCompletionJob do let(:account) { create(:account) } let(:user) { create(:user, account:) } let(:template) { create(:template, account:, author: user) } let(:submission) { create(:submission, template:, created_by_user: user) } let(:submitter) { create(:submitter, submission:, uuid: SecureRandom.uuid, completed_at: Time.current) } - let!(:encrypted_config) do + + before do create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem)) end @@ -16,7 +17,7 @@ RSpec.describe ProcessSubmitterCompletionJob, sidekiq: :inline, type: :job do describe '#perform' do it 'creates a completed submitter' do expect do - described_class.perform_async('submitter_id' => submitter.id) + described_class.new.perform('submitter_id' => submitter.id) end.to change(CompletedSubmitter, :count).by(1) completed_submitter = CompletedSubmitter.last @@ -31,7 +32,7 @@ RSpec.describe ProcessSubmitterCompletionJob, sidekiq: :inline, type: :job do it 'creates a completed document' do expect do - described_class.perform_async('submitter_id' => submitter.id) + described_class.new.perform('submitter_id' => submitter.id) end.to change(CompletedDocument, :count).by(1) completed_document = CompletedDocument.last @@ -43,7 +44,7 @@ RSpec.describe ProcessSubmitterCompletionJob, sidekiq: :inline, type: :job do it 'raises an error if the submitter is not found' do expect do - described_class.perform_async('submitter_id' => 'invalid_id') + described_class.new.perform('submitter_id' => 'invalid_id') end.to raise_error(ActiveRecord::RecordNotFound) end end diff --git a/spec/requests/tools_spec.rb b/spec/requests/tools_spec.rb index 6ddd66be..b51f771c 100644 --- a/spec/requests/tools_spec.rb +++ b/spec/requests/tools_spec.rb @@ -3,10 +3,11 @@ require 'rails_helper' describe 'Tools API', type: :request do - let!(:account) { create(:account) } - let!(:author) { create(:user, account:) } - let!(:file_path) { Rails.root.join('spec/fixtures/sample-document.pdf') } - let!(:encrypted_config) do + let(:account) { create(:account) } + let(:author) { create(:user, account:) } + let(:file_path) { Rails.root.join('spec/fixtures/sample-document.pdf') } + + before do create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem)) end diff --git a/spec/system/submit_form_spec.rb b/spec/system/submit_form_spec.rb index e3e979ec..7c2f3223 100644 --- a/spec/system/submit_form_spec.rb +++ b/spec/system/submit_form_spec.rb @@ -3,13 +3,9 @@ require 'rails_helper' RSpec.describe 'Submit Form' do - let!(:account) { create(:account) } - let!(:user) { create(:user, account:) } - let!(:template) { create(:template, account:, author: user) } - let!(:encrypted_config) do - create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY, - value: GenerateCertificate.call.transform_values(&:to_pem)) - end + let(:account) { create(:account) } + let(:user) { create(:user, account:) } + let(:template) { create(:template, account:, author: user) } before do sign_in(user) @@ -52,8 +48,8 @@ RSpec.describe 'Submit Form' do end context 'when initialized by shared email address' do - let!(:submission) { create(:submission, template:, created_by_user: user) } - let!(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } } + let(:submission) { create(:submission, template:, created_by_user: user) } + let(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } } let(:submitter) { submitters.first } before do @@ -76,5 +72,16 @@ RSpec.describe 'Submit Form' do expect(submitter.completed_at).to be_present expect(submitter.values.values).to include('Sally') end + + it 'sends completed email' do + fill_in 'First Name', with: 'Adam' + click_on 'next' + click_on 'type_text_button' + fill_in 'signature_text_input', with: 'Adam' + + expect do + click_on 'Sign and Complete' + end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1) + end end end From 90d86b58c21773b36d46ef6ef76e55625a75fe81 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 22 Oct 2024 23:40:30 +0300 Subject: [PATCH 13/28] adjust migration --- app/jobs/process_submitter_completion_job.rb | 25 +++++++----- app/models/completed_document.rb | 2 + app/models/completed_submitter.rb | 8 +++- ...eate_completed_submitters_and_documents.rb | 2 +- ...late_completed_submitters_and_documents.rb | 40 +++++++++++-------- db/schema.rb | 1 + 6 files changed, 48 insertions(+), 30 deletions(-) diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index 8ca357e5..56ed4c5d 100644 --- a/app/jobs/process_submitter_completion_job.rb +++ b/app/jobs/process_submitter_completion_job.rb @@ -6,8 +6,6 @@ class ProcessSubmitterCompletionJob def perform(params = {}) submitter = Submitter.find(params['submitter_id']) - create_completed_submitter!(submitter) - is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil) if !is_all_completed && submitter.submission.submitters_order_preserved? @@ -26,31 +24,36 @@ class ProcessSubmitterCompletionJob enqueue_completed_emails(submitter) end - create_completed_documents!(submitter) + create_completed_submitter!(submitter) enqueue_completed_webhooks(submitter, is_all_completed:) end def create_completed_submitter!(submitter) + completed_submitter = CompletedSubmitter.find_or_initialize_by(submitter_id: submitter.id) + + return completed_submitter if completed_submitter.persisted? + submission = submitter.submission - sms_count = submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count - completed_submitter = CompletedSubmitter.where(submitter_id: submitter.id).first_or_initialize + completed_submitter.assign_attributes( submission_id: submitter.submission_id, account_id: submission.account_id, template_id: submission.template_id, source: submission.source, - sms_count:, + sms_count: submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count, completed_at: submitter.completed_at ) - completed_submitter.save! - end + submitter.documents.each do |attachment| + next if attachment.metadata['sha256'].blank? - def create_completed_documents!(submitter) - submitter.documents.map { |s| s.metadata['sha256'] }.compact_blank.each do |sha256| - CompletedDocument.where(submitter_id: submitter.id, sha256:).first_or_create! + completed_submitter.completed_documents << CompletedDocument.new(sha256: attachment.metadata['sha256']) end + + completed_submitter.save! + + completed_submitter end def enqueue_completed_webhooks(submitter, is_all_completed: false) diff --git a/app/models/completed_document.rb b/app/models/completed_document.rb index fb4bac39..2c842e29 100644 --- a/app/models/completed_document.rb +++ b/app/models/completed_document.rb @@ -17,4 +17,6 @@ # class CompletedDocument < ApplicationRecord belongs_to :submitter + + has_one :completed_submitter, primary_key: :submitter_id, inverse_of: :completed_documents, dependent: :destroy end diff --git a/app/models/completed_submitter.rb b/app/models/completed_submitter.rb index 1018baf0..181f2241 100644 --- a/app/models/completed_submitter.rb +++ b/app/models/completed_submitter.rb @@ -17,11 +17,17 @@ # # Indexes # -# index_completed_submitters_on_account_id (account_id) +# index_completed_submitters_on_account_id (account_id) +# index_completed_submitters_on_submitter_id (submitter_id) # class CompletedSubmitter < ApplicationRecord belongs_to :submitter belongs_to :submission belongs_to :account belongs_to :template + + has_many :completed_documents, dependent: :destroy, + primary_key: :submitter_id, + foreign_key: :submitter_id, + inverse_of: :submitter end diff --git a/db/migrate/20241018115034_create_completed_submitters_and_documents.rb b/db/migrate/20241018115034_create_completed_submitters_and_documents.rb index e804b436..eb917606 100644 --- a/db/migrate/20241018115034_create_completed_submitters_and_documents.rb +++ b/db/migrate/20241018115034_create_completed_submitters_and_documents.rb @@ -3,7 +3,7 @@ class CreateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2] def change create_table :completed_submitters do |t| - t.bigint :submitter_id, null: false + t.bigint :submitter_id, null: false, index: true t.bigint :submission_id, null: false t.bigint :account_id, null: false, index: true t.bigint :template_id, null: false diff --git a/db/migrate/20241022125135_populate_completed_submitters_and_documents.rb b/db/migrate/20241022125135_populate_completed_submitters_and_documents.rb index 01acd4aa..c2c3dd57 100644 --- a/db/migrate/20241022125135_populate_completed_submitters_and_documents.rb +++ b/db/migrate/20241022125135_populate_completed_submitters_and_documents.rb @@ -7,7 +7,8 @@ class PopulateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2] self.table_name = 'submitters' belongs_to :submission, class_name: 'MigrationSubmission' - has_many :submission_events, class_name: 'MigrationSubmissionEvent', foreign_key: :submitter_id + has_many :submission_sms_events, -> { where(event_type: %w[send_sms send_2fa_sms]) }, + class_name: 'MigrationSubmissionEvent', foreign_key: :submitter_id end class MigrationSubmission < ApplicationRecord @@ -27,18 +28,26 @@ class PopulateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2] end def up - completed_submitters = MigrationSubmitter.where.not(completed_at: nil) + submitters = MigrationSubmitter.where.not(completed_at: nil) + .preload(:submission, :submission_sms_events) + + count = submitters.count + + puts "Updating the database - it might take ~#{(count / 1000 * 3) + 1} seconds" if count > 2000 + + submitters.find_each do |submitter| + completed_submitter = MigrationCompletedSubmitter.find_or_initialize_by(submitter_id: submitter.id) + + next if completed_submitter.persisted? - completed_submitters.order(created_at: :asc).preload(:submission).find_each do |submitter| submission = submitter.submission - sms_count = submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count - completed_submitter = MigrationCompletedSubmitter.where(submitter_id: submitter.id).first_or_initialize + completed_submitter.assign_attributes( submission_id: submitter.submission_id, account_id: submission.account_id, template_id: submission.template_id, source: submission.source, - sms_count:, + sms_count: submitter.submission_sms_events.size, completed_at: submitter.completed_at, created_at: submitter.completed_at, updated_at: submitter.completed_at @@ -47,21 +56,18 @@ class PopulateCompletedSubmittersAndDocuments < ActiveRecord::Migration[7.2] completed_submitter.save! end - ActiveStorage::Attachment.where(record_id: completed_submitters.select(:id), - record_type: 'Submitter', - name: 'documents') - .order(created_at: :asc) - .find_each do |attachment| + attachments = ActiveStorage::Attachment.where(record_type: 'Submitter', name: 'documents').preload(:blob) + + attachments.find_each do |attachment| sha256 = attachment.metadata['sha256'] - submitter_id = attachment.record_id next if sha256.blank? - completed_document = MigrationCompletedDocument.where(submitter_id:, sha256:).first_or_initialize - completed_document.assign_attributes( - created_at: attachment.created_at, - updated_at: attachment.created_at - ) + completed_document = MigrationCompletedDocument.find_or_initialize_by(submitter_id: attachment.record_id, sha256:) + + next if completed_document.persisted? + + completed_document.assign_attributes(created_at: attachment.created_at, updated_at: attachment.created_at) completed_document.save! end diff --git a/db/schema.rb b/db/schema.rb index 79b893b8..55651476 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -109,6 +109,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_22_125135) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["account_id"], name: "index_completed_submitters_on_account_id" + t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id" end create_table "document_generation_events", force: :cascade do |t| From 3c616a824cf0ac75417da93c548a66600f806b16 Mon Sep 17 00:00:00 2001 From: Oleksandr Turchyn Date: Thu, 24 Oct 2024 10:11:39 +0300 Subject: [PATCH 14/28] show error for cross-environment API key usage --- .rubocop.yml | 2 +- app/controllers/api/api_base_controller.rb | 36 +++++++++++++++++-- spec/factories/accounts.rb | 9 +++++ spec/requests/submissions_spec.rb | 41 ++++++++++++++++++--- spec/requests/submitters_spec.rb | 42 +++++++++++++++++++--- spec/requests/templates_spec.rb | 42 +++++++++++++++++++--- 6 files changed, 156 insertions(+), 16 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 165bc73a..ff5b5c4d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -66,7 +66,7 @@ RSpec/ExampleLength: Max: 40 RSpec/MultipleMemoizedHelpers: - Max: 6 + Max: 9 Metrics/BlockNesting: Max: 4 diff --git a/app/controllers/api/api_base_controller.rb b/app/controllers/api/api_base_controller.rb index 267fd9bd..514b2f3a 100644 --- a/app/controllers/api/api_base_controller.rb +++ b/app/controllers/api/api_base_controller.rb @@ -25,9 +25,9 @@ module Api render json: { error: 'Too many requests' }, status: :too_many_requests end - if Rails.env.production? + unless Rails.env.development? rescue_from CanCan::AccessDenied do |e| - render json: { error: e.message }, status: :forbidden + render json: { error: access_denied_error_message(e) }, status: :forbidden end rescue_from JSON::ParserError do |e| @@ -39,6 +39,38 @@ module Api private + def access_denied_error_message(error) + return 'Not authorized' if request.headers['X-Auth-Token'].blank? + return 'Not authorized' unless error.subject.is_a?(ActiveRecord::Base) + return 'Not authorized' unless error.subject.respond_to?(:account_id) + + linked_account_record_exists = + if current_user.account.testing? + current_user.account.linked_account_accounts.where(account_type: 'testing') + .exists?(account_id: error.subject.account_id) + else + current_user.account.testing_accounts.exists?(id: error.subject.account_id) + end + + return 'Not authorized' unless linked_account_record_exists + + object_name = error.subject.model_name.human + id = error.subject.id + + message = + if current_user.account.testing? + "#{object_name} #{id} not found using testing API key; Use production API key to " \ + "access production #{object_name.downcase.pluralize}." + else + "#{object_name} #{id} not found using production API key; Use testing API key to " \ + "access testing #{object_name.downcase.pluralize}." + end + + Rollbar.warning(message) if defined?(Rollbar) + + message + end + def paginate(relation, field: :id) result = relation.order(field => :desc) .limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min) diff --git a/spec/factories/accounts.rb b/spec/factories/accounts.rb index 785a37b4..b0551e06 100644 --- a/spec/factories/accounts.rb +++ b/spec/factories/accounts.rb @@ -5,5 +5,14 @@ FactoryBot.define do name { Faker::Company.name } locale { 'en-US' } timezone { 'UTC' } + + trait :with_testing_account do + after(:create) do |account| + testing_account = account.dup.tap { |a| a.name = "Testing - #{account.name}" } + testing_account.uuid = SecureRandom.uuid + account.testing_accounts << testing_account + account.save! + end + end end end diff --git a/spec/requests/submissions_spec.rb b/spec/requests/submissions_spec.rb index fe31dff8..c5d02f4b 100644 --- a/spec/requests/submissions_spec.rb +++ b/spec/requests/submissions_spec.rb @@ -3,11 +3,17 @@ require 'rails_helper' describe 'Submission API', type: :request do - let!(:account) { create(:account) } - let!(:author) { create(:user, account:) } - let!(:folder) { create(:template_folder, account:) } - let!(:templates) { create_list(:template, 2, account:, author:, folder:) } - let!(:multiple_submitters_template) { create(:template, submitter_count: 3, account:, author:, folder:) } + let(:account) { create(:account, :with_testing_account) } + let(:testing_account) { account.testing_accounts.first } + let(:author) { create(:user, account:) } + let(:testing_author) { create(:user, account: testing_account) } + let(:folder) { create(:template_folder, account:) } + let(:testing_folder) { create(:template_folder, account: testing_account) } + let(:templates) { create_list(:template, 2, account:, author:, folder:) } + let(:multiple_submitters_template) { create(:template, submitter_count: 3, account:, author:, folder:) } + let(:testing_templates) do + create_list(:template, 2, account: testing_account, author: testing_author, folder: testing_folder) + end describe 'GET /api/submissions' do it 'returns a list of submissions' do @@ -41,6 +47,31 @@ describe 'Submission API', type: :request do expect(response).to have_http_status(:ok) expect(response.parsed_body).to eq(JSON.parse(show_submission_body(submission).to_json)) end + + it 'returns an authorization error if test account API token is used with a production submission' do + submission = create(:submission, :with_submitters, :with_events, template: templates[0], created_by_user: author) + + get "/api/submissions/#{submission.id}", headers: { 'x-auth-token': testing_author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Submission #{submission.id} not found using testing API key; " \ + 'Use production API key to access production submissions.' }.to_json) + ) + end + + it 'returns an authorization error if production account API token is used with a test submission' do + submission = create(:submission, :with_submitters, :with_events, template: testing_templates[0], + created_by_user: testing_author) + + get "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Submission #{submission.id} not found using production API key; " \ + 'Use testing API key to access testing submissions.' }.to_json) + ) + end end describe 'POST /api/submissions' do diff --git a/spec/requests/submitters_spec.rb b/spec/requests/submitters_spec.rb index 37ceecce..ffe35cd9 100644 --- a/spec/requests/submitters_spec.rb +++ b/spec/requests/submitters_spec.rb @@ -3,10 +3,16 @@ require 'rails_helper' describe 'Submitter API', type: :request do - let!(:account) { create(:account) } - let!(:author) { create(:user, account:) } - let!(:folder) { create(:template_folder, account:) } - let!(:templates) { create_list(:template, 2, account:, author:, folder:) } + let(:account) { create(:account, :with_testing_account) } + let(:testing_account) { account.testing_accounts.first } + let(:author) { create(:user, account:) } + let(:testing_author) { create(:user, account: testing_account) } + let(:folder) { create(:template_folder, account:) } + let(:testing_folder) { create(:template_folder, account: testing_account) } + let(:templates) { create_list(:template, 2, account:, author:, folder:) } + let(:testing_templates) do + create_list(:template, 2, account: testing_account, author: testing_author, folder: testing_folder) + end describe 'GET /api/submitters' do it 'returns a list of submitters' do @@ -42,6 +48,34 @@ describe 'Submitter API', type: :request do expect(response).to have_http_status(:ok) expect(response.parsed_body).to eq(JSON.parse(submitter_body(submitter).to_json)) end + + it 'returns an authorization error if test account API token is used with a production submitter' do + submitter = create(:submission, :with_submitters, :with_events, + template: templates[0], + created_by_user: author).submitters.first + + get "/api/submitters/#{submitter.id}", headers: { 'x-auth-token': testing_author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Submitter #{submitter.id} not found using " \ + 'testing API key; Use production API key to access production submitters.' }.to_json) + ) + end + + it 'returns an authorization error if production account API token is used with a test submitter' do + submitter = create(:submission, :with_submitters, :with_events, + template: testing_templates[0], + created_by_user: testing_author).submitters.first + + get "/api/submitters/#{submitter.id}", headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Submitter #{submitter.id} not found using production API key; " \ + 'Use testing API key to access testing submitters.' }.to_json) + ) + end end describe 'PUT /api/submitters' do diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb index 67430ed8..26bdb546 100644 --- a/spec/requests/templates_spec.rb +++ b/spec/requests/templates_spec.rb @@ -3,10 +3,12 @@ require 'rails_helper' describe 'Templates API', type: :request do - let!(:account) { create(:account) } - let!(:author) { create(:user, account:) } - let!(:folder) { create(:template_folder, account:) } - let!(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } } + let(:account) { create(:account, :with_testing_account) } + let(:testing_account) { account.testing_accounts.first } + let(:author) { create(:user, account:) } + let(:testing_author) { create(:user, account: testing_account) } + let(:folder) { create(:template_folder, account:) } + let(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } } describe 'GET /api/templates' do it 'returns a list of templates' do @@ -48,6 +50,38 @@ describe 'Templates API', type: :request do expect(response).to have_http_status(:ok) expect(response.parsed_body).to eq(JSON.parse(template_body(template).to_json)) end + + it 'returns an authorization error if test account API token is used with a production template' do + template = create(:template, account:, + author:, + folder:, + external_id: SecureRandom.base58(10), + preferences: template_preferences) + + get "/api/templates/#{template.id}", headers: { 'x-auth-token': testing_author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Template #{template.id} not found using testing API key; " \ + 'Use production API key to access production templates.' }.to_json) + ) + end + + it 'returns an authorization error if production account API token is used with a test template' do + template = create(:template, account: testing_account, + author: testing_author, + folder:, + external_id: SecureRandom.base58(10), + preferences: template_preferences) + + get "/api/templates/#{template.id}", headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Template #{template.id} not found using production API key; " \ + 'Use testing API key to access testing templates.' }.to_json) + ) + end end describe 'PUT /api/templates' do From e1eddd31c48c5901a39b0190e29f1736c9511135 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 24 Oct 2024 12:08:10 +0300 Subject: [PATCH 15/28] fix field condition --- app/javascript/template_builder/conditions_modal.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/template_builder/conditions_modal.vue b/app/javascript/template_builder/conditions_modal.vue index 6b88aa96..cfb045cf 100644 --- a/app/javascript/template_builder/conditions_modal.vue +++ b/app/javascript/template_builder/conditions_modal.vue @@ -90,7 +90,7 @@ - <%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %> + <% token = current_user.access_token.token %> + + + + <%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
<%= button_to button_title(title: t('rotate'), disabled_with: t('rotate'), icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: t('remove_existing_api_token_and_generated_a_new_one_are_you_sure_') } %>
diff --git a/spec/system/api_settings_spec.rb b/spec/system/api_settings_spec.rb index 4870964c..648e202d 100644 --- a/spec/system/api_settings_spec.rb +++ b/spec/system/api_settings_spec.rb @@ -13,6 +13,7 @@ RSpec.describe 'API Settings' do it 'shows verify signed PDF page' do expect(page).to have_content('API') - expect(page).to have_field('X-Auth-Token', with: user.access_token.token) + token = user.access_token.token + expect(page).to have_field('X-Auth-Token', with: token.sub(token[5..], '*' * token[5..].size)) end end From 558a14a5f2d0cd18764e39508f7b68b3aacb33d8 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 25 Oct 2024 20:29:58 +0300 Subject: [PATCH 24/28] fix email validate --- lib/params/base_validator.rb | 4 +++- spec/lib/params/base_validator_spec.rb | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/params/base_validator.rb b/lib/params/base_validator.rb index 8075a126..29c1985f 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -70,8 +70,10 @@ module Params def email_format(params, key, message: nil) return if params.blank? return if params[key].blank? + return if params[key].to_s.include?('<') - if params[key].to_s.strip.split(/\s*[;,]\s*/).all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) } + if params[key].to_s.strip.split(/\s*[;,]\s*/).compact_blank + .all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) } return end diff --git a/spec/lib/params/base_validator_spec.rb b/spec/lib/params/base_validator_spec.rb index c7152bda..5a00e188 100644 --- a/spec/lib/params/base_validator_spec.rb +++ b/spec/lib/params/base_validator_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Params::BaseValidator do emails = [ ' john.doe@example.com ', 'john.doe@example.com', + 'Test ', 'jane+newsletter@domain.org', 'mike_smith@company.net', 'lisa-wong@sub.example.co.uk', @@ -49,7 +50,6 @@ RSpec.describe Params::BaseValidator do it 'when multiple emails are valid' do emails = [ - 'john.doe@example.com, jane.doe+newsletter@domain.org', 'joshua@automobile.car ; chloe+fashion@food.delivery', 'mike-smith@company.net;lisa.wong-sales@sub.example.co.uk', @@ -78,9 +78,7 @@ RSpec.describe Params::BaseValidator do it 'when multiple emails are invalid' do emails = [ - 'jone@gmail.com, ,mike@gmail.com', 'john.doe@example.com dave@nonprofit.org', - '; oliver.scott@example.com', 'amy.baker@ example.com, george.morris@ example.com', 'jenny.king@example.com . diana.robinson@example.com', 'nancy.davis@.com, henry.ford@.com', From 889103aa5ed9340f608242bf45c1c2d290b154a6 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 26 Oct 2024 09:54:41 +0300 Subject: [PATCH 25/28] use com --- README.md | 6 +++--- app/javascript/submission_form/completed.vue | 2 +- app/mailers/application_mailer.rb | 2 +- app/views/devise/shared/_select_server.html.erb | 4 ++-- app/views/shared/_navbar.html.erb | 2 +- lib/action_mailer_configs_interceptor.rb | 6 ------ lib/docuseal.rb | 4 ++-- lib/submissions/generate_audit_trail.rb | 2 +- 8 files changed, 11 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b50558e6..86b22694 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,12 @@ DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool.

- ✨ Live Demo + ✨ Live Demo | - ☁️ Try in Cloud + ☁️ Try in Cloud

-[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.co) +[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.tech) ## Features - PDF form fields builder (WYSIWYG) diff --git a/app/javascript/submission_form/completed.vue b/app/javascript/submission_form/completed.vue index b1ef464b..694e1b9c 100644 --- a/app/javascript/submission_form/completed.vue +++ b/app/javascript/submission_form/completed.vue @@ -76,7 +76,7 @@ diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d5c36618..39f27379 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base - default from: 'DocuSeal ' + default from: 'DocuSeal ' layout 'mailer' register_interceptor ActionMailerConfigsInterceptor diff --git a/app/views/devise/shared/_select_server.html.erb b/app/views/devise/shared/_select_server.html.erb index 60f5e56a..2b98ec18 100644 --- a/app/views/devise/shared/_select_server.html.erb +++ b/app/views/devise/shared/_select_server.html.erb @@ -1,10 +1,10 @@
- + <%= svg_icon 'world', class: 'w-5 h-5' %> Global - + <%= svg_icon 'eu_flag', class: 'w-5 h-5' %> Europe diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index 0bb4c41d..98acc80e 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -10,7 +10,7 @@ <% if signed_in? %>
<% if Docuseal.demo? %> - + <%= t('sign_up') %>