](https://repocloud.io/details/?app_id=252) | |
-
#### Docker
```sh
@@ -101,3 +102,9 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
Unless otherwise noted, all files © 2023 DocuSeal LLC.
+
+## Tools
+
+- [Signature Maker](https://www.docuseal.co/online-signature)
+- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
+- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
diff --git a/app/controllers/account_configs_controller.rb b/app/controllers/account_configs_controller.rb
index 5d33a547..3bc36334 100644
--- a/app/controllers/account_configs_controller.rb
+++ b/app/controllers/account_configs_controller.rb
@@ -8,9 +8,12 @@ class AccountConfigsController < ApplicationController
AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT,
- AccountConfig::ESIGNING_PREFERENCE_KEY
+ AccountConfig::ESIGNING_PREFERENCE_KEY,
+ AccountConfig::FORM_WITH_CONFETTI_KEY
].freeze
+ InvalidKey = Class.new(StandardError)
+
def create
@account_config.update!(account_config_params)
@@ -20,7 +23,7 @@ class AccountConfigsController < ApplicationController
private
def load_account_config
- return head :not_found unless ALLOWED_KEYS.include?(account_config_params[:key])
+ raise InvalidKey unless ALLOWED_KEYS.include?(account_config_params[:key])
@account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
diff --git a/app/controllers/api/active_storage_blobs_proxy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_controller.rb
index 4f2968cc..0a9c68c1 100644
--- a/app/controllers/api/active_storage_blobs_proxy_controller.rb
+++ b/app/controllers/api/active_storage_blobs_proxy_controller.rb
@@ -10,9 +10,9 @@ module Api
before_action :set_cors_headers
def show
- blob_uuid, = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
+ blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
- if blob_uuid.blank?
+ if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
Rollbar.error('Blob not found') if defined?(Rollbar)
return head :not_found
diff --git a/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb
index fca401f1..54c3ed72 100644
--- a/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb
+++ b/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb
@@ -11,7 +11,9 @@ module Api
def show
Rollbar.info('Blob legacy') if defined?(Rollbar)
- blob = ActiveStorage::Blob.find_signed!(params[:signed_blob_id] || params[:signed_id])
+ blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
+
+ return head :not_found unless blob
is_permitted = blob.attachments.any? do |a|
(current_user && a.record.account.id == current_user.account_id) ||
diff --git a/app/controllers/api/api_base_controller.rb b/app/controllers/api/api_base_controller.rb
index b5375445..86ead819 100644
--- a/app/controllers/api/api_base_controller.rb
+++ b/app/controllers/api/api_base_controller.rb
@@ -19,6 +19,12 @@ module Api
render json: { error: e.message }, status: :unprocessable_entity
end
+ rescue_from RateLimit::LimitApproached do |e|
+ Rollbar.error(e) if defined?(Rollbar)
+
+ render json: { error: 'Too many requests' }, status: :too_many_requests
+ end
+
if Rails.env.production?
rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar)
@@ -39,8 +45,8 @@ module Api
result = relation.order(id: :desc)
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
- result = result.where(relation.arel_table[:id].lt(params[:after])) if params[:after].present?
- result = result.where(relation.arel_table[:id].gt(params[:before])) if params[:before].present?
+ result = result.where(id: ...params[:after].to_i) if params[:after].present?
+ result = result.where(id: (params[:before].to_i + 1)...) if params[:before].present?
result
end
diff --git a/app/controllers/api/submitters_autocomplete_controller.rb b/app/controllers/api/submitters_autocomplete_controller.rb
index ff7dc2e6..e28e25c7 100644
--- a/app/controllers/api/submitters_autocomplete_controller.rb
+++ b/app/controllers/api/submitters_autocomplete_controller.rb
@@ -24,9 +24,9 @@ module Api
if SELECT_COLUMNS.include?(params[:field])
column = Submitter.arel_table[params[:field].to_sym]
- term = "%#{params[:q].downcase}%"
+ term = "#{params[:q].downcase}%"
- submitters.where(column.lower.matches(term))
+ submitters.where(column.matches(term, false, true))
else
Submitters.search(submitters, params[:q])
end
diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb
index f69f43f3..3d2c3567 100644
--- a/app/controllers/api/submitters_controller.rb
+++ b/app/controllers/api/submitters_controller.rb
@@ -10,6 +10,7 @@ module Api
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
+ submitters = maybe_filder_by_completed_at(submitters, params)
submitters = paginate(
submitters.preload(:template, :submission, :submission_events,
@@ -81,6 +82,18 @@ module Api
private
+ def maybe_filder_by_completed_at(submitters, params)
+ if params[:completed_after].present?
+ submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
+ end
+
+ if params[:completed_before].present?
+ submitters = submitters.where(completed_at: ..Time.zone.parse(params[:completed_before]))
+ end
+
+ submitters
+ end
+
def assign_submitter_attrs(submitter, attrs)
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
diff --git a/app/controllers/api/templates_clone_controller.rb b/app/controllers/api/templates_clone_controller.rb
index ebba87bb..a07fa310 100644
--- a/app/controllers/api/templates_clone_controller.rb
+++ b/app/controllers/api/templates_clone_controller.rb
@@ -20,16 +20,7 @@ module Api
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
- render json: cloned_template.as_json(serialize_params)
- end
-
- private
-
- def serialize_params
- {
- include: { author: { only: %i[id email first_name last_name] },
- documents: { only: %i[id uuid], methods: %i[url filename] } }
- }
+ render json: Templates::SerializeForApi.call(cloned_template)
end
end
end
diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb
index 61d45a8b..8e35e1ae 100644
--- a/app/controllers/api/templates_controller.rb
+++ b/app/controllers/api/templates_controller.rb
@@ -7,10 +7,31 @@ module Api
def index
templates = filter_templates(@templates, params)
- templates = paginate(templates.preload(:author, documents_attachments: :blob))
+ templates = paginate(templates.preload(:author, :folder))
+
+ schema_documents =
+ ActiveStorage::Attachment.where(record_id: templates.map(&:id),
+ record_type: 'Template',
+ name: :documents,
+ uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
+ .preload(:blob)
+
+ preview_image_attachments =
+ ActiveStorage::Attachment.joins(:blob)
+ .where(blob: { filename: '0.jpg' })
+ .where(record_id: schema_documents.map(&:id),
+ record_type: 'ActiveStorage::Attachment',
+ name: :preview_images)
+ .preload(:blob)
render json: {
- data: templates.as_json(serialize_params),
+ data: templates.map do |t|
+ Templates::SerializeForApi.call(
+ t,
+ schema_documents.select { |e| e.record_id == t.id },
+ preview_image_attachments
+ )
+ end,
pagination: {
count: templates.size,
next: templates.last&.id,
@@ -20,7 +41,7 @@ module Api
end
def show
- render json: @template.as_json(serialize_params)
+ render json: Templates::SerializeForApi.call(@template)
end
def update
@@ -61,22 +82,18 @@ module Api
templates
end
- def serialize_params
- {
- methods: %i[application_key],
- include: { author: { only: %i[id email first_name last_name] },
- documents: { only: %i[id uuid], methods: %i[url preview_image_url filename] } }
- }
- end
-
def template_params
permit_params = [
:name,
{ schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
- fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
+ fields: [[:uuid, :submitter_uuid, :name, :type,
+ :required, :readonly, :default_value,
+ :title, :description,
{ preferences: {},
+ conditions: [%i[field_uuid value action]],
options: [%i[value uuid]],
+ validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
]
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 19547214..0b3dd112 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -22,6 +22,12 @@ class ApplicationController < ActionController::Base
redirect_to request.path
end
+ rescue_from RateLimit::LimitApproached do |e|
+ Rollbar.error(e) if defined?(Rollbar)
+
+ redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
+ end
+
if Rails.env.production?
rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar)
diff --git a/app/controllers/console_redirect_controller.rb b/app/controllers/console_redirect_controller.rb
index f6042225..84fc0c89 100644
--- a/app/controllers/console_redirect_controller.rb
+++ b/app/controllers/console_redirect_controller.rb
@@ -5,6 +5,10 @@ class ConsoleRedirectController < ApplicationController
skip_authorization_check
def index
+ if request.path == '/upgrade'
+ params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise"
+ end
+
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
auth = JsonWebToken.encode(uuid: true_user.uuid,
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 0f5ab590..4047dda3 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -54,10 +54,14 @@ class DashboardController < ApplicationController
rel = templates.active.preload(:author).order(id: :desc)
if params[:q].blank?
- shared_template_ids =
- TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
-
- rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
+ if Docuseal.multitenant? && !current_account.testing?
+ rel = rel.where(folder_id: current_account.default_template_folder.id)
+ else
+ shared_template_ids =
+ TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
+
+ rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
+ end
end
Templates.search(rel, params[:q])
diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb
index 8d422205..b3619b70 100644
--- a/app/controllers/errors_controller.rb
+++ b/app/controllers/errors_controller.rb
@@ -22,6 +22,8 @@ class ErrorsController < ActionController::Base
respond_to do |f|
f.json do
+ set_cors_headers
+
render json: { status: error_status_code }, status: error_status_code
end
@@ -31,6 +33,14 @@ class ErrorsController < ActionController::Base
private
+ def set_cors_headers
+ headers['Access-Control-Allow-Origin'] = '*'
+ headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
+ headers['Access-Control-Allow-Headers'] = '*'
+ headers['Access-Control-Max-Age'] = '1728000'
+ headers['Access-Control-Allow-Credentials'] = true
+ end
+
def error_status_code
@error_status_code ||=
ActionDispatch::ExceptionWrapper.new(request.env,
diff --git a/app/controllers/personalization_settings_controller.rb b/app/controllers/personalization_settings_controller.rb
index 58a641de..76716fbd 100644
--- a/app/controllers/personalization_settings_controller.rb
+++ b/app/controllers/personalization_settings_controller.rb
@@ -1,26 +1,51 @@
# frozen_string_literal: true
class PersonalizationSettingsController < ApplicationController
+ ALLOWED_KEYS = [
+ AccountConfig::FORM_COMPLETED_BUTTON_KEY,
+ AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
+ AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
+ AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY
+ ].freeze
+
+ InvalidKey = Class.new(StandardError)
+
+ before_action :load_and_authorize_account_config, only: :create
+
def show
authorize!(:read, AccountConfig)
end
def create
- account_config =
- current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
-
- authorize!(:create, account_config)
-
- account_config.update!(account_config_params)
+ if @account_config.value != false && @account_config.value.blank?
+ @account_config.destroy!
+ else
+ @account_config.save!
+ end
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
end
private
+ def load_and_authorize_account_config
+ @account_config =
+ current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
+
+ @account_config.assign_attributes(account_config_params)
+
+ authorize!(:create, @account_config)
+
+ raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
+
+ @account_config
+ end
+
def account_config_params
attrs = params.require(:account_config).permit!
+ return attrs if attrs[:value].is_a?(String)
+
attrs[:value]&.transform_values! do |value|
if value.in?(%w[true false])
value == 'true'
diff --git a/app/controllers/preview_document_page_controller.rb b/app/controllers/preview_document_page_controller.rb
index cfee948a..271d6169 100644
--- a/app/controllers/preview_document_page_controller.rb
+++ b/app/controllers/preview_document_page_controller.rb
@@ -8,7 +8,7 @@ class PreviewDocumentPageController < ActionController::API
def show
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
- attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid, name: :preview_images) if attachment_uuid
+ attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid
return head :not_found unless attachment
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
deleted file mode 100644
index d0b4a403..00000000
--- a/app/controllers/registrations_controller.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-class RegistrationsController < Devise::RegistrationsController
- prepend_before_action :require_no_authentication, only: [:show]
- prepend_before_action :maybe_redirect_if_signed_in, only: [:show]
-
- def show; end
-
- def create
- super
-
- Accounts.create_default_template(resource.account) if resource.account.persisted?
- end
-
- private
-
- def after_sign_up_path_for(...)
- if params[:redir].present?
- return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
-
- return params[:redir]
- end
-
- super
- end
-
- def maybe_redirect_if_signed_in
- return unless signed_in?
- return if params[:redir].blank?
-
- redirect_to after_sign_up_path_for(current_user), allow_other_host: true
- end
-
- def set_flash_message(key, kind, options = {})
- return if key == :alert && kind == 'already_authenticated'
-
- super
- end
-
- def build_resource(_hash = {})
- account = Account.new(account_params)
- account.timezone = Accounts.normalize_timezone(account.timezone)
-
- self.resource = account.users.new(user_params)
-
- account.name ||= resource.full_name if params[:action] == 'create'
- end
-
- def user_params
- return {} if params[:user].blank?
-
- params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs|
- attrs[:password] ||= SecureRandom.hex if params[:action] == 'create'
- end
- end
-
- def account_params
- return {} if params[:account].blank?
-
- params.require(:account).permit(:name, :timezone).compact_blank
- end
-end
diff --git a/app/controllers/send_submission_email_controller.rb b/app/controllers/send_submission_email_controller.rb
index 28e2a364..8e74c348 100644
--- a/app/controllers/send_submission_email_controller.rb
+++ b/app/controllers/send_submission_email_controller.rb
@@ -12,13 +12,18 @@ class SendSubmissionEmailController < ApplicationController
def create
@submitter =
if params[:template_slug]
- Submitter.joins(submission: :template).find_by!(email: params[:email],
+ Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
template: { slug: params[:template_slug] })
+ elsif params[:submission_slug]
+ Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase,
+ submission: { slug: params[:submission_slug] })
else
Submitter.find_by!(slug: params[:submitter_slug])
end
- SubmitterMailer.documents_copy_email(@submitter).deliver_later!
+ RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
+
+ SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
respond_to do |f|
f.html { redirect_to success_send_submission_email_index_path }
diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb
index fcc7b90b..69f3aa49 100644
--- a/app/controllers/submissions_controller.rb
+++ b/app/controllers/submissions_controller.rb
@@ -6,23 +6,8 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy]
- PRELOAD_ALL_PAGES_AMOUNT = 200
-
def show
- ActiveRecord::Associations::Preloader.new(
- records: [@submission],
- associations: [:template, { template_schema_documents: :blob }]
- ).call
-
- total_pages =
- @submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
-
- if total_pages < PRELOAD_ALL_PAGES_AMOUNT
- ActiveRecord::Associations::Preloader.new(
- records: @submission.template_schema_documents,
- associations: [:blob, { preview_images_attachments: :blob }]
- ).call
- end
+ @submission = Submissions.preload_with_pages(@submission)
render :show, layout: 'plain'
end
diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb
index e9ce7adc..334204f8 100644
--- a/app/controllers/submissions_download_controller.rb
+++ b/app/controllers/submissions_download_controller.rb
@@ -4,10 +4,20 @@ class SubmissionsDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
- TTL = 20.minutes
+ TTL = 40.minutes
+ FILES_TTL = 5.minutes
def index
- submitter = Submitter.find_by!(slug: params[:submitter_slug])
+ submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
+
+ signature_valid =
+ if submitter&.slug == params[:submitter_slug]
+ true
+ else
+ submitter = nil
+ end
+
+ submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
Submissions::EnsureResultGenerated.call(submitter)
@@ -17,18 +27,24 @@ class SubmissionsDownloadController < ApplicationController
return head :not_found unless last_submitter.completed_at?
- if last_submitter.completed_at < TTL.ago &&
- (current_user.nil? || !current_user.account.submitters.exists?(id: last_submitter.id))
+ if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
- urls =
- Submitters.select_attachments_for_download(last_submitter).map do |attachment|
- ActiveStorage::Blob.proxy_url(attachment.blob)
- end
+ render json: build_urls(last_submitter)
+ end
- render json: urls
+ private
+
+ def current_user_submitter?(submitter)
+ current_user && current_user.account.submitters.exists?(id: submitter.id)
+ end
+
+ def build_urls(submitter)
+ Submitters.select_attachments_for_download(submitter).map do |attachment|
+ ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.minutes.from_now.to_i)
+ end
end
end
diff --git a/app/controllers/submissions_preview_controller.rb b/app/controllers/submissions_preview_controller.rb
index 12db79cf..dbf29294 100644
--- a/app/controllers/submissions_preview_controller.rb
+++ b/app/controllers/submissions_preview_controller.rb
@@ -4,37 +4,31 @@ class SubmissionsPreviewController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
- PRELOAD_ALL_PAGES_AMOUNT = 200
-
- TTL = 20.minutes
+ TTL = 40.minutes
def show
- @submission = Submission.find_by!(slug: params[:slug])
+ submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
+
+ signature_valid =
+ if submitter && submitter.submission.slug == params[:slug]
+ @submission = submitter.submission
+
+ true
+ end
+
+ @submission ||= Submission.find_by!(slug: params[:slug])
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
raise ActionController::RoutingError, 'Not Found'
end
- unless submission_valid_ttl?(@submission)
+ if !submission_valid_ttl?(@submission) && !signature_valid
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
return redirect_to submissions_preview_completed_path(@submission.slug)
end
- ActiveRecord::Associations::Preloader.new(
- records: [@submission],
- associations: [:template, { template_schema_documents: :blob }]
- ).call
-
- total_pages =
- @submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
-
- if total_pages < PRELOAD_ALL_PAGES_AMOUNT
- ActiveRecord::Associations::Preloader.new(
- records: @submission.template_schema_documents,
- associations: [:blob, { preview_images_attachments: :blob }]
- ).call
- end
+ @submission = Submissions.preload_with_pages(@submission)
render 'submissions/show', layout: 'plain'
end
@@ -42,7 +36,7 @@ class SubmissionsPreviewController < ApplicationController
def completed
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
- render :completed, layout: 'plain'
+ render :completed, layout: 'form'
end
private
diff --git a/app/controllers/user_configs_controller.rb b/app/controllers/user_configs_controller.rb
new file mode 100644
index 00000000..834cc1d9
--- /dev/null
+++ b/app/controllers/user_configs_controller.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class UserConfigsController < ApplicationController
+ before_action :load_user_config
+ authorize_resource :user_config
+
+ ALLOWED_KEYS = [
+ UserConfig::RECEIVE_COMPLETED_EMAIL
+ ].freeze
+
+ InvalidKey = Class.new(StandardError)
+
+ def create
+ @user_config.update!(user_config_params)
+
+ head :ok
+ end
+
+ private
+
+ def load_user_config
+ raise InvalidKey unless ALLOWED_KEYS.include?(user_config_params[:key])
+
+ @user_config =
+ UserConfig.find_or_initialize_by(user: current_user, key: user_config_params[:key])
+ end
+
+ def user_config_params
+ params.required(:user_config).permit!.tap do |attrs|
+ attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
+ end
+ end
+end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index dc2df889..0a9629fa 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -20,6 +20,7 @@ import FolderAutocomplete from './elements/folder_autocomplete'
import SignatureForm from './elements/signature_form'
import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password'
+import EmailsTextarea from './elements/emails_textarea'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -53,6 +54,7 @@ window.customElements.define('folder-autocomplete', FolderAutocomplete)
window.customElements.define('signature-form', SignatureForm)
window.customElements.define('submit-form', SubmitForm)
window.customElements.define('prompt-password', PromptPassword)
+window.customElements.define('emails-textarea', EmailsTextarea)
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
document.addEventListener('turbo:submit-end', async (event) => {
@@ -90,6 +92,8 @@ window.customElements.define('template-builder', class extends HTMLElement {
withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false',
withPayment: this.dataset.withPayment === 'true',
+ withFormula: this.dataset.withFormula === 'true',
+ withConditions: this.dataset.withConditions === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
isDirectUpload: this.dataset.isDirectUpload === 'true'
diff --git a/app/javascript/application.scss b/app/javascript/application.scss
index 9a220258..f0d6429a 100644
--- a/app/javascript/application.scss
+++ b/app/javascript/application.scss
@@ -75,16 +75,16 @@ button[disabled] .enabled {
@apply select base-input w-full font-normal;
}
-.tooltip-bottom-start:before {
- transform: translateX(-30%);
+.tooltip-bottom-end:before {
+ transform: translateX(-95%);
top: var(--tooltip-offset);
left: 100%;
right: auto;
bottom: auto;
}
-.tooltip-bottom-start:after {
- transform: translateX(-75%);
+.tooltip-bottom-end:after {
+ transform: translateX(-25%);
border-color: transparent transparent var(--tooltip-color) transparent;
top: var(--tooltip-tail-offset);
left: 50%;
@@ -125,3 +125,7 @@ button[disabled] .enabled {
outline-offset: 3px;
outline-color: hsl(var(--bc) / 0.2);
}
+
+select:required:invalid {
+ color: gray !important;
+}
diff --git a/app/javascript/elements/emails_textarea.js b/app/javascript/elements/emails_textarea.js
new file mode 100644
index 00000000..dde8109f
--- /dev/null
+++ b/app/javascript/elements/emails_textarea.js
@@ -0,0 +1,56 @@
+const emailRegexp = /([^@;,<>\s]+@[^@;,<>\s]+)/g
+
+export default class extends HTMLElement {
+ connectedCallback () {
+ if (this.dataset.limit) {
+ this.textarea.addEventListener('input', () => {
+ const emails = this.textarea.value.match(emailRegexp) || []
+
+ this.updateCounter(emails.length)
+ })
+ }
+ }
+
+ updateCounter (count) {
+ let counter = document.getElementById('emails_counter')
+ let bulkMessage = document.getElementById('bulk_message')
+
+ if (count < 2) {
+ counter?.remove()
+
+ return
+ }
+
+ if ((count + 10) > this.dataset.limit) {
+ if (!counter) {
+ counter = document.createElement('span')
+
+ counter.id = 'emails_counter'
+ counter.classList.add('text-xs', 'right-0', 'absolute')
+ counter.style.bottom = '-15px'
+
+ this.textarea.parentNode.append(counter)
+ }
+
+ counter.innerText = `${count} / ${this.dataset.limit}`
+ }
+
+ if (this.dataset.bulkEnabled !== 'true') {
+ if (!bulkMessage) {
+ bulkMessage = document.createElement('span')
+
+ bulkMessage.id = 'bulk_message'
+ bulkMessage.classList.add('text-xs', 'left-0', 'absolute')
+ bulkMessage.style.bottom = '-15px'
+
+ this.textarea.parentNode.append(bulkMessage)
+ }
+
+ bulkMessage.innerHTML = 'Upgrade to bulk send multiple recipients'
+ }
+ }
+
+ get textarea () {
+ return this.querySelector('textarea')
+ }
+}
diff --git a/app/javascript/elements/submitter_autocomplete.js b/app/javascript/elements/submitter_autocomplete.js
index e5820d8e..d13f3014 100644
--- a/app/javascript/elements/submitter_autocomplete.js
+++ b/app/javascript/elements/submitter_autocomplete.js
@@ -7,6 +7,7 @@ export default class extends HTMLElement {
preventSubmit: 1,
minLength: 1,
showOnFocus: true,
+ debounceWaitMs: 200,
onSelect: this.onSelect,
render: this.render,
fetch: this.fetch
@@ -27,6 +28,8 @@ export default class extends HTMLElement {
if (textarea && item[field]) {
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
+
+ textarea.dispatchEvent(new Event('input', { bubbles: true }))
}
})
}
@@ -37,7 +40,9 @@ export default class extends HTMLElement {
if (q) {
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
- fetch('/api/submitters_autocomplete?' + queryParams).then(async (resp) => {
+ this.currentFetch ||= fetch('/api/submitters_autocomplete?' + queryParams)
+
+ this.currentFetch.then(async (resp) => {
const items = await resp.json()
if (q.length < 3) {
@@ -47,6 +52,8 @@ export default class extends HTMLElement {
}
}).catch(() => {
resolve([])
+ }).finally(() => {
+ this.currentFetch = null
})
} else {
resolve([])
diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue
index 54fbb618..219b49b6 100644
--- a/app/javascript/submission_form/area.vue
+++ b/app/javascript/submission_form/area.vue
@@ -157,6 +157,7 @@
ref="textContainer"
dir="auto"
class="flex items-center px-0.5 w-full"
+ :class="alignClasses[field.preferences?.align]"
>
{{ modelValue.join(', ') }}
@@ -167,13 +168,14 @@
{{ modelValue }}
diff --git a/app/javascript/submission_form/i18n.js b/app/javascript/submission_form/i18n.js
index c82e8764..593a9da8 100644
--- a/app/javascript/submission_form/i18n.js
+++ b/app/javascript/submission_form/i18n.js
@@ -3,6 +3,7 @@ const en = {
signature: 'Signature',
initials: 'Initials',
date: 'Date',
+ number: 'Number',
image: 'Image',
take_photo: 'Take photo',
file: 'File',
@@ -16,6 +17,7 @@ const en = {
payment: 'Payment',
phone: 'Phone',
submit_form: 'Submit Form',
+ sign_now: 'Sign Now',
type_here_: 'Type here...',
optional: 'optional',
option: 'Option',
@@ -68,6 +70,7 @@ const es = {
signature: 'Firma',
initials: 'Iniciales',
date: 'Fecha',
+ number: 'Número',
image: 'Imagen',
file: 'Archivo',
select: 'Seleccionar',
@@ -80,6 +83,7 @@ const es = {
phone: 'Teléfono',
take_photo: 'Tomar foto',
submit_form: 'Enviar Formulario',
+ sign_now: 'Firmar ahora',
type_here_: 'Escribe aquí...',
optional: 'opcional',
appears_on: 'Aparece en',
@@ -131,6 +135,7 @@ const it = {
signature: 'Firma',
initials: 'Iniziali',
date: 'Data',
+ number: 'Numero',
image: 'Immagine',
file: 'File',
select: 'Seleziona',
@@ -142,6 +147,7 @@ const it = {
payment: 'Pagamento',
phone: 'Telefono',
submit_form: 'Invia Modulo',
+ sign_now: 'Firma ora',
type_here_: 'Digita qui...',
optional: 'opzionale',
appears_on: 'Compare su',
@@ -194,6 +200,7 @@ const de = {
signature: 'Unterschrift',
initials: 'Initialen',
date: 'Datum',
+ number: 'Nummer',
image: 'Bild',
file: 'Datei',
select: 'Auswählen',
@@ -205,6 +212,7 @@ const de = {
payment: 'Zahlung',
phone: 'Telefon',
submit_form: 'Formular absenden',
+ sign_now: 'Jetzt unterschreiben',
type_here_: 'Hier eingeben...',
optional: 'optional',
appears_on: 'Erscheint auf',
@@ -257,6 +265,7 @@ const fr = {
signature: 'Signature',
initials: 'Initiales',
date: 'Date',
+ number: 'Numéro',
image: 'Image',
file: 'Fichier',
select: 'Choisir',
@@ -268,6 +277,7 @@ const fr = {
payment: 'Paiement',
phone: 'Téléphone',
submit_form: 'Envoyer le Formulaire',
+ sign_now: 'Signer maintenant',
type_here_: 'Tapez ici...',
optional: 'facultatif',
appears_on: 'Apparaît sur',
@@ -320,6 +330,7 @@ const pl = {
signature: 'Podpis',
initials: 'Inicjały',
date: 'Data',
+ number: 'Numer',
image: 'Obraz',
file: 'Plik',
select: 'Wybierz',
@@ -331,6 +342,7 @@ const pl = {
payment: 'Płatność',
phone: 'Telefon',
submit_form: 'Wyślij Formularz',
+ sign_now: 'Podpisz teraz',
type_here_: 'Wpisz tutaj...',
optional: 'opcjonalny',
appears_on: 'Pojawia się na',
@@ -383,6 +395,7 @@ const uk = {
signature: 'Підпис',
initials: 'Ініціали',
date: 'Дата',
+ number: 'Число',
image: 'Зображення',
file: 'Файл',
select: 'Вибрати',
@@ -394,6 +407,7 @@ const uk = {
payment: 'Платіж',
phone: 'Телефон',
submit_form: 'Надіслати Форму',
+ sign_now: 'Підписати зараз',
type_here_: 'Введіть тут',
optional: 'необов’язково',
appears_on: "З'являється на",
@@ -446,6 +460,7 @@ const cs = {
signature: 'Podpis',
initials: 'Iniciály',
date: 'Datum',
+ number: 'Číslo',
image: 'Obrázek',
file: 'Soubor',
select: 'Vybrat',
@@ -457,6 +472,7 @@ const cs = {
payment: 'Platba',
phone: 'Telefon',
submit_form: 'Odeslat formulář',
+ sign_now: 'Podepsat nyní',
type_here_: 'Zadejte zde',
optional: 'volitelné',
appears_on: 'Zobrazuje se na',
@@ -509,6 +525,7 @@ const pt = {
signature: 'Assinatura',
initials: 'Iniciais',
date: 'Data',
+ number: 'Número',
image: 'Imagem',
file: 'Arquivo',
select: 'Selecionar',
@@ -520,6 +537,7 @@ const pt = {
payment: 'Pagamento',
phone: 'Telefone',
submit_form: 'Enviar Formulário',
+ sign_now: 'Assinar agora',
type_here_: 'Digite aqui',
optional: 'opcional',
appears_on: 'Aparece em',
@@ -570,8 +588,9 @@ const he = {
minimize: 'לקטן',
text: 'טקסט',
signature: 'חתימה',
- initials: 'ראשי תיקיות',
+ initials: 'ראשי תיבות',
date: 'תאריך',
+ number: 'מספר',
image: 'תמונה',
file: 'קובץ',
select: 'בחר',
@@ -583,6 +602,7 @@ const he = {
payment: 'תשלום',
phone: 'טלפון',
submit_form: 'שלח טופס',
+ sign_now: 'חתום כעת',
type_here_: 'הקלד כאן',
optional: 'אופציונלי',
option: 'אפשרות',
@@ -636,6 +656,7 @@ const nl = {
signature: 'Handtekening',
initials: 'Initialen',
date: 'Datum',
+ number: 'Nummer',
image: 'Afbeelding',
take_photo: 'Maak een foto',
file: 'Bestand',
@@ -648,6 +669,7 @@ const nl = {
payment: 'Betaling',
phone: 'Telefoon',
submit_form: 'Formulier verzenden',
+ sign_now: 'Nu ondertekenen',
type_here_: 'Typ hier...',
optional: 'Optioneel',
option: 'Optie',
@@ -694,6 +716,72 @@ const nl = {
files: 'Bestanden'
}
-const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl }
+const ar = {
+ text: 'نص',
+ signature: 'توقيع',
+ initials: 'الاختصارات',
+ date: 'تاريخ',
+ number: 'رقم',
+ image: 'صورة',
+ take_photo: 'التقاط صورة',
+ file: 'ملف',
+ select: 'اختيار',
+ checkbox: 'خانة اختيار',
+ multiple: 'متعدد',
+ radio: 'راديو',
+ cells: 'خلايا',
+ stamp: 'ختم',
+ minimize: 'تصغير',
+ payment: 'الدفع',
+ phone: 'هاتف',
+ submit_form: 'إرسال النموذج',
+ sign_now: 'وقع الآن',
+ type_here_: 'اكتب هنا',
+ optional: 'اختياري',
+ option: 'خيار',
+ appears_on: 'يظهر على',
+ page: 'صفحة',
+ select_your_option: 'اختر خيارك',
+ complete_hightlighted_checkboxes_and_click: 'أكمل الخانات المميزة وانقر',
+ submit: 'إرسال',
+ next: 'التالي',
+ click_to_upload: 'انقر للتحميل',
+ or_drag_and_drop_files: 'أو اسحب وأسقط الملفات',
+ send_copy_via_email: 'إرسال نسخة عبر البريد الإلكتروني',
+ download: 'تحميل',
+ clear: 'مسح',
+ redraw: 'إعادة الرسم',
+ draw_initials: 'ارسم الاختصارات',
+ type_signature_here: 'اكتب التوقيع هنا',
+ type_initial_here: 'اكتب الاختصارات هنا',
+ form_has_been_completed: 'تم إكمال النموذج!',
+ create_a_free_account: 'إنشاء حساب مجاني',
+ signed_with: 'تم التوقيع بواسطة',
+ please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة',
+ open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
+ verified_phone_number: 'تحقق من رقم الهاتف',
+ use_international_format: 'استخدم الشكل الدولي: +1xxx',
+ six_digits_code: 'رمز مكون من 6 أرقام',
+ change_phone_number: 'تغيير رقم الهاتف',
+ sending: 'جارٍ الإرسال',
+ resend_code: 'إعادة إرسال الرمز',
+ verification_code_has_been_resent: 'تم إعادة إرسال رمز التحقق عبر الرسائل القصيرة',
+ please_fill_all_required_fields: 'الرجاء ملء جميع الحقول المطلوبة',
+ set_today: 'تعيين اليوم',
+ toggle_multiline_text: 'تبديل النصوص متعددة الأسطر',
+ draw_signature: 'ارسم التوقيع',
+ type_initial: 'اكتب الاختصارات',
+ draw: 'ارسم',
+ type: 'اكتب',
+ type_text: 'اكتب نصًا',
+ email_has_been_sent: 'تم إرسال البريد الإلكتروني',
+ processing: 'جارٍ المعالجة',
+ pay_with_strip: 'الدفع بواسطة Stripe',
+ reupload: 'إعادة التحميل',
+ upload: 'تحميل',
+ files: 'الملفات'
+}
+
+const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar }
export default i18n
diff --git a/app/javascript/submission_form/image_step.vue b/app/javascript/submission_form/image_step.vue
index faed6dac..63cefa1b 100644
--- a/app/javascript/submission_form/image_step.vue
+++ b/app/javascript/submission_form/image_step.vue
@@ -24,9 +24,17 @@
:name="`values[${field.uuid}]`"
>
-
Send automatic email reminders to your recipients.
- ">Learn More
+ " data-turbo="false">Learn More
Display your company name and logo when signing documents.
- ">Learn More
+ " data-turbo="false">Learn More
Company Logo
<%= render 'logo_form' %> -Submitter Form
+Submission Form
<%= render 'form_completed_button_form' %> + <%= render 'form_customization_settings' %>Change Password
- <%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> -Change Password
+ <%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> +Two-Factor Authentication
++ <%= svg_icon('circle_check', class: 'stroke-success inline flex-none w-5 h-5') %> + + 2FA has been configured. + +
+ 🔓 Remove 2FA + <% else %> ++ <%= svg_icon('info_circle', class: 'stroke-warning inline flex-none w-5 h-5') %> + + 2FA is not configured. + +
+ 🔒 Set up 2FA + <% end %>Two-Factor Authentication
-- <%= svg_icon('circle_check', class: 'stroke-success inline flex-none w-5 h-5') %> - - 2FA has been configured. - -
- 🔓 Remove 2FA - <% else %> -- <%= svg_icon('info_circle', class: 'stroke-warning inline flex-none w-5 h-5') %> - - 2FA is not configured. - -
- 🔒 Set up 2FA - <% end %> -
Unlock with DocuSeal Pro
- ">Learn More
+ " data-turbo="false">Learn More
Unlock with DocuSeal Pro
- ">Learn More
+ " data-turbo="false">Learn More
Hi there,
"<%= @submitter.submission.template.name %>" form has been completed by <%= @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.join(', ') %>.
diff --git a/app/views/submitter_mailer/documents_copy_email.html.erb b/app/views/submitter_mailer/documents_copy_email.html.erb index ac438e76..b0ede421 100644 --- a/app/views/submitter_mailer/documents_copy_email.html.erb +++ b/app/views/submitter_mailer/documents_copy_email.html.erb @@ -1,11 +1,11 @@ <% if @email_config %> - <%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %> + <%= render 'custom_content', content: @email_config.value['body'], submitter: @submitter, sig: @sig %> <% else %>Hi there,
Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.
Alternatively, you can review and download your copy using:
- <%= link_to @submitter.template.name, submissions_preview_url(@submitter.submission.slug) %> + <%= link_to @submitter.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig }.compact) %>
Thanks,
<%= @current_account.name %>
diff --git a/app/views/submitter_mailer/invitation_email.html.erb b/app/views/submitter_mailer/invitation_email.html.erb
index 91bf7279..98f71906 100644
--- a/app/views/submitter_mailer/invitation_email.html.erb
+++ b/app/views/submitter_mailer/invitation_email.html.erb
@@ -1,6 +1,6 @@
<% if @email_config || @body.present? %>
<% body = (@body.presence || @email_config.value['body']).to_s %>
- <%= auto_link(simple_format(h(ReplaceEmailVariables.call(body, submitter: @submitter)))) %>
+ <%= render 'custom_content', content: body, submitter: @submitter %>
<% if !body.include?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.include?(ReplaceEmailVariables::SUBMITTER_ID) && !body.include?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.include?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
<%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %>
<% end %> diff --git a/app/views/templates_code_modal/_placeholder.html.erb b/app/views/templates_code_modal/_placeholder.html.erb index 9498f035..ad208c75 100644 --- a/app/views/templates_code_modal/_placeholder.html.erb +++ b/app/views/templates_code_modal/_placeholder.html.erb @@ -5,7 +5,7 @@
Unlock with DocuSeal Pro
- ">Learn More
+ " data-turbo="false">Learn More