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 9ea6f35ec..19e196b5a 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 94c836c3f..fdcc28610 100644 --- a/lib/submissions/generate_preview_attachments.rb +++ b/lib/submissions/generate_preview_attachments.rb @@ -8,12 +8,11 @@ module GeneratePreviewAttachments 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 6da101726..d41ca5705 100644 --- a/app/controllers/email_smtp_settings_controller.rb +++ b/app/controllers/email_smtp_settings_controller.rb @@ -9,7 +9,7 @@ def index; end 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 2aefcf3a2..4140ad395 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 5616f2168..ad047b2bb 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 3d10d04db..57f6a8306 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -153,9 +153,9 @@ 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 4c2571e84..4aaebb9c0 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -70,7 +70,10 @@ def format(params, key, regexp, message: nil) 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 193b9b33f..eb62d12aa 100644 --- a/spec/lib/params/base_validator_spec.rb +++ b/spec/lib/params/base_validator_spec.rb @@ -34,38 +34,9 @@ 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 4aaebb9c0..ec6a901e9 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -75,11 +75,7 @@ def email_format(params, key, message: nil) 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 ec6a901e9..5eab967ab 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -75,7 +75,7 @@ def email_format(params, key, message: nil) 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 eb62d12aa..b729941a0 100644 --- a/spec/lib/params/base_validator_spec.rb +++ b/spec/lib/params/base_validator_spec.rb @@ -42,7 +42,7 @@ 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 @@ 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 908a04b4c..1ae634e67 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -80,7 +80,8 @@ def create 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 5e0ab509e..d0af550cd 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 @@ def call(template:, user:, submissions_attrs:, source:, submitters_order:, param 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 @@ def call(template:, user:, submissions_attrs:, source:, submitters_order:, param 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 3a9590d8f..fe31dff86 100644 --- a/spec/requests/submissions_spec.rb +++ b/spec/requests/submissions_spec.rb @@ -48,7 +48,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 e9fc496b3..210b783d5 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -120,6 +120,7 @@ def assign_submitter_attrs(submitter, attrs) 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 0eeddfa7c..bc2adcfd2 100644 --- a/lib/action_mailer_configs_interceptor.rb +++ b/lib/action_mailer_configs_interceptor.rb @@ -29,14 +29,16 @@ def delivering_email(message) 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 165bc73a8..03fb8158f 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 32c59808d..7b2d5cb4e 100644 --- a/app/controllers/api/tools_controller.rb +++ b/app/controllers/api/tools_controller.rb @@ -20,10 +20,7 @@ def verify 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 cfedfee43..8ca357e53 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 @@ def perform(params = {}) 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 000000000..fb4bac395 --- /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 000000000..1018baf01 --- /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 8ec49114e..dd0ee3fd9 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 000000000..e804b4367 --- /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 000000000..01acd4aa0 --- /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 8a3123bc5..79b893b84 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 @@ 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 000000000..d670e9a3d --- /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 000000000..7d084d577 --- /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 b729941a0..c7152bda5 100644 --- a/spec/lib/params/base_validator_spec.rb +++ b/spec/lib/params/base_validator_spec.rb @@ -36,6 +36,7 @@ '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 000000000..6ddd66bea --- /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 da4c5463f..e3e979ec4 100644 --- a/spec/system/submit_form_spec.rb +++ b/spec/system/submit_form_spec.rb @@ -6,6 +6,10 @@ 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 @@ 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 03fb8158f..165bc73a8 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 7d084d577..632003b3c 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 @@ 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 @@ 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 @@ 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 6ddd66bea..b51f771c2 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 e3e979ec4..7c2f32230 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 @@ 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 @@ 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 8ca357e53..56ed4c5df 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 @@ def perform(params = {}) 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 fb4bac395..2c842e291 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 1018baf01..181f2241f 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 e804b4367..eb917606b 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 01acd4aa0..c2c3dd570 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 MigrationSubmitter < ApplicationRecord 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 MigrationCompletedDocument < ApplicationRecord 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 @@ def up 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 79b893b84..556514761 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -109,6 +109,7 @@ 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 165bc73a8..ff5b5c4dc 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 267fd9bd2..514b2f3a3 100644 --- a/app/controllers/api/api_base_controller.rb +++ b/app/controllers/api/api_base_controller.rb @@ -25,9 +25,9 @@ class ApiBaseController < ActionController::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 @@ class ApiBaseController < ActionController::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 785a37b48..b0551e061 100644 --- a/spec/factories/accounts.rb +++ b/spec/factories/accounts.rb @@ -5,5 +5,14 @@ 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 fe31dff86..c5d02f4b7 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 @@ 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 37ceecce8..ffe35cd98 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 @@ 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 67430ed80..26bdb5462 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 @@ 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 6b88aa965..cfb045cfd 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 4870964cf..648e202df 100644 --- a/spec/system/api_settings_spec.rb +++ b/spec/system/api_settings_spec.rb @@ -13,6 +13,7 @@ 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 8075a126a..29c1985f2 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -70,8 +70,10 @@ def format(params, key, regexp, message: nil) 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 c7152bda5..5a00e1886 100644 --- a/spec/lib/params/base_validator_spec.rb +++ b/spec/lib/params/base_validator_spec.rb @@ -10,6 +10,7 @@ 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 @@ 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 @@ 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 b50558e67..86b226948 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 b1ef464b2..694e1b9cf 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 d5c366185..39f273794 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 60f5e56ae..2b98ec18e 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 0bb4c41df..98acc80e8 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') %>