Compare commits

...
65 Commits
Author SHA1 Message Date
Alex Turchyn 50acff2e81 Merge from docusealco/wip 2024-11-04 16:13:29 +02:00
Pete Matsyburka cb6d24dfca add canonical urls 2024-11-04 11:37:58 +02:00
Pete Matsyburka ab4a22d8ba fix test links 2024-11-03 13:43:28 +02:00
Pete Matsyburka bc40a15de1 use com 2024-11-03 10:19:59 +02:00
Pete Matsyburka 2542f26d96 adjust webhooks 2024-11-03 09:49:30 +02:00
Alex Turchyn 84edb6dbda use webhook urls instead of encrypted configs 2024-11-02 22:12:41 +02:00
Pete Matsyburka 88776e7316 Merge from docusealco/wip 2024-11-02 14:48:21 +02:00
Pete Matsyburka f669045362 redirect com 2024-11-02 09:48:47 +02:00
Pete Matsyburka 68d3efbdba use com 2024-11-02 09:02:50 +02:00
Pete Matsyburka 22d4833496 allow to unarchive submissions 2024-11-01 21:37:02 +02:00
Pete Matsyburka ff14caa9f7 fix image 2024-11-01 19:01:51 +02:00
Pete Matsyburka 05647135bc add status to export 2024-10-31 17:22:14 +02:00
Pete Matsyburka ae76c2a4a8 disable draw when type 2024-10-31 16:43:43 +02:00
Pete Matsyburka fb2b3d44fe fix bulk 2024-10-31 00:51:21 +02:00
Pete Matsyburka 3868557efd add define submitters 2024-10-30 23:07:10 +02:00
dependabot[bot] 9bef28c8ba Bump rexml from 3.3.8 to 3.3.9 in the bundler group across 1 directory (#50) 2024-10-30 12:12:36 +02:00
Pete Matsyburka f25fd3b03d do not edit if declined 2024-10-29 17:11:44 +02:00
Pete Matsyburka d1d7571ee4 disable decline email 2024-10-29 16:44:07 +02:00
Pete Matsyburka 6a298a068c fix updated templates 2024-10-29 12:11:21 +02:00
Pete Matsyburka d4aef79b68 use tsa fallback url 2024-10-29 08:29:26 +02:00
Alex Turchyn 05d578d88f Merge from docusealco/wip 2024-10-28 14:44:22 +02:00
Pete Matsyburka 8773b30b10 adjust auto migration 2024-10-27 14:26:52 +02:00
Pete Matsyburka 1b636c5878 fix completed submitters 2024-10-26 19:46:22 +03:00
Pete Matsyburka 46e948ab99 download combined completed 2024-10-26 19:30:23 +03:00
Pete Matsyburka 889103aa5e use com 2024-10-26 09:54:41 +03:00
Pete Matsyburka 558a14a5f2 fix email validate 2024-10-25 22:17:23 +03:00
Pete Matsyburka 72bbdb38bf mask key 2024-10-25 16:17:57 +03:00
Pete Matsyburka 3975a03cb4 adjust from email 2024-10-24 23:32:36 +03:00
Pete Matsyburka e398327fb7 refactor 2024-10-24 20:27:29 +03:00
Oleksandr Turchyn 9266694d65 add integration users page 2024-10-24 20:09:57 +03:00
Pete Matsyburka 350c4b1c2a do not submit invalid field value 2024-10-24 18:45:08 +03:00
Pete Matsyburka c3a933d87d fix fields detection 2024-10-24 17:56:02 +03:00
Pete Matsyburka 970e53a5a3 remove log 2024-10-24 12:30:14 +03:00
Pete Matsyburka 520bac2ec7 add decline account preferences toggle 2024-10-24 12:15:32 +03:00
Pete Matsyburka e1eddd31c4 fix field condition 2024-10-24 12:08:10 +03:00
Oleksandr Turchyn 3c616a824c show error for cross-environment API key usage 2024-10-24 12:07:58 +03:00
Pete Matsyburka 90d86b58c2 adjust migration 2024-10-23 22:57:33 +03:00
Pete Matsyburka 4332b4ebb8 fix specs 2024-10-23 12:01:55 +03:00
Oleksandr Turchyn e2f930f6f7 add completed submitters and documents 2024-10-23 12:01:43 +03:00
Pete Matsyburka c303159c63 adjust email interceptor 2024-10-23 10:46:53 +03:00
Pete Matsyburka c1ce2bec00 fix update name 2024-10-23 10:45:30 +03:00
Oleksandr Turchyn 3d2046f227 add validation for submitters count 2024-10-22 22:27:11 +03:00
Pete Matsyburka babaf1aa48 adjust error message 2024-10-22 18:18:14 +03:00
Pete Matsyburka 3f03d343ca Revert "dry run email validation"
This reverts commit 910490528d.
2024-10-22 17:41:25 +03:00
Pete Matsyburka 110cdb5123 fix specs 2024-10-22 15:56:59 +03:00
Pete Matsyburka 440d1114be fix email validation 2024-10-22 09:58:07 +03:00
Pete Matsyburka 9032517d4e smtp settings 2024-10-21 23:40:56 +03:00
Pete Matsyburka 7c71fefc47 optimize query 2024-10-21 14:34:11 +03:00
Pete Matsyburka e8bd8ff76f fix cdn url 2024-10-21 12:33:36 +03:00
Pete Matsyburka fed420cae1 Merge pull request #39 from docusealco/wip 2024-10-21 12:15:46 +03:00
Pete Matsyburka 910490528d dry run email validation 2024-10-20 15:44:24 +03:00
Alex Turchyn ab810be250 validate email format 2024-10-20 15:42:20 +03:00
Alex Turchyn 178ee47093 add more specs 2024-10-20 15:26:14 +03:00
Alex Turchyn 6f90147a11 validate the uniqueness of submitter roles 2024-10-20 15:26:04 +03:00
Pete Matsyburka 3ebaacbb7a locale date format 2024-10-20 09:48:39 +03:00
Pete Matsyburka 64b39b53a2 fix com from 2024-10-19 10:09:53 +03:00
Pete Matsyburka 2948910c25 fix com selector 2024-10-19 09:41:13 +03:00
Pete Matsyburka 1d20c79cf4 com host 2024-10-18 23:15:55 +03:00
Pete Matsyburka 48f3b13f3e adjust smtp from 2024-10-18 23:07:40 +03:00
Pete Matsyburka 1fc1e9af16 fix font size 2024-10-18 17:48:31 +03:00
Pete Matsyburka ce70016603 update rails 2024-10-16 23:58:37 +03:00
Pete Matsyburka e0e79e343d update email 2024-10-16 12:16:20 +03:00
Pete Matsyburka 6568f81ee9 adjust pdf generation 2024-10-15 20:58:08 +03:00
Oleksandr Turchyn 386f130d7a allow to paste date in date field 2024-10-15 19:31:25 +03:00
Pete Matsyburka 3ee41975fd add documents preview endpoint 2024-10-15 19:31:24 +03:00
133 changed files with 2730 additions and 587 deletions

No files matched your search

+1 -1
View File
@@ -66,7 +66,7 @@ RSpec/ExampleLength:
Max: 40
RSpec/MultipleMemoizedHelpers:
Max: 6
Max: 9
Metrics/BlockNesting:
Max: 4
+57 -57
View File
@@ -1,29 +1,29 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.2.1)
actionpack (= 7.2.1)
activesupport (= 7.2.1)
actioncable (7.2.1.1)
actionpack (= 7.2.1.1)
activesupport (= 7.2.1.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.1)
actionpack (= 7.2.1)
activejob (= 7.2.1)
activerecord (= 7.2.1)
activestorage (= 7.2.1)
activesupport (= 7.2.1)
actionmailbox (7.2.1.1)
actionpack (= 7.2.1.1)
activejob (= 7.2.1.1)
activerecord (= 7.2.1.1)
activestorage (= 7.2.1.1)
activesupport (= 7.2.1.1)
mail (>= 2.8.0)
actionmailer (7.2.1)
actionpack (= 7.2.1)
actionview (= 7.2.1)
activejob (= 7.2.1)
activesupport (= 7.2.1)
actionmailer (7.2.1.1)
actionpack (= 7.2.1.1)
actionview (= 7.2.1.1)
activejob (= 7.2.1.1)
activesupport (= 7.2.1.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.1)
actionview (= 7.2.1)
activesupport (= 7.2.1)
actionpack (7.2.1.1)
actionview (= 7.2.1.1)
activesupport (= 7.2.1.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.2)
@@ -32,35 +32,35 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.1)
actionpack (= 7.2.1)
activerecord (= 7.2.1)
activestorage (= 7.2.1)
activesupport (= 7.2.1)
actiontext (7.2.1.1)
actionpack (= 7.2.1.1)
activerecord (= 7.2.1.1)
activestorage (= 7.2.1.1)
activesupport (= 7.2.1.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.2.1)
activesupport (= 7.2.1)
actionview (7.2.1.1)
activesupport (= 7.2.1.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.2.1)
activesupport (= 7.2.1)
activejob (7.2.1.1)
activesupport (= 7.2.1.1)
globalid (>= 0.3.6)
activemodel (7.2.1)
activesupport (= 7.2.1)
activerecord (7.2.1)
activemodel (= 7.2.1)
activesupport (= 7.2.1)
activemodel (7.2.1.1)
activesupport (= 7.2.1.1)
activerecord (7.2.1.1)
activemodel (= 7.2.1.1)
activesupport (= 7.2.1.1)
timeout (>= 0.4.0)
activestorage (7.2.1)
actionpack (= 7.2.1)
activejob (= 7.2.1)
activerecord (= 7.2.1)
activesupport (= 7.2.1)
activestorage (7.2.1.1)
actionpack (= 7.2.1.1)
activejob (= 7.2.1.1)
activerecord (= 7.2.1.1)
activesupport (= 7.2.1.1)
marcel (~> 1.0)
activesupport (7.2.1)
activesupport (7.2.1.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -320,7 +320,7 @@ GEM
mysql2 (0.5.6)
net-http-persistent (4.0.4)
connection_pool (~> 2.2)
net-imap (0.4.16)
net-imap (0.4.17)
date
net-protocol
net-pop (0.1.2)
@@ -372,7 +372,7 @@ GEM
puma (6.4.3)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.7)
rack (3.1.8)
rack-proxy (0.7.7)
rack
rack-session (2.0.0)
@@ -382,20 +382,20 @@ GEM
rackup (2.1.0)
rack (>= 3)
webrick (~> 1.8)
rails (7.2.1)
actioncable (= 7.2.1)
actionmailbox (= 7.2.1)
actionmailer (= 7.2.1)
actionpack (= 7.2.1)
actiontext (= 7.2.1)
actionview (= 7.2.1)
activejob (= 7.2.1)
activemodel (= 7.2.1)
activerecord (= 7.2.1)
activestorage (= 7.2.1)
activesupport (= 7.2.1)
rails (7.2.1.1)
actioncable (= 7.2.1.1)
actionmailbox (= 7.2.1.1)
actionmailer (= 7.2.1.1)
actionpack (= 7.2.1.1)
actiontext (= 7.2.1.1)
actionview (= 7.2.1.1)
activejob (= 7.2.1.1)
activemodel (= 7.2.1.1)
activerecord (= 7.2.1.1)
activestorage (= 7.2.1.1)
activesupport (= 7.2.1.1)
bundler (>= 1.15.0)
railties (= 7.2.1)
railties (= 7.2.1.1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@@ -410,9 +410,9 @@ GEM
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
railties (7.2.1)
actionpack (= 7.2.1)
activesupport (= 7.2.1)
railties (7.2.1.1)
actionpack (= 7.2.1.1)
activesupport (= 7.2.1.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -437,7 +437,7 @@ GEM
actionpack (>= 5.2)
railties (>= 5.2)
retriable (3.1.2)
rexml (3.3.8)
rexml (3.3.9)
rotp (6.3.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
@@ -559,7 +559,7 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.18)
zeitwerk (2.7.0)
PLATFORMS
aarch64-linux
+13 -13
View File
@@ -1,6 +1,6 @@
<h1 align="center" style="border-bottom: none">
<div>
<a href="https://www.docuseal.co">
<a href="https://www.docuseal.com">
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
<br>
</a>
@@ -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>
[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.co)
[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.tech)
## Features
- PDF form fields builder (WYSIWYG)
@@ -54,11 +54,11 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Conditional fields and formulas
- Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- [Learn more](https://www.docuseal.co/pricing)
- Template creation with HTML API ([Guide](https://www.docuseal.com/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
- [Learn more](https://www.docuseal.com/pricing)
## Deploy
@@ -93,7 +93,7 @@ sudo HOST=your-domain-name.com docker compose up
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[Book a Meeting](https://www.docuseal.co/contact)
[Book a Meeting](https://www.docuseal.com/contact)
## License
@@ -102,6 +102,6 @@ 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)
- [Signature Maker](https://www.docuseal.com/online-signature)
- [Sign Document Online](https://www.docuseal.com/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.com/fill-pdf)
+1 -1
View File
@@ -1,6 +1,6 @@
# Reporting a Vulnerability
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.com instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
**We have a bug bounty program to reward security researchers.**
@@ -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,
+29 -2
View File
@@ -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)
@@ -0,0 +1,45 @@
# frozen_string_literal: true
module Api
class SubmissionDocumentsController < ApiBaseController
load_and_authorize_resource :submission
def index
documents =
if @submission.submitters.all?(&:completed_at?)
last_submitter = @submission.submitters.max_by(&:completed_at)
if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
end
last_submitter.documents_attachments
else
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
if @submission.preview_documents.present? &&
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
@submission.preview_documents
else
ApplicationRecord.no_touching do
@submission.preview_documents.each(&:destroy)
end
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
end
end
ActiveRecord::Associations::Preloader.new(
records: documents,
associations: [:blob]
).call
render json: {
id: @submission.id,
documents: documents.map do |attachment|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
end
}
end
end
end
+13 -6
View File
@@ -67,20 +67,24 @@ module Api
submissions = create_submissions(@template, params)
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
'webhook_url_id' => webhook_url.id)
end
end
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
@@ -92,7 +96,10 @@ module Api
else
@submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
'webhook_url_id' => webhook_url.id)
end
end
render json: @submission.as_json(only: %i[id archived_at])
@@ -13,7 +13,10 @@ module Api
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url|
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id)
end
render json: {}
end
+2 -1
View File
@@ -68,7 +68,7 @@ module Api
end
if @submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id })
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id)
elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter])
end
@@ -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+]/, '')
@@ -25,7 +25,10 @@ module Api
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id)
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
'webhook_url_id' => webhook_url.id)
end
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
end
+4 -1
View File
@@ -65,7 +65,10 @@ module Api
@template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
'webhook_url_id' => webhook_url.id)
end
render json: @template.as_json(only: %i[id updated_at])
end
+1 -4
View File
@@ -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',
+10
View File
@@ -38,6 +38,10 @@ class ApplicationController < ActionController::Base
end
def default_url_options
if request.domain == 'docuseal.com'
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
end
Docuseal.default_url_options
end
@@ -100,4 +104,10 @@ class ApplicationController < ActionController::Base
def svg_icon(icon_name, class: '')
render_to_string(partial: "icons/#{icon_name}", locals: { class: })
end
def maybe_redirect_com
return if request.domain != 'docuseal.co'
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
end
end
@@ -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
+1 -1
View File
@@ -2,7 +2,7 @@
class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
ENTERPRISE_PATHS = [
'/templates/html',
+5 -1
View File
@@ -7,6 +7,7 @@ class StartFormController < ApplicationController
skip_authorization_check
around_action :with_browser_locale, only: %i[show completed]
before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_template
def show
@@ -37,7 +38,10 @@ class StartFormController < ApplicationController
if @submitter.save
if is_new_record
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id })
WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
'webhook_url_id' => webhook_url.id)
end
end
redirect_to submit_form_path(@submitter.slug)
+14 -4
View File
@@ -50,9 +50,7 @@ class SubmissionsController < ApplicationController
params: params.merge('send_completed_email' => true))
end
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
enqueue_submission_created_webhooks(@template, submissions)
Submissions.send_signature_requests(submissions)
@@ -68,7 +66,10 @@ class SubmissionsController < ApplicationController
else
@submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
'webhook_url_id' => webhook_url.id)
end
I18n.t('submission_has_been_archived')
end
@@ -85,6 +86,15 @@ class SubmissionsController < ApplicationController
template.save!
end
def enqueue_submission_created_webhooks(template, submissions)
WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url|
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
'webhook_url_id' => webhook_url.id)
end
end
end
def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] })
end
@@ -4,6 +4,8 @@ class SubmissionsPreviewController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
before_action :maybe_redirect_com, only: %i[show completed]
TTL = 40.minutes
def show
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class SubmissionsUnarchiveController < ApplicationController
load_and_authorize_resource :submission
def create
@submission.update!(archived_at: nil)
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
end
end
@@ -7,6 +7,8 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
before_action :maybe_redirect_com, only: %i[show completed]
CONFIG_KEYS = [].freeze
def show
@@ -21,8 +21,14 @@ class SubmitFormDeclineController < ApplicationController
user = submitter.submission.created_by_user || submitter.template.author
SubmitterMailer.declined_email(submitter, user).deliver_later!
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id)
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
SubmitterMailer.declined_email(submitter, user).deliver_later!
end
WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id)
end
redirect_to submit_form_path(submitter.slug)
end
+16 -2
View File
@@ -66,7 +66,7 @@ class TemplatesController < ApplicationController
if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
enqueue_template_created_webhooks(@template)
maybe_redirect_to_template(@template)
else
@@ -77,7 +77,7 @@ class TemplatesController < ApplicationController
def update
@template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
enqueue_template_updated_webhooks(@template)
head :ok
end
@@ -128,6 +128,20 @@ class TemplatesController < ApplicationController
end
end
def enqueue_template_created_webhooks(template)
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end
def enqueue_template_updated_webhooks(template)
WebhookUrls.for_account_id(template.account_id, 'template.updated').each do |webhook_url|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end
def load_base_template
return if params[:base_template_id].blank?
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
@template.update!(schema:)
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
enqueue_template_created_webhooks(@template)
redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted
@@ -65,4 +65,11 @@ class TemplatesUploadsController < ApplicationController
{ files: [file] }
end
def enqueue_template_created_webhooks(template)
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end
end
@@ -4,9 +4,8 @@ class TestingApiSettingsController < ApplicationController
def index
authorize!(:manage, current_user.access_token)
@webhook_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
@webhook_url = current_account.webhook_urls.first_or_initialize
authorize!(:manage, @webhook_config)
authorize!(:manage, @webhook_url)
end
end
+4 -2
View File
@@ -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))
@@ -1,37 +1,22 @@
# frozen_string_literal: true
class WebhookPreferencesController < ApplicationController
EVENTS = %w[
form.viewed
form.started
form.completed
form.declined
template.created
template.updated
submission.created
submission.archived
].freeze
load_and_authorize_resource :webhook_url, parent: false
before_action :load_account_config
authorize_resource :account_config, parent: false
def update
webhook_preferences_params[:events].each do |event, val|
@webhook_url.events.delete(event) if val == '0'
@webhook_url.events.push(event) if val == '1' && @webhook_url.events.exclude?(event)
end
def create
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
@account_config.save!
@webhook_url.save!
head :ok
end
private
def load_account_config
@account_config =
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
@account_config.value ||= {}
end
def account_config_params
params.permit(:event, :value)
def webhook_preferences_params
params.require(:webhook_url).permit(events: {})
end
end
+7 -15
View File
@@ -1,29 +1,21 @@
# frozen_string_literal: true
class WebhookSecretController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false
load_and_authorize_resource :webhook_url, parent: false
def index; end
def show; end
def create
@encrypted_config.assign_attributes(value: {
encrypted_config_params[:key] => encrypted_config_params[:value]
def update
@webhook_url.update!(secret: {
webhook_secret_params[:key] => webhook_secret_params[:value]
}.compact_blank)
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved'))
end
private
def load_encrypted_config
@encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY)
end
def encrypted_config_params
params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {})
def webhook_secret_params
params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {})
end
end
+10 -11
View File
@@ -1,15 +1,15 @@
# frozen_string_literal: true
class WebhookSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false
before_action :load_webhook_url
authorize_resource :webhook_url, parent: false
def show; end
def create
@encrypted_config.assign_attributes(encrypted_config_params)
@webhook_url.assign_attributes(webhook_params)
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
@webhook_url.url.present? ? @webhook_url.save! : @webhook_url.delete
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
end
@@ -17,20 +17,19 @@ class WebhookSettingsController < ApplicationController
def update
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
'encrypted_config_id' => @encrypted_config.id })
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => @webhook_url.id)
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
end
private
def load_encrypted_config
@encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
def load_webhook_url
@webhook_url = current_account.webhook_urls.first_or_initialize
end
def encrypted_config_params
params.require(:encrypted_config).permit(:value)
def webhook_params
params.require(:webhook_url).permit(:url)
end
end
+2
View File
@@ -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 () {
+18
View File
@@ -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')
}
}
+2 -1
View File
@@ -405,7 +405,8 @@ export default {
}
if (this.field.preferences?.font_size) {
style.fontSize = this.field.preferences.font_size + 'pt'
style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)`
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)`
}
return style
+2 -2
View File
@@ -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 />
@@ -91,7 +91,7 @@
>
{{ t('powered_by') }}
<a
href="https://www.docuseal.co/start"
href="https://www.docuseal.com/start"
target="_blank"
class="underline"
>DocuSeal</a> - {{ t('open_source_documents_software') }}
@@ -45,6 +45,7 @@
:name="`values[${field.uuid}]`"
@keydown.enter="onEnter"
@focus="$emit('focus')"
@paste="onPaste"
>
</div>
</div>
@@ -98,6 +99,25 @@ export default {
this.$emit('submit')
}
},
onPaste (e) {
e.preventDefault()
let pasteData = e.clipboardData.getData('text').trim()
if (pasteData.match(/^\d{2}\.\d{2}\.\d{4}$/)) {
pasteData = pasteData.split('.').reverse().join('-')
}
const parsedDate = new Date(pasteData)
if (!isNaN(parsedDate)) {
const inputEl = this.$refs.input
inputEl.valueAsDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000)
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
}
},
setCurrentDate () {
const inputEl = this.$refs.input
+4
View File
@@ -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]
@@ -145,6 +145,10 @@
{{ t('clear') }}
</a>
</div>
<div
v-if="isTextSignature"
class="absolute top-0 right-0 left-0 bottom-0"
/>
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
@@ -255,7 +259,7 @@
class="text-base-content/60 text-xs text-center w-full mt-1"
>
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
href="https://www.docuseal.co/esign-disclosure"
href="https://www.docuseal.com/esign-disclosure"
target="_blank"
>
<span class="inline md:hidden">
@@ -415,6 +415,7 @@ import Contenteditable from './contenteditable'
import DocumentPreview from './preview'
import DocumentControls from './controls'
import MobileFields from './mobile_fields'
import FieldSubmitter from './field_submitter'
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
import { v4 } from 'uuid'
import { ref, computed } from 'vue'
@@ -558,6 +559,11 @@ export default {
required: false,
default: () => []
},
defineSubmitters: {
type: Array,
required: false,
default: () => []
},
acceptFileTypes: {
type: String,
required: false,
@@ -677,6 +683,7 @@ export default {
}
},
computed: {
submitterDefaultNames: FieldSubmitter.computed.names,
selectedAreaRef: () => ref(),
fieldsDragFieldRef: () => ref(),
language () {
@@ -693,6 +700,7 @@ export default {
'de-DE': 'DD.MM.YYYY',
'fr-FR': 'DD/MM/YYYY',
'it-IT': 'DD/MM/YYYY',
'en-GB': 'DD/MM/YYYY',
'es-ES': 'DD/MM/YYYY'
}
},
@@ -748,6 +756,18 @@ export default {
}
})
this.defineSubmitters.forEach((name, index) => {
const submitter = (this.template.submitters[index] ||= {})
submitter.name = name || this.submitterDefaultNames[index]
if (existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = existingSubmittersUuids[index] || submitter.uuid || v4()
} else {
submitter.uuid ||= v4()
}
})
this.selectedSubmitter = this.template.submitters[0]
},
mounted () {
@@ -23,7 +23,7 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center"
>
<a
href="https://www.docuseal.co/pricing"
href="https://www.docuseal.com/pricing"
target="_blank"
class="link"
>{{ t('available_in_pro') }}</a>
@@ -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
+1 -1
View File
@@ -140,7 +140,7 @@
:data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')"
>
<a
href="https://www.docuseal.co/pricing"
href="https://www.docuseal.com/pricing"
target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
:style="{ backgroundColor }"
@@ -23,7 +23,7 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center"
>
<a
href="https://www.docuseal.co/pricing"
href="https://www.docuseal.com/pricing"
target="_blank"
class="link"
>{{ t('available_in_pro') }}</a>
@@ -77,7 +77,7 @@
<div class="w-full relative">
<select
class="base-select !select-sm !h-10"
:class="{ '!text-gray-300': !mapping.column_index }"
:class="{ '!text-gray-300': !mapping.column_index && mapping.column_index != 0 }"
required
@change="mapping.column_index = parseInt($event.target.value)"
>
@@ -285,7 +285,7 @@ export default {
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index]
}
const fieldType = this.fieldTypesIndex[mapping.submitter_uuid][mapping.field_name]
const fieldType = this.fieldTypesIndex[mapping.submitter_uuid]?.[mapping.field_name]
if (fieldType && fieldType !== 'phone') {
submittersIndex[mapping.submitter_uuid].fields.push({
@@ -178,7 +178,7 @@
<a
v-if="!isConnected"
class="block link text-center mt-1"
href="https://www.docuseal.co/blog/accept-payments-and-request-signatures-with-ease"
href="https://www.docuseal.com/blog/accept-payments-and-request-signatures-with-ease"
target="_blank"
data-turbo="false"
>{{ t('learn_more') }}</a>
+36 -18
View File
@@ -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,36 +26,52 @@ class ProcessSubmitterCompletionJob
enqueue_completed_emails(submitter)
end
create_completed_documents!(submitter)
enqueue_completed_webhooks(submitter, is_all_completed:)
end
def enqueue_completed_webhooks(submitter, is_all_completed: false)
webhook_config = Accounts.load_webhook_config(submitter.account)
def create_completed_submitter!(submitter)
completed_submitter = CompletedSubmitter.find_or_initialize_by(submitter_id: submitter.id)
if webhook_config
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
'encrypted_config_id' => webhook_config.id })
end
return completed_submitter if completed_submitter.persisted?
webhook_urls = submitter.account.webhook_urls
submission = submitter.submission
webhook_urls = webhook_urls.where(
Arel::Table.new(:webhook_urls)[:events].matches('%"form.completed"%')
).or(
webhook_urls.where(
Arel::Table.new(:webhook_urls)[:events].matches('%"submission.completed"%')
)
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
)
webhook_urls.each do |webhook|
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)
WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
if webhook.events.include?('form.completed')
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
'webhook_url_id' => webhook.id })
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook.id)
end
if webhook.events.include?('submission.completed') && is_all_completed
SendSubmissionCompletedWebhookRequestJob.perform_async({ 'submission_id' => submitter.submission_id,
'webhook_url_id' => webhook.id })
SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
'webhook_url_id' => webhook.id)
end
end
end
@@ -5,31 +5,30 @@ class SendFormCompletedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
url, secret = load_url_and_secret(submitter, params)
return if url.blank?
return if webhook_url.url.blank? || webhook_url.events.exclude?('form.completed')
Submissions::EnsureResultGenerated.call(submitter)
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(url,
Faraday.post(webhook_url.url,
{
event_type: 'form.completed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
**secret.to_h,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -45,27 +44,4 @@ class SendFormCompletedWebhookRequestJob
})
end
end
def load_url_and_secret(submitter, params)
if params['encrypted_config_id']
config = EncryptedConfig.find(params['encrypted_config_id'])
url = config.value
return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.completed'] == false
secret = EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h
[url, secret]
elsif params['webhook_url_id']
webhook_url = submitter.account.webhook_urls.find(params['webhook_url_id'])
webhook_url.url if webhook_url.events.include?('form.completed')
end
end
end
@@ -5,34 +5,28 @@ class SendFormDeclinedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.declined'] == false
return if webhook_url.url.blank? || webhook_url.events.exclude?('form.declined')
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(url,
Faraday.post(webhook_url.url,
{
event_type: 'form.declined',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -43,6 +37,7 @@ class SendFormDeclinedWebhookRequestJob
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
@@ -5,34 +5,28 @@ class SendFormStartedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.started'] == false
return if webhook_url.url.blank? || webhook_url.events.exclude?('form.started')
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(url,
Faraday.post(webhook_url.url,
{
event_type: 'form.started',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -43,6 +37,7 @@ class SendFormStartedWebhookRequestJob
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
@@ -5,34 +5,28 @@ class SendFormViewedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.viewed'] == false
return if webhook_url.url.blank? || webhook_url.events.exclude?('form.viewed')
ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin
Faraday.post(url,
Faraday.post(webhook_url.url,
{
event_type: 'form.viewed',
timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -43,6 +37,7 @@ class SendFormViewedWebhookRequestJob
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
@@ -5,33 +5,26 @@ class SendSubmissionArchivedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submission.account)
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(submission.account)
return if preferences['submission.archived'].blank?
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived')
resp = begin
Faraday.post(url,
Faraday.post(webhook_url.url,
{
event_type: 'submission.archived',
timestamp: Time.current,
data: submission.as_json(only: %i[id archived_at])
}.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendSubmissionArchivedWebhookRequestJob
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submission_id' => submission.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
@@ -5,30 +5,26 @@ class SendSubmissionCompletedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
webhook_url = submission.account.webhook_urls.find(params['webhook_url_id'])
url = webhook_url.url if webhook_url.events.include?('submission.completed')
return if url.blank?
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed')
resp = begin
Faraday.post(url,
Faraday.post(webhook_url.url,
{
event_type: 'submission.completed',
timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission)
}.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: submission.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -5,33 +5,26 @@ class SendSubmissionCreatedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submission.account)
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(submission.account)
return if preferences['submission.created'].blank?
return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created')
resp = begin
Faraday.post(url,
Faraday.post(webhook_url.url,
{
event_type: 'submission.created',
timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission)
}.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendSubmissionCreatedWebhookRequestJob
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submission_id' => submission.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
@@ -5,33 +5,26 @@ class SendTemplateCreatedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
template = Template.find(params['template_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(template.account)
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(template.account)
return if preferences['template.created'].blank?
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created')
resp = begin
Faraday.post(url,
Faraday.post(webhook_url.url,
{
event_type: 'template.created',
timestamp: Time.current,
data: Templates::SerializeForApi.call(template)
}.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendTemplateCreatedWebhookRequestJob
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'template_id' => template.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
@@ -5,33 +5,26 @@ class SendTemplateUpdatedWebhookRequestJob
sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook'
USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10
def perform(params = {})
template = Template.find(params['template_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(template.account)
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(template.account)
return if preferences['template.updated'].blank?
return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated')
resp = begin
Faraday.post(url,
Faraday.post(webhook_url.url,
{
event_type: 'template.updated',
timestamp: Time.current,
data: Templates::SerializeForApi.call(template)
}.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
**webhook_url.secret.to_h,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendTemplateUpdatedWebhookRequestJob
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'template_id' => template.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1,
'last_status' => resp&.status.to_i
})
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -28,13 +28,13 @@ 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'
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
WEBHOOK_PREFERENCES_KEY = 'webhook_preferences'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
+22
View File
@@ -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
+33
View File
@@ -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
+1 -3
View File
@@ -26,9 +26,7 @@ class EncryptedConfig < ApplicationRecord
EMAIL_SMTP_KEY = 'action_mailer_smtp',
ESIGN_CERTS_KEY = 'esign_certs',
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
APP_URL_KEY = 'app_url',
WEBHOOK_URL_KEY = 'webhook_url',
WEBHOOK_SECRET_KEY = 'webhook_secret'
APP_URL_KEY = 'app_url'
].freeze
belongs_to :account
+2
View File
@@ -55,6 +55,8 @@ class Submission < ApplicationRecord
has_one_attached :audit_trail
has_one_attached :combined_document
has_many_attached :preview_documents
has_many :template_schema_documents,
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
through: :template, source: :documents_attachments
+3
View File
@@ -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
+1
View File
@@ -24,6 +24,7 @@ class UserConfig < ApplicationRecord
SIGNATURE_KEY = 'signature'
INITIALS_KEY = 'initials'
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
RECEIVE_DECLINED_EMAIL = 'receive_declined_email'
belongs_to :user
+16 -2
View File
@@ -6,6 +6,7 @@
#
# id :bigint not null, primary key
# events :text not null
# secret :text not null
# sha1 :string not null
# url :text not null
# created_at :datetime not null
@@ -22,15 +23,28 @@
# fk_rails_... (account_id => accounts.id)
#
class WebhookUrl < ApplicationRecord
EVENTS = %w[
form.viewed
form.started
form.completed
form.declined
template.created
template.updated
submission.created
submission.archived
].freeze
belongs_to :account
attribute :events, :string, default: -> { [] }
attribute :events, :string, default: -> { %w[form.viewed form.started form.completed form.declined] }
attribute :secret, :string, default: -> { {} }
serialize :events, coder: JSON
serialize :secret, coder: JSON
before_validation :set_sha1
encrypts :url
encrypts :url, :secret
def set_sha1
self.sha1 = Digest::SHA1.hexdigest(url)
+12
View File
@@ -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| %>
+5 -2
View File
@@ -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
View File
@@ -1,3 +1,4 @@
<%= content_for(:canonical_url, new_user_session_url) %>
<div class="max-w-lg mx-auto px-2">
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
<h1 class="text-4xl font-bold text-center mt-8"><%= t('sign_in') %></h1>
@@ -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' %>">
<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>
+1 -1
View File
@@ -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">
+3
View File
@@ -11,6 +11,9 @@
<% else %>
<%= javascript_pack_tag 'application', defer: true %>
<% end %>
<% if canonical_url = content_for(:canonical_url) %>
<link href="<%= canonical_url %>" rel="canonical">
<% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
+1 -1
View File
@@ -27,7 +27,7 @@
</div>
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
<p class="text-base text-gray-500">
Run on your own host using Docker container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
Run on your own host using Docker container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.com/install" class="link link-neutral font-bold">click</a>.
</p>
</div>
</div>
+1 -1
View File
@@ -1,3 +1,3 @@
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
<img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px">
<img alt="GitHub Repo stars" src="https://www.docuseal.com/github-badge.svg" style="height: 22px">
</a>
+1 -1
View File
@@ -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 -1
View File
@@ -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 %>
+3 -3
View File
@@ -56,7 +56,7 @@
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::WEBHOOK_URL_KEY, account: current_account)) %>
<% if can?(:read, WebhookUrl) %>
<li>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
</li>
@@ -72,13 +72,13 @@
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
</li>
<% if Docuseal.multitenant? %>
<li>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<%= t('embedding') %>
<% end %>
</li>
+1 -1
View File
@@ -1,5 +1,5 @@
<% align = field.dig('preferences', 'align') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: #{field['preferences']['font_size']}pt" if field.dig('preferences', 'font_size') %>">
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-serif' if field.dig('preferences', 'font') == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size']}pt); line-height: `clamp(6pt, 2.0vw, #{field['preferences']['font_size'].to_i + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
+44 -38
View File
@@ -9,50 +9,56 @@
<span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% @submission.template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %></span>
</a>
<div class="space-x-3 flex items-center">
<% last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
<% if signed_in? && can?(:create, @submission) && @submission.archived_at? && !is_all_completed %>
<%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %>
<% end %>
<% if @submission.audit_trail.present? %>
<a href="<%= ActiveStorage::Blob.proxy_url(@submission.audit_trail.blob, expires_at: 4.hours.from_now) %>" class="white-button" target="_blank">
<%= svg_icon('external_link', class: 'w-6 h-6') %>
<span class="hidden md:inline"><%= t('audit_log') %></span>
</a>
<% 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>
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? %>
<% if last_submitter %>
<% 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? && !@submission.archived_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 %>
</div>
+7 -5
View File
@@ -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') %>
+2 -2
View File
@@ -51,7 +51,7 @@
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
<%= submitter.name || submitter.email || submitter.phone %>
</a>
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? %>
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %>
@@ -144,7 +144,7 @@
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
<%= submitter.name || submitter.email || submitter.phone %>
</a>
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? %>
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %>
@@ -8,10 +8,10 @@
<%= 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') %>
</div>
</div>
<%= form_for @webhook_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
<%= form_for @webhook_url, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
<div class="space-y-2 md:flex-nowrap mt-2">
<%= f.url_field :value, class: 'base-input w-full', placeholder: 'https://example.com/hook' %>
<%= f.url_field :url, class: 'base-input w-full', placeholder: 'https://example.com/hook' %>
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full' %>
</div>
<% end %>
+16
View File
@@ -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>
+14 -19
View File
@@ -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,13 +1,13 @@
<%= render 'shared/turbo_modal', title: 'Webhook Secret' do %>
<%= form_for @encrypted_config, url: webhook_secret_index_path, method: :post, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
<%= render 'shared/turbo_modal', title: t('webhook_secret') do %>
<%= form_for @webhook_url, url: webhook_secret_path, method: :patch, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
<div class="space-y-2">
<%= f.fields_for :value, Struct.new(:key, :value).new(*@encrypted_config.value.to_a.first) do |ff| %>
<%= f.fields_for :secret, Struct.new(:key, :value).new(*@webhook_url.secret.to_a.first) do |ff| %>
<div class="form-control">
<%= ff.label :key, class: 'label' %>
<%= ff.label :key, t('key'), class: 'label' %>
<%= ff.text_field :key, class: 'base-input', placeholder: 'X-Example-Header' %>
</div>
<div class="form-control">
<%= ff.label :value, class: 'label' %>
<%= ff.label :value, t('value'), class: 'label' %>
<%= ff.text_field :value, class: 'base-input' %>
</div>
<% end %>
+26 -25
View File
@@ -7,35 +7,36 @@
</div>
<div class="card bg-base-200">
<div class="card-body p-6">
<%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
<%= form_for @webhook_url, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
<%= f.label :url, 'Webhook URL', class: 'text-sm font-semibold' %>
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-2 mt-2">
<%= f.url_field :value, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %>
<%= f.url_field :url, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %>
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button w-full md:w-32' %>
<a href="<%= webhook_secret_index_path %>" data-turbo-frame="modal" class="white-button w-full md:w-auto">
<%= t('add_secret') %>
</a>
</div>
<% end %>
<% preference = current_account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)&.value || {} %>
<% WebhookPreferencesController::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 mt-2 gap-y-2">
<% events.each do |event| %>
<%= form_for '', url: webhook_preferences_path, method: :post do |f| %>
<%= f.hidden_field :event, value: event %>
<% uuid = SecureRandom.uuid %>
<div class="flex">
<label for="<%= uuid %>" class="flex items-center space-x-2">
<%= f.check_box :value, class: 'base-checkbox', checked: preference[event] || (!preference.key?(event) && event.starts_with?('form.')), onchange: 'this.form.requestSubmit()', id: uuid %>
<span>
<%= event %>
</span>
</label>
</div>
<% end %>
<% if @webhook_url.persisted? %>
<a href="<%= webhook_secret_path(@webhook_url) %>" data-turbo-frame="modal" class="white-button w-full md:w-auto">
<%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %>
</a>
<% end %>
</div>
<% end %>
<%= form_for @webhook_url, url: @webhook_url.url.present? ? webhook_preference_path(@webhook_url) : '', method: :put, html: { autocomplete: 'off' } do |f| %>
<% WebhookUrl::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 mt-4 gap-y-2">
<% events.each do |event| %>
<%= f.fields_for :events do |ff| %>
<div class="flex">
<label class="flex items-center space-x-2 cursor-pointer">
<%= ff.check_box event, class: 'base-checkbox', checked: @webhook_url.events.include?(event), onchange: 'this.form.requestSubmit()', disabled: @webhook_url.url.blank? %>
<span>
<%= event %>
</span>
</label>
</div>
<% end %>
<% end %>
</div>
<% end %>
<% end %>
</div>
</div>
<% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %>
@@ -47,7 +48,7 @@
<span>
<%= t('submission_example_payload') %>
</span>
<% if @encrypted_config.value.present? %>
<% if @webhook_url.url.present? && @webhook_url.events.include?('form.completed') %>
<%= button_to button_title(title: 'Test Webhook', disabled_with: t('sending'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %>
<% end %>
</div>
+1 -1
View File
@@ -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
+73 -19
View File
@@ -20,6 +20,7 @@ en: &en
language_ko: 한국어
hi_there: Hi there
thanks: Thanks
unarchive: Unarchive
first_party: 'First Party'
docuseal_trusted_signature: DocuSeal Trusted Signature
hello_name: Hello %{name}
@@ -119,7 +120,7 @@ en: &en
profile_details: Profile Details
sign_up_with_google: Sign up with Google
sign_up_with_microsoft: Sign up with Microsoft
by_creating_an_account_you_agree_to_our_html: 'By creating an account, you agree to our <a target="_blank" href="https://www.docuseal.co/privacy">Privacy Policy</a> and <a target="_blank" href="https://www.docuseal.co/terms">Terms of Service</a>.'
by_creating_an_account_you_agree_to_our_html: 'By creating an account, you agree to our <a target="_blank" href="https://www.docuseal.com/privacy">Privacy Policy</a> and <a target="_blank" href="https://www.docuseal.com/terms">Terms of Service</a>.'
enter_email_to_continue: Enter email to continue
account: Account
preferences: Preferences
@@ -134,6 +135,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
@@ -472,8 +474,8 @@ en: &en
logo: Logo
back: Back
add_secret: Add Secret
edit_secret: Edit 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
@@ -498,6 +500,7 @@ en: &en
document_template_has_been_moved: Document template has been moved.
unable_to_move_template_into_folder: Unable to move template into folder.
template_has_been_unarchived: Template has been unarchived.
submission_has_been_unarchived: Submission has been unarchived.
unable_to_update_file: Unable to upload file.
user_has_been_invited: User has been invited.
unable_to_update_user: Unable to update user.
@@ -618,6 +621,12 @@ 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
key: Key
value: Value
webhook_secret: Webhook Secret
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}'
@@ -654,6 +663,7 @@ en: &en
read: Read your data
es: &es
unarchive: Desarchivar
awaiting_completion_by_the_other_party: "Esperando la finalización por la otra parte"
enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios'
first_party: 'Primera Parte'
@@ -755,7 +765,7 @@ es: &es
profile_details: Detalles del perfil
sign_up_with_google: Registrarse con Google
sign_up_with_microsoft: Registrarse con Microsoft
by_creating_an_account_you_agree_to_our_html: 'Al crear una cuenta, aceptas nuestra <a target="_blank" href="https://www.docuseal.co/privacy">Política de Privacidad</a> y <a target="_blank" href="https://www.docuseal.co/terms">Términos de Servicio</a>.'
by_creating_an_account_you_agree_to_our_html: 'Al crear una cuenta, aceptas nuestra <a target="_blank" href="https://www.docuseal.com/privacy">Política de Privacidad</a> y <a target="_blank" href="https://www.docuseal.com/terms">Términos de Servicio</a>.'
enter_email_to_continue: Ingresa tu correo electrónico para continuar
account: Cuenta
preferences: Preferencias
@@ -770,6 +780,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
@@ -1108,8 +1119,8 @@ es: &es
logo: Logotipo
back: Atrás
add_secret: Agregar secreto
edit_secret: Editar 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
@@ -1134,6 +1145,7 @@ es: &es
document_template_has_been_moved: La plantilla de documento ha sido movida.
unable_to_move_template_into_folder: No se pudo mover la plantilla a la carpeta.
template_has_been_unarchived: La plantilla ha sido desarchivada.
submission_has_been_unarchived: La presentación ha sido desarchivada.
unable_to_update_file: No se pudo subir el archivo.
user_has_been_invited: El usuario ha sido invitado.
unable_to_update_user: No se pudo actualizar el usuario.
@@ -1254,6 +1266,12 @@ 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
key: Clave
value: Valor
webhook_secret: Secreto del Webhook
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}'
@@ -1290,6 +1308,7 @@ es: &es
read: Leer tus datos
it: &it
unarchive: Ripristina
awaiting_completion_by_the_other_party: "In attesa di completamento da parte dell'altra parte"
enforce_recipients_order: Aplicar el orden de los destinatarios
first_party: 'Prima parte'
@@ -1391,7 +1410,7 @@ it: &it
profile_details: Dettagli del profilo
sign_up_with_google: Registrati con Google
sign_up_with_microsoft: Registrati con Microsoft
by_creating_an_account_you_agree_to_our_html: 'Creando un account, accetti la nostra <a target="_blank" href="https://www.docuseal.co/privacy">Privacy Policy</a> e i nostri <a target="_blank" href="https://www.docuseal.co/terms">Termini di servizio</a>.'
by_creating_an_account_you_agree_to_our_html: 'Creando un account, accetti la nostra <a target="_blank" href="https://www.docuseal.com/privacy">Privacy Policy</a> e i nostri <a target="_blank" href="https://www.docuseal.com/terms">Termini di servizio</a>.'
enter_email_to_continue: "Inserisci l'email per continuare"
account: Account
preferences: Preferenze
@@ -1406,6 +1425,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
@@ -1744,8 +1764,8 @@ it: &it
logo: Logo
back: Indietro
add_secret: Aggiungi segreto
edit_secret: Modifica 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
@@ -1770,6 +1790,7 @@ it: &it
document_template_has_been_moved: Il modello di documento è stato spostato.
unable_to_move_template_into_folder: Impossibile spostare il modello nella cartella.
template_has_been_unarchived: Il modello è stato disarchiviato.
submission_has_been_unarchived: La sottomissione è stata dearchiviata.
unable_to_update_file: Impossibile caricare il file.
user_has_been_invited: "L'utente è stato invitato."
unable_to_update_user: Impossibile aggiornare l'utente.
@@ -1890,6 +1911,12 @@ 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
key: Chiave
value: Valore
webhook_secret: Segreto del Webhook
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}'
@@ -1926,6 +1953,7 @@ it: &it
read: Leggi i tuoi dati
fr: &fr
unarchive: Désarchiver
awaiting_completion_by_the_other_party: "En attente de la complétion par l'autre partie"
enforce_recipients_order: "Respecter l'ordre des destinataires"
first_party: 'Première partie'
@@ -2028,7 +2056,7 @@ fr: &fr
sign_up_with_google: S'inscrire avec Google
sign_up_with_microsoft: S'inscrire avec Microsoft
by_creating_an_account_you_agree_to_our_html: |
En créant un compte, vous acceptez notre <a target="_blank" href="https://www.docuseal.co/privacy">Politique de Confidentialité</a> et nos <a target="_blank" href="https://www.docuseal.co/terms">Conditions d'Utilisation</a>.
En créant un compte, vous acceptez notre <a target="_blank" href="https://www.docuseal.com/privacy">Politique de Confidentialité</a> et nos <a target="_blank" href="https://www.docuseal.com/terms">Conditions d'Utilisation</a>.
enter_email_to_continue: Entrez votre e-mail pour continuer
account: Compte
preferences: Préférences
@@ -2043,6 +2071,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"
@@ -2381,8 +2410,8 @@ fr: &fr
logo: Logo
back: Retour
add_secret: Ajouter un secret
edit_secret: Modifier le 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
@@ -2407,6 +2436,7 @@ fr: &fr
document_template_has_been_moved: Le modèle de document a été déplacé.
unable_to_move_template_into_folder: Impossible de déplacer le modèle dans le dossier.
template_has_been_unarchived: Le modèle a été désarchivé.
submission_has_been_unarchived: La soumission a été désarchivée.
unable_to_update_file: Impossible de télécharger le fichier.
user_has_been_updated: "L'utilisateur a été mis à jour."
unable_to_remove_user: Impossible de supprimer l'utilisateur.
@@ -2527,6 +2557,12 @@ 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
key: Clé
value: Valeur
webhook_secret: Secret du Webhook
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}'
@@ -2563,6 +2599,7 @@ fr: &fr
read: Lire vos données
pt: &pt
unarchive: Desarquivar
awaiting_completion_by_the_other_party: "Aguardando a conclusão pela outra parte"
enforce_recipients_order: 'Forçar a ordem dos recipientes'
first_party: 'Primeira Parte'
@@ -2664,7 +2701,7 @@ pt: &pt
profile_details: Detalhes do perfil
sign_up_with_google: Inscrever-se com Google
sign_up_with_microsoft: Inscrever-se com Microsoft
by_creating_an_account_you_agree_to_our_html: 'Ao criar uma conta, você concorda com nossa <a target="_blank" href="https://www.docuseal.co/privacy">Política de Privacidade</a> e <a target="_blank" href="https://www.docuseal.co/terms">Termos de Serviço</a>.'
by_creating_an_account_you_agree_to_our_html: 'Ao criar uma conta, você concorda com nossa <a target="_blank" href="https://www.docuseal.com/privacy">Política de Privacidade</a> e <a target="_blank" href="https://www.docuseal.com/terms">Termos de Serviço</a>.'
enter_email_to_continue: Insira o e-mail para continuar
account: Conta
preferences: Preferências
@@ -2679,6 +2716,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
@@ -3017,8 +3055,8 @@ pt: &pt
logo: Logotipo
back: Voltar
add_secret: Adicionar segredo
edit_secret: Editar 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
@@ -3043,6 +3081,7 @@ pt: &pt
document_template_has_been_moved: O modelo de documento foi movido.
unable_to_move_template_into_folder: Não foi possível mover o modelo para a pasta.
template_has_been_unarchived: O modelo foi desarquivado.
submission_has_been_unarchived: A submissao foi desarquivada.
unable_to_update_file: Não foi possível atualizar o arquivo.
user_has_been_updated: O usuário foi atualizado.
unable_to_remove_user: Não foi possível remover o usuário.
@@ -3163,6 +3202,12 @@ 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
key: Chave
value: Valor
webhook_secret: Segredo do Webhook
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}'
@@ -3199,6 +3244,7 @@ pt: &pt
read: Ler seus dados
de: &de
unarchive: Wiederherstellen
awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei"
enforce_recipients_order: 'Empfängerreihenfolge durchsetzen'
first_party: 'Erste Partei'
@@ -3300,7 +3346,7 @@ de: &de
profile_details: Profildetails
sign_up_with_google: Mit Google registrieren
sign_up_with_microsoft: Mit Microsoft registrieren
by_creating_an_account_you_agree_to_our_html: 'Durch die Erstellung eines Kontos stimmst du unseren <a target="_blank" href="https://www.docuseal.co/privacy">Datenschutzrichtlinien</a> und <a target="_blank" href="https://www.docuseal.co/terms">Nutzungsbedingungen</a> zu.'
by_creating_an_account_you_agree_to_our_html: 'Durch die Erstellung eines Kontos stimmst du unseren <a target="_blank" href="https://www.docuseal.com/privacy">Datenschutzrichtlinien</a> und <a target="_blank" href="https://www.docuseal.com/terms">Nutzungsbedingungen</a> zu.'
enter_email_to_continue: E-Mail eingeben, um fortzufahren
account: Konto
preferences: Einstellungen
@@ -3315,6 +3361,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
@@ -3653,8 +3700,8 @@ de: &de
logo: Logo
back: Zurück
add_secret: Geheimnis hinzufügen
edit_secret: Geheimnis bearbeiten
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
@@ -3679,6 +3726,7 @@ de: &de
document_template_has_been_moved: Die Dokumentvorlage wurde verschoben.
unable_to_move_template_into_folder: Vorlage konnte nicht in den Ordner verschoben werden.
template_has_been_unarchived: Die Vorlage wurde entarchiviert.
submission_has_been_unarchived: Die Einreichung wurde wiederhergestellt.
unable_to_update_file: Datei konnte nicht hochgeladen werden.
user_has_been_updated: Der Benutzer wurde aktualisiert.
unable_to_remove_user: Benutzer konnte nicht entfernt werden.
@@ -3799,6 +3847,12 @@ 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
key: Schlüssel
value: Wert
webhook_secret: Webhook-Geheimnis
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}'
@@ -3885,7 +3939,7 @@ pl:
profile_details: Szczegóły profilu
sign_up_with_google: Zarejestruj się przez Google
sign_up_with_microsoft: Zarejestruj się przez Microsoft
by_creating_an_account_you_agree_to_our_html: 'Tworząc konto, akceptujesz naszą <a target="_blank" href="https://www.docuseal.co/privacy">Politykę Prywatności</a> i <a target="_blank" href="https://www.docuseal.co/terms">Regulamin</a>.'
by_creating_an_account_you_agree_to_our_html: 'Tworząc konto, akceptujesz naszą <a target="_blank" href="https://www.docuseal.com/privacy">Politykę Prywatności</a> i <a target="_blank" href="https://www.docuseal.com/terms">Regulamin</a>.'
enter_email_to_continue: Wprowadź e-mail, aby kontynuować
uk:
@@ -3939,7 +3993,7 @@ uk:
profile_details: Дані профілю
sign_up_with_google: Зареєструватися через Google
sign_up_with_microsoft: Зареєструватися через Microsoft
by_creating_an_account_you_agree_to_our_html: 'Створюючи акаунт, ви погоджуєтесь з нашою <a target="_blank" href="https://www.docuseal.co/privacy">Політикою конфіденційності</a> і <a target="_blank" href="https://www.docuseal.co/terms">Умовами надання послуг</a>.'
by_creating_an_account_you_agree_to_our_html: 'Створюючи акаунт, ви погоджуєтесь з нашою <a target="_blank" href="https://www.docuseal.com/privacy">Політикою конфіденційності</a> і <a target="_blank" href="https://www.docuseal.com/terms">Умовами надання послуг</a>.'
enter_email_to_continue: Введіть електронну пошту, щоб продовжити
cs:
@@ -3993,7 +4047,7 @@ cs:
profile_details: Detaily profilu
sign_up_with_google: Zaregistrovat se pomocí Googlu
sign_up_with_microsoft: Zaregistrovat se pomocí Microsoftu
by_creating_an_account_you_agree_to_our_html: 'Vytvořením účtu souhlasíte s našimi <a target="_blank" href="https://www.docuseal.co/privacy">Zásadami ochrany osobních údajů</a> a <a target="_blank" href="https://www.docuseal.co/terms">Podmínkami služby</a>.'
by_creating_an_account_you_agree_to_our_html: 'Vytvořením účtu souhlasíte s našimi <a target="_blank" href="https://www.docuseal.com/privacy">Zásadami ochrany osobních údajů</a> a <a target="_blank" href="https://www.docuseal.com/terms">Podmínkami služby</a>.'
enter_email_to_continue: Zadejte e-mail pro pokračování
he:
@@ -4047,7 +4101,7 @@ he:
profile_details: פרטי הפרופיל
sign_up_with_google: הירשם עם גוגל
sign_up_with_microsoft: הירשם עם מיקרוסופט
by_creating_an_account_you_agree_to_our_html: 'על ידי יצירת חשבון, אתה מסכים ל<a target="_blank" href="https://www.docuseal.co/privacy">מדיניות הפרטיות</a> ול<a target="_blank" href="https://www.docuseal.co/terms">תנאי השירות</a> שלנו.'
by_creating_an_account_you_agree_to_our_html: 'על ידי יצירת חשבון, אתה מסכים ל<a target="_blank" href="https://www.docuseal.com/privacy">מדיניות הפרטיות</a> ול<a target="_blank" href="https://www.docuseal.com/terms">תנאי השירות</a> שלנו.'
enter_email_to_continue: הכנס דוא"ל כדי להמשיך
nl:
@@ -4101,7 +4155,7 @@ nl:
profile_details: Profielgegevens
sign_up_with_google: Aanmelden met Google
sign_up_with_microsoft: Aanmelden met Microsoft
by_creating_an_account_you_agree_to_our_html: 'Door een account aan te maken, ga je akkoord met ons <a target="_blank" href="https://www.docuseal.co/privacy">Privacybeleid</a> en onze <a target="_blank" href="https://www.docuseal.co/terms">Gebruiksvoorwaarden</a>.'
by_creating_an_account_you_agree_to_our_html: 'Door een account aan te maken, ga je akkoord met ons <a target="_blank" href="https://www.docuseal.com/privacy">Privacybeleid</a> en onze <a target="_blank" href="https://www.docuseal.com/terms">Gebruiksvoorwaarden</a>.'
enter_email_to_continue: Voer e-mail in om door te gaan
ar:
@@ -4155,7 +4209,7 @@ ar:
profile_details: تفاصيل الملف الشخصي
sign_up_with_google: الاشتراك باستخدام جوجل
sign_up_with_microsoft: الاشتراك باستخدام مايكروسوفت
by_creating_an_account_you_agree_to_our_html: 'من خلال إنشاء حساب، فإنك توافق على <a target="_blank" href="https://www.docuseal.co/privacy">سياسة الخصوصية</a> و<a target="_blank" href="https://www.docuseal.co/terms">شروط الخدمة</a> الخاصة بنا.'
by_creating_an_account_you_agree_to_our_html: 'من خلال إنشاء حساب، فإنك توافق على <a target="_blank" href="https://www.docuseal.com/privacy">سياسة الخصوصية</a> و<a target="_blank" href="https://www.docuseal.com/terms">شروط الخدمة</a> الخاصة بنا.'
enter_email_to_continue: أدخل البريد الإلكتروني للمتابعة
ko:
@@ -4209,7 +4263,7 @@ ko:
profile_details: 프로필 세부 정보
sign_up_with_google: Google로 가입
sign_up_with_microsoft: Microsoft로 가입
by_creating_an_account_you_agree_to_our_html: '계정을 생성함으로써, 귀하는 우리의 <a target="_blank" href="https://www.docuseal.co/privacy">개인정보 보호정책</a> 및 <a target="_blank" href="https://www.docuseal.co/terms">서비스 약관</a>에 동의하는 것입니다.'
by_creating_an_account_you_agree_to_our_html: '계정을 생성함으로써, 귀하는 우리의 <a target="_blank" href="https://www.docuseal.com/privacy">개인정보 보호정책</a> 및 <a target="_blank" href="https://www.docuseal.com/terms">서비스 약관</a>에 동의하는 것입니다.'
enter_email_to_continue: 계속하려면 이메일을 입력하세요
en-US:
+9 -4
View File
@@ -34,6 +34,7 @@ Rails.application.routes.draw do
resources :submitter_form_views, only: %i[create]
resources :submitters, only: %i[index show update]
resources :submissions, only: %i[index show create destroy] do
resources :documents, only: %i[index], controller: 'submission_documents'
collection do
resources :init, only: %i[create], controller: 'submissions'
resources :emails, only: %i[create], controller: 'submissions', as: :submissions_emails
@@ -68,7 +69,9 @@ Rails.application.routes.draw do
resource :user_initials, only: %i[edit update destroy]
resources :submissions_archived, only: %i[index], path: 'submissions/archived'
resources :submissions, only: %i[index], controller: 'submissions_dashboard'
resources :submissions, only: %i[show destroy]
resources :submissions, only: %i[show destroy] do
resources :unarchive, only: %i[create], controller: 'submissions_unarchive'
end
resources :submitters, only: %i[edit update]
resources :console_redirect, only: %i[index]
resources :upgrade, only: %i[index], controller: 'console_redirect'
@@ -77,8 +80,8 @@ Rails.application.routes.draw do
resources :testing_api_settings, only: %i[index]
resources :submitters_autocomplete, only: %i[index]
resources :template_folders_autocomplete, only: %i[index]
resources :webhook_preferences, only: %i[create]
resources :webhook_secret, only: %i[index create]
resources :webhook_secret, only: %i[show update]
resources :webhook_preferences, only: %i[update]
resource :templates_upload, only: %i[create]
authenticated do
resource :templates_upload, only: %i[show], path: 'new'
@@ -152,15 +155,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
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class AddSecretToWebhookUrls < ActiveRecord::Migration[7.2]
class MigrationWebhookUrl < ApplicationRecord
self.table_name = 'webhook_urls'
serialize :secret, coder: JSON
encrypts :url, :secret
end
def change
add_column :webhook_urls, :secret, :text
MigrationWebhookUrl.all.each do |url|
url.update_columns(secret: {})
end
change_column_null :webhook_urls, :secret, false
end
end
@@ -0,0 +1,53 @@
# frozen_string_literal: true
class PopulateWebhookUrls < ActiveRecord::Migration[7.2]
disable_ddl_transaction
class MigrationWebhookUrl < ActiveRecord::Base
self.table_name = 'webhook_urls'
serialize :events, coder: JSON
serialize :secret, coder: JSON
encrypts :url, :secret
end
class MigrationEncryptedConfig < ActiveRecord::Base
self.table_name = 'encrypted_configs'
encrypts :value
serialize :value, coder: JSON
end
class MigrationAccountConfig < ActiveRecord::Base
self.table_name = 'account_configs'
serialize :value, coder: JSON
end
def up
MigrationEncryptedConfig.where(key: 'webhook_url').find_each do |config|
webhook_url = MigrationWebhookUrl.find_or_initialize_by(account_id: config.account_id,
sha1: Digest::SHA1.hexdigest(config.value))
webhook_url.secret =
MigrationEncryptedConfig.find_by(account_id: config.account_id, key: 'webhook_secret')&.value.to_h
preferences =
MigrationAccountConfig.find_by(account_id: config.account_id, key: 'webhook_preferences')&.value.to_h
events = %w[form.viewed form.started form.completed form.declined].reject { |event| preferences[event] == false }
events += preferences.compact_blank.keys
webhook_url.events = events.uniq
webhook_url.url = config.value
webhook_url.save!
end
end
def down
nil
end
end
+25 -1
View File
@@ -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_29_192232) 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
@@ -344,6 +367,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_08_20_180922) do
t.string "sha1", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "secret", null: false
t.index ["account_id"], name: "index_webhook_urls_on_account_id"
t.index ["sha1"], name: "index_webhook_urls_on_sha1"
end
+1
View File
@@ -22,5 +22,6 @@ class Ability
can :manage, UserConfig, user_id: user.id
can :manage, Account, id: user.account_id
can :manage, AccessToken, user_id: user.id
can :manage, WebhookUrl, account_id: user.account_id
end
end
+1 -25
View File
@@ -11,7 +11,7 @@ module Accounts
new_user.uuid = SecureRandom.uuid
new_user.account = new_account
new_user.encrypted_password = SecureRandom.hex
new_user.email = "#{SecureRandom.hex}@docuseal.co"
new_user.email = "#{SecureRandom.hex}@docuseal.com"
account.templates.each do |template|
new_template = template.dup
@@ -78,30 +78,6 @@ module Accounts
new_template
end
def load_webhook_url(account)
load_webhook_config(account)&.value.presence
end
def load_webhook_config(account)
configs = account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
if !configs && !Docuseal.multitenant? && !account.testing?
configs = Account.order(:id).first.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end
configs
end
def load_webhook_preferences(account)
configs = account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
unless Docuseal.multitenant?
configs ||= Account.order(:id).first.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
end
configs&.value.presence || {}
end
def load_signing_pkcs(account)
cert_data =
if Docuseal.multitenant?
+15 -7
View File
@@ -16,19 +16,27 @@ module ActionMailerConfigsInterceptor
end
if Rails.env.production? && Rails.application.config.action_mailer.delivery_method
message.from = ENV.fetch('SMTP_FROM')
from = ENV.fetch('SMTP_FROM').to_s.split(',').sample
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
+1 -1
View File
@@ -22,7 +22,7 @@ module ActionMailerEventsObserver
emailable_type:,
event_type: :send,
email:,
data: { method: mail.delivery_method.class.name.underscore },
data: { from: mail.from, method: mail.delivery_method.class.name.underscore },
event_datetime: Time.current
)
end
+5 -5
View File
@@ -2,7 +2,7 @@
module Docuseal
URL_CACHE = ActiveSupport::Cache::MemoryStore.new
PRODUCT_URL = 'https://www.docuseal.co'
PRODUCT_URL = 'https://www.docuseal.com'
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
PRODUCT_NAME = 'DocuSeal'
@@ -12,7 +12,7 @@ module Docuseal
TWITTER_URL = 'https://twitter.com/docusealco'
TWITTER_HANDLE = '@docusealco'
CHATGPT_URL = 'https://chatgpt.com/g/g-9hg8AAw0r-docuseal'
SUPPORT_EMAIL = 'support@docuseal.co'
SUPPORT_EMAIL = 'support@docuseal.com'
HOST = ENV.fetch('HOST', 'localhost')
AATL_CERT_NAME = 'docuseal_aatl'
CONSOLE_URL = if Rails.env.development?
@@ -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', '{}'))
+24
View File
@@ -2,6 +2,8 @@
module Params
class BaseValidator
EMAIL_REGEXP = User::FULL_EMAIL_REGEXP
InvalidParameterError = Class.new(StandardError)
def self.call(...)
@@ -65,6 +67,28 @@ module Params
raise_error(message || "#{key} must follow the #{regexp.source} format")
end
def email_format(params, key, message: nil)
return if params.blank?
return if params[key].blank?
return if params[key].to_s.include?('<')
if params[key].to_s.strip.split(/\s*[;,]\s*/).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)
return unless params.is_a?(Array)
return if params.none?
return if params.all? { |p| p[key].blank? }
return if params.pluck(key).compact_blank.uniq.size == params.pluck(key).compact_blank.size
raise_error(message || "#{key} must be unique")
end
def in_path(params, path = [])
old_path = @current_path
+7 -5
View File
@@ -22,6 +22,7 @@ module Params
required(params, %i[emails email])
type(params, :emails, String)
email_format(params, :emails, message: 'emails are invalid')
boolean(params, :send_email)
type(params, :message, Hash)
@@ -43,8 +44,8 @@ module Params
type(params, :completed_redirect_url, String)
type(params, :bcc_completed, String)
type(params, :reply_to, String)
format(params, :bcc_completed, /@/, message: 'bcc_completed email is invalid')
format(params, :reply_to, /@/, message: 'reply_to email is invalid')
email_format(params, :bcc_completed, message: 'bcc_completed email is invalid')
email_format(params, :reply_to, message: 'reply_to email is invalid')
type(params, :message, Hash)
type(params, :submitters, Array)
@@ -60,6 +61,7 @@ module Params
if params[:submitters].present?
in_path(params, :submitters) do |submitters_params|
type(submitters_params, 0, Hash)
unique_value(submitters_params, :role)
end
end
@@ -74,8 +76,8 @@ module Params
type(submitter_params, :name, String)
type(submitter_params, :reply_to, String)
type(submitter_params, :email, String)
format(submitter_params, :email, /@/, message: 'email is invalid')
format(submitter_params, :reply_to, /@/, message: 'reply_to email is invalid')
email_format(submitter_params, :email, message: 'email is invalid')
email_format(submitter_params, :reply_to, message: 'reply_to email is invalid')
type(submitter_params, :phone, String)
format(submitter_params, :phone, /\A\+\d+\z/,
message: 'phone should start with +<country code> and contain only digits')
@@ -105,7 +107,7 @@ module Params
type(params, :order, String)
type(params, :completed_redirect_url, String)
type(params, :bcc_completed, String)
format(params, :bcc_completed, /@/, message: 'bcc_completed email is invalid')
email_format(params, :bcc_completed, message: 'bcc_completed email is invalid')
type(params, :message, Hash)
in_path(params, :message) do |message_params|
@@ -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|
+11 -2
View File
@@ -291,7 +291,16 @@ module Submissions
),
if field['type'].in?(%w[image signature initials stamp])
attachment = submitter.attachments.find { |a| a.uuid == value }
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
image =
begin
Vips::Image.new_from_buffer(attachment.download, '').autorot
rescue Vips::Error
next unless attachment.content_type.starts_with?('image/')
next if attachment.byte_size.zero?
raise
end
scale = [600.0 / image.width, 600.0 / image.height].min
@@ -393,7 +402,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
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff. Show more