Compare commits

...
35 Commits
Author SHA1 Message Date
Pete Matsyburka 8ef1c309e0 fix email messages user assoc 2023-12-10 21:59:50 +02:00
Pete Matsyburka a32dcc0dc5 fix image scale result generation 2023-12-07 22:46:15 +02:00
Pete Matsyburka 16a2fde4b7 fix result annots 2023-12-07 22:46:08 +02:00
Pete Matsyburka 7856f7a134 add template clone api endpoint 2023-12-06 11:29:55 +02:00
Pete Matsyburka c545924fe9 adjust errors controller 2023-12-06 10:50:20 +02:00
Pete Matsyburka b1510bb88c fix GCS credentials set via ENV var 2023-12-06 00:12:59 +02:00
Pete Matsyburka 31d2024ce5 add send_2fa_sms event type 2023-12-05 19:42:47 +02:00
Pete Matsyburka 23c49f8706 add sign up email placeholder 2023-12-05 18:30:07 +02:00
Pete Matsyburka 62eb0cda6b return 422 on form update values when completed 2023-12-05 18:18:32 +02:00
Alex Turchyn 9f48c6e625 add MS oauth 2023-12-05 17:38:56 +02:00
Pete Matsyburka 83ce401cf2 rescue timestamp response error 2023-12-04 12:57:22 +02:00
Pete Matsyburka 541b025ad0 fix timeserver config loading 2023-12-04 12:47:19 +02:00
Pete Matsyburka 5fd018df46 adjust audit trail verified message 2023-12-03 22:30:03 +02:00
Pete Matsyburka 2822b0086a fix spec 2023-12-03 20:24:43 +02:00
Pete Matsyburka e27606f443 use rails storage proxy for audit log url 2023-12-03 13:03:36 +02:00
Pete Matsyburka 6c95cd5945 use template archive icon 2023-12-03 03:27:56 +02:00
Pete Matsyburka d723146060 allow to invite fewer submitters than in template 2023-12-03 00:27:29 +02:00
Pete Matsyburka 3b03e61ae3 use template links with 2+ submitters 2023-12-03 00:06:03 +02:00
Pete Matsyburka 172513b96c fix submission email sending preferences 2023-12-02 23:16:38 +02:00
Pete Matsyburka 68be42a48d add form attachments to completed emails 2023-12-02 23:03:24 +02:00
Pete Matsyburka 0370e363bc make webhook URL removable 2023-12-02 22:19:24 +02:00
Pete Matsyburka ced88476cc add ability to use timestamp server 2023-12-02 18:14:33 +02:00
Pete Matsyburka c5d71505ef allow to disable text signature via account prefs 2023-12-02 16:26:37 +02:00
Pete Matsyburka 489f9859be add force 2fa toggle 2023-12-02 01:50:59 +02:00
Pete Matsyburka a87ca65839 adjust audit trail 2023-12-01 18:44:00 +02:00
Pete Matsyburka fa78d0976d fix spec 2023-12-01 16:59:28 +02:00
Pete Matsyburka 4aaab2563b configurable values normalize exception 2023-12-01 16:59:19 +02:00
Pete Matsyburka e073cf2c27 refactor 2023-12-01 00:38:11 +02:00
Pete Matsyburka 173d406b1f adjust api email sending 2023-11-30 20:14:09 +02:00
Pete Matsyburka 3bcbad6f04 preserve submitters order via api 2023-11-30 12:32:17 +02:00
Pete Matsyburka e67854fe56 better sign yourself flow 2023-11-30 11:56:11 +02:00
Pete Matsyburka 7510e0cb6b do not delete area in preview mode 2023-11-29 21:59:22 +02:00
Pete Matsyburka 30c913b54f add template api folder filters 2023-11-29 20:23:40 +02:00
Pete Matsyburka 2cda9cd736 make default fields non editable 2023-11-27 20:25:20 +02:00
Pete Matsyburka 342d29f272 allow to permanently delete template in the cloud 2023-11-27 20:25:11 +02:00
73 changed files with 684 additions and 174 deletions

No files matched your search

+2
View File
@@ -21,7 +21,9 @@ gem 'jwt'
gem 'lograge' gem 'lograge'
gem 'mysql2', require: false gem 'mysql2', require: false
gem 'oj' gem 'oj'
gem 'omniauth-github'
gem 'omniauth-google-oauth2' gem 'omniauth-google-oauth2'
gem 'omniauth-microsoft-office365'
gem 'omniauth-rails_csrf_protection' gem 'omniauth-rails_csrf_protection'
gem 'pagy' gem 'pagy'
gem 'pdf-reader' gem 'pdf-reader'
+8
View File
@@ -328,11 +328,17 @@ GEM
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 2.2.3) rack (>= 2.2.3)
rack-protection rack-protection
omniauth-github (2.0.1)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8)
omniauth-google-oauth2 (1.1.1) omniauth-google-oauth2 (1.1.1)
jwt (>= 2.0) jwt (>= 2.0)
oauth2 (~> 2.0.6) oauth2 (~> 2.0.6)
omniauth (~> 2.0) omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8.0) omniauth-oauth2 (~> 1.8.0)
omniauth-microsoft-office365 (0.0.8)
omniauth
omniauth-oauth2
omniauth-oauth2 (1.8.0) omniauth-oauth2 (1.8.0)
oauth2 (>= 1.4, < 3) oauth2 (>= 1.4, < 3)
omniauth (~> 2.0) omniauth (~> 2.0)
@@ -589,7 +595,9 @@ DEPENDENCIES
lograge lograge
mysql2 mysql2
oj oj
omniauth-github
omniauth-google-oauth2 omniauth-google-oauth2
omniauth-microsoft-office365
omniauth-rails_csrf_protection omniauth-rails_csrf_protection
pagy pagy
pdf-reader pdf-reader
@@ -0,0 +1,32 @@
# frozen_string_literal: true
class AccountConfigsController < ApplicationController
before_action :load_account_config
authorize_resource :account_config
ALLOWED_KEYS = [
AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA
].freeze
def create
@account_config.update!(account_config_params)
head :ok
end
private
def load_account_config
return head :not_found unless ALLOWED_KEYS.include?(account_config_params[:key])
@account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
end
def account_config_params
params.required(:account_config).permit!.tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end
end
end
@@ -13,6 +13,10 @@ module Api
submissions = Submissions.search(@submissions, params[:q]) submissions = Submissions.search(@submissions, params[:q])
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present? submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
if params[:template_folder].present?
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters, submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
audit_trail_attachment: :blob)) audit_trail_attachment: :blob))
@@ -144,7 +148,8 @@ module Api
values, new_attachments = values, new_attachments =
Submitters::NormalizeValues.call(template, Submitters::NormalizeValues.call(template,
default_values, default_values,
submitter[:role] || template.submitters[index]['name']) submitter[:role] || template.submitters[index]['name'],
throw_errors: true)
attachments.push(*new_attachments) attachments.push(*new_attachments)
@@ -0,0 +1,38 @@
# frozen_string_literal: true
module Api
class TemplatesCloneController < ApiBaseController
load_and_authorize_resource :template
def create
authorize!(:manage, @template)
template = current_account.templates.new(source: :api)
template.application_key = params[:application_key]
template.name = params[:name] || "#{@template.name} (Clone)"
template.account = @template.account
template.author = current_user
template.assign_attributes(@template.slice(:folder_id, :fields, :schema, :submitters))
if params[:folder_name].present?
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
end
template.save!
Templates::CloneAttachments.call(template:, original_template: @template)
render json: template.as_json(serialize_params)
end
private
def serialize_params
{
include: { author: { only: %i[id email first_name last_name] },
documents: { only: %i[id uuid], methods: %i[url filename] } }
}
end
end
end
@@ -9,6 +9,7 @@ module Api
templates = params[:archived] ? templates.archived : templates.active templates = params[:archived] ? templates.archived : templates.active
templates = templates.where(application_key: params[:application_key]) if params[:application_key].present? templates = templates.where(application_key: params[:application_key]) if params[:application_key].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
templates = paginate(templates.preload(:author, documents_attachments: :blob)) templates = paginate(templates.preload(:author, documents_attachments: :blob))
+3 -2
View File
@@ -44,8 +44,9 @@ class ApplicationController < ActionController::Base
redirect_to setup_index_path unless User.exists? redirect_to setup_index_path unless User.exists?
end end
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil) def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil)
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: }) render_to_string(partial: 'shared/button_title',
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
end end
def svg_icon(icon_name, class: '') def svg_icon(icon_name, class: '')
+12
View File
@@ -5,6 +5,7 @@ class DashboardController < ApplicationController
before_action :maybe_redirect_product_url before_action :maybe_redirect_product_url
before_action :maybe_render_landing before_action :maybe_render_landing
before_action :maybe_redirect_mfa_setup
load_and_authorize_resource :template_folder, parent: false load_and_authorize_resource :template_folder, parent: false
load_and_authorize_resource :template, parent: false load_and_authorize_resource :template, parent: false
@@ -62,6 +63,17 @@ class DashboardController < ApplicationController
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
end end
def maybe_redirect_mfa_setup
return unless signed_in?
return if current_user.otp_required_for_login
return if !current_user.otp_required_for_login && !AccountConfig.exists?(value: true,
account_id: current_user.account_id,
key: AccountConfig::FORCE_MFA)
redirect_to mfa_setup_path, notice: 'Setup 2FA to continue'
end
def maybe_render_landing def maybe_render_landing
return if signed_in? return if signed_in?
+1 -1
View File
@@ -12,7 +12,7 @@ class ErrorsController < ActionController::Base
].freeze ].freeze
def show def show
if request.original_fullpath.in?(ENTERPRISE_PATHS) if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
end end
+16 -6
View File
@@ -5,13 +5,11 @@ class MfaSetupController < ApplicationController
authorize!(:update, current_user) authorize!(:update, current_user)
end end
def new before_action :set_provision_url, only: %i[show new]
current_user.otp_secret ||= User.generate_otp_secret
current_user.save! def show; end
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name) def new; end
end
def edit; end def edit; end
@@ -26,7 +24,7 @@ class MfaSetupController < ApplicationController
@error_message = 'Code is invalid' @error_message = 'Code is invalid'
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/new'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
end end
end end
@@ -41,4 +39,16 @@ class MfaSetupController < ApplicationController
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
end end
end end
private
def set_provision_url
return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login
current_user.otp_secret ||= User.generate_otp_secret
current_user.save!
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
end
end end
+22 -9
View File
@@ -14,22 +14,20 @@ class StartFormController < ApplicationController
def update def update
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil)) @submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
.order(id: :desc)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank) .find_or_initialize_by(**submitter_params.compact_blank)
if @submitter.completed_at? if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
else else
@submitter.assign_attributes( if @template.submitters.to_a.size > 1 && @submitter.new_record?
uuid: @template.submitters.first['uuid'], @error_message = 'Not found'
ip: request.remote_ip,
ua: request.user_agent,
preferences: { 'send_email' => true }
)
@submitter.submission ||= Submission.new(template: @template, return render :show
template_submitters: @template.submitters, end
source: :link)
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
if @submitter.save if @submitter.save
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
@@ -47,6 +45,21 @@ class StartFormController < ApplicationController
private private
def assign_submission_attributes(submitter, template)
submitter.assign_attributes(
uuid: template.submitters.first['uuid'],
ip: request.remote_ip,
ua: request.user_agent,
preferences: { 'send_email' => true }
)
submitter.submission ||= Submission.new(template:,
template_submitters: template.submitters,
source: :link)
submitter
end
def submitter_params def submitter_params
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs| params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email]) attrs[:email] = Submissions.normalize_email(attrs[:email])
+2 -2
View File
@@ -46,7 +46,7 @@ class SubmissionsController < ApplicationController
source: :invite, source: :invite,
mark_as_sent: params[:send_email] == '1', mark_as_sent: params[:send_email] == '1',
emails: params[:emails], emails: params[:emails],
params:) params: params.merge('send_completed_email' => true))
else else
Submissions.create_from_submitters(template: @template, Submissions.create_from_submitters(template: @template,
user: current_user, user: current_user,
@@ -54,7 +54,7 @@ class SubmissionsController < ApplicationController
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random', submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
mark_as_sent: params[:send_email] == '1', mark_as_sent: params[:send_email] == '1',
submissions_attrs: submissions_params[:submission].to_h.values, submissions_attrs: submissions_params[:submission].to_h.values,
params:) params: params.merge('send_completed_email' => true))
end end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
@@ -38,6 +38,10 @@ class SubmitFormController < ApplicationController
def update def update
submitter = Submitter.find_by!(slug: params[:slug]) submitter = Submitter.find_by!(slug: params[:slug])
if submitter.completed_at?
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
end
Submitters::SubmitValues.call(submitter, params, request) Submitters::SubmitValues.call(submitter, params, request)
head :ok head :ok
+3 -1
View File
@@ -58,9 +58,11 @@ class TemplatesController < ApplicationController
def destroy def destroy
notice = notice =
if !Docuseal.multitenant? && params[:permanently].present? if params[:permanently].present?
@template.destroy! @template.destroy!
Rollbar.info("Remove template: #{@template.id}") if defined?(Rollbar)
'Template has been removed.' 'Template has been removed.'
else else
@template.update!(deleted_at: Time.current) @template.update!(deleted_at: Time.current)
@@ -0,0 +1,52 @@
# frozen_string_literal: true
class TimestampServerController < ApplicationController
before_action :build_encrypted_config
authorize_resource :encrypted_config
def create
return head :not_found if Docuseal.multitenant?
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
else
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
end
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
end
private
def test_timeserver_url(url)
pdf = HexaPDF::Document.new
pdf.pages.add
pkcs = Accounts.load_signing_pkcs(current_account)
pdf.sign(StringIO.new,
reason: 'Test',
certificate: pkcs.certificate,
key: pkcs.key,
certificate_chain: pkcs.ca_certs || [],
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
end
def load_encrypted_config
@encrypted_config
end
def build_encrypted_config
@encrypted_config =
EncryptedConfig.find_or_initialize_by(account: current_account,
key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY)
@encrypted_config.assign_attributes(encrypted_config_params)
end
def encrypted_config_params
params.require(:encrypted_config).permit(:value)
end
end
@@ -7,7 +7,9 @@ class WebhookSettingsController < ApplicationController
def show; end def show; end
def create def create
@encrypted_config.update!(encrypted_config_params) @encrypted_config.assign_attributes(encrypted_config_params)
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.') redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
end end
+1
View File
@@ -17,6 +17,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
isDemo: this.dataset.isDemo === 'true', isDemo: this.dataset.isDemo === 'true',
attribution: this.dataset.attribution !== 'false', attribution: this.dataset.attribution !== 'false',
withConfetti: true, withConfetti: true,
withTypedSignature: this.dataset.withTypedSignature !== 'false',
values: reactive(JSON.parse(this.dataset.values)), values: reactive(JSON.parse(this.dataset.values)),
completedButton: JSON.parse(this.dataset.completedButton), completedButton: JSON.parse(this.dataset.completedButton),
attachments: reactive(JSON.parse(this.dataset.attachments)), attachments: reactive(JSON.parse(this.dataset.attachments)),
+6
View File
@@ -223,6 +223,7 @@
:field="currentField" :field="currentField"
:previous-value="previousSignatureValue" :previous-value="previousSignatureValue"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:with-typed-signature="withTypedSignature"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@@ -386,6 +387,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
withTypedSignature: {
type: Boolean,
required: false,
default: true
},
baseUrl: { baseUrl: {
type: String, type: String,
required: false, required: false,
@@ -23,7 +23,7 @@
</a> </a>
</span> </span>
<span <span
v-else v-else-if="withTypedSignature"
class="tooltip" class="tooltip"
:data-tip="t('type_text')" :data-tip="t('type_text')"
> >
@@ -149,6 +149,11 @@ export default {
required: true, required: true,
default: false default: false
}, },
withTypedSignature: {
type: Boolean,
required: false,
default: true
},
attachmentsIndex: { attachmentsIndex: {
type: Object, type: Object,
required: false, required: false,
+8 -3
View File
@@ -42,7 +42,7 @@
v-model="field.submitter_uuid" v-model="field.submitter_uuid"
class="border-r" class="border-r"
:compact="true" :compact="true"
:editable="editable" :editable="editable && !defaultField"
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'" :menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'"
:submitters="template.submitters" :submitters="template.submitters"
@update:model-value="save" @update:model-value="save"
@@ -51,7 +51,7 @@
<FieldType <FieldType
v-model="field.type" v-model="field.type"
:button-width="27" :button-width="27"
:editable="editable" :editable="editable && !defaultField"
:button-classes="'px-1'" :button-classes="'px-1'"
:menu-classes="'bg-white rounded-t-none'" :menu-classes="'bg-white rounded-t-none'"
@update:model-value="[maybeUpdateOptions(), save()]" @update:model-value="[maybeUpdateOptions(), save()]"
@@ -60,7 +60,7 @@
<span <span
v-if="field.type !== 'checkbox' || field.name" v-if="field.type !== 'checkbox' || field.name"
ref="name" ref="name"
:contenteditable="editable" :contenteditable="editable && !defaultField"
class="pr-1 cursor-text outline-none block" class="pr-1 cursor-text outline-none block"
style="min-width: 2px" style="min-width: 2px"
@keydown.enter.prevent="onNameEnter" @keydown.enter.prevent="onNameEnter"
@@ -160,6 +160,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
defaultField: {
type: Object,
required: false,
default: null
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
+23 -6
View File
@@ -8,7 +8,7 @@
class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10" class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10"
:style="{ backgroundColor }" :style="{ backgroundColor }"
> >
<div class="flex space-x-3"> <div class="flex items-center space-x-3">
<a <a
v-if="withLogo" v-if="withLogo"
href="/" href="/"
@@ -19,7 +19,7 @@
v-if="withTitle" v-if="withTitle"
:model-value="template.name" :model-value="template.name"
:editable="editable" :editable="editable"
class="text-3xl font-semibold focus:text-clip" class="text-xl md:text-3xl font-semibold focus:text-clip"
:icon-stroke-width="2.3" :icon-stroke-width="2.3"
@update:model-value="updateName" @update:model-value="updateName"
/> />
@@ -30,10 +30,25 @@
name="buttons" name="buttons"
/> />
<template v-else> <template v-else>
<a
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
class="btn btn-primary btn-ghost text-base hidden md:flex"
:target="template.submitters.length > 1 ? '' : '_blank'"
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
@click="maybeShowEmptyTemplateAlert"
>
<IconWritingSign
width="20"
class="inline"
/>
<span class="hidden md:inline">
Sign Yourself
</span>
</a>
<a <a
:href="`/templates/${template.id}/submissions/new`" :href="`/templates/${template.id}/submissions/new`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="btn btn-primary text-base" class="white-button md:!px-6"
@click="maybeShowEmptyTemplateAlert" @click="maybeShowEmptyTemplateAlert"
> >
<IconUsersPlus <IconUsersPlus
@@ -41,7 +56,7 @@
class="inline" class="inline"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Recipients Send
</span> </span>
</a> </a>
<button <button
@@ -129,6 +144,7 @@
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:document="document" :document="document"
:is-drag="!!dragField" :is-drag="!!dragField"
:default-fields="defaultFields"
:draw-field="drawField" :draw-field="drawField"
:editable="editable" :editable="editable"
:base-url="baseUrl" :base-url="baseUrl"
@@ -254,7 +270,7 @@ import Contenteditable from './contenteditable'
import DocumentPreview from './preview' import DocumentPreview from './preview'
import DocumentControls from './controls' import DocumentControls from './controls'
import FieldType from './field_type' import FieldType from './field_type'
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop, IconPlus, IconX } from '@tabler/icons-vue' import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop, IconPlus, IconX } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
@@ -268,6 +284,7 @@ export default {
IconPlus, IconPlus,
FieldType, FieldType,
IconX, IconX,
IconWritingSign,
Logo, Logo,
Dropzone, Dropzone,
DocumentPreview, DocumentPreview,
@@ -499,7 +516,7 @@ export default {
this.selectedAreaRef.value = null this.selectedAreaRef.value = null
} }
if (['Backspace', 'Delete'].includes(e.key) && this.selectedAreaRef.value && document.activeElement === document.body) { if (this.editable && ['Backspace', 'Delete'].includes(e.key) && this.selectedAreaRef.value && document.activeElement === document.body) {
this.removeArea(this.selectedAreaRef.value) this.removeArea(this.selectedAreaRef.value)
this.selectedAreaRef.value = null this.selectedAreaRef.value = null
@@ -8,6 +8,7 @@
:editable="editable" :editable="editable"
:areas="areasIndex[index]" :areas="areasIndex[index]"
:is-drag="isDrag" :is-drag="isDrag"
:default-fields="defaultFields"
:draw-field="drawField" :draw-field="drawField"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:image="image" :image="image"
@@ -35,6 +36,11 @@ export default {
required: false, required: false,
default: () => ({}) default: () => ({})
}, },
defaultFields: {
type: Array,
required: false,
default: () => []
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: true required: true
+8 -2
View File
@@ -14,7 +14,7 @@
<div class="flex items-center p-1 space-x-1"> <div class="flex items-center p-1 space-x-1">
<FieldType <FieldType
v-model="field.type" v-model="field.type"
:editable="editable" :editable="editable && !defaultField"
:button-width="20" :button-width="20"
@update:model-value="[maybeUpdateOptions(), save()]" @update:model-value="[maybeUpdateOptions(), save()]"
@click="scrollToFirstArea" @click="scrollToFirstArea"
@@ -22,7 +22,7 @@
<Contenteditable <Contenteditable
ref="name" ref="name"
:model-value="field.name || defaultName" :model-value="field.name || defaultName"
:editable="editable" :editable="editable && !defaultField"
:icon-inline="true" :icon-inline="true"
:icon-width="18" :icon-width="18"
:icon-stroke-width="1.6" :icon-stroke-width="1.6"
@@ -66,6 +66,7 @@
/> />
</button> </button>
<span <span
v-if="!defaultField"
class="dropdown dropdown-end" class="dropdown dropdown-end"
> >
<label <label
@@ -280,6 +281,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
defaultField: {
type: Object,
required: false,
default: null
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -24,6 +24,7 @@
:field="field" :field="field"
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)" :type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
:editable="editable && !dragField" :editable="editable && !dragField"
:default-field="defaultFields.find((f) => f.name === field.name)"
:draggable="editable" :draggable="editable"
@dragstart="dragField = field" @dragstart="dragField = field"
@dragend="dragField = null" @dragend="dragField = null"
+6
View File
@@ -23,6 +23,7 @@
:area="item.area" :area="item.area"
:field="item.field" :field="item.field"
:editable="editable" :editable="editable"
:default-field="defaultFields.find((f) => f.name === item.field.name)"
@start-resize="resizeDirection = $event" @start-resize="resizeDirection = $event"
@stop-resize="resizeDirection = null" @stop-resize="resizeDirection = null"
@start-drag="isMove = true" @start-drag="isMove = true"
@@ -69,6 +70,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defaultFields: {
type: Array,
required: false,
default: () => []
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: true required: true
+2 -1
View File
@@ -24,7 +24,8 @@ class ProcessSubmitterCompletionJob < ApplicationJob
def enqueue_completed_emails(submitter) def enqueue_completed_emails(submitter)
user = submitter.submission.created_by_user || submitter.template.author user = submitter.submission.created_by_user || submitter.template.author
if submitter.template.account.users.exists?(id: user.id) if submitter.template.account.users.exists?(id: user.id) &&
submitter.submission.preferences['send_email'] != false
bcc = submitter.submission.template.account.account_configs bcc = submitter.submission.template.account.account_configs
.find_by(key: AccountConfig::BCC_EMAILS)&.value .find_by(key: AccountConfig::BCC_EMAILS)&.value
+21 -4
View File
@@ -94,7 +94,26 @@ class SubmitterMailer < ApplicationMailer
total_size = audit_trail_data.size total_size = audit_trail_data.size
end end
documents.each do |attachment| total_size = add_attachments_with_size_limit(documents, total_size)
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
file_fields = submitter.submission.template_fields.select { |e| e['type'] == 'file' }
if file_fields.pluck('submitter_uuid').uniq.size == 1
storage_attachments =
submitter.attachments.where(uuid: submitter.values.values_at(*file_fields.pluck('uuid')).flatten)
add_attachments_with_size_limit(storage_attachments, total_size)
end
documents
end
def add_attachments_with_size_limit(storage_attachments, current_size)
total_size = current_size
storage_attachments.each do |attachment|
total_size += attachment.byte_size total_size += attachment.byte_size
break if total_size >= MAX_ATTACHMENTS_SIZE break if total_size >= MAX_ATTACHMENTS_SIZE
@@ -102,9 +121,7 @@ class SubmitterMailer < ApplicationMailer
attachments[attachment.filename.to_s] = attachment.download attachments[attachment.filename.to_s] = attachment.download
end end
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data total_size
documents
end end
def from_address_for_submitter(submitter) def from_address_for_submitter(submitter)
+2
View File
@@ -25,6 +25,8 @@ class AccountConfig < ApplicationRecord
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email' SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email' SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
BCC_EMAILS = 'bcc_emails' BCC_EMAILS = 'bcc_emails'
FORCE_MFA = 'force_mfa'
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
SUBMITTER_REMAILERS = 'submitter_reminders' SUBMITTER_REMAILERS = 'submitter_reminders'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button' FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
+1
View File
@@ -25,6 +25,7 @@ class EncryptedConfig < ApplicationRecord
FILES_STORAGE_KEY = 'active_storage', FILES_STORAGE_KEY = 'active_storage',
EMAIL_SMTP_KEY = 'action_mailer_smtp', EMAIL_SMTP_KEY = 'action_mailer_smtp',
ESIGN_CERTS_KEY = 'esign_certs', ESIGN_CERTS_KEY = 'esign_certs',
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
APP_URL_KEY = 'app_url', APP_URL_KEY = 'app_url',
WEBHOOK_URL_KEY = 'webhook_url' WEBHOOK_URL_KEY = 'webhook_url'
].freeze ].freeze
+3 -1
View File
@@ -72,7 +72,9 @@ class Submission < ApplicationRecord
}, scope: false, prefix: true }, scope: false, prefix: true
def audit_trail_url def audit_trail_url
audit_trail&.url return if audit_trail.blank?
Rails.application.routes.url_helpers.rails_storage_proxy_url(audit_trail, **Docuseal.default_url_options)
end end
alias audit_log_url audit_trail_url alias audit_log_url audit_trail_url
end end
+1
View File
@@ -39,6 +39,7 @@ class SubmissionEvent < ApplicationRecord
send_email: 'send_email', send_email: 'send_email',
send_reminder_email: 'send_reminder_email', send_reminder_email: 'send_reminder_email',
send_sms: 'send_sms', send_sms: 'send_sms',
send_2fa_sms: 'send_2fa_sms',
open_email: 'open_email', open_email: 'open_email',
click_email: 'click_email', click_email: 'click_email',
click_sms: 'click_sms', click_sms: 'click_sms',
+5 -2
View File
@@ -55,10 +55,13 @@ class User < ApplicationRecord
has_many :template_folders, dependent: :destroy, foreign_key: :author_id, inverse_of: :author has_many :template_folders, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
has_many :user_configs, dependent: :destroy has_many :user_configs, dependent: :destroy
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig' has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
has_many :email_messages, dependent: :destroy 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
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
if Docuseal.multitenant?
devise :registerable, :omniauthable, omniauth_providers: %i[google_oauth2 microsoft_office365 github]
end
attribute :role, :string, default: ADMIN_ROLE attribute :role, :string, default: ADMIN_ROLE
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
+29
View File
@@ -36,6 +36,35 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% if can?(:manage, AccountConfig) %>
<div class="px-1 mt-8">
<h2 class="text-2xl font-bold mb-2">Preferences</h2>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::FORCE_MFA) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
Force 2FA with Authenticator App
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TYPED_SIGNATURE) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
Allow typed text signatures
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div> </div>
<div class="w-0 md:w-52"></div> <div class="w-0 md:w-52"></div>
</div> </div>
+1 -1
View File
@@ -8,7 +8,7 @@
<% end %> <% end %>
<% if can?(:create, ::Template) %> <% if can?(:create, ::Template) %>
<%= render 'templates/upload_button' %> <%= render 'templates/upload_button' %>
<%= link_to new_template_path, class: 'btn btn-primary text-base btn-md gap-2', data: { turbo_frame: :modal } do %> <%= 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') %> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Create</span> <span class="hidden md:block">Create</span>
<% end %> <% end %>
+13 -6
View File
@@ -8,7 +8,7 @@
<div class="space-y-2"> <div class="space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :email, class: 'label' %> <%= f.label :email, class: 'label' %>
<%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'base-input' %> <%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'base-input', placeholder: 'Enter email to continue' %>
</div> </div>
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -16,11 +16,18 @@
</div> </div>
<% end %> <% end %>
<% if devise_mapping.omniauthable? %> <% if devise_mapping.omniauthable? %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %> <div class="space-y-4">
<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 %>
<% end %> <%= 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 %>
<%= 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 %>
</div>
<% end %> <% end %>
<%= render 'devise/shared/links' %> <%= render 'devise/shared/links' %>
</div> </div>
+12 -5
View File
@@ -20,11 +20,18 @@
</div> </div>
<% end %> <% end %>
<% if devise_mapping.omniauthable? %> <% if devise_mapping.omniauthable? %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %> <div class="space-y-4">
<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 %>
<% end %> <%= 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 %>
<%= 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 %>
</div>
<% end %> <% end %>
<%= render 'devise/shared/links' %> <%= render 'devise/shared/links' %>
</div> </div>
+26
View File
@@ -97,5 +97,31 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<% encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY) %>
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %>
<div class="flex-grow max-w-xl">
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">Timestamp Server</h2>
</div>
<%= form_for encrypted_config, url: timestamp_server_index_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<div class="form-control">
<%= f.label :value, class: 'label' do %>
<span class="flex items-center space-x-1">
<span>
Timeserver URL
</span>
<span class="tooltip" data-tip="URL of the trusted timeserver to be used to generate timestamp signatures.">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</span>
<% end %>
<%= f.url_field :value, autocomplete: 'off', class: 'base-input', placeholder: 'URL (optional)' %>
</div>
<div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
</div>
<% end %>
<% end %>
</div>
</div> </div>
</div> </div>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="24" height="24" 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="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10" /><path d="M10 12l4 0" /></svg>

After

Width:  |  Height:  |  Size: 449 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<path fill="#ff5722" d="M6 6H22V22H6z" transform="rotate(-180 14 14)"></path><path fill="#4caf50" d="M26 6H42V22H26z" transform="rotate(-180 34 14)"></path><path fill="#ffc107" d="M26 26H42V42H26z" transform="rotate(-180 34 34)"></path><path fill="#03a9f4" d="M6 26H22V42H6z" transform="rotate(-180 14 34)"></path>
</svg>

After

Width:  |  Height:  |  Size: 463 B

+17
View File
@@ -0,0 +1,17 @@
<%= form_for '', url: mfa_setup_path, data: { turbo_frame: :_top }, html: { id: 'mfa_form' } do |f| %>
<p class="text-center">
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' }) %>
</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' %>
<span>
<%= @error_message %>
</span>
</div>
<div class="form-control mt-4">
<%= f.button button_title(title: 'Save'), class: 'base-button' %>
</div>
<% end %>
+1 -17
View File
@@ -1,19 +1,3 @@
<%= render 'shared/turbo_modal', title: 'Setup 2FA' do %> <%= render 'shared/turbo_modal', title: 'Setup 2FA' do %>
<%= form_for '', url: mfa_setup_path, data: { turbo_frame: :_top } do |f| %> <%= render 'mfa_setup/form' %>
<p class="text-center">
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' }) %>
</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' %>
<span>
<%= @error_message %>
</span>
</div>
<div class="form-control mt-4">
<%= f.button button_title(title: 'Save'), class: 'base-button' %>
</div>
<% end %>
<% end %> <% end %>
+4
View File
@@ -0,0 +1,4 @@
<div class="max-w-lg mx-auto px-2">
<h1 class="text-4xl font-bold text-center mt-8">Setup 2FA</h1>
<%= render 'mfa_setup/form' %>
</div>
+2 -2
View File
@@ -2,13 +2,13 @@
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
<%= icon %> <%= icon %>
<% if title %> <% if title %>
<span><%= title %></span> <span class="<%= local_assigns[:title_class] %>"><%= title %></span>
<% end %> <% end %>
</span> </span>
</span> </span>
<span class="disabled"> <span class="disabled">
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-5 h-5 animate-spin') %> <%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
<span><%= disabled_with %>...</span> <span class="<%= local_assigns[:title_class] %>"><%= disabled_with %>...</span>
</span> </span>
</span> </span>
+5 -3
View File
@@ -24,9 +24,11 @@
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %> <%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
</div> </div>
<% end %> <% end %>
<div> <% if @template.submitters.to_a.size == 1 %>
<%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %> <div>
</div> <%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
</div>
<% end %>
</div> </div>
</div> </div>
<%= render 'shared/attribution', link_path: '/start' %> <%= render 'shared/attribution', link_path: '/start' %>
+2 -1
View File
@@ -27,7 +27,8 @@
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %> <%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %>
<div class="form-control !mt-0"> <div class="form-control !mt-0">
<%= f.label :email, class: 'label' %> <%= f.label :email, class: 'label' %>
<%= f.email_field :email, value: current_user&.email || params[:email], required: true, class: 'base-input', placeholder: 'Provide your email to start' %> <%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: 'Provide your email to start' %>
<%= @error_message %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %> <%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %>
@@ -9,7 +9,7 @@
</a> </a>
</div> </div>
<div class="grid <%= 'md:grid-cols-2' if template.submitters.size > 1 %> gap-4"> <div class="grid <%= 'md:grid-cols-2' if template.submitters.size > 1 %> gap-4">
<% template.submitters.each do |item| %> <% template.submitters.each_with_index do |item, index| %>
<submitter-item class="form-control"> <submitter-item class="form-control">
<% if template.submitters.size > 1 %> <% if template.submitters.size > 1 %>
<label class="label pt-0 pb-1 text-xs"> <label class="label pt-0 pb-1 text-xs">
@@ -18,11 +18,11 @@
<% end %> <% end %>
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name" required> <%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.full_name : '' %>
</submitters-autocomplete> </submitters-autocomplete>
<div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>"> <div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>">
<submitters-autocomplete data-field="email"> <submitters-autocomplete data-field="email">
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Email (optional)"> <input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Email (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.email : '' %>">
</submitters-autocomplete> </submitters-autocomplete>
<submitters-autocomplete data-field="phone"> <submitters-autocomplete data-field="phone">
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Phone (optional)"> <input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Phone (optional)">
@@ -34,10 +34,12 @@
</div> </div>
</div> </div>
</div> </div>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem"> <% if params[:selfsign].blank? %>
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> <a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
<span>Add New</span> <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
</a> <span>Add New</span>
</a>
<% end %>
</dynamic-list> </dynamic-list>
<div> <div>
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %> <%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
+8 -6
View File
@@ -18,14 +18,14 @@
</a> </a>
</div> </div>
<div class="grid md:grid-cols-2 gap-4"> <div class="grid md:grid-cols-2 gap-4">
<% template.submitters.each do |item| %> <% template.submitters.each_with_index do |item, index| %>
<submitter-item class="form-control"> <submitter-item class="form-control">
<label class="label pt-0 pb-1 text-xs"> <label class="label pt-0 pb-1 text-xs">
<span class="label-text"> <%= item['name'] %></span> <span class="label-text"> <%= item['name'] %></span>
</label> </label>
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="email"> <submitters-autocomplete data-field="email">
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Email" required> <%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Email', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.email : '' %>
</submitters-autocomplete> </submitters-autocomplete>
</submitter-item> </submitter-item>
<% end %> <% end %>
@@ -33,10 +33,12 @@
</div> </div>
</div> </div>
</div> </div>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem"> <% if params[:selfsign].blank? %>
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> <a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
<span>Add New</span> <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
</a> <span>Add New</span>
</a>
<% end %>
</dynamic-list> </dynamic-list>
<% end %> <% end %>
<div> <div>
+10 -8
View File
@@ -9,7 +9,7 @@
</a> </a>
</div> </div>
<div class="grid <%= template.submitters.size > 1 ? 'md:grid-cols-2 gap-4' : 'gap-1' %>"> <div class="grid <%= template.submitters.size > 1 ? 'md:grid-cols-2 gap-4' : 'gap-1' %>">
<% template.submitters.each do |item| %> <% template.submitters.each_with_index do |item, index| %>
<submitter-item class="grid <%= template.submitters.size > 1 ? 'gap-4' : 'md:grid-cols-2 gap-1' %>"> <submitter-item class="grid <%= template.submitters.size > 1 ? 'gap-4' : 'md:grid-cols-2 gap-1' %>">
<div class="form-control"> <div class="form-control">
<% if template.submitters.size > 1 %> <% if template.submitters.size > 1 %>
@@ -19,18 +19,18 @@
<% end %> <% end %>
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="phone"> <submitters-autocomplete data-field="phone">
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Phone" required> <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Phone', required: index.zero? %>
</submitters-autocomplete> </submitters-autocomplete>
<% if template.submitters.size > 1 %> <% if template.submitters.size > 1 %>
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)"> <input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5 w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>">
</submitters-autocomplete> </submitters-autocomplete>
<% end %> <% end %>
</div> </div>
<% if template.submitters.size == 1 %> <% if template.submitters.size == 1 %>
<div class="form-control flex"> <div class="form-control flex">
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)"> <input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered w-full" placeholder="Name (optional)" value="<%= params[:selfsign] && index.zero? ? current_user.full_name : '' %>">
</submitters-autocomplete> </submitters-autocomplete>
</div> </div>
<% end %> <% end %>
@@ -40,10 +40,12 @@
</div> </div>
</div> </div>
</div> </div>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem"> <% if params[:selfsign].blank? %>
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> <a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
<span>Add New</span> <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
</a> <span>Add New</span>
</a>
<% end %>
</dynamic-list> </dynamic-list>
<div> <div>
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %> <%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
+1 -1
View File
@@ -1,4 +1,4 @@
<%= render 'shared/turbo_modal', title: 'Add New Recipients' do %> <%= render 'shared/turbo_modal', title: params[:selfsign] ? 'Add Recipients' : 'Add New Recipients' do %>
<% options = [['via Email', 'email'], ['via Phone', 'phone'], ['Detailed', 'detailed']] %> <% options = [['via Email', 'email'], ['via Phone', 'phone'], ['Detailed', 'detailed']] %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-4 block"> <toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-4 block">
<div class="join"> <div class="join">
+1 -1
View File
@@ -55,7 +55,7 @@
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %> <% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %> <% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative"> <div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="shadow-md mb-4" height="<%= page.metadata['height'] %>"> <img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="border rounded mb-4" height="<%= page.metadata['height'] %>">
<div class="top-0 bottom-0 left-0 right-0 absolute"> <div class="top-0 bottom-0 left-0 right-0 absolute">
<% document_annots_index[index]&.each do |annot| %> <% document_annots_index[index]&.each do |annot| %>
<%= render 'submissions/annotation', annot: %> <%= render 'submissions/annotation', annot: %>
@@ -1,4 +1,4 @@
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %> <% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %> <% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
<% completed_button_params = submitter.submission.template.account.account_configs.find_by(key: AccountConfig::FORM_COMPLETED_BUTTON_KEY)&.value || {} %> <% configs = Submitters::FormConfigs.call(submitter) %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-button="<%= completed_button_params.to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>"></submission-form> <submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
+24 -2
View File
@@ -35,7 +35,18 @@
</button> </button>
</form> </form>
<% else %> <% else %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white md:w-36 flex">
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
<span class="hidden md:inline">Sign now</span>
</span>
</button>
</form>
<% else %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
<% end %>
<% end %> <% end %>
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span> <span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
<% if !submission.deleted_at? && can?(:destroy, submission) %> <% if !submission.deleted_at? && can?(:destroy, submission) %>
@@ -85,7 +96,18 @@
</form> </form>
<% elsif !is_submission_completed %> <% elsif !is_submission_completed %>
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>">
<button onclick="event.stopPropagation()" class="btn btn-xs btn-outline btn-neutral bg-white md:w-36">
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
<span class="hidden md:inline">Sign now</span>
</span>
</button>
</form>
<% else %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %>
<% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
+4 -4
View File
@@ -52,10 +52,10 @@
</a> </a>
</span> </span>
<% end %> <% end %>
<% if (!template.deleted_at? || !Docuseal.multitenant?) && can?(:destroy, template) %> <% if can?(:destroy, template) %>
<span class="tooltip tooltip-left" data-tip="<%= template.deleted_at? ? 'Remove' : 'Archive' %>"> <span class="tooltip tooltip-left" data-tip="<%= template.deleted_at? ? 'Delete' : 'Archive' %>">
<%= button_to template_path(template), data: { turbo_confirm: 'Are you sure?' }, params: { permanently: template.deleted_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %> <%= button_to template_path(template), data: { turbo_confirm: template.deleted_at? ? 'Template deletion is irreversible and will permanently remove all associated signed documents with it. Are you sure?' : 'Are you sure?' }, params: { permanently: template.deleted_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %>
<%= svg_icon('trash', class: 'w-4 h-4 enabled') %> <%= svg_icon(template.deleted_at? ? 'trash' : 'archive', class: 'w-4 h-4 enabled') %>
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %> <%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
<% end %> <% end %>
</span> </span>
+6 -3
View File
@@ -14,7 +14,7 @@
</span> </span>
</a> </a>
<% if can?(:update, template) %> <% if can?(:update, template) %>
<span class="pl-1 tooltip tooltip-right opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="Move"> <span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="Move">
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal"> <a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal">
<%= svg_icon('pencil_share', class: 'w-5 h-5') %> <%= svg_icon('pencil_share', class: 'w-5 h-5') %>
</a> </a>
@@ -23,13 +23,16 @@
</div> </div>
</div> </div>
<div class="flex md:justify-between space-x-2 flex-none pt-1"> <div class="flex md:justify-between space-x-2 flex-none pt-1">
<% if !template.deleted_at? %>
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'btn btn-sm btn-neutral text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copied_title: 'Copied', copy_title_md: 'Link', copied_title_md: 'Copied' %>
<% end %>
<% if !template.deleted_at? && can?(:destroy, template) %> <% if !template.deleted_at? && can?(:destroy, template) %>
<%= button_to button_title(title: 'Remove', disabled_with: 'Removing', icon: svg_icon('trash', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm', method: :delete, data: { turbo_confirm: 'Are you sure?' } %> <%= button_to button_title(title: 'Archive', disabled_with: 'Archiving', title_class: 'hidden md:inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm', method: :delete, data: { turbo_confirm: 'Are you sure?' } %>
<% end %> <% end %>
<% if can?(:create, template) %> <% if can?(:create, template) %>
<%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm', data: { turbo_frame: :modal } do %> <%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm', data: { turbo_frame: :modal } do %>
<%= svg_icon('copy', class: 'w-6 h-6') %> <%= svg_icon('copy', class: 'w-6 h-6') %>
<span>Clone</span> <span class="hidden md:inline">Clone</span>
<% end %> <% end %>
<% end %> <% end %>
<% if !template.deleted_at? && can?(:update, template) %> <% if !template.deleted_at? && can?(:update, template) %>
+1 -1
View File
@@ -4,7 +4,7 @@
<%= hidden_field_tag :base_template_id, @base_template.id %> <%= hidden_field_tag :base_template_id, @base_template.id %>
<% end %> <% end %>
<div class="form-control mt-6"> <div class="form-control mt-6">
<%= f.text_field :name, required: true, placeholder: 'Document Template Name', class: 'base-input' %> <%= f.text_field :name, required: true, placeholder: 'Document Name', class: 'base-input' %>
</div> </div>
<div class="mt-3 mb-4 flex items-center justify-between"> <div class="mt-3 mb-4 flex items-center justify-between">
<a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]"> <a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]">
+15 -13
View File
@@ -1,23 +1,18 @@
<%= render 'title', template: @template %> <%= render 'title', template: @template %>
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %> <% is_show_tabs = @pagy.pages > 1 || params[:status].present? %>
<% if !@pagy.count.zero? || @template.submitters.to_a.size == 1 || params[:q].present? %> <% if !@pagy.count.zero? || params[:q].present? %>
<div class="flex justify-between md:items-end flex-col md:flex-row <%= is_show_tabs ? 'mb-4' : 'mb-6' %>"> <div class="flex justify-between md:items-end flex-col md:flex-row <%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
<p class="text-3xl font-bold">Submissions</p> <p class="text-3xl font-bold">Submissions</p>
<div class="flex md:space-x-2 mt-3 md:mt-0"> <div class="flex justify-end space-x-2 mt-3 md:mt-0">
<% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %> <% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input' %> <%= render 'shared/search_input' %>
<% end %> <% end %>
<%= link_to new_template_submissions_export_path(@template), class: 'order-3 md:order-1 btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submissions_export_path(@template), class: 'btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
<span>Export</span> <span>Export</span>
<% end %> <% end %>
<% if @template.submitters.to_a.size == 1 && !@template.deleted_at? %>
<span class="order-1">
<%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Share Link', copied_title: 'Copied to Clipboard', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
</span>
<% end %>
<% if !@template.deleted_at? && can?(:create, Submission) %> <% if !@template.deleted_at? && can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), class: 'order-1 btn btn-primary text-base', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submission_path(@template), class: 'white-button !border', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span>Add <span class="hidden md:inline">Recipients</span></span> <span>Add <span class="hidden md:inline">Recipients</span></span>
<% end %> <% end %>
@@ -85,12 +80,19 @@
<% if can?(:create, Submission) %> <% if can?(:create, Submission) %>
<%= link_to new_template_submission_path(@template), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %> <%= link_to new_template_submission_path(@template), class: 'base-button mt-6', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %> <%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="mr-1">Add Recipients</span> <span class="mr-1">Send to Recipients</span>
<% end %> <% end %>
<% end %> <% end %>
<%= link_to start_form_url(slug: @template.slug), class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %> <% if @template.submitters.size == 1 %>
<%= svg_icon('writing', class: 'w-6 h-6') %> <%= link_to start_form_url(slug: @template.slug), class: 'white-button mt-6', target: '_blank', rel: 'noopener' do %>
<span class="mr-1">Sign it Yourself</span> <%= svg_icon('writing', class: 'w-6 h-6') %>
<span class="mr-1">Sign it Yourself</span>
<% end %>
<% else %>
<%= link_to new_template_submission_path(@template, selfsign: true), class: 'white-button mt-6', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('writing', class: 'w-6 h-6') %>
<span class="mr-1">Sign it Yourself</span>
<% end %>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>
+7 -5
View File
@@ -3,12 +3,14 @@
<div class="md:flex-grow"> <div class="md:flex-grow">
<div class="flex justify-between mb-4"> <div class="flex justify-between mb-4">
<h1 class="text-4xl font-bold">Team</h1> <h1 class="text-4xl font-bold">Team</h1>
<% if can?(:create, User.new(account: current_account)) %> <div class="flex items-center space-x-4">
<%= link_to new_user_path, class: 'btn btn-primary btn-md gap-2', data: { turbo_frame: 'modal' } do %> <% if can?(:create, User.new(account: current_account)) %>
<%= svg_icon('plus', class: 'w-6 h-6') %> <%= link_to new_user_path, class: 'btn btn-primary btn-md gap-2', data: { turbo_frame: 'modal' } do %>
<span>New User</span> <%= svg_icon('plus', class: 'w-6 h-6') %>
<span>New User</span>
<% end %>
<% end %> <% end %>
<% end %> </div>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table w-full table-lg rounded-b-none overflow-hidden"> <table class="table w-full table-lg rounded-b-none overflow-hidden">
+1 -1
View File
@@ -7,7 +7,7 @@
<%= 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| %>
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %> <%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
<div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-4 mt-2"> <div class="flex flex-row flex-wrap space-y-2 md:space-y-0 md:flex-nowrap md:space-x-4 mt-2">
<%= f.text_field :value, required: true, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %> <%= f.url_field :value, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %>
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-full md:w-32' %> <%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-full md:w-32' %>
</div> </div>
<% end %> <% end %>
+8
View File
@@ -14,6 +14,7 @@ Devise.otp_allowed_drift = 60.seconds
# #
# Use this hook to configure devise mailer, warden hooks and so forth. # Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model. # Many of these configuration options can be set straight in your model.
# rubocop:disable Metrics/BlockLength
Devise.setup do |config| Devise.setup do |config|
config.warden do |manager| config.warden do |manager|
manager.default_strategies(scope: :user).unshift(:two_factor_authenticatable) manager.default_strategies(scope: :user).unshift(:two_factor_authenticatable)
@@ -277,6 +278,12 @@ Devise.setup do |config|
# Add a new OmniAuth provider. Check the wiki for more information on setting # Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks. # up on your models and hooks.
config.omniauth :google_oauth2, ENV.fetch('GOOGLE_CLIENT_ID', nil), ENV.fetch('GOOGLE_CLIENT_SECRET', nil), {} config.omniauth :google_oauth2, ENV.fetch('GOOGLE_CLIENT_ID', nil), ENV.fetch('GOOGLE_CLIENT_SECRET', nil), {}
config.omniauth :microsoft_office365, ENV.fetch('OFFICE365_CLIENT_ID', nil),
ENV.fetch('OFFICE365_CLIENT_SECRET', nil), {}
if ENV['GITHUB_CLIENT_ID']
config.omniauth :github, ENV.fetch('GITHUB_CLIENT_ID', nil), ENV.fetch('GITHUB_CLIENT_SECRET', nil), {}
end
# ==> Warden configuration # ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or # If you want to use other strategies, that are not supported by Devise, or
@@ -316,3 +323,4 @@ Devise.setup do |config|
# changed. Defaults to true, so a user is signed in automatically after changing a password. # changed. Defaults to true, so a user is signed in automatically after changing a password.
# config.sign_in_after_change_password = true # config.sign_in_after_change_password = true
end end
# rubocop:enable Metrics/BlockLength
+4 -1
View File
@@ -42,13 +42,16 @@ Rails.application.routes.draw do
end end
end end
resources :templates, only: %i[update show index destroy] do resources :templates, only: %i[update show index destroy] do
resources :clone, only: %i[create], controller: 'templates_clone'
resources :submissions, only: %i[index create] resources :submissions, only: %i[index create]
resources :documents, only: %i[create], controller: 'templates_documents' resources :documents, only: %i[create], controller: 'templates_documents'
end end
end end
resources :verify_pdf_signature, only: %i[create] resources :verify_pdf_signature, only: %i[create]
resource :mfa_setup, only: %i[new edit create destroy], controller: 'mfa_setup' resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
resources :account_configs, only: %i[create]
resources :timestamp_server, only: %i[create]
resources :dashboard, only: %i[index] resources :dashboard, only: %i[index]
resources :setup, only: %i[index create] resources :setup, only: %i[index create]
resource :newsletter, only: %i[show update] resource :newsletter, only: %i[show update]
+1 -1
View File
@@ -15,7 +15,7 @@ aws_s3:
google: google:
service: GCS service: GCS
credentials: <%= JSON.parse(ENV['GCS_CREDENTIALS'] || '{}') %> credentials: <%= ENV['GCS_CREDENTIALS'] || '{}' %>
project: <%= ENV['GCS_PROJECT'] %> project: <%= ENV['GCS_PROJECT'] %>
bucket: <%= ENV['GCS_BUCKET'] %> bucket: <%= ENV['GCS_BUCKET'] %>
public: <%= ENV['ACTIVE_STORAGE_PUBLIC'] == 'true' %> public: <%= ENV['ACTIVE_STORAGE_PUBLIC'] == 'true' %>
+15
View File
@@ -71,6 +71,21 @@ module Accounts
end end
end end
def load_timeserver_url(account)
if Docuseal.multitenant?
Docuseal::TIMESERVER_URL
else
url = EncryptedConfig.find_by(account:, key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY)&.value
unless Docuseal.multitenant?
url ||=
Account.order(:id).first.encrypted_configs.find_by(key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY)&.value
end
url
end.presence
end
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? return true if ENV['SMTP_ADDRESS'].present?
+1
View File
@@ -28,6 +28,7 @@ module Docuseal
end end
CERTS = JSON.parse(ENV.fetch('CERTS', '{}')) CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
TIMESERVER_URL = ENV.fetch('TIMESERVER_URL', nil)
DEFAULT_URL_OPTIONS = { DEFAULT_URL_OPTIONS = {
host: HOST, host: HOST,
+1
View File
@@ -7,6 +7,7 @@ module SubmissionEvents
send_email: 'Email sent', send_email: 'Email sent',
send_reminder_email: 'Reminder email sent', send_reminder_email: 'Reminder email sent',
send_sms: 'SMS sent', send_sms: 'SMS sent',
send_2fa_sms: 'Verification SMS sent',
open_email: 'Email opened', open_email: 'Email opened',
click_email: 'Email link clicked', click_email: 'Email link clicked',
click_sms: 'SMS link clicked', click_sms: 'SMS link clicked',
+11 -3
View File
@@ -8,8 +8,15 @@ module Submissions
preferences = Submitters.normalize_preferences(template.account, user, params) preferences = Submitters.normalize_preferences(template.account, user, params)
Array.wrap(submissions_attrs).map do |attrs| Array.wrap(submissions_attrs).map do |attrs|
submission_preferences = Submitters.normalize_preferences(template.account, user, attrs)
submission_preferences = preferences.merge(submission_preferences)
set_submission_preferences = submission_preferences.slice('send_email')
set_submission_preferences['send_email'] = true if params['send_completed_email']
submission = template.submissions.new(created_by_user: user, source:, submission = template.submissions.new(created_by_user: user, source:,
template_submitters: template.submitters, submitters_order:) preferences: set_submission_preferences,
template_submitters: [], submitters_order:)
maybe_set_template_fields(submission, attrs[:submitters]) maybe_set_template_fields(submission, attrs[:submitters])
@@ -17,11 +24,12 @@ module Submissions
uuid = find_submitter_uuid(template, submitter_attrs, index) uuid = find_submitter_uuid(template, submitter_attrs, index)
next if uuid.blank? next if uuid.blank?
next if submitter_attrs.slice('email', 'phone', 'name').compact_blank.blank?
submission.template_submitters << template.submitters.find { |e| e['uuid'] == uuid }
is_order_sent = submitters_order == 'random' || index.zero? is_order_sent = submitters_order == 'random' || index.zero?
submission_preferences = Submitters.normalize_preferences(template.account, user, attrs)
build_submitter(submission:, attrs: submitter_attrs, uuid:, build_submitter(submission:, attrs: submitter_attrs, uuid:,
is_order_sent:, mark_as_sent:, user:, is_order_sent:, mark_as_sent:, user:,
preferences: preferences.merge(submission_preferences)) preferences: preferences.merge(submission_preferences))
+18 -15
View File
@@ -19,11 +19,7 @@ module Submissions
INFO_CREATOR = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})".freeze INFO_CREATOR = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})".freeze
SIGN_REASON = 'Signed with DocuSeal.co' SIGN_REASON = 'Signed with DocuSeal.co'
VERIFIED_TEXT = if Docuseal.multitenant? VERIFIED_TEXT = 'Verified'
'Verified by DocuSeal'
else
'Verified'
end
UNVERIFIED_TEXT = 'Unverified' UNVERIFIED_TEXT = 'Unverified'
module_function module_function
@@ -32,6 +28,7 @@ module Submissions
def call(submission) def call(submission)
account = submission.template.account account = submission.template.account
pkcs = Accounts.load_signing_pkcs(account) pkcs = Accounts.load_signing_pkcs(account)
tsa_url = Accounts.load_timeserver_url(account)
verify_url = Rails.application.routes.url_helpers.settings_esign_url(**Docuseal.default_url_options) verify_url = Rails.application.routes.url_helpers.settings_esign_url(**Docuseal.default_url_options)
composer = HexaPDF::Composer.new(skip_page_creation: true) composer = HexaPDF::Composer.new(skip_page_creation: true)
@@ -63,7 +60,7 @@ module Submissions
composer.new_page composer.new_page
composer.column(columns: 1) do |column| composer.column(columns: 1) do |column|
add_logo(column, account) add_logo(column, submission)
column.text('Audit Log', column.text('Audit Log',
font_size: 16, font_size: 16,
@@ -151,11 +148,11 @@ module Submissions
[ [
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
submitter.email && { submitter.email && click_email_event && {
text: "Email verification: #{click_email_event ? VERIFIED_TEXT : UNVERIFIED_TEXT}\n" text: "Email verification: #{VERIFIED_TEXT}\n"
}, },
submitter.phone && { submitter.phone && is_phone_verified && {
text: "Phone verification: #{is_phone_verified ? VERIFIED_TEXT : UNVERIFIED_TEXT}\n" text: "Phone verification: #{VERIFIED_TEXT}\n"
}, },
completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" }, completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" },
completed_event.data['sid'] && { text: "Session ID: #{completed_event.data['sid']}\n" }, completed_event.data['sid'] && { text: "Session ID: #{completed_event.data['sid']}\n" },
@@ -254,10 +251,16 @@ module Submissions
composer.document.trailer.info[:Creator] = INFO_CREATOR composer.document.trailer.info[:Creator] = INFO_CREATOR
composer.document.sign(io, reason: SIGN_REASON, sign_params = {
certificate: pkcs.certificate, reason: SIGN_REASON,
key: pkcs.key, certificate: pkcs.certificate,
certificate_chain: pkcs.ca_certs || []) key: pkcs.key,
certificate_chain: pkcs.ca_certs || []
}
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:) if tsa_url
composer.document.sign(io, **sign_params)
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!( blob: ActiveStorage::Blob.create_and_upload!(
@@ -268,7 +271,7 @@ module Submissions
) )
end end
def add_logo(column, _account = nil) def add_logo(column, _submission = nil)
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float) column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
column.formatted_text([{ text: 'DocuSeal', column.formatted_text([{ text: 'DocuSeal',
+19 -8
View File
@@ -29,6 +29,7 @@ module Submissions
account = submitter.submission.template.account account = submitter.submission.template.account
pkcs = Accounts.load_signing_pkcs(account) pkcs = Accounts.load_signing_pkcs(account)
tsa_url = Accounts.load_timeserver_url(account)
pdfs_index = build_pdfs_index(submitter) pdfs_index = build_pdfs_index(submitter)
@@ -42,7 +43,9 @@ module Submissions
page.rotate(0, flatten: true) if page[:Rotate] != 0 page.rotate(0, flatten: true) if page[:Rotate] != 0
page[:Annots] ||= [] page[:Annots] ||= []
page[:Annots] = page[:Annots].reject { |e| e[:A] && e[:A][:URI].to_s.starts_with?('file:///docuseal_field') } page[:Annots] = page[:Annots].reject do |e|
e.present? && e[:A] && e[:A][:URI].to_s.starts_with?('file:///docuseal_field')
end
width = page.box.width width = page.box.width
height = page.box.height height = page.box.height
@@ -66,7 +69,7 @@ module Submissions
scale = [(area['w'] * width) / image.width, scale = [(area['w'] * width) / image.width,
(area['h'] * height) / image.height].min (area['h'] * height) / image.height].min
io = StringIO.new(image.resize([scale * 4, 1].min).write_to_buffer('.png')) io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
canvas.image( canvas.image(
io, io,
@@ -190,7 +193,9 @@ module Submissions
submitter.submission.template_schema.map do |item| submitter.submission.template_schema.map do |item|
pdf = pdfs_index[item['attachment_uuid']] pdf = pdfs_index[item['attachment_uuid']]
attachment = save_signed_pdf(pdf:, submitter:, pkcs:, uuid: item['attachment_uuid'], name: item['name']) attachment = save_signed_pdf(pdf:, submitter:, pkcs:, tsa_url:,
uuid: item['attachment_uuid'],
name: item['name'])
image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image? image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
@@ -217,15 +222,21 @@ module Submissions
end end
# rubocop:enable Metrics # rubocop:enable Metrics
def save_signed_pdf(pdf:, submitter:, pkcs:, uuid:, name:) def save_signed_pdf(pdf:, submitter:, pkcs:, tsa_url:, uuid:, name:)
io = StringIO.new io = StringIO.new
pdf.trailer.info[:Creator] = info_creator pdf.trailer.info[:Creator] = info_creator
pdf.sign(io, reason: sign_reason(submitter.email), sign_params = {
certificate: pkcs.certificate, reason: sign_reason(submitter.email),
key: pkcs.key, certificate: pkcs.certificate,
certificate_chain: pkcs.ca_certs || []) key: pkcs.key,
certificate_chain: pkcs.ca_certs || []
}
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:) if tsa_url
pdf.sign(io, **sign_params)
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
uuid:, uuid:,
+53
View File
@@ -0,0 +1,53 @@
# frozen_string_literal: true
module Submissions
class TimestampHandler
HASH_ALGORITHM = 'SHA512'
TimestampError = Class.new(StandardError)
attr_reader :tsa_url
def initialize(tsa_url:)
@tsa_url = tsa_url
end
def finalize_objects(_signature_field, signature)
signature.document.version = '2.0'
signature[:Type] = :DocTimeStamp
signature[:Filter] = :'Adobe.PPKLite'
signature[:SubFilter] = :'ETSI.RFC3161'
end
def sign(io, byte_range)
digest = OpenSSL::Digest.new(HASH_ALGORITHM)
io.pos = byte_range[0]
digest << io.read(byte_range[1])
io.pos = byte_range[2]
digest << io.read(byte_range[3])
uri = Addressable::URI.parse(tsa_url)
conn = Faraday.new(uri.origin) do |c|
c.basic_auth(uri.user, uri.password) if uri.password.present?
end
response = conn.post(uri.path, build_payload(digest.digest),
'content-type' => 'application/timestamp-query')
raise TimestampError if response.status != 200 || response.body.blank?
OpenSSL::Timestamp::Response.new(response.body).token.to_der
end
def build_payload(digest)
req = OpenSSL::Timestamp::Request.new
req.algorithm = HASH_ALGORITHM
req.message_imprint = digest
req.to_der
end
end
end
+19
View File
@@ -0,0 +1,19 @@
# frozen_string_literal: true
module Submitters
module FormConfigs
module_function
def call(submitter)
configs = submitter.submission.template.account.account_configs
.where(key: [AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE])
completed_button = configs.find { |e| e.key == AccountConfig::FORM_COMPLETED_BUTTON_KEY }&.value || {}
with_typed_signature = configs.find { |e| e.key == AccountConfig::ALLOW_TYPED_SIGNATURE }&.value != false
{ completed_button:, with_typed_signature: }
end
end
end
+8 -4
View File
@@ -9,7 +9,7 @@ module Submitters
module_function module_function
def call(template, values, submitter_name) def call(template, values, submitter_name, throw_errors: false)
submitter = submitter =
template.submitters.find { |e| e['name'] == submitter_name } || template.submitters.find { |e| e['name'] == submitter_name } ||
raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}") raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}")
@@ -21,11 +21,15 @@ module Submitters
attachments = [] attachments = []
normalized_values = values.to_h.to_h do |key, value| normalized_values = values.to_h.filter_map do |key, value|
if fields_uuid_index[key].blank? if fields_uuid_index[key].blank?
key = fields_name_index[key]&.dig('uuid') || raise(UnknownFieldName, "Unknown field: #{key}") key = fields_name_index[key]&.dig('uuid')
raise(UnknownFieldName, "Unknown field: #{key}") if key.blank? && throw_errors
end end
next if key.blank?
if fields_uuid_index[key]['type'].in?(%w[initials signature image file]) if fields_uuid_index[key]['type'].in?(%w[initials signature image file])
new_value, new_attachments = normalize_attachment_value(value, template.account) new_value, new_attachments = normalize_attachment_value(value, template.account)
@@ -35,7 +39,7 @@ module Submitters
end end
[key, value] [key, value]
end end.to_h
[normalized_values, attachments] [normalized_values, attachments]
end end
+1 -3
View File
@@ -21,9 +21,7 @@ module Submitters
submitter.submission.save! submitter.submission.save!
return submitter unless submitter.completed_at? ProcessSubmitterCompletionJob.perform_later(submitter) if submitter.completed_at?
ProcessSubmitterCompletionJob.perform_later(submitter)
submitter submitter
end end
+13 -4
View File
@@ -4,12 +4,21 @@ module Users
module_function module_function
def from_omniauth(oauth) def from_omniauth(oauth)
user = User.find_by(email: oauth.info.email) user = User.find_by(email: oauth.info.email.to_s.downcase)
return user if user return user if user
User.new(email: oauth.info.email, case oauth['provider'].to_s
first_name: oauth.extra.id_info.given_name, when 'google_oauth2'
last_name: oauth.extra.id_info.family_name) User.new(email: oauth.info.email,
first_name: oauth.extra.id_info.given_name,
last_name: oauth.extra.id_info.family_name)
when 'microsoft_office365'
User.new(email: oauth.info.email,
first_name: oauth.info.first_name,
last_name: oauth.info.last_name)
when 'github'
User.new(email: oauth.info.email, first_name: oauth.info.name)
end
end end
end end
+2 -3
View File
@@ -18,7 +18,6 @@ RSpec.describe 'Template' do
expect(page).to have_content(template.name) expect(page).to have_content(template.name)
expect(page).to have_content('There are no Submissions') expect(page).to have_content('There are no Submissions')
expect(page).to have_content('Send an invitation to fill and complete the form') expect(page).to have_content('Send an invitation to fill and complete the form')
expect(page).to have_link('Add Recipients', href: new_template_submission_path(template))
expect(page).to have_link('Sign it Yourself') expect(page).to have_link('Sign it Yourself')
end end
end end
@@ -43,10 +42,10 @@ RSpec.describe 'Template' do
visit template_path(template) visit template_path(template)
end end
it 'removes a template' do it 'archives a template' do
expect do expect do
accept_confirm('Are you sure?') do accept_confirm('Are you sure?') do
click_button 'Remove' click_button 'Archive'
end end
end.to change { Template.active.count }.by(-1) end.to change { Template.active.count }.by(-1)