mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94a13ffca2 | ||
|
|
64c8707957 | ||
|
|
d421a73988 | ||
|
|
f7eef1f8d6 | ||
|
|
08c780d309 | ||
|
|
baaf1f510f | ||
|
|
8898dcc054 | ||
|
|
33521c8aeb | ||
|
|
9db03bd7b9 | ||
|
|
bd600c1fba | ||
|
|
82b18b08f1 | ||
|
|
0aefe616b3 | ||
|
|
59a66bbd16 | ||
|
|
155c708e4f | ||
|
|
5502c2ab1b | ||
|
|
e89b42de40 | ||
|
|
2562c4f432 | ||
|
|
17323074ca | ||
|
|
8a4c5c5e09 | ||
|
|
3033faff0d | ||
|
|
0e6aab0e8d | ||
|
|
75e3dd839a | ||
|
|
830e6eb88d |
No files matched your search
@@ -6,7 +6,8 @@ class AccountConfigsController < ApplicationController
|
|||||||
|
|
||||||
ALLOWED_KEYS = [
|
ALLOWED_KEYS = [
|
||||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
AccountConfig::FORCE_MFA
|
AccountConfig::FORCE_MFA,
|
||||||
|
AccountConfig::ESIGNING_PREFERENCE_KEY
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ module Api
|
|||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def authenticate_user!
|
||||||
|
@current_user ||=
|
||||||
|
if request.headers['X-Auth-Token'].present?
|
||||||
|
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
||||||
|
|
||||||
|
User.joins(:access_token).find_by(access_token: { sha256: })
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||||
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -31,24 +31,33 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
serialized_subbmitters =
|
submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
@submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob).map do |submitter|
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
|
|
||||||
|
|
||||||
Submitters::SerializeForApi.call(submitter)
|
serialized_submitters = submitters.map do |submitter|
|
||||||
end
|
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
|
||||||
|
|
||||||
|
Submitters::SerializeForApi.call(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
json = @submission.as_json(
|
json = @submission.as_json(
|
||||||
serialize_params.deep_merge(
|
serialize_params.deep_merge(
|
||||||
include: {
|
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
|
||||||
submission_events: {
|
|
||||||
only: %i[id submitter_id event_type event_timestamp]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
json[:submitters] = serialized_subbmitters
|
if submitters.all?(&:completed_at?)
|
||||||
|
last_submitter = submitters.max_by(&:completed_at)
|
||||||
|
|
||||||
|
json[:documents] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
|
||||||
|
json[:status] = 'completed'
|
||||||
|
json[:completed_at] = last_submitter.completed_at
|
||||||
|
else
|
||||||
|
json[:documents] = []
|
||||||
|
json[:status] = 'pending'
|
||||||
|
json[:completed_at] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
json[:submitters] = serialized_submitters
|
||||||
|
|
||||||
render json:
|
render json:
|
||||||
end
|
end
|
||||||
@@ -69,7 +78,7 @@ module Api
|
|||||||
def destroy
|
def destroy
|
||||||
@submission.update!(deleted_at: Time.current)
|
@submission.update!(deleted_at: Time.current)
|
||||||
|
|
||||||
render json: @submission.as_json(only: %i[id deleted_at])
|
render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -108,7 +117,7 @@ module Api
|
|||||||
def serialize_params
|
def serialize_params
|
||||||
{
|
{
|
||||||
only: %i[id source submitters_order created_at updated_at],
|
only: %i[id source submitters_order created_at updated_at],
|
||||||
methods: %i[audit_log_url],
|
methods: %i[audit_log_url archived_at],
|
||||||
include: {
|
include: {
|
||||||
submitters: { only: %i[id slug uuid name email phone
|
submitters: { only: %i[id slug uuid name email phone
|
||||||
completed_at opened_at sent_at
|
completed_at opened_at sent_at
|
||||||
|
|||||||
@@ -7,23 +7,18 @@ module Api
|
|||||||
def create
|
def create
|
||||||
authorize!(:manage, @template)
|
authorize!(:manage, @template)
|
||||||
|
|
||||||
template = current_account.templates.new(source: :api)
|
cloned_template = Templates::Clone.call(@template,
|
||||||
|
author: current_user,
|
||||||
|
name: params[:name],
|
||||||
|
application_key: params[:application_key],
|
||||||
|
folder_name: params[:folder_name])
|
||||||
|
|
||||||
template.application_key = params[:application_key]
|
cloned_template.source = :api
|
||||||
template.name = params[:name] || "#{@template.name} (Clone)"
|
cloned_template.save!
|
||||||
template.account = @template.account
|
|
||||||
template.author = current_user
|
|
||||||
template.assign_attributes(@template.slice(:folder_id, :fields, :schema, :submitters))
|
|
||||||
|
|
||||||
if params[:folder_name].present?
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||||
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
|
||||||
end
|
|
||||||
|
|
||||||
template.save!
|
render json: cloned_template.as_json(serialize_params)
|
||||||
|
|
||||||
Templates::CloneAttachments.call(template:, original_template: @template)
|
|
||||||
|
|
||||||
render json: template.as_json(serialize_params)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ class ErrorsController < ActionController::Base
|
|||||||
'/templates/html',
|
'/templates/html',
|
||||||
'/api/templates/html',
|
'/api/templates/html',
|
||||||
'/templates/pdf',
|
'/templates/pdf',
|
||||||
'/api/templates/pdf'
|
'/api/templates/pdf',
|
||||||
|
'/templates/doc',
|
||||||
|
'/api/templates/doc',
|
||||||
|
'/templates/docx',
|
||||||
|
'/api/templates/docx'
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|||||||
@@ -42,10 +42,15 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@template.account = current_account
|
if @base_template
|
||||||
@template.author = current_user
|
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
name: params.dig(:template, :name),
|
||||||
@template.assign_attributes(@base_template.slice(:fields, :schema, :submitters)) if @base_template
|
folder_name: params[:folder_name])
|
||||||
|
else
|
||||||
|
@template.account = current_account
|
||||||
|
@template.author = current_user
|
||||||
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
|
end
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class TimestampServerController < ApplicationController
|
|||||||
reason: 'Test',
|
reason: 'Test',
|
||||||
certificate: pkcs.certificate,
|
certificate: pkcs.certificate,
|
||||||
key: pkcs.key,
|
key: pkcs.key,
|
||||||
|
signature_size: 10_000,
|
||||||
certificate_chain: pkcs.ca_certs || [],
|
certificate_chain: pkcs.ca_certs || [],
|
||||||
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
|
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -75,23 +75,6 @@ button[disabled] .enabled {
|
|||||||
@apply select base-input w-full font-normal;
|
@apply select base-input w-full font-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-bottom-end:before {
|
|
||||||
transform: translateX(-95%);
|
|
||||||
top: var(--tooltip-offset);
|
|
||||||
left: 100%;
|
|
||||||
right: auto;
|
|
||||||
bottom: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-bottom-end:after {
|
|
||||||
transform: translateX(-25%);
|
|
||||||
border-color: transparent transparent var(--tooltip-color) transparent;
|
|
||||||
top: var(--tooltip-tail-offset);
|
|
||||||
left: 50%;
|
|
||||||
right: auto;
|
|
||||||
bottom: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete {
|
.autocomplete {
|
||||||
background: white;
|
background: white;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
|||||||
@@ -42,6 +42,11 @@
|
|||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-else-if="field.type === 'stamp' && stamp"
|
||||||
|
class="object-contain mx-auto"
|
||||||
|
:src="stamp.url"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="field.type === 'signature' && signature"
|
v-else-if="field.type === 'signature' && signature"
|
||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
@@ -158,7 +163,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -225,6 +230,7 @@ export default {
|
|||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
|
stamp: 'Stamp',
|
||||||
initials: 'Initials',
|
initials: 'Initials',
|
||||||
file: 'File',
|
file: 'File',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
@@ -246,6 +252,7 @@ export default {
|
|||||||
select: IconSelect,
|
select: IconSelect,
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
|
stamp: IconRubberStamp,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck,
|
phone: IconPhoneCheck,
|
||||||
@@ -259,6 +266,13 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
stamp () {
|
||||||
|
if (this.field.type === 'stamp') {
|
||||||
|
return this.attachmentsIndex[this.modelValue]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
signature () {
|
signature () {
|
||||||
if (this.field.type === 'signature') {
|
if (this.field.type === 'signature') {
|
||||||
return this.attachmentsIndex[this.modelValue]
|
return this.attachmentsIndex[this.modelValue]
|
||||||
|
|||||||
@@ -206,7 +206,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ImageStep
|
<ImageStep
|
||||||
v-else-if="currentField.type === 'image'"
|
v-else-if="currentField.type === 'image' || currentField.type === 'stamp'"
|
||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="$slots.buttons || withTitle"
|
v-if="$slots.buttons || withTitle"
|
||||||
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 top-0 z-10"
|
||||||
|
:class="{ sticky: withStickySubmitters }"
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
|
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
v-if="withDocumentsList"
|
||||||
ref="previews"
|
ref="previews"
|
||||||
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
||||||
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
||||||
@@ -216,6 +218,7 @@
|
|||||||
</FieldType>
|
</FieldType>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="withFieldsList"
|
||||||
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
||||||
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
|
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
|
||||||
>
|
>
|
||||||
@@ -373,6 +376,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withFieldsList: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withDocumentsList: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withPhone: {
|
withPhone: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -429,11 +442,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
const existingSubmittersUuids = this.defaultSubmitters.map((name) => {
|
||||||
|
return this.template.submitters.find(e => e.name === name)?.uuid
|
||||||
|
})
|
||||||
|
|
||||||
this.defaultSubmitters.forEach((name, index) => {
|
this.defaultSubmitters.forEach((name, index) => {
|
||||||
const submitter = (this.template.submitters[index] ||= {})
|
const submitter = (this.template.submitters[index] ||= {})
|
||||||
|
|
||||||
submitter.name = name
|
submitter.name = name
|
||||||
submitter.uuid ||= v4()
|
|
||||||
|
if (existingSubmittersUuids.filter(Boolean).length) {
|
||||||
|
submitter.uuid = existingSubmittersUuids[index] || v4()
|
||||||
|
} else {
|
||||||
|
submitter.uuid ||= v4()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.selectedSubmitter = this.template.submitters[0]
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
@@ -481,6 +503,10 @@ export default {
|
|||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'date') {
|
if (type === 'date') {
|
||||||
field.preferences = {
|
field.preferences = {
|
||||||
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
@@ -595,6 +621,41 @@ export default {
|
|||||||
area.option_uuid = this.drawOption.uuid
|
area.option_uuid = this.drawOption.uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (area.w === 0 || area.h === 0) {
|
||||||
|
const previousField = [...this.template.fields].reverse().find((f) => f.type === this.drawField.type && f !== this.drawField)
|
||||||
|
|
||||||
|
if (this.selectedField?.type === this.drawField.type) {
|
||||||
|
area.w = this.selectedAreaRef.value.w
|
||||||
|
area.h = this.selectedAreaRef.value.h
|
||||||
|
} else if (previousField?.areas?.length) {
|
||||||
|
area.w = previousField.areas[0].w
|
||||||
|
area.h = previousField.areas[0].h
|
||||||
|
} else {
|
||||||
|
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||||
|
const pageMask = documentRef.pageRefs[area.page].$refs.mask
|
||||||
|
|
||||||
|
if (this.drawField.type === 'checkbox' || this.drawOption) {
|
||||||
|
area.w = pageMask.clientWidth / 30 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 30 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
||||||
|
} else if (this.drawField.type === 'image') {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
||||||
|
} else if (this.drawField.type === 'signature' || this.drawField.type === 'stamp') {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
|
||||||
|
} else if (this.drawField.type === 'initials') {
|
||||||
|
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
|
} else {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
area.x -= area.w / 2
|
||||||
|
area.y -= area.h / 2
|
||||||
|
}
|
||||||
|
|
||||||
this.drawField.areas ||= []
|
this.drawField.areas ||= []
|
||||||
this.drawField.areas.push(area)
|
this.drawField.areas.push(area)
|
||||||
|
|
||||||
@@ -663,6 +724,10 @@ export default {
|
|||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
if (field.type === 'date') {
|
if (field.type === 'date') {
|
||||||
field.preferences = {
|
field.preferences = {
|
||||||
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
@@ -695,7 +760,7 @@ export default {
|
|||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
||||||
}
|
}
|
||||||
} else if (field.type === 'signature') {
|
} else if (field.type === 'signature' || field.type === 'stamp') {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
||||||
@@ -739,6 +804,10 @@ export default {
|
|||||||
this.scrollIntoDocument(schema[0])
|
this.scrollIntoDocument(schema[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.template.name === 'New Document') {
|
||||||
|
this.template.name = this.template.schema[0].name
|
||||||
|
}
|
||||||
|
|
||||||
if (this.onUpload) {
|
if (this.onUpload) {
|
||||||
this.onUpload(this.template)
|
this.onUpload(this.template)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,6 +247,7 @@
|
|||||||
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
|
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
|
:placeholder="`Option ${index + 1}`"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -264,6 +265,7 @@
|
|||||||
v-else
|
v-else
|
||||||
v-model="option.value"
|
v-model="option.value"
|
||||||
class="w-full input input-primary input-xs text-sm bg-transparent"
|
class="w-full input input-primary input-xs text-sm bg-transparent"
|
||||||
|
:placeholder="`Option ${index + 1}`"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
@focus="maybeFocusOnOptionArea(option)"
|
@focus="maybeFocusOnOptionArea(option)"
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
@@ -92,8 +92,9 @@ export default {
|
|||||||
multiple: 'Multiple',
|
multiple: 'Multiple',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Cells',
|
cells: 'Cells',
|
||||||
phone: 'Phone',
|
stamp: 'Stamp',
|
||||||
payment: 'Payment'
|
payment: 'Payment',
|
||||||
|
phone: 'Phone'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -104,13 +105,14 @@ export default {
|
|||||||
date: IconCalendarEvent,
|
date: IconCalendarEvent,
|
||||||
image: IconPhoto,
|
image: IconPhoto,
|
||||||
file: IconPaperclip,
|
file: IconPaperclip,
|
||||||
select: IconSelect,
|
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
|
select: IconSelect,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
phone: IconPhoneCheck,
|
stamp: IconRubberStamp,
|
||||||
payment: IconCreditCard
|
payment: IconCreditCard,
|
||||||
|
phone: IconPhoneCheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else-if="type == 'phone'"
|
v-else-if="type == 'phone'"
|
||||||
class="tooltip tooltip-bottom-end flex"
|
class="tooltip tooltip-bottom flex"
|
||||||
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -261,6 +261,10 @@ export default {
|
|||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'date') {
|
if (type === 'date') {
|
||||||
field.preferences = {
|
field.preferences = {
|
||||||
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
@@ -269,7 +273,7 @@ export default {
|
|||||||
|
|
||||||
this.fields.push(field)
|
this.fields.push(field)
|
||||||
|
|
||||||
if (['signature', 'initials', 'cells'].includes(type)) {
|
if (['signature', 'initials', 'cells', 'stamp'].includes(type)) {
|
||||||
this.$emit('set-draw', { field })
|
this.$emit('set-draw', { field })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
<Contenteditable
|
<Contenteditable
|
||||||
:model-value="item.name"
|
:model-value="item.name"
|
||||||
:icon-width="16"
|
:icon-width="16"
|
||||||
|
:editable="editable"
|
||||||
style="max-width: 95%"
|
style="max-width: 95%"
|
||||||
class="mx-auto"
|
class="mx-auto"
|
||||||
@update:model-value="onUpdateName"
|
@update:model-value="onUpdateName"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
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'
|
||||||
|
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
||||||
|
|
||||||
DEFAULT_VALUES = {
|
DEFAULT_VALUES = {
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY => {
|
SUBMITTER_INVITATION_EMAIL_KEY => {
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ class Submission < ApplicationRecord
|
|||||||
preserved: 'preserved'
|
preserved: 'preserved'
|
||||||
}, scope: false, prefix: true
|
}, scope: false, prefix: true
|
||||||
|
|
||||||
|
def archived_at
|
||||||
|
deleted_at
|
||||||
|
end
|
||||||
|
|
||||||
def audit_trail_url
|
def audit_trail_url
|
||||||
return if audit_trail.blank?
|
return if audit_trail.blank?
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,28 @@
|
|||||||
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if !Docuseal.multitenant? %>
|
||||||
|
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'multiple') %>
|
||||||
|
<% if can?(:manage, account_config) %>
|
||||||
|
<div class="px-1 mt-8 max-w-xl">
|
||||||
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
|
<h2 class="text-3xl font-bold">Preferences</h2>
|
||||||
|
</div>
|
||||||
|
<% 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>
|
||||||
|
Apply multiple PDF digital signatures in the document per each signer
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, { class: 'toggle', checked: account_config.value == 'multiple', onchange: 'this.form.requestSubmit()' }, 'multiple', 'single' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
||||||
<%= render 'shared/settings_nav' %>
|
<%= render 'shared/settings_nav' %>
|
||||||
<div class="flex-grow max-w-xl mx-auto">
|
<div class="flex-grow max-w-xl mx-auto">
|
||||||
<h1 class="text-4xl font-bold mb-4">SMS</h1>
|
<h1 class="text-4xl font-bold mb-4">SAML SSO</h1>
|
||||||
<%= render 'placeholder' %>
|
<%= render 'placeholder' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
||||||
<% if field['type'].in?(['signature', 'image', 'initials']) %>
|
<% if field['type'].in?(['signature', 'image', 'initials', 'stamp']) %>
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'].in?(['file', 'payment']) %>
|
<% elsif field['type'].in?(['file', 'payment']) %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
<div class="w-full bg-base-300 py-1">
|
<div class="w-full bg-base-300 py-1">
|
||||||
<img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'image' %>
|
<% elsif field['type'].in?(['image', 'stamp']) %>
|
||||||
<img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'] == 'file' || field['type'] == 'payment' %>
|
<% elsif field['type'] == 'file' || field['type'] == 'payment' %>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative '../../lib/auth_with_token_strategy'
|
|
||||||
|
|
||||||
Warden::Strategies.add(:auth_token, AuthWithTokenStrategy)
|
|
||||||
|
|
||||||
Devise.otp_allowed_drift = 60.seconds
|
Devise.otp_allowed_drift = 60.seconds
|
||||||
|
|
||||||
# Assuming you have not yet modified this file, each configuration option below
|
# Assuming you have not yet modified this file, each configuration option below
|
||||||
@@ -279,7 +275,7 @@ Devise.setup do |config|
|
|||||||
#
|
#
|
||||||
config.warden do |manager|
|
config.warden do |manager|
|
||||||
# manager.intercept_401 = false
|
# manager.intercept_401 = false
|
||||||
manager.default_strategies(scope: :user).unshift(:auth_token)
|
# manager.default_strategies(scope: :user).unshift(:auth_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
# ==> Mountable engine configurations
|
# ==> Mountable engine configurations
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class AuthWithTokenStrategy < Devise::Strategies::Base
|
|
||||||
def valid?
|
|
||||||
request.headers['X-Auth-Token'].present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate!
|
|
||||||
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
|
||||||
|
|
||||||
user = User.joins(:access_token).find_by(access_token: { sha256: })
|
|
||||||
|
|
||||||
if user
|
|
||||||
success!(user)
|
|
||||||
else
|
|
||||||
fail!('Invalid token')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+15
-3
@@ -9,14 +9,26 @@ module PdfIcons
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def check_io
|
def check_io
|
||||||
@check_io ||= StringIO.new(PATH.join('check.png').read)
|
StringIO.new(check_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def paperclip_io
|
def paperclip_io
|
||||||
@paperclip_io ||= StringIO.new(PATH.join('paperclip.png').read)
|
StringIO.new(paperclip_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def logo_io
|
def logo_io
|
||||||
@logo_io ||= StringIO.new(PATH.join('logo.png').read)
|
StringIO.new(logo_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_data
|
||||||
|
@check_data ||= PATH.join('check.png').read
|
||||||
|
end
|
||||||
|
|
||||||
|
def paperclip_data
|
||||||
|
@paperclip_data ||= PATH.join('paperclip.png').read
|
||||||
|
end
|
||||||
|
|
||||||
|
def logo_data
|
||||||
|
@logo_data ||= PATH.join('logo.png').read
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -192,7 +192,7 @@ module Submissions
|
|||||||
}
|
}
|
||||||
].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0]
|
].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0]
|
||||||
),
|
),
|
||||||
if field['type'].in?(%w[image signature initials])
|
if field['type'].in?(%w[image signature initials stamp])
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||||
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ module Submissions
|
|||||||
if event.event_type.include?('sms') || event.event_type.include?('phone')
|
if event.event_type.include?('sms') || event.event_type.include?('phone')
|
||||||
submitter.phone
|
submitter.phone
|
||||||
else
|
else
|
||||||
(submitter.name || submitter.email || submitter.phone)
|
submitter.name || submitter.email || submitter.phone
|
||||||
end
|
end
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -277,7 +277,10 @@ module Submissions
|
|||||||
certificate_chain: pkcs.ca_certs || []
|
certificate_chain: pkcs.ca_certs || []
|
||||||
}
|
}
|
||||||
|
|
||||||
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:) if tsa_url
|
if tsa_url
|
||||||
|
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:)
|
||||||
|
sign_params[:signature_size] = 10_000
|
||||||
|
end
|
||||||
|
|
||||||
composer.document.sign(io, **sign_params)
|
composer.document.sign(io, **sign_params)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
INFO_CREATOR = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})".freeze
|
INFO_CREATOR = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})".freeze
|
||||||
SIGN_REASON = 'Signed by %<email>s with DocuSeal.co'
|
SIGN_REASON = 'Signed by %<name>s with DocuSeal.co'
|
||||||
|
SIGN_SIGNLE_REASON = 'Digitally signed with DocuSeal.co'
|
||||||
|
|
||||||
TEXT_LEFT_MARGIN = 1
|
TEXT_LEFT_MARGIN = 1
|
||||||
TEXT_TOP_MARGIN = 1
|
TEXT_TOP_MARGIN = 1
|
||||||
@@ -66,7 +67,7 @@ module Submissions
|
|||||||
canvas.font(FONT_NAME, size: font_size)
|
canvas.font(FONT_NAME, size: font_size)
|
||||||
|
|
||||||
case field['type']
|
case field['type']
|
||||||
when 'image', 'signature', 'initials'
|
when 'image', 'signature', 'initials', 'stamp'
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||||
|
|
||||||
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
image = Vips::Image.new_from_buffer(attachment.download, '').autorot
|
||||||
@@ -200,9 +201,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:, tsa_url:,
|
attachment = save_pdf(pdf:, submitter:, pkcs:, tsa_url:,
|
||||||
uuid: item['attachment_uuid'],
|
uuid: item['attachment_uuid'],
|
||||||
name: item['name'])
|
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,9 +218,10 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
images_pdf_result =
|
images_pdf_result =
|
||||||
save_signed_pdf(
|
save_pdf(
|
||||||
pdf: images_pdf,
|
pdf: images_pdf,
|
||||||
submitter:,
|
submitter:,
|
||||||
|
tsa_url:,
|
||||||
pkcs:,
|
pkcs:,
|
||||||
uuid: images_pdf_uuid(original_documents.select(&:image?)),
|
uuid: images_pdf_uuid(original_documents.select(&:image?)),
|
||||||
name: template.name
|
name: template.name
|
||||||
@@ -229,27 +231,34 @@ module Submissions
|
|||||||
end
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
def save_signed_pdf(pdf:, submitter:, pkcs:, tsa_url:, uuid:, name:)
|
def save_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
|
||||||
|
|
||||||
sign_params = {
|
sign_reason = fetch_sign_reason(submitter)
|
||||||
reason: sign_reason(submitter.email),
|
|
||||||
certificate: pkcs.certificate,
|
|
||||||
key: pkcs.key,
|
|
||||||
certificate_chain: pkcs.ca_certs || []
|
|
||||||
}
|
|
||||||
|
|
||||||
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:) if tsa_url
|
if sign_reason
|
||||||
|
sign_params = {
|
||||||
|
reason: sign_reason,
|
||||||
|
certificate: pkcs.certificate,
|
||||||
|
key: pkcs.key,
|
||||||
|
certificate_chain: pkcs.ca_certs || []
|
||||||
|
}
|
||||||
|
|
||||||
pdf.sign(io, **sign_params)
|
if tsa_url
|
||||||
|
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:)
|
||||||
|
sign_params[:signature_size] = 10_000
|
||||||
|
end
|
||||||
|
|
||||||
|
pdf.sign(io, **sign_params)
|
||||||
|
else
|
||||||
|
pdf.write(io, incremental: true)
|
||||||
|
end
|
||||||
|
|
||||||
ActiveStorage::Attachment.create!(
|
ActiveStorage::Attachment.create!(
|
||||||
uuid:,
|
uuid:,
|
||||||
blob: ActiveStorage::Blob.create_and_upload!(
|
blob: ActiveStorage::Blob.create_and_upload!(io: StringIO.new(io.string), filename: "#{name}.pdf"),
|
||||||
io: StringIO.new(io.string), filename: "#{name}.pdf"
|
|
||||||
),
|
|
||||||
metadata: { sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) },
|
metadata: { sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) },
|
||||||
name: 'documents',
|
name: 'documents',
|
||||||
record: submitter
|
record: submitter
|
||||||
@@ -304,8 +313,34 @@ module Submissions
|
|||||||
pdf
|
pdf
|
||||||
end
|
end
|
||||||
|
|
||||||
def sign_reason(email)
|
def sign_reason(name)
|
||||||
format(SIGN_REASON, email:)
|
format(SIGN_REASON, name:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def single_sign_reason
|
||||||
|
SIGN_SIGNLE_REASON
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_sign_reason(submitter)
|
||||||
|
reason_name = submitter.email || submitter.name || submitter.phone
|
||||||
|
|
||||||
|
return sign_reason(reason_name) if Docuseal.multitenant?
|
||||||
|
|
||||||
|
config =
|
||||||
|
if Docuseal.multitenant?
|
||||||
|
AccountConfig.where(account: submitter.account, key: AccountConfig::ESIGNING_PREFERENCE_KEY)
|
||||||
|
.first_or_initialize(value: 'multiple')
|
||||||
|
else
|
||||||
|
AccountConfig.where(key: AccountConfig::ESIGNING_PREFERENCE_KEY)
|
||||||
|
.first_or_initialize(value: 'multiple')
|
||||||
|
end
|
||||||
|
|
||||||
|
return sign_reason(reason_name) if config.value == 'multiple'
|
||||||
|
|
||||||
|
return single_sign_reason if !submitter.submission.submitters.exists?(completed_at: nil) &&
|
||||||
|
submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
|
||||||
|
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def info_creator
|
def info_creator
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ module Submissions
|
|||||||
attachments_index = attachments.index_by(&:uuid)
|
attachments_index = attachments.index_by(&:uuid)
|
||||||
|
|
||||||
submitters.each do |submitter|
|
submitters.each do |submitter|
|
||||||
submitter.values.to_a.each do |_, value|
|
submitter.values.each_value do |value|
|
||||||
attachment = attachments_index[value]
|
attachment = attachments_index[value]
|
||||||
|
|
||||||
next unless attachment
|
next unless attachment
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submitters
|
||||||
|
module CreateStampAttachment
|
||||||
|
WIDTH = 400
|
||||||
|
HEIGHT = 200
|
||||||
|
|
||||||
|
TRANSPARENT_PIXEL = "\x89PNG\r\n\u001A\n\u0000\u0000\u0000\rIHDR\u0000" \
|
||||||
|
"\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0004" \
|
||||||
|
"\u0000\u0000\u0000\xB5\u001C\f\u0002\u0000\u0000\u0000" \
|
||||||
|
"\vIDATx\xDAc\xFC_\u000F\u0000\u0002\x83\u0001\x804\xC3ڨ" \
|
||||||
|
"\u0000\u0000\u0000\u0000IEND\xAEB`\x82"
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(submitter)
|
||||||
|
image = generate_stamp_image(submitter)
|
||||||
|
|
||||||
|
image_data = image.write_to_buffer('.png')
|
||||||
|
|
||||||
|
checksum = Digest::MD5.base64digest(image_data)
|
||||||
|
|
||||||
|
attachment = submitter.attachments.joins(:blob).find_by(blob: { checksum: })
|
||||||
|
|
||||||
|
attachment || ActiveStorage::Attachment.create!(
|
||||||
|
blob: ActiveStorage::Blob.create_and_upload!(io: StringIO.new(image_data), filename: 'stamp.png'),
|
||||||
|
metadata: { analyzed: true, identified: true, width: image.width, height: image.height },
|
||||||
|
name: 'attachments',
|
||||||
|
record: submitter
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
|
def generate_stamp_image(submitter)
|
||||||
|
logo = Vips::Image.new_from_buffer(load_logo(submitter).read, '')
|
||||||
|
|
||||||
|
logo = logo.resize([WIDTH / logo.width.to_f, HEIGHT / logo.height.to_f].min)
|
||||||
|
|
||||||
|
base_layer = Vips::Image.black(WIDTH, HEIGHT).new_from_image([255, 255, 255]).copy(interpretation: :srgb)
|
||||||
|
|
||||||
|
opacity_layer = Vips::Image.new_from_buffer(TRANSPARENT_PIXEL, '').resize(WIDTH)
|
||||||
|
|
||||||
|
text = build_text_image(submitter)
|
||||||
|
|
||||||
|
text_layer = text.new_from_image([0, 0, 0]).copy(interpretation: :srgb)
|
||||||
|
text_layer = text_layer.bandjoin(text)
|
||||||
|
|
||||||
|
base_layer = base_layer.composite(logo, 'over',
|
||||||
|
x: (WIDTH - logo.width) / 2,
|
||||||
|
y: (HEIGHT - logo.height) / 2)
|
||||||
|
|
||||||
|
base_layer = base_layer.composite(opacity_layer, 'over')
|
||||||
|
|
||||||
|
base_layer.composite(text_layer, 'over',
|
||||||
|
x: (WIDTH - text_layer.width) / 2,
|
||||||
|
y: (HEIGHT - text_layer.height) / 2)
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
|
def build_text_image(submitter)
|
||||||
|
time = I18n.l(submitter.completed_at.in_time_zone(submitter.account.timezone), format: :long,
|
||||||
|
locale: submitter.account.locale)
|
||||||
|
|
||||||
|
timezone = TimeUtils.timezone_abbr(submitter.account.timezone, submitter.completed_at)
|
||||||
|
|
||||||
|
name = if submitter.name.present? && submitter.email.present?
|
||||||
|
"#{submitter.name} #{submitter.email}"
|
||||||
|
else
|
||||||
|
submitter.name || submitter.email || submitter.phone
|
||||||
|
end
|
||||||
|
|
||||||
|
role = if submitter.submission.template_submitters.size > 1
|
||||||
|
item = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }
|
||||||
|
|
||||||
|
"Role: #{item['name']}\n"
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
|
text = %(<span size="90">Digitally signed by: <b>#{name}</b>\n#{role}#{time} #{timezone}</span>)
|
||||||
|
|
||||||
|
Vips::Image.text(text, width: WIDTH, height: HEIGHT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_logo(_submitter)
|
||||||
|
PdfIcons.logo_io
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -19,7 +19,8 @@ module Submitters
|
|||||||
serialize_params = {
|
serialize_params = {
|
||||||
include: {},
|
include: {},
|
||||||
only: %i[id slug uuid name email phone completed_at application_key
|
only: %i[id slug uuid name email phone completed_at application_key
|
||||||
opened_at sent_at created_at updated_at]
|
opened_at sent_at created_at updated_at],
|
||||||
|
methods: %i[status]
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize_params[:include][:template] = { only: %i[id name created_at updated_at] } if with_template
|
serialize_params[:include][:template] = { only: %i[id name created_at updated_at] } if with_template
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ module Submitters
|
|||||||
default_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
|
default_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
|
||||||
next if field['submitter_uuid'] != submitter.uuid
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
|
|
||||||
|
if field['type'] == 'stamp'
|
||||||
|
acc[field['uuid']] ||= Submitters::CreateStampAttachment.call(submitter).uuid
|
||||||
|
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
value = field['default_value']
|
value = field['default_value']
|
||||||
|
|
||||||
next if value.blank?
|
next if value.blank?
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Templates
|
||||||
|
module Clone
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(original_template, author:, application_key: nil, name: nil, folder_name: nil)
|
||||||
|
original_template_account = original_template.account
|
||||||
|
|
||||||
|
template = original_template_account.templates.new
|
||||||
|
|
||||||
|
template.application_key = application_key
|
||||||
|
template.author = author
|
||||||
|
template.name = name || "#{original_template.name} (Clone)"
|
||||||
|
|
||||||
|
template.assign_attributes(original_template.slice(:folder_id, :schema))
|
||||||
|
|
||||||
|
template.folder = TemplateFolders.find_or_create_by_name(author, folder_name) if folder_name.present?
|
||||||
|
|
||||||
|
submitter_uuids_replacements = {}
|
||||||
|
|
||||||
|
cloned_submitters = original_template['submitters'].deep_dup
|
||||||
|
cloned_fields = original_template['fields'].deep_dup
|
||||||
|
|
||||||
|
cloned_submitters.each do |submitter|
|
||||||
|
new_submitter_uuid = SecureRandom.uuid
|
||||||
|
|
||||||
|
submitter_uuids_replacements[submitter['uuid']] = new_submitter_uuid
|
||||||
|
submitter['uuid'] = new_submitter_uuid
|
||||||
|
end
|
||||||
|
|
||||||
|
cloned_fields.each do |field|
|
||||||
|
field['uuid'] = SecureRandom.uuid
|
||||||
|
field['submitter_uuid'] = submitter_uuids_replacements[field['submitter_uuid']]
|
||||||
|
end
|
||||||
|
|
||||||
|
template.assign_attributes(fields: cloned_fields, submitters: cloned_submitters)
|
||||||
|
|
||||||
|
template
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,23 +5,48 @@ module Templates
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(template:, original_template:)
|
def call(template:, original_template:)
|
||||||
original_template.documents.preload(:preview_images_attachments).each do |document|
|
schema_uuids_replacements = {}
|
||||||
|
|
||||||
|
cloned_schema = original_template['schema'].deep_dup
|
||||||
|
cloned_fields = template['fields'].deep_dup
|
||||||
|
|
||||||
|
cloned_schema.each do |schema_item|
|
||||||
|
new_schema_item_uuid = SecureRandom.uuid
|
||||||
|
|
||||||
|
schema_uuids_replacements[schema_item['attachment_uuid']] = new_schema_item_uuid
|
||||||
|
schema_item['attachment_uuid'] = new_schema_item_uuid
|
||||||
|
end
|
||||||
|
|
||||||
|
cloned_fields.each do |field|
|
||||||
|
next if field['areas'].blank?
|
||||||
|
|
||||||
|
field['areas'].each do |area|
|
||||||
|
area['attachment_uuid'] = schema_uuids_replacements[area['attachment_uuid']]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
template.update!(schema: cloned_schema, fields: cloned_fields)
|
||||||
|
|
||||||
|
original_template.schema_documents.preload(:preview_images_attachments).each do |document|
|
||||||
new_document = ActiveStorage::Attachment.create!(
|
new_document = ActiveStorage::Attachment.create!(
|
||||||
uuid: document.uuid,
|
uuid: schema_uuids_replacements[document.uuid],
|
||||||
blob_id: document.blob_id,
|
blob_id: document.blob_id,
|
||||||
name: 'documents',
|
name: 'documents',
|
||||||
record: template
|
record: template
|
||||||
)
|
)
|
||||||
|
|
||||||
ApplicationRecord.no_touching do
|
clone_document_preview_images_attachments(document:, new_document:)
|
||||||
document.preview_images_attachments.each do |preview_image|
|
end
|
||||||
ActiveStorage::Attachment.create!(
|
end
|
||||||
uuid: preview_image.uuid,
|
|
||||||
blob_id: preview_image.blob_id,
|
def clone_document_preview_images_attachments(document:, new_document:)
|
||||||
name: 'preview_images',
|
ApplicationRecord.no_touching do
|
||||||
record: new_document
|
document.preview_images_attachments.each do |preview_image|
|
||||||
)
|
ActiveStorage::Attachment.create!(
|
||||||
end
|
blob_id: preview_image.blob_id,
|
||||||
|
name: 'preview_images',
|
||||||
|
record: new_document
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ module TimeUtils
|
|||||||
end
|
end
|
||||||
|
|
||||||
def format_date_string(string, format, locale)
|
def format_date_string(string, format, locale)
|
||||||
date = Date.parse(string)
|
date = Date.parse(string.to_s)
|
||||||
|
|
||||||
format ||= locale.to_s.ends_with?('US') ? DEFAULT_DATE_FORMAT_US : DEFAULT_DATE_FORMAT
|
format ||= locale.to_s.ends_with?('US') ? DEFAULT_DATE_FORMAT_US : DEFAULT_DATE_FORMAT
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Users
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def from_omniauth(oauth)
|
|
||||||
user = User.find_by(email: oauth.info.email.to_s.downcase)
|
|
||||||
|
|
||||||
return user if user
|
|
||||||
|
|
||||||
case oauth['provider'].to_s
|
|
||||||
when 'google_oauth2'
|
|
||||||
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
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :template_folder do
|
||||||
|
account
|
||||||
|
|
||||||
|
author factory: %i[user]
|
||||||
|
name { Faker::Book.title }
|
||||||
|
|
||||||
|
trait :with_templates do
|
||||||
|
after(:create) do |template_folder|
|
||||||
|
create_list(:template, 2, folder: template_folder, account: template_folder.account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -74,5 +74,70 @@ RSpec.describe 'Template' do
|
|||||||
expect(page).to have_current_path(edit_template_path(template), ignore_query: true)
|
expect(page).to have_current_path(edit_template_path(template), ignore_query: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'clone a template and move it to a new folder' do
|
||||||
|
click_link 'Clone'
|
||||||
|
|
||||||
|
within '#modal' do
|
||||||
|
fill_in 'template[name]', with: 'New Template Name'
|
||||||
|
click_link 'Change Folder'
|
||||||
|
fill_in 'folder_name', with: 'New Folder Name'
|
||||||
|
|
||||||
|
expect do
|
||||||
|
click_button 'Submit'
|
||||||
|
end.to change { Template.active.count }.by(1).and change { TemplateFolder.active.count }.by(1)
|
||||||
|
|
||||||
|
template = Template.last
|
||||||
|
|
||||||
|
expect(template.name).to eq('New Template Name')
|
||||||
|
expect(template.folder.name).to eq('New Folder Name')
|
||||||
|
expect(page).to have_current_path(edit_template_path(template), ignore_query: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'clones a template and moves it to an existing folder' do
|
||||||
|
template_folder = create(:template_folder, :with_templates, account:, author: user)
|
||||||
|
|
||||||
|
click_link 'Clone'
|
||||||
|
|
||||||
|
within '#modal' do
|
||||||
|
template_folder.reload
|
||||||
|
fill_in 'template[name]', with: 'New Template Name'
|
||||||
|
click_link 'Change Folder'
|
||||||
|
end
|
||||||
|
|
||||||
|
within '.autocomplete' do
|
||||||
|
find('div', text: template_folder.name).click
|
||||||
|
end
|
||||||
|
|
||||||
|
within '#modal' do
|
||||||
|
expect do
|
||||||
|
click_button 'Submit'
|
||||||
|
end.not_to(change { TemplateFolder.active.count })
|
||||||
|
end
|
||||||
|
|
||||||
|
template = Template.last
|
||||||
|
expect(template.name).to eq('New Template Name')
|
||||||
|
expect(template.folder.name).to eq(template_folder.name)
|
||||||
|
expect(page).to have_current_path(edit_template_path(template), ignore_query: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'moves a template' do
|
||||||
|
find('[data-tip="Move"]', visible: false).hover
|
||||||
|
find('[data-tip="Move"]').click
|
||||||
|
|
||||||
|
within '#modal' do
|
||||||
|
fill_in 'name', with: 'New Folder Name'
|
||||||
|
|
||||||
|
expect do
|
||||||
|
click_button 'Move'
|
||||||
|
end.to change { TemplateFolder.active.count }.by(1)
|
||||||
|
|
||||||
|
template_folder = TemplateFolder.last
|
||||||
|
|
||||||
|
expect(template_folder.name).to eq('New Folder Name')
|
||||||
|
expect(page).to have_current_path(template_path(template), ignore_query: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Reference in new issue
Block a user