Merge from docusealco/wip
This commit is contained in:
+1
-1
@@ -66,7 +66,7 @@ RSpec/ExampleLength:
|
||||
Max: 40
|
||||
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 6
|
||||
Max: 9
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 4
|
||||
|
||||
@@ -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.
|
||||
</p>
|
||||
<h2 align="center">
|
||||
<a href="https://demo.docuseal.co">✨ Live Demo</a>
|
||||
<a href="https://demo.docuseal.tech">✨ Live Demo</a>
|
||||
<span>|</span>
|
||||
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
|
||||
<a href="https://docuseal.com/sign_up">☁️ Try in Cloud</a>
|
||||
</h2>
|
||||
|
||||
[](https://demo.docuseal.co)
|
||||
[](https://demo.docuseal.tech)
|
||||
|
||||
## Features
|
||||
- PDF form fields builder (WYSIWYG)
|
||||
|
||||
@@ -8,6 +8,7 @@ class AccountConfigsController < ApplicationController
|
||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||
AccountConfig::FORCE_MFA,
|
||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
||||
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||
|
||||
@@ -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,33 @@ 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
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def paginate(relation, field: :id)
|
||||
result = relation.order(field => :desc)
|
||||
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||
|
||||
@@ -74,13 +74,14 @@ module Api
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
submissions.each do |submission|
|
||||
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
|
||||
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
|
||||
submission.submitters.each do |submitter|
|
||||
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submitter.id }) if submitter.completed_at?
|
||||
end
|
||||
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
|
||||
|
||||
@@ -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+]/, '')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,9 +9,11 @@ class UsersController < ApplicationController
|
||||
def index
|
||||
@users =
|
||||
if params[:status] == 'archived'
|
||||
@users.archived
|
||||
@users.archived.where.not(role: 'integration')
|
||||
elsif params[:status] == 'integration'
|
||||
@users.active.where(role: 'integration')
|
||||
else
|
||||
@users.active
|
||||
@users.active.where.not(role: 'integration')
|
||||
end
|
||||
|
||||
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
||||
|
||||
@@ -29,6 +29,7 @@ import SearchInput from './elements/search_input'
|
||||
import ToggleAttribute from './elements/toggle_attribute'
|
||||
import LinkedInput from './elements/linked_input'
|
||||
import CheckboxGroup from './elements/checkbox_group'
|
||||
import MaskedInput from './elements/masked_input'
|
||||
|
||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||
|
||||
@@ -95,6 +96,7 @@ safeRegisterElement('search-input', SearchInput)
|
||||
safeRegisterElement('toggle-attribute', ToggleAttribute)
|
||||
safeRegisterElement('linked-input', LinkedInput)
|
||||
safeRegisterElement('checkbox-group', CheckboxGroup)
|
||||
safeRegisterElement('masked-input', MaskedInput)
|
||||
|
||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const maskedToken = this.input.value
|
||||
|
||||
this.input.addEventListener('focus', () => {
|
||||
this.input.value = this.dataset.token
|
||||
this.input.select()
|
||||
})
|
||||
|
||||
this.input.addEventListener('focusout', () => {
|
||||
this.input.value = maskedToken
|
||||
})
|
||||
}
|
||||
|
||||
get input () {
|
||||
return this.querySelector('input')
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@
|
||||
</a>
|
||||
<a
|
||||
v-if="isDemo"
|
||||
href="https://docuseal.co/sign_up"
|
||||
href="https://docuseal.com/sign_up"
|
||||
class="white-button flex items-center space-x-1 w-full"
|
||||
>
|
||||
<IconLogin />
|
||||
|
||||
@@ -1107,6 +1107,10 @@ export default {
|
||||
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
||||
const currentFieldType = this.currentField.type
|
||||
|
||||
if (!formData && !this.$refs.form.checkValidity()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.dryRun) {
|
||||
currentFieldUuids.forEach((fieldUuid) => {
|
||||
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
v-if="conditionField(condition)?.options?.length"
|
||||
v-if="['radio', 'select', 'multiple'].includes(conditionField(condition)?.type) && conditionField(condition)?.options"
|
||||
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
||||
:class="{ 'text-gray-300': !condition.value }"
|
||||
required
|
||||
|
||||
@@ -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,42 @@ class ProcessSubmitterCompletionJob
|
||||
enqueue_completed_emails(submitter)
|
||||
end
|
||||
|
||||
create_completed_documents!(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
|
||||
|
||||
completed_submitter.assign_attributes(
|
||||
submission_id: submitter.submission_id,
|
||||
account_id: submission.account_id,
|
||||
template_id: submission.template_id,
|
||||
source: submission.source,
|
||||
sms_count: submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count,
|
||||
completed_at: submitter.completed_at
|
||||
)
|
||||
|
||||
completed_submitter.save!
|
||||
|
||||
completed_submitter
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
retry
|
||||
end
|
||||
|
||||
def create_completed_documents!(submitter)
|
||||
submitter.documents.filter_map do |attachment|
|
||||
next if attachment.metadata['sha256'].blank?
|
||||
|
||||
CompletedDocument.find_or_create_by!(sha256: attachment.metadata['sha256'], submitter_id: submitter.id)
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
||||
webhook_config = Accounts.load_webhook_config(submitter.account)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: 'DocuSeal <info@docuseal.co>'
|
||||
default from: 'DocuSeal <info@docuseal.com>'
|
||||
layout 'mailer'
|
||||
|
||||
register_interceptor ActionMailerConfigsInterceptor
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,6 +28,7 @@ class AccountConfig < ApplicationRecord
|
||||
FORCE_MFA = 'force_mfa'
|
||||
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
||||
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
||||
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
|
||||
SUBMITTER_REMINDERS = 'submitter_reminders'
|
||||
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
||||
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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, optional: true
|
||||
|
||||
has_one :completed_submitter, primary_key: :submitter_id, inverse_of: :completed_documents, dependent: :destroy
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# 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)
|
||||
# index_completed_submitters_on_submitter_id (submitter_id) UNIQUE
|
||||
#
|
||||
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
|
||||
@@ -49,6 +49,9 @@ class User < ApplicationRecord
|
||||
|
||||
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
|
||||
|
||||
FULL_EMAIL_REGEXP =
|
||||
/\A[a-z0-9][\.']?(?:(?:[a-z0-9_-]+[\.\+'])*[a-z0-9_-]+)*@(?:[a-z0-9]+[\.-])*[a-z0-9]+\.[a-z]{2,}\z/i
|
||||
|
||||
has_one_attached :signature
|
||||
has_one_attached :initials
|
||||
|
||||
|
||||
@@ -103,6 +103,18 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TO_DECLINE_KEY) %>
|
||||
<% if can?(:manage, account_config) %>
|
||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||
<%= f.hidden_field :key %>
|
||||
<div class="flex items-center justify-between py-2.5">
|
||||
<span>
|
||||
<%= t('allow_to_decline_documents') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::FORM_PREFILL_SIGNATURE_KEY) %>
|
||||
<% if can?(:manage, account_config) %>
|
||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex w-full space-x-4">
|
||||
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
<%= 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 %>
|
||||
<masked-input class="block w-full" data-token="<%= token %>">
|
||||
<input id="api_key" type="text" value="<%= token.sub(token[5..], '*' * token[5..].size) %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
</masked-input>
|
||||
<%= 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') %>
|
||||
</div>
|
||||
<%= 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_') } %>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="text-center">
|
||||
<div class="join">
|
||||
<a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' || request.host == 'docuseal.com' %>">
|
||||
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.com' %>">
|
||||
<%= svg_icon 'world', class: 'w-5 h-5' %>
|
||||
Global
|
||||
</a>
|
||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.co', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
|
||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
|
||||
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
|
||||
Europe
|
||||
</a>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= 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' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control pt-2">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<% if signed_in? %>
|
||||
<div class="space-x-4 flex items-center">
|
||||
<% if Docuseal.demo? %>
|
||||
<a href="https://docuseal.co/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
|
||||
<a href="https://docuseal.com/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
|
||||
<%= t('sign_up') %>
|
||||
</a>
|
||||
<span class="hidden sm:inline">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="text-center px-2">
|
||||
<% 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 %>
|
||||
|
||||
@@ -17,41 +17,43 @@
|
||||
<% end %>
|
||||
<% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
|
||||
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
|
||||
<div class="join relative">
|
||||
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||
<span class="hidden md:inline"><%= t('download') %></span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<span class="hidden md:inline"><%= t('downloading') %></span>
|
||||
</span>
|
||||
</download-button>
|
||||
<% if is_all_completed && !is_combined_enabled %>
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500">
|
||||
<span class="text-sm align-text-top">
|
||||
<%= svg_icon('chevron_down', class: 'w-6 h-6 flex-shrink-0 stroke-2') %>
|
||||
</span>
|
||||
</label>
|
||||
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right">
|
||||
<li>
|
||||
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: true }.compact) %>" class="flex items-center">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
|
||||
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<span><%= t('downloading') %></span>
|
||||
</span>
|
||||
</download-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if is_all_completed || !is_combined_enabled %>
|
||||
<div class="join relative">
|
||||
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||
<span class="hidden md:inline"><%= t('download') %></span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<span class="hidden md:inline"><%= t('downloading') %></span>
|
||||
</span>
|
||||
</download-button>
|
||||
<% if is_all_completed && !is_combined_enabled %>
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500">
|
||||
<span class="text-sm align-text-top">
|
||||
<%= svg_icon('chevron_down', class: 'w-6 h-6 flex-shrink-0 stroke-2') %>
|
||||
</span>
|
||||
</label>
|
||||
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right">
|
||||
<li>
|
||||
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug, { sig: params[:sig], combined: true }.compact) %>" class="flex items-center">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
|
||||
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<span><%= t('downloading') %></span>
|
||||
</span>
|
||||
</download-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? %>
|
||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
|
||||
<% end %>
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
<%= @submitter.submission.template.name %>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
|
||||
<div>
|
||||
<%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button' do %>
|
||||
<%= render 'submit_form/decline_form', submitter: @submitter %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @form_configs[:with_decline] %>
|
||||
<div>
|
||||
<%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button' do %>
|
||||
<%= render 'submit_form/decline_form', submitter: @submitter %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4">
|
||||
<span class="flex items-center justify-center" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="flex items-center space-x-1">
|
||||
<% if %w[archived integration].include?(params[:status]) %>
|
||||
<%= link_to t('view_active'), settings_users_path, class: 'link text-sm' %>
|
||||
<% else %>
|
||||
<% archived_exists = current_account.users.archived.where.not(role: 'integration').exists? %>
|
||||
<% if current_account.users.active.exists?(role: 'integration') %>
|
||||
<%= link_to t('view_embedding_users'), settings_integration_users_path, class: 'link text-sm' %>
|
||||
<% if archived_exists %>
|
||||
<span class="text-neutral-700">|</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if archived_exists %>
|
||||
<%= link_to t('view_archived'), settings_archived_users_path, class: 'link text-sm' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,12 +1,18 @@
|
||||
<div class="flex-wrap space-y-4 md:flex md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="md:flex-grow">
|
||||
<div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4">
|
||||
<div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4 min-h-12">
|
||||
<h1 class="text-4xl font-bold">
|
||||
<%= t('status_users', status: params[:status].to_s.titleize) %>
|
||||
<% if params[:status] == 'archived' %>
|
||||
<%= t('archived_users') %>
|
||||
<% elsif params[:status] == 'integration' %>
|
||||
<%= t('embedding_users') %>
|
||||
<% else %>
|
||||
<%= t('users') %>
|
||||
<% end %>
|
||||
</h1>
|
||||
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
|
||||
<% if can?(:create, User.new(account: current_account)) %>
|
||||
<% if params[:status].blank? && can?(:create, User.new(account: current_account)) %>
|
||||
<%= render 'users/extra_buttons' %>
|
||||
<% if content_for(:add_user_button) %>
|
||||
<%= content_for(:add_user_button) %>
|
||||
@@ -57,17 +63,17 @@
|
||||
<%= user.last_sign_in_at ? l(user.last_sign_in_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) : '-' %>
|
||||
</td>
|
||||
<td class="flex items-center space-x-2 justify-end">
|
||||
<% if can?(:update, user) && user.archived_at.blank? %>
|
||||
<% if params[:status].blank? && can?(:update, user) && user.archived_at.blank? %>
|
||||
<%= link_to edit_user_path(user), class: 'btn btn-outline btn-xs', title: t('edit'), data: { turbo_frame: 'modal' } do %>
|
||||
<%= t('edit') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if params[:status].blank? && can?(:destroy, user) && user != current_user %>
|
||||
<% if params[:status] != 'archived' && can?(:destroy, user) && user != current_user %>
|
||||
<%= button_to user_path(user), method: :delete, class: 'btn btn-outline btn-error btn-xs', title: t('remove'), data: { turbo_confirm: t('are_you_sure_') } do %>
|
||||
<%= t('remove') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if params[:status].present? && can?(:manage, user) && user != current_user %>
|
||||
<% if params[:status] == 'archived' && can?(:manage, user) && user != current_user && user.archived_at? %>
|
||||
<%= button_to user_path(user), method: :put, params: { user: { archived_at: nil } }, class: 'btn btn-outline btn-xs', title: t('unarchive'), data: { turbo_confirm: t('are_you_sure_') } do %>
|
||||
<%= t('unarchive') %>
|
||||
<% end %>
|
||||
@@ -78,22 +84,11 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% view_archived_html = capture do %>
|
||||
<% if current_account.users.archived.exists? %>
|
||||
<div>
|
||||
<% if params[:status] == 'archived' %>
|
||||
<a href="<%= settings_users_path %>" class="link text-sm"><%= t('view_active') %></a>
|
||||
<% else %>
|
||||
<a href="<%= settings_archived_users_path %>" class="link text-sm"><%= t('view_archived') %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if @pagy.pages > 1 %>
|
||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'users', left_additional_html: view_archived_html %>
|
||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'users', left_additional_html: render('bottom_links') %>
|
||||
<% else %>
|
||||
<div class="mt-2">
|
||||
<%= view_archived_html %>
|
||||
<%= render 'bottom_links' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Rails.configuration.to_prepare do
|
||||
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production'
|
||||
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production' && ENV['RUN_MIGRATIONS'] != 'false'
|
||||
end
|
||||
|
||||
+25
-6
@@ -134,6 +134,7 @@ en: &en
|
||||
require_signing_reason: Require signing reason
|
||||
allow_typed_text_signatures: Allow typed text signatures
|
||||
allow_to_resubmit_completed_forms: Allow to resubmit completed forms
|
||||
allow_to_decline_documents: Allow to decline documents
|
||||
remember_and_pre_fill_signatures: Remember and pre-fill signatures
|
||||
require_authentication_for_file_download_links: Require authentication for file download links
|
||||
combine_completed_documents_and_audit_log: Combine completed documents and Audit Log
|
||||
@@ -473,7 +474,6 @@ en: &en
|
||||
back: Back
|
||||
add_secret: Add Secret
|
||||
submission_example_payload: Submission example payload
|
||||
status_users: '%{status} Users'
|
||||
there_are_no_signatures: There are no signatures
|
||||
signed_with_trusted_certificate: Signed with trusted certificate
|
||||
signed_with_external_certificate: Signed with external certificate
|
||||
@@ -618,6 +618,10 @@ en: &en
|
||||
use_international_format_1xxx_: 'Use internatioanl format: +1xxx...'
|
||||
submitter_cannot_be_updated: Submitter cannot be updated.
|
||||
at_least_one_field_must_be_filled: At least one field must be filled.
|
||||
archived_users: Archived Users
|
||||
embedding_users: Embedding Users
|
||||
view_embedding_users: View Embedding Users
|
||||
view_users: View Users
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
|
||||
@@ -770,6 +774,7 @@ es: &es
|
||||
require_signing_reason: Requerir motivo de firma
|
||||
allow_typed_text_signatures: Permitir firmas de texto mecanografiadas
|
||||
allow_to_resubmit_completed_forms: Permitir reenviar formularios completados
|
||||
allow_to_decline_documents: Permitir rechazar documentos
|
||||
remember_and_pre_fill_signatures: Recordar y prellenar firmas
|
||||
require_authentication_for_file_download_links: Requerir autenticación para enlaces de descarga de archivos
|
||||
combine_completed_documents_and_audit_log: Combinar documentos completados y Registro de Auditoría
|
||||
@@ -1109,7 +1114,6 @@ es: &es
|
||||
back: Atrás
|
||||
add_secret: Agregar secreto
|
||||
submission_example_payload: Ejemplo de payload de envío
|
||||
status_users: '%{status} Usuarios'
|
||||
there_are_no_signatures: No hay firmas
|
||||
signed_with_trusted_certificate: Firmado con certificado de confianza
|
||||
signed_with_external_certificate: Firmado con certificado externo
|
||||
@@ -1254,6 +1258,9 @@ es: &es
|
||||
use_international_format_1xxx_: 'Usa el formato internacional: +1xxx...'
|
||||
submitter_cannot_be_updated: El remitente no puede ser actualizado.
|
||||
at_least_one_field_must_be_filled: Al menos un campo debe estar completo.
|
||||
archived_users: Usuarios Archivados
|
||||
embedding_users: Usuarios Integrados
|
||||
view_embedding_users: Ver Usuarios Integrado
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
|
||||
@@ -1406,6 +1413,7 @@ it: &it
|
||||
require_signing_reason: Richiedere il motivo della firma
|
||||
allow_typed_text_signatures: Permettere firme con testo digitato
|
||||
allow_to_resubmit_completed_forms: Permettere di reinviare i moduli completati
|
||||
allow_to_decline_documents: Permettere di rifiutare i documenti
|
||||
remember_and_pre_fill_signatures: Ricordare e precompilare le firme
|
||||
require_authentication_for_file_download_links: "Richiedere l'autenticazione per i link di download dei file"
|
||||
combine_completed_documents_and_audit_log: Combinare i documenti completati e il Registro di Audit
|
||||
@@ -1745,7 +1753,6 @@ it: &it
|
||||
back: Indietro
|
||||
add_secret: Aggiungi segreto
|
||||
submission_example_payload: Esempio di payload di invio
|
||||
status_users: '%{status} Utenti'
|
||||
there_are_no_signatures: Non ci sono firme
|
||||
signed_with_trusted_certificate: Firmato con certificato affidabile
|
||||
signed_with_external_certificate: Firmato con certificato esterno
|
||||
@@ -1890,6 +1897,9 @@ it: &it
|
||||
use_international_format_1xxx_: 'Utilizza il formato internazionale: +1xxx...'
|
||||
submitter_cannot_be_updated: Il mittente non può essere aggiornato.
|
||||
at_least_one_field_must_be_filled: Almeno un campo deve essere compilato.
|
||||
archived_users: Utenti Archiviati
|
||||
embedding_users: Utenti Incorporati
|
||||
view_embedding_users: Visualizza Utenti Incorporati
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
|
||||
@@ -2043,6 +2053,7 @@ fr: &fr
|
||||
require_signing_reason: Demander une raison pour la signature
|
||||
allow_typed_text_signatures: Autoriser les signatures en texte tapé
|
||||
allow_to_resubmit_completed_forms: Autoriser la resoumission des formulaires complétés
|
||||
allow_to_decline_documents: Autoriser le refus des documents
|
||||
remember_and_pre_fill_signatures: Se souvenir et préremplir les signatures
|
||||
require_authentication_for_file_download_links: Exiger une authentification pour les liens de téléchargement des fichiers
|
||||
combine_completed_documents_and_audit_log: "Combiner les documents complétés et le journal d'audit"
|
||||
@@ -2382,7 +2393,6 @@ fr: &fr
|
||||
back: Retour
|
||||
add_secret: Ajouter un secret
|
||||
submission_example_payload: Exemple de payload de soumission
|
||||
status_users: '%{status} Utilisateurs'
|
||||
there_are_no_signatures: "Il n'y a pas de signatures"
|
||||
signed_with_trusted_certificate: Signé avec un certificat de confiance
|
||||
signed_with_external_certificate: Signé avec un certificat externe
|
||||
@@ -2527,6 +2537,9 @@ fr: &fr
|
||||
use_international_format_1xxx_: 'Utilizza il formato internazionale: +1xxx...'
|
||||
submitter_cannot_be_updated: Il mittente non può essere aggiornato.
|
||||
at_least_one_field_must_be_filled: Almeno un campo deve essere compilato.
|
||||
archived_users: Utilisateurs Archivés
|
||||
embedding_users: Utilisateurs Intégrés
|
||||
view_embedding_users: Voir les Utilisateurs Intégrés
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
|
||||
@@ -2679,6 +2692,7 @@ pt: &pt
|
||||
require_signing_reason: Requerer motivo para assinatura
|
||||
allow_typed_text_signatures: Permitir assinaturas digitadas
|
||||
allow_to_resubmit_completed_forms: Permitir reenviar formulários concluídos
|
||||
allow_to_decline_documents: Permitir recusar documentos
|
||||
remember_and_pre_fill_signatures: Lembrar e preencher assinaturas automaticamente
|
||||
require_authentication_for_file_download_links: Requerer autenticação para links de download de arquivos
|
||||
combine_completed_documents_and_audit_log: Combinar documentos concluídos e log de auditoria
|
||||
@@ -3018,7 +3032,6 @@ pt: &pt
|
||||
back: Voltar
|
||||
add_secret: Adicionar segredo
|
||||
submission_example_payload: Exemplo de payload de submissão
|
||||
status_users: '%{status} Usuários'
|
||||
there_are_no_signatures: Não há assinaturas
|
||||
signed_with_trusted_certificate: Assinado com certificado confiável
|
||||
signed_with_external_certificate: Assinado com certificado externo
|
||||
@@ -3163,6 +3176,9 @@ pt: &pt
|
||||
use_international_format_1xxx_: 'Use o formato internacional: +1xxx...'
|
||||
submitter_cannot_be_updated: O remetente não pode ser atualizado.
|
||||
at_least_one_field_must_be_filled: Pelo menos um campo deve ser preenchido.
|
||||
archived_users: Usuários Arquivados
|
||||
embedding_users: Usuários Incorporados
|
||||
view_embedding_users: Ver Usuários Incorporados
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
|
||||
@@ -3315,6 +3331,7 @@ de: &de
|
||||
require_signing_reason: Grund für die Signatur erforderlich
|
||||
allow_typed_text_signatures: Unterschriften mit getipptem Text zulassen
|
||||
allow_to_resubmit_completed_forms: Erneutes Einreichen abgeschlossener Formulare zulassen
|
||||
allow_to_decline_documents: Erlauben, Dokumente abzulehnen
|
||||
remember_and_pre_fill_signatures: Signaturen merken und vorab ausfüllen
|
||||
require_authentication_for_file_download_links: Authentifizierung für Dateidownload-Links erforderlich
|
||||
combine_completed_documents_and_audit_log: Abgeschlossene Dokumente und Prüfprotokoll kombinieren
|
||||
@@ -3654,7 +3671,6 @@ de: &de
|
||||
back: Zurück
|
||||
add_secret: Geheimnis hinzufügen
|
||||
submission_example_payload: Beispiel-Payload für Einreichung
|
||||
status_users: '%{status} Benutzer'
|
||||
there_are_no_signatures: Es gibt keine Unterschriften
|
||||
signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
|
||||
signed_with_external_certificate: Signiert mit externem Zertifikat
|
||||
@@ -3799,6 +3815,9 @@ de: &de
|
||||
use_international_format_1xxx_: 'Verwenden Sie das internationale Format: +1xxx...'
|
||||
submitter_cannot_be_updated: Der Absender kann nicht aktualisiert werden.
|
||||
at_least_one_field_must_be_filled: Mindestens ein Feld muss ausgefüllt werden.
|
||||
archived_users: Archivierte Benutzer
|
||||
embedding_users: Einbettende Benutzer
|
||||
view_embedding_users: Einbettende Benutzer anzeigen
|
||||
submission_event_names:
|
||||
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
|
||||
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
|
||||
|
||||
+3
-1
@@ -153,15 +153,17 @@ 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'
|
||||
resources :users, only: %i[index]
|
||||
resources :archived_users, only: %i[index], path: 'users/:status', controller: 'users',
|
||||
defaults: { status: :archived }
|
||||
resources :integration_users, only: %i[index], path: 'users/:status', controller: 'users',
|
||||
defaults: { status: :integration }
|
||||
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
||||
resources :api, only: %i[index create], controller: 'api_settings'
|
||||
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
|
||||
|
||||
@@ -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, index: true
|
||||
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
|
||||
@@ -0,0 +1,79 @@
|
||||
# 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_sms_events, -> { where(event_type: %w[send_sms send_2fa_sms]) },
|
||||
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
|
||||
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?
|
||||
|
||||
submission = submitter.submission
|
||||
|
||||
completed_submitter.assign_attributes(
|
||||
submission_id: submitter.submission_id,
|
||||
account_id: submission.account_id,
|
||||
template_id: submission.template_id,
|
||||
source: submission.source,
|
||||
sms_count: submitter.submission_sms_events.size,
|
||||
completed_at: submitter.completed_at,
|
||||
created_at: submitter.completed_at,
|
||||
updated_at: submitter.completed_at
|
||||
)
|
||||
|
||||
completed_submitter.save!
|
||||
end
|
||||
|
||||
attachments = ActiveStorage::Attachment.where(record_type: 'Submitter', name: 'documents').preload(:blob)
|
||||
|
||||
attachments.find_each do |attachment|
|
||||
sha256 = attachment.metadata['sha256']
|
||||
|
||||
next if sha256.blank?
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def down
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddUniqueIndexOnCompletedSubmitters < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
remove_index :completed_submitters, :submitter_id
|
||||
add_index :completed_submitters, :submitter_id, unique: true
|
||||
end
|
||||
end
|
||||
+24
-1
@@ -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_26_161207) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
@@ -89,6 +89,29 @@ 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"
|
||||
t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true
|
||||
end
|
||||
|
||||
create_table "document_generation_events", force: :cascade do |t|
|
||||
t.bigint "submitter_id", null: false
|
||||
t.string "event_name", null: false
|
||||
|
||||
@@ -18,25 +18,25 @@ module ActionMailerConfigsInterceptor
|
||||
if Rails.env.production? && Rails.application.config.action_mailer.delivery_method
|
||||
from = ENV.fetch('SMTP_FROM').to_s.split(',').sample
|
||||
|
||||
message.from = from
|
||||
|
||||
if from == 'DocuSeal <info@docuseal.com>'
|
||||
message.body.instance_variable_set(
|
||||
:@raw_source, message.body.raw_source.gsub('https://docuseal.co/', 'https://docuseal.com/')
|
||||
)
|
||||
if from.match?(User::FULL_EMAIL_REGEXP)
|
||||
message[:from] = message[:from].to_s.sub(User::EMAIL_REGEXP, from)
|
||||
else
|
||||
message.from = from
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
+3
-3
@@ -20,19 +20,19 @@ module Docuseal
|
||||
elsif ENV['MULTITENANT'] == 'true'
|
||||
"https://console.#{HOST}"
|
||||
else
|
||||
'https://console.docuseal.co'
|
||||
'https://console.docuseal.com'
|
||||
end
|
||||
CLOUD_URL = if Rails.env.development?
|
||||
'http://localhost:3000'
|
||||
else
|
||||
'https://docuseal.co'
|
||||
'https://docuseal.com'
|
||||
end
|
||||
CDN_URL = if Rails.env.development?
|
||||
'http://localhost:3000'
|
||||
elsif ENV['MULTITENANT'] == 'true'
|
||||
"https://cdn.#{HOST}"
|
||||
else
|
||||
'https://cdn.docuseal.co'
|
||||
'https://cdn.docuseal.com'
|
||||
end
|
||||
|
||||
CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module Params
|
||||
class BaseValidator
|
||||
EMAIL_REGEXP = /\A[a-z0-9][\.']?(?:(?:[a-z0-9_-]+[\.\+'])*[a-z0-9_-]+)*@(?:[a-z0-9]+[\.-])*[a-z0-9]+\.[a-z]{2,}\z/i
|
||||
EMAIL_REGEXP = User::FULL_EMAIL_REGEXP
|
||||
|
||||
InvalidParameterError = Class.new(StandardError)
|
||||
|
||||
@@ -70,13 +70,14 @@ 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) }
|
||||
return if params[key].to_s.include?('<')
|
||||
|
||||
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")
|
||||
if params[key].to_s.strip.split(/\s*[;,]\s*/).compact_blank
|
||||
.all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) }
|
||||
return
|
||||
end
|
||||
|
||||
raise_error(message || "#{key} must follow the email format: '#{params[key]}'")
|
||||
end
|
||||
|
||||
def unique_value(params, key, message: nil)
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -393,7 +393,7 @@ module Submissions
|
||||
end
|
||||
|
||||
def sign_reason
|
||||
'Signed with DocuSeal.co'
|
||||
'Signed with DocuSeal.com'
|
||||
end
|
||||
|
||||
def maybe_add_background(_canvas, _submission, _page_size); end
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ module Submitters
|
||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID,
|
||||
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||
AccountConfig::REUSE_SIGNATURE_KEY,
|
||||
AccountConfig::ALLOW_TYPED_SIGNATURE].freeze
|
||||
@@ -22,6 +23,7 @@ module Submitters
|
||||
with_confetti = find_safe_value(configs, AccountConfig::FORM_WITH_CONFETTI_KEY) != false
|
||||
prefill_signature = find_safe_value(configs, AccountConfig::FORM_PREFILL_SIGNATURE_KEY) != false
|
||||
reuse_signature = find_safe_value(configs, AccountConfig::REUSE_SIGNATURE_KEY) != false
|
||||
with_decline = find_safe_value(configs, AccountConfig::ALLOW_TO_DECLINE_KEY) != false
|
||||
with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true
|
||||
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true
|
||||
|
||||
@@ -30,6 +32,7 @@ module Submitters
|
||||
with_typed_signature:,
|
||||
with_confetti:,
|
||||
reuse_signature:,
|
||||
with_decline:,
|
||||
completed_message:,
|
||||
require_signing_reason:,
|
||||
prefill_signature:,
|
||||
|
||||
@@ -25,6 +25,8 @@ module Templates
|
||||
media_box_start = [media_box[0], media_box[1]]
|
||||
crop_shift = [crop_box[0] - media_box[0], crop_box[1] - media_box[1]]
|
||||
|
||||
next unless child_field[:Rect]
|
||||
|
||||
x0, y0, x1, y1 = child_field[:Rect]
|
||||
|
||||
x0, y0 = correct_coordinates(x0, y0, crop_shift, media_box_start)
|
||||
@@ -202,6 +204,7 @@ module Templates
|
||||
field = annot[:Parent]
|
||||
field = field[:Parent] while field[:Parent]
|
||||
|
||||
annots_index[field.hash] ||= page
|
||||
fields_index[field.hash] ||= HexaPDF::Type::AcroForm::Field.wrap(pdf, field)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :completed_document do
|
||||
submitter
|
||||
sha256 { SecureRandom.hex(32) }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
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) }
|
||||
|
||||
before 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.new.perform('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.new.perform('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.new.perform('submitter_id' => 'invalid_id')
|
||||
end.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -10,6 +10,7 @@ RSpec.describe Params::BaseValidator do
|
||||
emails = [
|
||||
' john.doe@example.com ',
|
||||
'john.doe@example.com',
|
||||
'Test <john.doe@example.com>',
|
||||
'jane+newsletter@domain.org',
|
||||
'mike_smith@company.net',
|
||||
'lisa-wong@sub.example.co.uk',
|
||||
@@ -34,50 +35,21 @@ 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|
|
||||
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
|
||||
|
||||
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',
|
||||
@@ -106,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',
|
||||
@@ -126,7 +96,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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -48,7 +79,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 +94,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 +119,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 +134,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 +146,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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') }
|
||||
|
||||
before 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
|
||||
@@ -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
|
||||
|
||||
@@ -3,9 +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(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
let(:template) { create(:template, account:, author: user) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
@@ -48,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
|
||||
|
||||
@@ -18,13 +18,19 @@ RSpec.describe 'Team Settings' do
|
||||
visit settings_users_path
|
||||
end
|
||||
|
||||
it 'shows all users' do
|
||||
it 'shows only active users' do
|
||||
within '.table' do
|
||||
users.each do |user|
|
||||
expect(page).to have_content(user.full_name)
|
||||
expect(page).to have_content(user.email)
|
||||
expect(page).to have_no_content(other_user.email)
|
||||
expect(page).to have_link('Edit', href: edit_user_path(user))
|
||||
end
|
||||
|
||||
expect(page).to have_button('Remove')
|
||||
expect(page).to have_no_button('Unarchive')
|
||||
|
||||
expect(page).to have_no_content(other_user.full_name)
|
||||
expect(page).to have_no_content(other_user.email)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -89,4 +95,58 @@ RSpec.describe 'Team Settings' do
|
||||
expect(page).to have_no_content('User has been removed')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when some users are archived' do
|
||||
let!(:users) { create_list(:user, 2, account:) }
|
||||
let!(:archived_users) { create_list(:user, 2, account:, archived_at: Time.current) }
|
||||
let!(:other_user) { create(:user) }
|
||||
|
||||
it 'shows only active users' do
|
||||
visit settings_users_path
|
||||
|
||||
within '.table' do
|
||||
users.each do |user|
|
||||
expect(page).to have_content(user.full_name)
|
||||
expect(page).to have_content(user.email)
|
||||
end
|
||||
|
||||
archived_users.each do |user|
|
||||
expect(page).to have_no_content(user.full_name)
|
||||
expect(page).to have_no_content(user.email)
|
||||
end
|
||||
|
||||
expect(page).to have_no_content(other_user.full_name)
|
||||
expect(page).to have_no_content(other_user.email)
|
||||
end
|
||||
|
||||
expect(page).to have_link('View Archived', href: settings_archived_users_path)
|
||||
end
|
||||
|
||||
it 'shows only archived users' do
|
||||
visit settings_archived_users_path
|
||||
|
||||
within '.table' do
|
||||
archived_users.each do |user|
|
||||
expect(page).to have_content(user.full_name)
|
||||
expect(page).to have_content(user.email)
|
||||
expect(page).to have_no_link('Edit', href: edit_user_path(user))
|
||||
end
|
||||
|
||||
users.each do |user|
|
||||
expect(page).to have_no_content(user.full_name)
|
||||
expect(page).to have_no_content(user.email)
|
||||
expect(page).to have_no_link('Edit', href: edit_user_path(user))
|
||||
end
|
||||
|
||||
expect(page).to have_button('Unarchive')
|
||||
expect(page).to have_no_button('Remove')
|
||||
|
||||
expect(page).to have_no_content(other_user.full_name)
|
||||
expect(page).to have_no_content(other_user.email)
|
||||
end
|
||||
|
||||
expect(page).to have_content('Archived Users')
|
||||
expect(page).to have_link('View Active', href: settings_users_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user