mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6871a3f840 | ||
|
|
c57148eada | ||
|
|
d875477ee6 | ||
|
|
1daee8214c | ||
|
|
9cc85595e3 |
No files matched your search
@@ -35,3 +35,4 @@ yarn-debug.log*
|
|||||||
/coverage
|
/coverage
|
||||||
/attachments
|
/attachments
|
||||||
/docuseal
|
/docuseal
|
||||||
|
/ee
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
module Api
|
module Api
|
||||||
class SubmissionsController < ApiBaseController
|
class SubmissionsController < ApiBaseController
|
||||||
|
UnknownFieldName = Class.new(StandardError)
|
||||||
|
UnknownSubmitterName = Class.new(StandardError)
|
||||||
|
|
||||||
def create
|
def create
|
||||||
template = current_account.templates.find(params[:template_id])
|
template = current_account.templates.find(params[:template_id])
|
||||||
|
|
||||||
@@ -13,28 +16,65 @@ module Api
|
|||||||
send_email: params[:send_email] != 'false',
|
send_email: params[:send_email] != 'false',
|
||||||
emails: params[:emails] || params[:email])
|
emails: params[:emails] || params[:email])
|
||||||
else
|
else
|
||||||
|
submissions_attrs = normalize_submissions_params!(submissions_params[:submission], template)
|
||||||
|
|
||||||
Submissions.create_from_submitters(template:,
|
Submissions.create_from_submitters(template:,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :api,
|
source: :api,
|
||||||
send_email: params[:send_email] != 'false',
|
send_email: params[:send_email] != 'false',
|
||||||
submissions_attrs: submissions_params[:submission])
|
submissions_attrs:)
|
||||||
end
|
end
|
||||||
|
|
||||||
submitters = submissions.flat_map(&:submitters)
|
submitters = submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
if params[:send_email] != 'false'
|
send_invitation_emails(submitters) if params[:send_email] != 'false'
|
||||||
submitters.each do |submitter|
|
|
||||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: submitters
|
render json: submitters
|
||||||
|
rescue UnknownFieldName, UnknownSubmitterName => e
|
||||||
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def send_invitation_emails(submitters)
|
||||||
|
submitters.each do |submitter|
|
||||||
|
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: [{ submitters: [%i[uuid name email]] }])
|
params.permit(submission: [{ submitters: [[:uuid, :name, :email, { values: {} }]] }])
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_submissions_params!(submissions_params, template)
|
||||||
|
submissions_params.each do |submission|
|
||||||
|
submission[:submitters].each_with_index do |submitter, index|
|
||||||
|
next if submitter[:values].blank?
|
||||||
|
|
||||||
|
submitter[:values] =
|
||||||
|
normalize_submitter_values(template,
|
||||||
|
submitter[:values], submitter[:name] || template.submitters[index]['name'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
submissions_params
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_submitter_values(template, values, submitter_name)
|
||||||
|
submitter =
|
||||||
|
template.submitters.find { |e| e['name'] == submitter_name } ||
|
||||||
|
raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}")
|
||||||
|
|
||||||
|
fields = template.fields.select { |e| e['submitter_uuid'] == submitter['uuid'] }
|
||||||
|
|
||||||
|
fields_uuid_index = fields.index_by { |e| e['uuid'] }
|
||||||
|
fields_name_index = fields.index_by { |e| e['name'] }
|
||||||
|
|
||||||
|
values.transform_keys do |key|
|
||||||
|
next key if fields_uuid_index[key].present?
|
||||||
|
|
||||||
|
fields_name_index[key]&.dig('uuid') || raise(UnknownFieldName, "Unknown field: #{key}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2,13 +2,17 @@
|
|||||||
|
|
||||||
module Api
|
module Api
|
||||||
class TemplatesController < ApiBaseController
|
class TemplatesController < ApiBaseController
|
||||||
|
before_action :load_template, only: %i[show update]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: current_account.templates
|
render json: current_account.templates
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def show
|
||||||
@template = current_account.templates.find(params[:id])
|
render json: @template.as_json(include: { author: { only: %i[id email first_name last_name] } })
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
render :ok
|
render :ok
|
||||||
@@ -23,5 +27,9 @@ module Api
|
|||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
||||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_template
|
||||||
|
@template = current_account.templates.find(params[:id])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -7,10 +7,16 @@ class EmailSettingsController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
if @encrypted_config.update(storage_configs)
|
if @encrypted_config.update(storage_configs)
|
||||||
|
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
||||||
|
|
||||||
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
rescue Net::SMTPError, OpenSSL::SSL::SSLError, Net::ReadTimeout => e
|
||||||
|
flash[:alert] = e.message
|
||||||
|
|
||||||
|
render :index, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -70,3 +70,7 @@ button[disabled] .enabled {
|
|||||||
.base-radio {
|
.base-radio {
|
||||||
@apply radio bg-white radio-sm no-animation;
|
@apply radio bg-white radio-sm no-animation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.base-select {
|
||||||
|
@apply select base-input w-full font-normal;
|
||||||
|
}
|
||||||
@@ -195,6 +195,13 @@
|
|||||||
>
|
>
|
||||||
<template v-if="isAnonymousChecboxes">
|
<template v-if="isAnonymousChecboxes">
|
||||||
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
|
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
|
||||||
|
<input
|
||||||
|
v-for="field in currentStepFields"
|
||||||
|
:key="field.uuid"
|
||||||
|
type="hidden"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
:value="!!values[field.uuid]"
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SettingsMailer < ApplicationMailer
|
||||||
|
def smtp_successful_setup(email)
|
||||||
|
mail(to: email, subject: 'SMTP has been configured')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
<div class="grid md:grid-cols-2 gap-4">
|
<div class="grid md:grid-cols-2 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :timezone, class: 'label' %>
|
<%= ff.label :timezone, class: 'label' %>
|
||||||
<%= ff.select :timezone, nil, {}, class: 'select base-input w-full font-normal' do %>
|
<%= ff.select :timezone, nil, {}, class: 'base-select' do %>
|
||||||
<%= time_zone_options_for_select(current_account.timezone) %>
|
<%= time_zone_options_for_select(current_account.timezone) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :locale, 'Time format', class: 'label' %>
|
<%= ff.label :locale, 'Time format', class: 'label' %>
|
||||||
<%= ff.select :locale, options_for_select(controller.class::LOCALE_OPTIONS.invert, current_account.locale), {}, class: 'select base-input w-full font-normal' %>
|
<%= ff.select :locale, options_for_select(controller.class::LOCALE_OPTIONS.invert, current_account.locale), {}, class: 'base-select' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
<div>
|
<div>
|
||||||
Request signature, multiple submitters
|
Request signature, multiple submitters with default values
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<div class="badge badge-warning badge-lg">POST</div>
|
<div class="badge badge-warning badge-lg">POST</div>
|
||||||
@@ -58,7 +58,13 @@
|
|||||||
"submission": [
|
"submission": [
|
||||||
{
|
{
|
||||||
"submitters": [
|
"submitters": [
|
||||||
{ "name": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>", "email": "<%= current_user.email.sub('@', '+test@') %>" },
|
{
|
||||||
|
"name": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>",
|
||||||
|
"email": "<%= current_user.email.sub('@', '+test@') %>",
|
||||||
|
"values": {
|
||||||
|
"Form Text Field Name": "Default Value"
|
||||||
|
}
|
||||||
|
},
|
||||||
{ "name": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
|
{ "name": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -71,6 +77,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||||
|
<input type="checkbox">
|
||||||
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
<div>
|
||||||
|
Template details
|
||||||
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
<div class="badge badge-info badge-lg">GET</div>
|
||||||
|
<div class="badge badge-primary badge-lg"><%= api_template_path(':id') %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content" style="display: inherit">
|
||||||
|
<div class="mockup-code overflow-hidden">
|
||||||
|
<% text = capture do %>curl '<%= api_template_url(current_account.templates.last) %>' \
|
||||||
|
--header 'X-Auth-Token: <%= current_user.access_token.token %>'<% end.to_str %>
|
||||||
|
<span class="top-0 right-0 absolute">
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||||
|
</span>
|
||||||
|
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,6 +25,27 @@
|
|||||||
<%= ff.password_field :password, value: value['password'], required: true, class: 'base-input' %>
|
<%= ff.password_field :password, value: value['password'], required: true, class: 'base-input' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid md:grid-cols-2 gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<%= ff.label :domain, 'Domain (optional)', class: 'label' %>
|
||||||
|
<%= ff.text_field :domain, value: value['domain'], class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= ff.label :authentication, class: 'label' %>
|
||||||
|
<%= ff.select :authentication, options_for_select([%w[Plain plain], %w[Login login], %w[CRAM-MD5 cram_md5]], value.fetch('authentication', 'plain')), { prompt: true }, required: true, class: 'base-select' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
|
||||||
|
<div class="flex items-center space-x-6">
|
||||||
|
<% [%w[None none], %w[SSL ssl], %w[TLS tls]].each do |(label, val)| %>
|
||||||
|
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
|
||||||
|
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
|
||||||
|
<%= label %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :from_email, 'Send from Email', class: 'label' %>
|
<%= ff.label :from_email, 'Send from Email', class: 'label' %>
|
||||||
<%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %>
|
<%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<p>SMTP has been successfully configured!</p>
|
||||||
+1
-1
@@ -30,7 +30,7 @@ Rails.application.routes.draw do
|
|||||||
namespace :api, defaults: { format: :json } do
|
namespace :api, defaults: { format: :json } do
|
||||||
resources :attachments, only: %i[create]
|
resources :attachments, only: %i[create]
|
||||||
resources :submissions, only: %i[create]
|
resources :submissions, only: %i[create]
|
||||||
resources :templates, only: %i[update index] do
|
resources :templates, only: %i[update show index] do
|
||||||
resources :submissions, only: %i[create]
|
resources :submissions, only: %i[create]
|
||||||
resources :documents, only: %i[create destroy], controller: 'templates_documents'
|
resources :documents, only: %i[create destroy], controller: 'templates_documents'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ module Accounts
|
|||||||
|
|
||||||
def can_send_emails?(account)
|
def can_send_emails?(account)
|
||||||
return true if Docuseal.multitenant?
|
return true if Docuseal.multitenant?
|
||||||
|
return true if ENV['SMTP_ADDRESS'].present?
|
||||||
|
|
||||||
EncryptedConfig.exists?(account_id: account.id, key: EncryptedConfig::EMAIL_SMTP_KEY)
|
EncryptedConfig.exists?(account_id: account.id, key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,11 +19,7 @@ module ActionMailerConfigsInterceptor
|
|||||||
email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
|
email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||||
|
|
||||||
if email_configs
|
if email_configs
|
||||||
message.delivery_method(:smtp, user_name: email_configs.value['username'],
|
message.delivery_method(:smtp, build_smtp_configs_hash(email_configs))
|
||||||
password: email_configs.value['password'],
|
|
||||||
address: email_configs.value['host'],
|
|
||||||
port: email_configs.value['port'],
|
|
||||||
tls: email_configs.value['port'].to_s == '465')
|
|
||||||
|
|
||||||
message.from = "#{email_configs.account.name} <#{email_configs.value['from_email']}>"
|
message.from = "#{email_configs.account.name} <#{email_configs.value['from_email']}>"
|
||||||
else
|
else
|
||||||
@@ -32,4 +28,20 @@ module ActionMailerConfigsInterceptor
|
|||||||
|
|
||||||
message
|
message
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_smtp_configs_hash(email_configs)
|
||||||
|
value = email_configs.value
|
||||||
|
|
||||||
|
{
|
||||||
|
user_name: value['username'],
|
||||||
|
password: value['password'],
|
||||||
|
address: value['host'],
|
||||||
|
port: value['port'],
|
||||||
|
domain: value['domain'],
|
||||||
|
authentication: value.fetch('authentication', 'plain'),
|
||||||
|
enable_starttls_auto: true,
|
||||||
|
ssl: value['security'] == 'ssl',
|
||||||
|
tls: value['security'] == 'tls' || (value['security'].blank? && value['port'].to_s == '465')
|
||||||
|
}.compact_blank
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -38,6 +38,7 @@ module Submissions
|
|||||||
|
|
||||||
submission.submitters.new(email: submitter_attrs[:email],
|
submission.submitters.new(email: submitter_attrs[:email],
|
||||||
sent_at: send_email ? Time.current : nil,
|
sent_at: send_email ? Time.current : nil,
|
||||||
|
values: submitter_attrs[:values] || {},
|
||||||
uuid:)
|
uuid:)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ module Submissions
|
|||||||
|
|
||||||
width = page.box.width
|
width = page.box.width
|
||||||
height = page.box.height
|
height = page.box.height
|
||||||
|
font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
||||||
|
|
||||||
value = submitter.values[field['uuid']]
|
value = submitter.values[field['uuid']]
|
||||||
|
|
||||||
@@ -70,13 +71,13 @@ module Submissions
|
|||||||
items = Array.wrap(value).each_with_object([]) do |uuid, acc|
|
items = Array.wrap(value).each_with_object([]) do |uuid, acc|
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
||||||
|
|
||||||
acc << HexaPDF::Layout::InlineBox.create(width: FONT_SIZE, height: FONT_SIZE,
|
acc << HexaPDF::Layout::InlineBox.create(width: font_size, height: font_size,
|
||||||
margin: [0, 1, -2, 0]) do |cv, box|
|
margin: [0, 1, -2, 0]) do |cv, box|
|
||||||
cv.image(PdfIcons.paperclip_io, at: [0, 0], width: box.content_width)
|
cv.image(PdfIcons.paperclip_io, at: [0, 0], width: box.content_width)
|
||||||
end
|
end
|
||||||
|
|
||||||
acc << HexaPDF::Layout::TextFragment.create("#{attachment.filename}\n", font: pdf.fonts.add(FONT_NAME),
|
acc << HexaPDF::Layout::TextFragment.create("#{attachment.filename}\n", font: pdf.fonts.add(FONT_NAME),
|
||||||
font_size: FONT_SIZE)
|
font_size:)
|
||||||
end
|
end
|
||||||
|
|
||||||
lines = layouter.fit(items, area['w'] * width, height).lines
|
lines = layouter.fit(items, area['w'] * width, height).lines
|
||||||
@@ -132,7 +133,7 @@ module Submissions
|
|||||||
|
|
||||||
value.chars.each_with_index do |char, index|
|
value.chars.each_with_index do |char, index|
|
||||||
text = HexaPDF::Layout::TextFragment.create(char, font: pdf.fonts.add(FONT_NAME),
|
text = HexaPDF::Layout::TextFragment.create(char, font: pdf.fonts.add(FONT_NAME),
|
||||||
font_size: FONT_SIZE)
|
font_size:)
|
||||||
|
|
||||||
cell_layouter.fit([text], cell_width, area['h'] * height)
|
cell_layouter.fit([text], cell_width, area['h'] * height)
|
||||||
.draw(canvas, ((area['x'] * width) + (cell_width * index)),
|
.draw(canvas, ((area['x'] * width) + (cell_width * index)),
|
||||||
@@ -142,7 +143,7 @@ module Submissions
|
|||||||
value = I18n.l(Date.parse(value), format: :long) if field['type'] == 'date'
|
value = I18n.l(Date.parse(value), format: :long) if field['type'] == 'date'
|
||||||
|
|
||||||
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), font: pdf.fonts.add(FONT_NAME),
|
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), font: pdf.fonts.add(FONT_NAME),
|
||||||
font_size: FONT_SIZE)
|
font_size:)
|
||||||
|
|
||||||
lines = layouter.fit([text], area['w'] * width, height).lines
|
lines = layouter.fit([text], area['w'] * width, height).lines
|
||||||
box_height = lines.sum(&:height)
|
box_height = lines.sum(&:height)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SettingsMailerPreview < ActionMailer::Preview
|
||||||
|
def smtp_successful_setup
|
||||||
|
SettingsMailer.smtp_successful_setup('example@gmail.com')
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user