Compare commits

..
20 Commits
Author SHA1 Message Date
Pete Matsyburka e6785b0afd fix sign in form without oauth 2024-01-22 09:32:19 +02:00
Pete Matsyburka 009033a411 fix params validation 2024-01-22 09:32:18 +02:00
Alex Turchyn fdc3d7742a fix recipients switch 2024-01-21 18:07:29 +02:00
Alex Turchyn f9ccca1376 add base API error messages 2024-01-21 15:19:16 +02:00
Alex Turchyn 0a00fe6ab8 fix minor mobile UI issues 2024-01-21 13:36:58 +02:00
Pete Matsyburka 6253e80a70 add testing environment 2024-01-21 11:20:34 +02:00
Pete Matsyburka 3dcb7c813f fix rspec 2024-01-19 12:45:43 +02:00
Pete Matsyburka 39cdc41a67 fix erblint 2024-01-19 12:41:36 +02:00
Pete Matsyburka d3000487f4 prevent builder submitter click # nav 2024-01-19 10:15:21 +02:00
Pete Matsyburka 80254da1c1 adjust consider json middleware 2024-01-19 09:54:49 +02:00
Pete Matsyburka 73fc2d92f7 fix annotations 2024-01-19 01:15:13 +02:00
Pete Matsyburka f69aa33f3d i18n builder confirm 2024-01-18 21:12:08 +02:00
Pete Matsyburka e16202c9f7 ajust 2fa message 2024-01-18 10:33:25 +02:00
Pete Matsyburka 7453d0d274 reduce image size 2024-01-17 22:22:50 +02:00
Pete Matsyburka c102828c93 fix manual send button 2024-01-17 13:52:38 +02:00
Alex Turchyn afeb6dd318 optimize submissions show page for mobile devices 2024-01-17 01:45:44 +02:00
Pete Matsyburka 6156623f0e remove welcome message 2024-01-17 01:45:44 +02:00
Pete Matsyburka ccc06fcc3d allow to remove template permanently 2024-01-17 01:45:40 +02:00
Pete Matsyburka bbe3942056 validate sender email 2024-01-16 23:46:41 +02:00
Pete Matsyburka 49cea59b94 do not authenticate archived users 2024-01-16 00:18:58 +02:00
50 changed files with 611 additions and 161 deletions

No files matched your search

+1 -1
View File
@@ -43,7 +43,7 @@ RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips
COPY ./Gemfile ./Gemfile.lock ./
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle /usr/local/bundle/cache && find /usr/local/bundle -type d \( -name "rdoc" -o -name "spec" \) -exec rm -rf {} +
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
COPY ./bin ./bin
COPY ./app ./app
+1
View File
@@ -26,6 +26,7 @@ gem 'pagy'
gem 'pdf-reader'
gem 'pg', require: false
gem 'premailer-rails'
gem 'pretender'
gem 'puma'
gem 'rack'
gem 'rails'
+3
View File
@@ -354,6 +354,8 @@ GEM
actionmailer (>= 3)
net-smtp
premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0)
actionpack (>= 6.1)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
@@ -596,6 +598,7 @@ DEPENDENCIES
pdf-reader
pg
premailer-rails
pretender
pry-rails
puma
rack
+7 -1
View File
@@ -8,11 +8,17 @@ module Api
DEFAULT_LIMIT = 10
MAX_LIMIT = 100
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
wrap_parameters false
before_action :authenticate_user!
check_authorization
rescue_from Params::BaseValidator::InvalidParameterError do |e|
render json: { error: e.message }, status: :unprocessable_entity
end
if Rails.env.production?
rescue_from CanCan::AccessDenied do |e|
Rollbar.error(e) if defined?(Rollbar)
@@ -44,7 +50,7 @@ module Api
if request.headers['X-Auth-Token'].present?
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
User.joins(:access_token).find_by(access_token: { sha256: })
User.joins(:access_token).active.find_by(access_token: { sha256: })
end
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
@@ -63,6 +63,8 @@ module Api
end
def create
Params::SubmissionCreateValidator.call(params, dry_run: true)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
if @template.fields.blank?
+5 -1
View File
@@ -38,7 +38,11 @@ module Api
end
def destroy
@template.update!(archived_at: Time.current)
if params[:permanently] == 'true' && !Docuseal.multitenant?
@template.destroy!
else
@template.update!(archived_at: Time.current)
end
render json: @template.as_json(only: %i[id archived_at])
end
+12 -1
View File
@@ -16,6 +16,8 @@ class ApplicationController < ActionController::Base
:current_account,
:svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
rescue_from Pagy::OverflowError do
redirect_to request.path
end
@@ -24,7 +26,7 @@ class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied do |e|
Rollbar.error(e) if defined?(Rollbar)
redirect_back(fallback_location: root_path, alert: e.message)
redirect_to root_path, alert: e.message
end
end
@@ -32,6 +34,15 @@ class ApplicationController < ActionController::Base
Docuseal.default_url_options
end
def impersonate_user(user)
raise ArgumentError unless user
raise Pretender::Error unless true_user
@impersonated_user = user
request.session[:impersonated_user_id] = user.uuid
end
private
def with_browser_locale(&)
@@ -5,9 +5,9 @@ class ConsoleRedirectController < ApplicationController
skip_authorization_check
def index
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if current_user.blank?
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
auth = JsonWebToken.encode(uuid: current_user.uuid,
auth = JsonWebToken.encode(uuid: true_user.uuid,
scope: :console,
exp: 1.minute.from_now.to_i)
@@ -11,9 +11,7 @@ class SubmittersSendEmailController < ApplicationController
alert: 'Email has been sent already.')
end
SubmitterMailer.invitation_email(@submitter).deliver_later!
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
SendSubmitterInvitationEmailJob.perform_later('submitter_id' => @submitter.id)
@submitter.sent_at ||= Time.current
@submitter.save!
@@ -0,0 +1,20 @@
# frozen_string_literal: true
class TestingAccountsController < ApplicationController
skip_authorization_check only: :destroy
def show
authorize!(:manage, current_account)
authorize!(:manage, current_user)
impersonate_user(Accounts.find_or_create_testing_user(current_account))
redirect_back(fallback_location: root_path)
end
def destroy
stop_impersonating_user
redirect_back(fallback_location: root_path)
end
end
@@ -0,0 +1,12 @@
# frozen_string_literal: true
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)
authorize!(:manage, @webhook_config)
end
end
+2 -2
View File
@@ -112,7 +112,7 @@
/>
<div
class="sticky bottom-0 py-2"
:style="withStickySubmitters ? { backgroundColor } : {}"
:style="{ backgroundColor }"
>
<Upload
v-if="sortedDocuments.length && editable && withUploadButton"
@@ -884,7 +884,7 @@ export default {
this.save()
},
onDocumentRemove (item) {
if (window.confirm('Are you sure?')) {
if (window.confirm(this.t('are_you_sure'))) {
this.template.schema.splice(this.template.schema.indexOf(item), 1)
}
@@ -61,7 +61,7 @@
<button
v-if="submitters.length > 1 && editable"
class="px-2"
@click.stop="remove(submitter)"
@click.prevent.stop="remove(submitter)"
>
<IconTrashX :width="18" />
</button>
@@ -165,7 +165,7 @@
title="Up"
class="relative w-2"
style="font-size: 10px; margin-bottom: -4px"
@click.stop="[move(submitter, -1), $refs.label.focus()] "
@click.prevent.stop="[move(submitter, -1), $refs.label.focus()] "
>
▲
</button>
@@ -173,7 +173,7 @@
title="Down"
class="relative w-2"
style="font-size: 10px; margin-top: -4px"
@click.stop="[move(submitter, 1), $refs.label.focus()] "
@click.prevent.stop="[move(submitter, 1), $refs.label.focus()] "
>
▼
</button>
@@ -181,7 +181,7 @@
<button
v-if="!compact && submitters.length > 1 && editable"
class="hidden group-hover:block px-2"
@click.stop="remove(submitter)"
@click.prevent.stop="remove(submitter)"
>
<IconTrashX :width="18" />
</button>
@@ -296,7 +296,7 @@ export default {
this.$emit('update:model-value', submitter.uuid)
},
remove (submitter) {
if (window.confirm('Are you sure?')) {
if (window.confirm(this.t('are_you_sure'))) {
this.$emit('remove', submitter)
}
},
+1
View File
@@ -1,4 +1,5 @@
const en = {
are_you_sure: 'Are you sure?',
sign_yourself: 'Sign Yourself',
send: 'Send',
save: 'Save',
@@ -4,7 +4,11 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
SubmitterMailer.invitation_email(submitter).deliver_now!
mail = SubmitterMailer.invitation_email(submitter)
Submitters::ValidateSending.call(submitter, mail)
mail.deliver_now!
SubmissionEvent.create!(submitter:, event_type: 'send_email')
+14
View File
@@ -22,12 +22,26 @@ class Account < ApplicationRecord
class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account
has_many :submissions, through: :templates
has_many :submitters, through: :submissions
has_many :account_linked_accounts, dependent: :destroy
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
class_name: 'AccountLinkedAccount',
inverse_of: :account
has_one :linked_account_account, dependent: :destroy,
foreign_key: :linked_account_id,
class_name: 'AccountLinkedAccount',
inverse_of: :linked_account
has_many :linked_accounts, through: :account_linked_accounts
has_many :testing_accounts, through: :account_testing_accounts, source: :linked_account
has_many :active_users, -> { active }, dependent: :destroy,
inverse_of: :account, class_name: 'User'
attribute :timezone, :string, default: 'UTC'
attribute :locale, :string, default: 'en-US'
def testing?
linked_account_account&.testing?
end
def default_template_folder
super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME,
author_id: users.minimum(:id)).tap(&:save!)
+36
View File
@@ -0,0 +1,36 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_linked_accounts
#
# id :bigint not null, primary key
# account_type :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# linked_account_id :bigint not null
#
# Indexes
#
# idx_on_account_id_linked_account_id_48ab9f79d2 (account_id,linked_account_id) UNIQUE
# index_account_linked_accounts_on_account_id (account_id)
# index_account_linked_accounts_on_linked_account_id (linked_account_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (linked_account_id => accounts.id)
#
class AccountLinkedAccount < ApplicationRecord
belongs_to :account
belongs_to :linked_account, class_name: 'Account'
attribute :account_type, :string, default: 'testing'
scope :testing, -> { where(account_type: :testing) }
def testing?
account_type == 'testing'
end
end
+2 -2
View File
@@ -59,7 +59,7 @@ class User < ApplicationRecord
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable, :lockable
attribute :role, :string, default: ADMIN_ROLE
attribute :uuid, :string, default: -> { SecureRandom.uuid }
@@ -72,7 +72,7 @@ class User < ApplicationRecord
end
def active_for_authentication?
!archived_at?
super && !archived_at?
end
def remember_me
+1 -1
View File
@@ -22,7 +22,7 @@
</div>
<% end %>
<% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %>
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %>
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) && !current_account.testing? %>
<%= f.fields_for encrypted_config do |ff| %>
<div class="form-control">
<%= ff.label :value, 'App URL', class: 'label' %>
+10 -5
View File
@@ -1,14 +1,19 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="flex-grow">
<h1 class="text-4xl font-bold mb-4">API</h1>
<div class="flex justify-between">
<h1 class="text-4xl font-bold mb-4">API</h1>
<%= render 'shared/test_mode_toggle' %>
</div>
<div class="card bg-base-200">
<div class="card-body p-6">
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
<div class="flex flex-row 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: 'Copy', copied_title: 'Copied' %>
<%= button_to button_title(title: 'Rotate', disabled_with: 'Rotate', icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button', data: { turbo_confirm: 'Remove existing API token and generated a new one. Are you sure?' } %>
<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: 'Copy', copied_title: 'Copied' %>
</div>
<%= button_to button_title(title: 'Rotate', disabled_with: 'Rotate', icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: 'Remove existing API token and generated a new one. Are you sure?' } %>
</div>
</div>
</div>
+69 -90
View File
@@ -1,101 +1,80 @@
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<% if @pagy.count > 0 || params[:q].present? || @template_folders.present? %>
<div class="flex justify-between mb-4 items-center">
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1>
<div class="flex space-x-2">
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<%= render 'shared/search_input' %>
<div class="flex justify-between mb-4 items-center">
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1>
<div class="flex space-x-2">
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<%= render 'shared/search_input' %>
<% end %>
<% if can?(:create, ::Template) %>
<%= render 'templates/upload_button' %>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Create</span>
<% end %>
<% if can?(:create, ::Template) %>
<%= render 'templates/upload_button' %>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Create</span>
<% end %>
<% end %>
</div>
<% end %>
</div>
<% view_archived_html = capture do %>
<% if current_account.templates.where.not(archived_at: nil).exists? %>
<div>
<a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a>
</div>
<% end %>
<% end %>
<% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
</div>
<% view_archived_html = capture do %>
<% if current_account.templates.where.not(archived_at: nil).exists? %>
<div>
<a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a>
</div>
<% end %>
<% if @templates.present? %>
<div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %>
</div>
<% end %>
<% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
<%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
<input type="hidden" name="form_id" value="<%= form_id %>">
<button type="submit" class="hidden"></button>
<file-dropzone data-submit-on-upload="true" class="w-full">
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
<div class="flex flex-col items-center">
<span data-target="file-dropzone.icon" class="flex flex-col items-center">
<span>
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
</span>
<div class="font-medium mb-1">
Upload New Document
</div>
<div class="text-xs">
<span class="font-medium">Click to upload</span> or drag and drop
</div>
<% end %>
<% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
</div>
<% end %>
<% if @templates.present? %>
<div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %>
</div>
<% end %>
<% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
<%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
<input type="hidden" name="form_id" value="<%= form_id %>">
<button type="submit" class="hidden"></button>
<file-dropzone data-submit-on-upload="true" class="w-full">
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
<div class="flex flex-col items-center">
<span data-target="file-dropzone.icon" class="flex flex-col items-center">
<span>
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
</span>
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
<div class="font-medium mb-1">
Uploading...
</div>
</span>
</div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple>
</div>
</label>
</file-dropzone>
<% end %>
<% end %>
<% if @templates.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
Templates not Found
</div>
</div>
<% end %>
<% else %>
<div class="card bg-base-200 h-96 mb-2">
<div class="card-body text-center">
<div class="flex items-center h-full">
<div class="mx-auto">
<div class="max-w-xl mx-auto">
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to <%= Docuseal.product_name %></h1>
</div>
<div class="max-w-lg mx-auto">
<p class="py-6 text-gray-600">Streamline document workflows, from creating customizable templates to filling and signing document forms</p>
<%= link_to new_template_path, class: 'base-button', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="mr-1">Create Template</span>
<% end %>
<div class="font-medium mb-1">
Upload New Document
</div>
<div class="text-xs">
<span class="font-medium">Click to upload</span> or drag and drop
</div>
</span>
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
<div class="font-medium mb-1">
Uploading...
</div>
</span>
</div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple>
</div>
</div>
</label>
</file-dropzone>
<% end %>
<% end %>
<% if @templates.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
Templates not Found
</div>
</div>
<% end %>
+12 -8
View File
@@ -17,15 +17,19 @@
<% end %>
<% if devise_mapping.omniauthable? %>
<div class="space-y-4">
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
<%= f.button button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
<% if User.omniauth_providers.include?(:google_oauth2) %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
<%= f.button button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
<% end %>
<% end %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
<%= f.button button_title(title: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
<% if User.omniauth_providers.include?(:microsoft_office365) %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
<%= f.button button_title(title: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
<% end %>
<% end %>
</div>
<% end %>
+12 -8
View File
@@ -21,15 +21,19 @@
<% end %>
<% if devise_mapping.omniauthable? %>
<div class="space-y-4">
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
<%= f.button button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
<% if User.omniauth_providers.include?(:google_oauth2) %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
<%= f.button button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
<% end %>
<% end %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
<%= f.button button_title(title: 'Sign in with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
<% if User.omniauth_providers.include?(:microsoft_office365) %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
<%= f.button button_title(title: 'Sign in with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
<% end %>
<% end %>
</div>
<% end %>
+1 -1
View File
@@ -8,7 +8,7 @@
<% end %>
<div class="space-y-2">
<div class="form-control">
<%= f.label :otp_attempt, 'Two-Factor Code', class: 'label' %>
<%= f.label :otp_attempt, 'Two-Factor Code from Authenticator App', class: 'label' %>
<%= f.text_field :otp_attempt, autofocus: true, placeholder: 'XXX-XXX', required: true, class: 'base-input' %>
</div>
</div>
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 13h5" />
<path d="M12 16v-8h3a2 2 0 0 1 2 2v1a2 2 0 0 1 -2 2h-3" />
<path d="M20 8v8" />
<path d="M9 16v-5.5a2.5 2.5 0 0 0 -5 0v5.5" />
</svg>

After

Width:  |  Height:  |  Size: 439 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 6l-6 6l6 6" />
</svg>

After

Width:  |  Height:  |  Size: 313 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M8.5 13.5l-1.5 -1.5l1.5 -1.5" />
<path d="M15.5 10.5l1.5 1.5l-1.5 1.5" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M13 9.5l-2 5.5" />
</svg>

After

Width:  |  Height:  |  Size: 458 B

+1 -1
View File
@@ -3,7 +3,7 @@
Use an authenticator mobile app like Google Authenticator or 1Password to scan the QR code below.
</p>
<div>
<%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'w-80 h-80 my-4 mx-auto' }) %>
<%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'md:w-80 md:h-80 my-4 mx-auto' }) %>
</div>
<div class="form-control my-6 space-y-2">
<%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %>
@@ -2,7 +2,7 @@
<%= f.hidden_field :key %>
<div class="form-control">
<% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %>
<div class="flex space-x-4">
<div class="flex flex-col md:flex-row gap-4">
<div class="w-full">
<%= f.fields_for :value, record do |ff| %>
<%= ff.label :first_duration, 'First reminder in', class: 'label' %>
@@ -1,4 +1,8 @@
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
Upgrade
<% end %>
<% if signed_in? && current_user != true_user %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
<%= render 'shared/test_alert' %>
<% end %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
+17 -5
View File
@@ -11,17 +11,17 @@
<%= link_to 'Account', settings_account_path, class: 'text-base hover:bg-base-300' %>
</li>
<% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<li>
<%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
</li>
@@ -43,7 +43,7 @@
</li>
<% end %>
<% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) %>
<% if can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
</li>
@@ -88,10 +88,22 @@
<% end %>
</li>
<% end %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<li>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex w-full' } do |f| %>
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
<span class="mr-2 w-full">
Test Environment
</span>
<%= f.check_box :testing_toggle, class: 'toggle toggle-sm', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
</label>
<% end %>
</li>
<% end %>
<% end %>
</ul>
</menu-active>
<% if !can?(:manage, :tenants) %>
<% if Docuseal.multitenant? || cannot?(:manage, :tenants) %>
<div class="mx-4 border-t border-base-300 hidden md:block">
<div class="text-sm mt-3">
Need help? Ask a question:
+17
View File
@@ -0,0 +1,17 @@
<% if signed_in? && current_user != true_user && current_account.testing? %>
<div class="alert py-1 text-sm font-medium gap-x-2 flex whitespace-nowrap">
<a href="<%= testing_api_settings_path %>" data-turbo-frame="modal" class="link font-semibold flex">
<%= svg_icon('code_circle', class: 'w-5 h-5 mr-1') %>Testing Environment
</a>
<span>
|
</span>
<%= button_to testing_account_path, method: :delete, class: 'inline flex' do %>
<% title = capture do %>
<span class="link">Exit</span>
<span>&times;</span>
<% end %>
<%= button_title(title:, disabled_with: 'Leave') %>
<% end %>
</div>
<% end %>
@@ -0,0 +1,10 @@
<% if can?(:manage, EncryptedConfig) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex' } do |f| %>
<label class="flex items-center justify-between py-2.5" for="testing_toggle">
<span class="mr-2 text-lg">
Test Environment
</span>
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, onchange: 'this.form.requestSubmit()' %>
</label>
<% end %>
<% end %>
+1 -1
View File
@@ -5,7 +5,7 @@
<% options.each_with_index do |(label, value), index| %>
<span>
<%= radio_button_tag 'option', value, value == 'email', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item w-28 peer-checked:btn-active normal-case">
<label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item md:w-28 peer-checked:btn-active normal-case">
<%= label %>
</label>
</span>
+13 -2
View File
@@ -41,7 +41,7 @@
</a>
<% end %>
</div>
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
<div id="document_view" class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
<div class="pr-3.5 pl-0.5">
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
@@ -71,7 +71,7 @@
<% end %>
</div>
</div>
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
<div id="parties_view" class="hidden md:block relative w-full md:w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600 bg-pink-500 bg-cyan-500 bg-orange-500 bg-lime-500 bg-indigo-500] %>
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
<% submitter_field_counters = Hash.new { 0 } %>
@@ -173,5 +173,16 @@
<% end %>
</div>
</div>
<label class="md:hidden btn btn-sm btn-neutral text-white text-base z-10 fixed bottom-2 right-2 h-16 shadow-lg">
<input type="checkbox" class="peer hidden" onclick="[document_view.classList.toggle('hidden'), parties_view.classList.toggle('hidden')]">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('users', class: 'w-8 h-8') %>
<span>Signers</span>
</span>
<span class="hidden peer-checked:flex items-center">
<%= svg_icon('chevron_left', class: 'w-8 h-8') %>
<span>Back</span>
</span>
</label>
</div>
<%= render 'scripts/autosize_field' %>
+2 -2
View File
@@ -11,8 +11,8 @@
<%= svg_icon('folder', class: 'w-6 h-6') %>
</a>
<folder-autocomplete class="flex justify-between w-full">
<input id="folder_name" placeholder="Folder Name" type="text" class="w-64 outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="link peer-focus:hidden mr-1.5">
<input id="folder_name" placeholder="Folder Name" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="shrink-0 link peer-focus:hidden mr-1.5">
Change Folder
</a>
</folder-autocomplete>
@@ -0,0 +1,18 @@
<%= render 'shared/turbo_modal', title: 'Testing Environment' do %>
<div>
<label class="text-sm font-semibold" for="api_key">
x-Auth-Token
</label>
<div class="flex gap-2 mb-4 mt-2">
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="base-input 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: 'Copy', copied_title: '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' %>
<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.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-full' %>
</div>
<% end %>
<% end %>
+1 -1
View File
@@ -5,7 +5,7 @@
<% options.each_with_index do |(label, value), index| %>
<span>
<%= radio_button_tag 'option', value, value == 'draw', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item w-28 peer-checked:btn-active normal-case">
<label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item md:w-28 peer-checked:btn-active normal-case">
<%= label %>
</label>
</span>
+4 -1
View File
@@ -1,7 +1,10 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="flex-grow">
<h1 class="text-4xl font-bold mb-4">Webhooks</h1>
<div class="flex justify-between">
<h1 class="text-4xl font-bold mb-4">Webhooks</h1>
<%= render 'shared/test_mode_toggle' %>
</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| %>
+4 -4
View File
@@ -62,6 +62,8 @@ Rails.application.routes.draw do
resource :user_signature, only: %i[edit update destroy]
resources :submissions, only: %i[show destroy]
resources :console_redirect, only: %i[index]
resource :testing_account, only: %i[show destroy]
resources :testing_api_settings, only: %i[index]
resource :templates_upload, only: %i[create]
authenticated do
resource :templates_upload, only: %i[show], path: 'new'
@@ -112,10 +114,8 @@ Rails.application.routes.draw do
resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
resources :users, only: %i[index]
resource :personalization, only: %i[show create], controller: 'personalization_settings'
if !Docuseal.multitenant? || Docuseal.demo?
resources :api, only: %i[index create], controller: 'api_settings'
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
end
resources :api, only: %i[index create], controller: 'api_settings'
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
resource :account, only: %i[show update]
resources :profile, only: %i[index] do
collection do
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateAccountLinkedAccounts < ActiveRecord::Migration[7.1]
def change
create_table :account_linked_accounts do |t|
t.references :account, null: false, foreign_key: true
t.references :linked_account, null: false, foreign_key: { to_table: :accounts }
t.text :account_type, null: false
t.index %i[account_id linked_account_id], unique: true
t.timestamps
end
end
end
+14 -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: 2023_12_29_220819) do
ActiveRecord::Schema[7.1].define(version: 2024_01_20_192055) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -34,6 +34,17 @@ ActiveRecord::Schema[7.1].define(version: 2023_12_29_220819) do
t.index ["account_id"], name: "index_account_configs_on_account_id"
end
create_table "account_linked_accounts", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "linked_account_id", null: false
t.text "account_type", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "linked_account_id"], name: "idx_on_account_id_linked_account_id_48ab9f79d2", unique: true
t.index ["account_id"], name: "index_account_linked_accounts_on_account_id"
t.index ["linked_account_id"], name: "index_account_linked_accounts_on_linked_account_id"
end
create_table "accounts", force: :cascade do |t|
t.string "name", null: false
t.string "timezone", null: false
@@ -242,6 +253,8 @@ ActiveRecord::Schema[7.1].define(version: 2023_12_29_220819) do
add_foreign_key "access_tokens", "users"
add_foreign_key "account_configs", "accounts"
add_foreign_key "account_linked_accounts", "accounts"
add_foreign_key "account_linked_accounts", "accounts", column: "linked_account_id"
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "document_generation_events", "submitters"
+22 -1
View File
@@ -31,6 +31,26 @@ module Accounts
new_account
end
def find_or_create_testing_user(account)
user = User.find_by(account: account.testing_accounts)
return user if user
testing_account = account.dup.tap { |a| a.name = "Testing - #{a.name}" }
ApplicationRecord.transaction do
account.testing_accounts << testing_account
testing_account.users.create!(
email: account.users.order(:id).first.email.sub('@', '+test@'),
first_name: 'Testing',
last_name: 'Environment',
password: SecureRandom.hex,
role: :admin
)
end
end
def create_default_template(account)
template = Template.find(1)
@@ -61,7 +81,8 @@ module Accounts
if Docuseal.multitenant?
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || Docuseal::CERTS
else
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value ||
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value
end
if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' })
+1 -1
View File
@@ -7,7 +7,7 @@ class ApiPathConsiderJsonMiddleware
def call(env)
if env['PATH_INFO'].starts_with?('/api') &&
!env['PATH_INFO'].ends_with?('/documents') &&
(!env['PATH_INFO'].ends_with?('/documents') || env['REQUEST_METHOD'] != 'POST') &&
!env['PATH_INFO'].ends_with?('/attachments')
env['CONTENT_TYPE'] = 'application/json'
end
+106
View File
@@ -0,0 +1,106 @@
# frozen_string_literal: true
module Params
class BaseValidator
InvalidParameterError = Class.new(StandardError)
def self.call(...)
validator = new(...)
validator.call
rescue InvalidParameterError => e
Rollbar.error(e) if defined?(Rollbar)
raise e unless validator.dry_run?
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
raise e unless Rails.env.production?
end
attr_reader :params, :dry_run
alias dry_run? dry_run
def initialize(params, dry_run: false)
@params = params
@dry_run = dry_run
@current_path = ''
end
def call
raise NotImplementedError
end
def raise_error(message)
message += " in `#{@current_path}`." if @current_path.present?
raise InvalidParameterError, message unless dry_run?
Rollbar.error(message) if defined?(Rollbar)
end
def required(params, keys, message: nil)
keys = Array.wrap(keys)
return if keys.any? { |key| params&.dig(key).present? }
raise_error(message || "#{keys.join(' or ')} is required")
end
def type(params, key, type, message: nil)
return if params.blank?
return if params[key].blank?
return if params[key].is_a?(type) || (type == Hash && params[key].is_a?(ActionController::Parameters))
type = 'Object' if type == Hash
raise_error(message || "#{key} must be a #{type}")
end
def in_path(params, path = [])
old_path = @current_path
@current_path = [old_path, *path].compact_blank.map(&:to_s).join('.')
param = params.dig(*path)
yield params.dig(*path) if param
@current_path = old_path
end
def in_path_each(params, path = [])
old_path = @current_path
params.dig(*path)&.each_with_index do |item, index|
@current_path = old_path + [*path].map(&:to_s).join('.') + "[#{index}]"
yield item if item
end
@current_path = old_path
end
def boolean(params, key, message: nil)
return if params.blank?
return unless params.key?(key)
return if params[key].nil?
value = ActiveModel::Type::Boolean.new.cast(params[key])
return if value.is_a?(TrueClass) || value.is_a?(FalseClass)
raise_error(message || "#{key} must be true or false")
end
def value_in(params, key, values, allow_nil: false, message: nil)
return if params.blank?
return if allow_nil && params[key].nil?
return if values.include?(params[key])
raise_error(message || "#{key} must be one of #{values.join(', ')}")
end
end
end
+85
View File
@@ -0,0 +1,85 @@
# frozen_string_literal: true
module Params
class SubmissionCreateValidator < BaseValidator
# rubocop:disable Metrics
def call
if params[:submission].blank? && (params[:emails].present? || params[:email].present?)
validate_creation_from_emails(params)
else
validate_creation_from_submission(params)
end
end
def validate_creation_from_emails(params)
required(params, :template_id)
required(params, %i[emails email])
type(params, :emails, String)
boolean(params, :send_email)
type(params, :message, Hash)
in_path(params, :message) do |message_params|
type(message_params, :subject, String)
type(message_params, :body, String)
required(message_params, :body)
end
true
end
def validate_creation_from_submission(params)
required(params, :template_id)
required(params, %i[submission submissions])
boolean(params, :send_email)
boolean(params, :send_sms)
type(params, :order, String)
type(params, :completed_redirect_url, String)
type(params, :bcc_completed, String)
type(params, :message, Hash)
in_path(params, :message) do |message_params|
type(message_params, :subject, String)
type(message_params, :body, String)
required(message_params, :body)
end
value_in(params, :order, %w[preserved random], allow_nil: true)
return true if params[:submission].is_a?(Array)
in_path(params, :submission) do |submission_params|
required(submission_params, :submitters) if params[:submission]
end
in_path_each(params, %i[submission submitters]) do |submitter_params|
required(submitter_params, %i[email phone name])
type(submitter_params, :name, String)
type(submitter_params, :email, String)
type(submitter_params, :phone, String)
type(submitter_params, :values, Hash)
boolean(submitter_params, :send_email)
boolean(submitter_params, :send_sms)
type(submitter_params, :completed_redirect_url, String)
type(submitter_params, :fields, Array)
in_path_each(submitter_params, %i[fields]) do |field_params|
required(field_params, :name)
type(field_params, :name, String)
type(field_params, :default_value, String)
type(field_params, :validation_pattern, String)
type(field_params, :invalid_message, String)
boolean(field_params, :readonly)
end
end
true
end
# rubocop:enable Metrics
end
end
+15
View File
@@ -0,0 +1,15 @@
# frozen_string_literal: true
module Submitters
module ValidateSending
InvalidEmail = Class.new(StandardError)
module_function
def call(submitter, _mail)
raise InvalidEmail unless submitter.email.to_s.include?('@')
true
end
end
end
+4 -4
View File
@@ -29,10 +29,10 @@ module Templates
{
'type' => 'external_link',
'value' => annot[:A][:URI],
'x' => left / page.width,
'y' => (page.height - top) / page.height,
'w' => (right - left) / page.width,
'h' => (top - bottom) / page.height
'x' => left / page.width.to_f,
'y' => (page.height - top) / page.height.to_f,
'w' => (right - left) / page.width.to_f,
'h' => (top - bottom) / page.height.to_f
}
end
end
+1 -3
View File
@@ -14,9 +14,7 @@ RSpec.describe 'Dashboard Page' do
it 'shows empty state' do
visit root_path
expect(page).to have_content('Welcome to DocuSeal')
expect(page).to have_content('Streamline document workflows, from creating customizable')
expect(page).to have_link('Create Template', href: new_template_path)
expect(page).to have_link('Create', href: new_template_path)
end
end
+1 -1
View File
@@ -71,7 +71,7 @@ RSpec.describe 'App Setup' do
sign_in(user)
visit setup_index_path
expect(page).to have_content('Welcome to DocuSeal')
expect(page).to have_link('Create', href: new_template_path)
end
end