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 ./ 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 ./bin ./bin
COPY ./app ./app COPY ./app ./app
+1
View File
@@ -26,6 +26,7 @@ gem 'pagy'
gem 'pdf-reader' gem 'pdf-reader'
gem 'pg', require: false gem 'pg', require: false
gem 'premailer-rails' gem 'premailer-rails'
gem 'pretender'
gem 'puma' gem 'puma'
gem 'rack' gem 'rack'
gem 'rails' gem 'rails'
+3
View File
@@ -354,6 +354,8 @@ GEM
actionmailer (>= 3) actionmailer (>= 3)
net-smtp net-smtp
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0)
actionpack (>= 6.1)
pry (0.14.2) pry (0.14.2)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
@@ -596,6 +598,7 @@ DEPENDENCIES
pdf-reader pdf-reader
pg pg
premailer-rails premailer-rails
pretender
pry-rails pry-rails
puma puma
rack rack
+7 -1
View File
@@ -8,11 +8,17 @@ module Api
DEFAULT_LIMIT = 10 DEFAULT_LIMIT = 10
MAX_LIMIT = 100 MAX_LIMIT = 100
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
wrap_parameters false wrap_parameters false
before_action :authenticate_user! before_action :authenticate_user!
check_authorization check_authorization
rescue_from Params::BaseValidator::InvalidParameterError do |e|
render json: { error: e.message }, status: :unprocessable_entity
end
if Rails.env.production? if Rails.env.production?
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(e) if defined?(Rollbar)
@@ -44,7 +50,7 @@ module Api
if request.headers['X-Auth-Token'].present? if request.headers['X-Auth-Token'].present?
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token']) 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 end
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
@@ -63,6 +63,8 @@ module Api
end end
def create def create
Params::SubmissionCreateValidator.call(params, dry_run: true)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil? return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
if @template.fields.blank? if @template.fields.blank?
+5 -1
View File
@@ -38,7 +38,11 @@ module Api
end end
def destroy 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]) render json: @template.as_json(only: %i[id archived_at])
end end
+12 -1
View File
@@ -16,6 +16,8 @@ class ApplicationController < ActionController::Base
:current_account, :current_account,
:svg_icon :svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
rescue_from Pagy::OverflowError do rescue_from Pagy::OverflowError do
redirect_to request.path redirect_to request.path
end end
@@ -24,7 +26,7 @@ class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(e) if defined?(Rollbar)
redirect_back(fallback_location: root_path, alert: e.message) redirect_to root_path, alert: e.message
end end
end end
@@ -32,6 +34,15 @@ class ApplicationController < ActionController::Base
Docuseal.default_url_options Docuseal.default_url_options
end 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 private
def with_browser_locale(&) def with_browser_locale(&)
@@ -5,9 +5,9 @@ class ConsoleRedirectController < ApplicationController
skip_authorization_check skip_authorization_check
def index 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, scope: :console,
exp: 1.minute.from_now.to_i) exp: 1.minute.from_now.to_i)
@@ -11,9 +11,7 @@ class SubmittersSendEmailController < ApplicationController
alert: 'Email has been sent already.') alert: 'Email has been sent already.')
end end
SubmitterMailer.invitation_email(@submitter).deliver_later! SendSubmitterInvitationEmailJob.perform_later('submitter_id' => @submitter.id)
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
@submitter.sent_at ||= Time.current @submitter.sent_at ||= Time.current
@submitter.save! @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 <div
class="sticky bottom-0 py-2" class="sticky bottom-0 py-2"
:style="withStickySubmitters ? { backgroundColor } : {}" :style="{ backgroundColor }"
> >
<Upload <Upload
v-if="sortedDocuments.length && editable && withUploadButton" v-if="sortedDocuments.length && editable && withUploadButton"
@@ -884,7 +884,7 @@ export default {
this.save() this.save()
}, },
onDocumentRemove (item) { 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) this.template.schema.splice(this.template.schema.indexOf(item), 1)
} }
@@ -61,7 +61,7 @@
<button <button
v-if="submitters.length > 1 && editable" v-if="submitters.length > 1 && editable"
class="px-2" class="px-2"
@click.stop="remove(submitter)" @click.prevent.stop="remove(submitter)"
> >
<IconTrashX :width="18" /> <IconTrashX :width="18" />
</button> </button>
@@ -165,7 +165,7 @@
title="Up" title="Up"
class="relative w-2" class="relative w-2"
style="font-size: 10px; margin-bottom: -4px" 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> </button>
@@ -173,7 +173,7 @@
title="Down" title="Down"
class="relative w-2" class="relative w-2"
style="font-size: 10px; margin-top: -4px" 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> </button>
@@ -181,7 +181,7 @@
<button <button
v-if="!compact && submitters.length > 1 && editable" v-if="!compact && submitters.length > 1 && editable"
class="hidden group-hover:block px-2" class="hidden group-hover:block px-2"
@click.stop="remove(submitter)" @click.prevent.stop="remove(submitter)"
> >
<IconTrashX :width="18" /> <IconTrashX :width="18" />
</button> </button>
@@ -296,7 +296,7 @@ export default {
this.$emit('update:model-value', submitter.uuid) this.$emit('update:model-value', submitter.uuid)
}, },
remove (submitter) { remove (submitter) {
if (window.confirm('Are you sure?')) { if (window.confirm(this.t('are_you_sure'))) {
this.$emit('remove', submitter) this.$emit('remove', submitter)
} }
}, },
+1
View File
@@ -1,4 +1,5 @@
const en = { const en = {
are_you_sure: 'Are you sure?',
sign_yourself: 'Sign Yourself', sign_yourself: 'Sign Yourself',
send: 'Send', send: 'Send',
save: 'Save', save: 'Save',
@@ -4,7 +4,11 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) 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') 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 class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account
has_many :submissions, through: :templates has_many :submissions, through: :templates
has_many :submitters, through: :submissions 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, has_many :active_users, -> { active }, dependent: :destroy,
inverse_of: :account, class_name: 'User' inverse_of: :account, class_name: 'User'
attribute :timezone, :string, default: 'UTC' attribute :timezone, :string, default: 'UTC'
attribute :locale, :string, default: 'en-US' attribute :locale, :string, default: 'en-US'
def testing?
linked_account_account&.testing?
end
def default_template_folder def default_template_folder
super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME, super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME,
author_id: users.minimum(:id)).tap(&:save!) 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 :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author 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 :role, :string, default: ADMIN_ROLE
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
@@ -72,7 +72,7 @@ class User < ApplicationRecord
end end
def active_for_authentication? def active_for_authentication?
!archived_at? super && !archived_at?
end end
def remember_me def remember_me
+1 -1
View File
@@ -22,7 +22,7 @@
</div> </div>
<% end %> <% end %>
<% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %> <% 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| %> <%= f.fields_for encrypted_config do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :value, 'App URL', class: 'label' %> <%= 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"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow"> <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 bg-base-200">
<div class="card-body p-6"> <div class="card-body p-6">
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label> <label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
<div class="flex flex-row space-x-4"> <div class="flex flex-col md:flex-row gap-4">
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly> <div class="flex w-full space-x-4">
<%= 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' %> <input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
<%= 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?' } %> <%= 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> </div>
</div> </div>
+69 -90
View File
@@ -1,101 +1,80 @@
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %> <% 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">
<div class="flex justify-between mb-4 items-center"> <h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1>
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1> <div class="flex space-x-2">
<div class="flex space-x-2"> <% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %> <%= render 'shared/search_input' %>
<%= 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 %> <% end %>
<% if can?(:create, ::Template) %> <% end %>
<%= 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>
</div> </div>
<% view_archived_html = capture do %> </div>
<% if current_account.templates.where.not(archived_at: nil).exists? %> <% view_archived_html = capture do %>
<div> <% if current_account.templates.where.not(archived_at: nil).exists? %>
<a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a> <div>
</div> <a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a>
<% 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> </div>
<% end %> <% end %>
<% if @templates.present? %> <% end %>
<div class="grid gap-4 md:grid-cols-3"> <% if @template_folders.present? %>
<%= render partial: 'templates/template', collection: @templates %> <div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
</div> <%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
<% end %> </div>
<% 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)) %> <% end %>
<%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %> <% if @templates.present? %>
<input type="hidden" name="form_id" value="<%= form_id %>"> <div class="grid gap-4 md:grid-cols-3">
<button type="submit" class="hidden"></button> <%= render partial: 'templates/template', collection: @templates %>
<file-dropzone data-submit-on-upload="true" class="w-full"> </div>
<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"> <% end %>
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center"> <% 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)) %>
<div class="flex flex-col items-center"> <%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
<span data-target="file-dropzone.icon" class="flex flex-col items-center"> <input type="hidden" name="form_id" value="<%= form_id %>">
<span> <button type="submit" class="hidden"></button>
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %> <file-dropzone data-submit-on-upload="true" class="w-full">
</span> <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="font-medium mb-1"> <div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
Upload New Document <div class="flex flex-col items-center">
</div> <span data-target="file-dropzone.icon" class="flex flex-col items-center">
<div class="text-xs"> <span>
<span class="font-medium">Click to upload</span> or drag and drop <%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
</div>
</span> </span>
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden"> <div class="font-medium mb-1">
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %> Upload New Document
<div class="font-medium mb-1"> </div>
Uploading... <div class="text-xs">
</div> <span class="font-medium">Click to upload</span> or drag and drop
</span> </div>
</div> </span>
<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> <span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
</div> <%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
</label> <div class="font-medium mb-1">
</file-dropzone> Uploading...
<% end %> </div>
<% end %> </span>
<% 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> </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>
</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>
</div> </div>
<% end %> <% end %>
+12 -8
View File
@@ -17,15 +17,19 @@
<% end %> <% end %>
<% if devise_mapping.omniauthable? %> <% if devise_mapping.omniauthable? %>
<div class="space-y-4"> <div class="space-y-4">
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:google_oauth2) %>
<set-timezone data-input-id="state" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %> <set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= 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' %> <%= 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 %> <% end %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:microsoft_office365) %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %> <set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %> <%= 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 %> <% end %>
</div> </div>
<% end %> <% end %>
+12 -8
View File
@@ -21,15 +21,19 @@
<% end %> <% end %>
<% if devise_mapping.omniauthable? %> <% if devise_mapping.omniauthable? %>
<div class="space-y-4"> <div class="space-y-4">
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:google_oauth2) %>
<set-timezone data-input-id="state" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %> <set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= 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' %> <%= 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 %> <% end %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:microsoft_office365) %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %> <set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign in with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %> <%= 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 %> <% end %>
</div> </div>
<% end %> <% end %>
+1 -1
View File
@@ -8,7 +8,7 @@
<% end %> <% end %>
<div class="space-y-2"> <div class="space-y-2">
<div class="form-control"> <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' %> <%= f.text_field :otp_attempt, autofocus: true, placeholder: 'XXX-XXX', required: true, class: 'base-input' %>
</div> </div>
</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. Use an authenticator mobile app like Google Authenticator or 1Password to scan the QR code below.
</p> </p>
<div> <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>
<div class="form-control my-6 space-y-2"> <div class="form-control my-6 space-y-2">
<%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %> <%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %>
@@ -2,7 +2,7 @@
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="form-control"> <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')) %> <% 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"> <div class="w-full">
<%= f.fields_for :value, record do |ff| %> <%= f.fields_for :value, record do |ff| %>
<%= ff.label :first_duration, 'First reminder in', class: 'label' %> <%= 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 %> <%= 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 Upgrade
<% end %> <% 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> <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' %> <%= link_to 'Account', settings_account_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% unless Docuseal.multitenant? %> <% 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> <li>
<%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% 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> <li>
<%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% 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> <li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
@@ -43,7 +43,7 @@
</li> </li>
<% end %> <% end %>
<% unless Docuseal.multitenant? %> <% 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> <li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
@@ -88,10 +88,22 @@
<% end %> <% end %>
</li> </li>
<% end %> <% 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 %> <% end %>
</ul> </ul>
</menu-active> </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="mx-4 border-t border-base-300 hidden md:block">
<div class="text-sm mt-3"> <div class="text-sm mt-3">
Need help? Ask a question: 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| %> <% options.each_with_index do |(label, value), index| %>
<span> <span>
<%= radio_button_tag 'option', value, value == 'email', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> <%= 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 %>
</label> </label>
</span> </span>
+13 -2
View File
@@ -41,7 +41,7 @@
</a> </a>
<% end %> <% end %>
</div> </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"> <div class="pr-3.5 pl-0.5">
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %> <% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
@@ -71,7 +71,7 @@
<% end %> <% end %>
</div> </div>
</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] %> <% 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_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
<% submitter_field_counters = Hash.new { 0 } %> <% submitter_field_counters = Hash.new { 0 } %>
@@ -173,5 +173,16 @@
<% end %> <% end %>
</div> </div>
</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> </div>
<%= render 'scripts/autosize_field' %> <%= render 'scripts/autosize_field' %>
+2 -2
View File
@@ -11,8 +11,8 @@
<%= svg_icon('folder', class: 'w-6 h-6') %> <%= svg_icon('folder', class: 'w-6 h-6') %>
</a> </a>
<folder-autocomplete class="flex justify-between w-full"> <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"> <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="link peer-focus:hidden mr-1.5"> <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 Change Folder
</a> </a>
</folder-autocomplete> </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| %> <% options.each_with_index do |(label, value), index| %>
<span> <span>
<%= radio_button_tag 'option', value, value == 'draw', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> <%= 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 %>
</label> </label>
</span> </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"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow"> <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 bg-base-200">
<div class="card-body p-6"> <div class="card-body p-6">
<%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %> <%= 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] resource :user_signature, only: %i[edit update destroy]
resources :submissions, only: %i[show destroy] resources :submissions, only: %i[show destroy]
resources :console_redirect, only: %i[index] 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] resource :templates_upload, only: %i[create]
authenticated do authenticated do
resource :templates_upload, only: %i[show], path: 'new' 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' resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
resources :users, only: %i[index] resources :users, only: %i[index]
resource :personalization, only: %i[show create], controller: 'personalization_settings' resource :personalization, only: %i[show create], controller: 'personalization_settings'
if !Docuseal.multitenant? || Docuseal.demo? resources :api, only: %i[index create], controller: 'api_settings'
resources :api, only: %i[index create], controller: 'api_settings' resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
end
resource :account, only: %i[show update] resource :account, only: %i[show update]
resources :profile, only: %i[index] do resources :profile, only: %i[index] do
collection 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. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" 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" t.index ["account_id"], name: "index_account_configs_on_account_id"
end 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| create_table "accounts", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "timezone", 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 "access_tokens", "users"
add_foreign_key "account_configs", "accounts" 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_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "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" add_foreign_key "document_generation_events", "submitters"
+22 -1
View File
@@ -31,6 +31,26 @@ module Accounts
new_account new_account
end 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) def create_default_template(account)
template = Template.find(1) template = Template.find(1)
@@ -61,7 +81,8 @@ module Accounts
if Docuseal.multitenant? if Docuseal.multitenant?
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || Docuseal::CERTS EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || Docuseal::CERTS
else 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 end
if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' }) if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' })
+1 -1
View File
@@ -7,7 +7,7 @@ class ApiPathConsiderJsonMiddleware
def call(env) def call(env)
if env['PATH_INFO'].starts_with?('/api') && 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['PATH_INFO'].ends_with?('/attachments')
env['CONTENT_TYPE'] = 'application/json' env['CONTENT_TYPE'] = 'application/json'
end 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', 'type' => 'external_link',
'value' => annot[:A][:URI], 'value' => annot[:A][:URI],
'x' => left / page.width, 'x' => left / page.width.to_f,
'y' => (page.height - top) / page.height, 'y' => (page.height - top) / page.height.to_f,
'w' => (right - left) / page.width, 'w' => (right - left) / page.width.to_f,
'h' => (top - bottom) / page.height 'h' => (top - bottom) / page.height.to_f
} }
end end
end end
+1 -3
View File
@@ -14,9 +14,7 @@ RSpec.describe 'Dashboard Page' do
it 'shows empty state' do it 'shows empty state' do
visit root_path visit root_path
expect(page).to have_content('Welcome to DocuSeal') expect(page).to have_link('Create', href: new_template_path)
expect(page).to have_content('Streamline document workflows, from creating customizable')
expect(page).to have_link('Create Template', href: new_template_path)
end end
end end
+1 -1
View File
@@ -71,7 +71,7 @@ RSpec.describe 'App Setup' do
sign_in(user) sign_in(user)
visit setup_index_path visit setup_index_path
expect(page).to have_content('Welcome to DocuSeal') expect(page).to have_link('Create', href: new_template_path)
end end
end end