Skip to content

Commit

Permalink
Merge from docusealco/wip
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexBTurchyn authored Oct 28, 2024
2 parents fed420c + 8773b30 commit 05d578d
Show file tree
Hide file tree
Showing 57 changed files with 782 additions and 183 deletions.
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ RSpec/ExampleLength:
Max: 40

RSpec/MultipleMemoizedHelpers:
Max: 6
Max: 9

Metrics/BlockNesting:
Max: 4
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions app/controllers/account_configs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 29 additions & 2 deletions app/controllers/api/api_base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ class ApiBaseController < ActionController::API
render json: { error: 'Too many requests' }, status: :too_many_requests
end

if Rails.env.production?
unless Rails.env.development?
rescue_from CanCan::AccessDenied do |e|
render json: { error: e.message }, status: :forbidden
render json: { error: access_denied_error_message(e) }, status: :forbidden
end

rescue_from JSON::ParserError do |e|
Expand All @@ -39,6 +39,33 @@ class ApiBaseController < ActionController::API

private

def access_denied_error_message(error)
return 'Not authorized' if request.headers['X-Auth-Token'].blank?
return 'Not authorized' unless error.subject.is_a?(ActiveRecord::Base)
return 'Not authorized' unless error.subject.respond_to?(:account_id)

linked_account_record_exists =
if current_user.account.testing?
current_user.account.linked_account_accounts.where(account_type: 'testing')
.exists?(account_id: error.subject.account_id)
else
current_user.account.testing_accounts.exists?(id: error.subject.account_id)
end

return 'Not authorized' unless linked_account_record_exists

object_name = error.subject.model_name.human
id = error.subject.id

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)
Expand Down
7 changes: 4 additions & 3 deletions app/controllers/api/submissions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,14 @@ def create
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
Expand Down
1 change: 1 addition & 0 deletions app/controllers/api/submitters_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def assign_submitter_attrs(submitter, attrs)
end&.dig('uuid')

submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.name = attrs[:name] if attrs.key?(:name)

if attrs.key?(:phone)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
Expand Down
5 changes: 1 addition & 4 deletions app/controllers/api/tools_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ def verify
pdf = HexaPDF::Document.new(io: StringIO.new(file))

trusted_certs = Accounts.load_trusted_certs(current_account)

is_checksum_found = ActiveStorage::Attachment.joins(:blob)
.where(name: 'documents', record_type: 'Submitter')
.exists?(blob: { checksum: Digest::MD5.base64digest(file) })
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))

render json: {
checksum_status: is_checksum_found ? 'verified' : 'not_found',
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/email_smtp_settings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def index; end

def create
if @encrypted_config.update(email_configs)
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!

redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
else
Expand Down
6 changes: 4 additions & 2 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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 () {
Expand Down
18 changes: 18 additions & 0 deletions app/javascript/elements/masked_input.js
Original file line number Diff line number Diff line change
@@ -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 changes: 1 addition & 1 deletion app/javascript/submission_form/completed.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 />
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/submission_form/form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/template_builder/conditions_modal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions app/jobs/process_submitter_completion_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -24,9 +26,42 @@ def perform(params = {})
enqueue_completed_emails(submitter)
end

create_completed_documents!(submitter)

enqueue_completed_webhooks(submitter, is_all_completed:)
end

def create_completed_submitter!(submitter)
completed_submitter = CompletedSubmitter.find_or_initialize_by(submitter_id: submitter.id)

return completed_submitter if completed_submitter.persisted?

submission = submitter.submission

completed_submitter.assign_attributes(
submission_id: submitter.submission_id,
account_id: submission.account_id,
template_id: submission.template_id,
source: submission.source,
sms_count: submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count,
completed_at: submitter.completed_at
)

completed_submitter.save!

completed_submitter
rescue ActiveRecord::RecordNotUnique
retry
end

def create_completed_documents!(submitter)
submitter.documents.filter_map do |attachment|
next if attachment.metadata['sha256'].blank?

CompletedDocument.find_or_create_by!(sha256: attachment.metadata['sha256'], submitter_id: submitter.id)
end
end

def enqueue_completed_webhooks(submitter, is_all_completed: false)
webhook_config = Accounts.load_webhook_config(submitter.account)

Expand Down
2 changes: 1 addition & 1 deletion app/mailers/application_mailer.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/mailers/settings_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions app/models/account_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class AccountConfig < ApplicationRecord
FORCE_MFA = 'force_mfa'
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
SUBMITTER_REMINDERS = 'submitter_reminders'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
Expand Down
22 changes: 22 additions & 0 deletions app/models/completed_document.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions app/models/completed_submitter.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions app/views/accounts/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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| %>
Expand Down
7 changes: 5 additions & 2 deletions app/views/api_settings/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
4 changes: 2 additions & 2 deletions app/views/devise/shared/_select_server.html.erb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<div class="text-center">
<div class="join">
<a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' || request.host == 'docuseal.com' %>">
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.com' %>">
<%= svg_icon 'world', class: 'w-5 h-5' %>
Global
</a>
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.co', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
Europe
</a>
Expand Down
Loading

0 comments on commit 05d578d

Please sign in to comment.