Compare commits

..
35 Commits
Author SHA1 Message Date
Alex Turchyn 13a1835c00 Merge from docusealco/wip 2026-10-26 12:27:46 +02:00
Pete Matsyburka c35407d7a9 cleanup routes 2026-10-25 21:43:38 +02:00
Pete Matsyburka e55ccc0b76 update dep 2026-10-25 16:09:58 +02:00
Pete Matsyburka b8d00870b4 optimize query 2026-10-25 15:02:22 +02:00
Pete Matsyburka 5413fbc248 add template_id to submitters 2026-10-25 09:39:47 +02:00
Pete Matsyburka 2d2eb7ce4d ruby 4.0.9 2026-10-23 23:42:14 +03:00
Pete Matsyburka ed2ddbf74e remove legacy route 2026-10-23 11:10:14 +03:00
Pete Matsyburka b99e5d1262 adjust autosave 2026-10-23 10:02:32 +03:00
Pete Matsyburka 87f6878eda fix brakeman 2026-10-23 08:29:39 +03:00
Pete Matsyburka 99b815eb4b fix generation 2026-10-23 08:25:07 +03:00
Pete Matsyburka e40a29b9ab update certs 2026-10-22 18:47:09 +03:00
Pete Matsyburka 151890bb72 add autosave config 2026-10-22 17:17:47 +03:00
Pete Matsyburka 78edd87cc1 add main tag 2026-10-22 15:01:54 +03:00
Pete Matsyburka 88c99a2b35 fix turbo morph 2026-10-22 14:48:03 +03:00
Pete Matsyburka 282b8cfa73 fix search 2026-10-22 11:02:24 +03:00
Pete Matsyburka e9888c7466 add validation 2026-10-21 15:41:53 +03:00
Pete Matsyburka 0647befb3a escape email variables 2026-10-21 09:54:29 +03:00
Pete Matsyburka 85aa3d0451 mask email field value 2026-10-21 09:35:38 +03:00
Pete Matsyburka 6d2b3bb654 adjust test webhook 2026-10-20 20:15:37 +03:00
Pete Matsyburka b394854e9d adjust send form 2026-10-20 09:50:21 +03:00
Pete Matsyburka 2e586e930e adjust signature validation 2026-10-19 19:03:26 +03:00
Alex Turchyn 8f8f69f0bc Merge from docusealco/wip 2026-10-19 14:57:12 +03:00
Pete Matsyburka 04663d55eb update deps 2026-10-19 11:10:48 +03:00
Pete Matsyburka 93d0b935e2 fix dynamic document 2026-10-17 13:17:52 +03:00
Pete Matsyburka 829b2b2855 disable jemalloc 2026-10-17 10:42:20 +03:00
Alex Turchyn cf6de444f4 disable the review form for native apps 2026-10-16 23:22:32 +03:00
Pete Matsyburka 126f5d58a2 convert docx 2026-10-16 20:18:10 +03:00
Pete Matsyburka 09b93067f4 fix close modal 2026-10-16 19:56:36 +03:00
Pete Matsyburka f14458f238 fix generate 2026-10-16 19:56:36 +03:00
Pete Matsyburka 445139f05c adjust mobile nav 2026-10-16 19:56:36 +03:00
Pete Matsyburka e5e4965f05 adjust kba 2026-10-16 19:56:36 +03:00
Pete Matsyburka 1c8f7b0316 use jemalloc 2026-10-15 15:28:54 +03:00
Alex Turchyn 9fe3656652 fix icons shrinking 2026-10-14 08:11:45 +03:00
Pete Matsyburka 17ae46bd61 make remember signature permanent 2026-10-13 16:45:42 +03:00
Pete Matsyburka a036483938 tenants test flag 2026-10-13 09:23:38 +03:00
58 changed files with 377 additions and 210 deletions

No files matched your search

+4 -4
View File
@@ -12,7 +12,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 4.0.5
ruby-version: 4.0.9
- name: Cache gems
uses: actions/cache@v4
with:
@@ -37,7 +37,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 4.0.5
ruby-version: 4.0.9
- name: Cache gems
uses: actions/cache@v4
with:
@@ -89,7 +89,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 4.0.5
ruby-version: 4.0.9
- name: Cache gems
uses: actions/cache@v4
with:
@@ -113,7 +113,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: ruby:4.0.5-alpine
image: ruby:4.0.9-alpine
services:
postgres:
+8 -5
View File
@@ -1,4 +1,4 @@
FROM ruby:4.0.5-alpine AS download
FROM ruby:4.0.9-alpine AS download
WORKDIR /fonts
@@ -17,7 +17,7 @@ RUN apk --no-cache add wget unzip && \
mkdir -p /pdfium-linux && \
unzip -q pdfium-linux.zip -d /pdfium-linux
FROM ruby:4.0.5-alpine AS webpack
FROM ruby:4.0.9-alpine AS webpack
ENV RAILS_ENV=production
ENV NODE_ENV=production
@@ -44,7 +44,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:4.0.5-alpine AS app
FROM ruby:4.0.9-alpine AS app
ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test"
@@ -52,7 +52,8 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
WORKDIR /app
RUN apk add --no-cache libpq vips redis onnxruntime leptonica && \
RUN apk upgrade --no-cache ca-certificates-bundle && \
apk add --no-cache libpq vips redis onnxruntime leptonica jemalloc font-liberation font-carlito && \
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
@@ -91,7 +92,7 @@ COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \
mkdir -p /usr/share/fonts/noto && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/ && fc-cache -f && \
mkdir -p /usr/share/fonts/noto/bold && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/bold/ && fc-cache -f && \
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
chown -R docuseal:docuseal /app/tmp/cache
@@ -100,6 +101,8 @@ ENV HOME=/home/docuseal
ENV WORKDIR=/data/docuseal
ENV VIPS_MAX_COORD=17000
ENV VIPS_BLOCK_UNTRUSTED=1
# ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2
ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true
EXPOSE 3000
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
+2 -1
View File
@@ -2,7 +2,7 @@
source 'https://rubygems.org'
ruby '4.0.5'
ruby '4.0.9'
gem 'addressable'
gem 'arabic-letter-connector', require: false
@@ -45,6 +45,7 @@ gem 'trilogy', require: false
gem 'turbo-rails'
gem 'twitter_cldr', require: false
gem 'tzinfo-data'
gem 'wordcraft', require: false, group: :ce
gem 'xlsxtream', require: false
group :development, :test do
+12 -1
View File
@@ -572,6 +572,16 @@ GEM
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wordcraft (0.11.0-aarch64-linux-gnu)
ffi (>= 1.15)
wordcraft (0.11.0-aarch64-linux-musl)
ffi (>= 1.15)
wordcraft (0.11.0-arm64-darwin)
ffi (>= 1.15)
wordcraft (0.11.0-x86_64-linux-gnu)
ffi (>= 1.15)
wordcraft (0.11.0-x86_64-linux-musl)
ffi (>= 1.15)
xlsxtream (3.1.0)
zip_kit (>= 6.2, < 7)
xpath (3.2.0)
@@ -650,10 +660,11 @@ DEPENDENCIES
tzinfo-data
web-console
webmock
wordcraft
xlsxtream
RUBY VERSION
ruby 4.0.5
ruby 4.0.9
BUNDLED WITH
4.0.3
@@ -1,74 +0,0 @@
# frozen_string_literal: true
module Api
class AttachmentsController < ActionController::API
include ActionController::Cookies
include ActiveStorage::SetCurrent
COOKIE_STORE_LIMIT = 10
def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
unless can_upload?(@submitter)
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end
file = params[:file]
if params[:type].in?(%w[initials signature])
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
if ImageUtils.blank?(image)
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
end
if ImageUtils.error?(image)
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} error, try to sign on another device" },
status: :unprocessable_content
end
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
end
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
if params[:remember_signature] == 'true' && @submitter.email.present?
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
end
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
rescue Submitters::MaliciousFileExtension => e
Rollbar.error(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_content
end
def can_upload?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at?
end
def build_new_cookie_signatures_json(submitter, attachment)
values =
begin
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
rescue JSON::ParserError
{}
end
values[submitter.email] = attachment.uuid
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
values.to_json
end
end
end
@@ -1,15 +0,0 @@
# frozen_string_literal: true
module Api
class SubmitterEmailClicksController < ActionController::API
def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
end
render json: {}
end
end
end
+1 -4
View File
@@ -179,10 +179,7 @@ module Api
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
end
submitters = submitters.where(template_id: params[:template_id]) if params[:template_id].present?
maybe_filter_by_completed_at(submitters, params)
end
@@ -34,7 +34,7 @@ class SubmitFormUploadController < ActionController::API
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
if params[:remember_signature] == 'true' && @submitter.email.present?
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
cookies.permanent.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
end
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
@@ -8,7 +8,9 @@ class TemplateSharingsTestingController < ApplicationController
end
def create
testing_account = Accounts.find_or_create_testing_user(true_user.account).account
account = can?(:manage, :tenants_test) ? current_account : true_user.account
testing_account = Accounts.find_or_create_testing_user(account).account
if params[:value] == '1'
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
+1
View File
@@ -228,6 +228,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
template,
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
dateFormats: JSON.parse(this.dataset.dateFormats || '[]'),
autosave: this.dataset.autosave !== 'false',
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
backgroundColor: '#faf7f5',
locale: this.dataset.locale,
+1 -1
View File
@@ -22,7 +22,7 @@ export default class extends HTMLElement {
}
async flashFrom (fetchResponse) {
const html = await fetchResponse?.responseHTML
const html = await fetchResponse?.responseHTML.catch(() => null)
const flash = html && new DOMParser().parseFromString(html, 'text/html').getElementById('native_flash')
if (flash?.dataset.alert) return { alert: flash.dataset.alert }
+14 -2
View File
@@ -12,7 +12,14 @@ export default class extends HTMLElement {
toggle = (event) => {
if (event?.type === 'input' && event.target.matches(this.fieldSelector('name'))) return
const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field)
if (event?.type === 'focusin' && !this.isKeepSendUsed && this.isFocusFromName(event)) {
this.isKeepSendUsed = true
this.keepSendInput = event.target
} else if (event?.type === 'focusout' && event.target === this.keepSendInput) {
this.keepSendInput = null
}
const isNameOnly = (field) => this.isFilled('name') && !this.isFilled(field) && !this.keepSendInput?.matches(this.fieldSelector(field))
const isSend = (this.isChecked('send_email') && !isNameOnly('email')) ||
(this.isChecked('send_sms') && !isNameOnly('phone'))
@@ -29,12 +36,17 @@ export default class extends HTMLElement {
return [...this.form.querySelectorAll(this.fieldSelector(field))].some((input) => input.value.trim())
}
isFocusFromName ({ target, relatedTarget }) {
return !!relatedTarget?.matches(this.fieldSelector('name')) && !!relatedTarget.value.trim() &&
['email', 'phone'].some((field) => target.matches(this.fieldSelector(field)))
}
fieldSelector (field) {
return `[name$="[][${field}]"]`
}
get events () {
return ['change', 'input', 'linked-input.update']
return ['change', 'input', 'linked-input.update', 'focusin', 'focusout']
}
get form () {
+16 -7
View File
@@ -3,11 +3,19 @@
v-if="!error"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
<span class="flex items-end">
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
<span
class="tooltip tooltip-bottom ml-2 mb-1"
data-tip="To verify your identity, the details you enter are checked against third-party records, which may include credit bureau data, and used to generate questions based on those records. Only these identity details are shared with the identity verification provider; this document and your tax return information are not shared with it or with any credit reporting company. If the check is recorded on your credit report, it appears as a soft inquiry, which does not affect your credit score and is not shown to lenders. This check does not give the sender of this document or the IRS access to your credit report."
>
<IconInfoCircle class="w-4 h-4 text-neutral-500" />
</span>
</span>
<span
v-if="questions"
class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm"
@@ -282,13 +290,14 @@
<script>
import MarkdownContent from './markdown_content'
import { IconInnerShadowTop } from '@tabler/icons-vue'
import { IconInnerShadowTop, IconInfoCircle } from '@tabler/icons-vue'
export default {
name: 'KbaStep',
components: {
MarkdownContent,
IconInnerShadowTop
IconInnerShadowTop,
IconInfoCircle
},
inject: ['baseUrl', 't'],
props: {
+49 -2
View File
@@ -415,9 +415,9 @@
<a
:href="`/templates/${template.id}/form`"
:data-native-actions="template.submitters.length > 1 ? 'users' : null"
data-turbo="false"
data-turbo-prefetch="false"
class="flex items-center justify-center space-x-2"
@click.exact="isPreviewLoading = true"
@click.exact="onSaveAndPreviewClick"
>
<IconInnerShadowTop
v-if="isPreviewLoading"
@@ -640,6 +640,7 @@
:draw-field-type="drawFieldType"
:draw-custom-field="drawCustomField"
:draw-option="drawOption"
:autosave="autosave"
@update="onDynamicDocumentUpdate"
@draw="clearDrawField"
@add-custom-field="addCustomField"
@@ -1379,6 +1380,7 @@ export default {
isPreviewLoading: false,
isLoadingBlankPage: false,
isSaving: false,
hasUnsavedChanges: false,
isDetectingPageFields: false,
detectFieldsQueue: [],
detectingAnalyzingProgress: null,
@@ -1662,6 +1664,11 @@ export default {
window.addEventListener('dragleave', this.onWindowDragLeave)
if (!this.autosave) {
window.addEventListener('beforeunload', this.onBeforeUnload)
document.addEventListener('turbo:before-visit', this.onBeforeVisit)
}
this.$nextTick(() => {
if (document.location.search?.includes('stripe_connect_success')) {
document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
@@ -1685,6 +1692,11 @@ export default {
this.resizeObserver.disconnect()
window.removeEventListener('dragleave', this.onWindowDragLeave)
if (!this.autosave) {
window.removeEventListener('beforeunload', this.onBeforeUnload)
document.removeEventListener('turbo:before-visit', this.onBeforeVisit)
}
},
beforeUpdate () {
this.documentRefs = []
@@ -2241,6 +2253,37 @@ export default {
this.insertField(field)
}
},
onSaveAndPreviewClick (event) {
this.isPreviewLoading = true
if (!this.autosave) {
event.preventDefault()
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
dynamicDocumentRefs.forEach((ref) => ref.update())
this.rebuildVariablesSchema({ disable: false })
Promise.all([this.save({ force: true }), ...dynamicDocumentRefs.map((ref) => ref.saveBody())]).then(() => {
window.Turbo.visit(`/templates/${this.template.id}/form`)
})
}
},
onBeforeUnload (event) {
if (this.hasUnsavedChanges) {
event.preventDefault()
event.returnValue = ''
return ''
}
},
onBeforeVisit (event) {
if (this.hasUnsavedChanges && !window.confirm(this.t('close_without_saving_changes'))) {
event.preventDefault()
}
},
closeDropdown () {
document.activeElement.blur()
},
@@ -4036,6 +4079,8 @@ export default {
}
if (!this.autosave && !force) {
this.hasUnsavedChanges = true
return Promise.resolve({})
}
@@ -4058,6 +4103,8 @@ export default {
}),
headers: { 'Content-Type': 'application/json' }
}).then(() => {
this.hasUnsavedChanges = false
if (this.onSave) {
this.onSave(this.template)
}
@@ -84,6 +84,11 @@ export default {
required: false,
default: true
},
autosave: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: {
type: Object,
required: false,
@@ -279,7 +284,7 @@ export default {
this.updatedSectionEditors.set(section.id, editor)
this.saveTimer = setTimeout(() => this.updateSectionsAndSave(), 1000)
this.saveTimer = setTimeout(() => this.autosave ? this.updateSectionsAndSave() : this.updateSections(), 1000)
},
loadListsStyle () {
this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null
@@ -299,7 +304,9 @@ export default {
updateVariablesSchema () {
this.document.variables_schema = buildVariablesSchema(this.bodyDom.body)
},
updateSectionsAndSave () {
updateSections () {
delete this.saveTimer
this.updatedSectionEditors.forEach((editor, sectionId) => {
const target = this.bodyDom.getElementById(sectionId)
@@ -315,6 +322,9 @@ export default {
this.updateVariablesSchema()
this.$emit('update', this.document)
},
updateSectionsAndSave () {
this.updateSections()
return this.saveBody()
},
@@ -27,13 +27,17 @@ tiptapStylesheet.replaceSync(
font-feature-settings: "liga" 0;
display: flex;
flex-flow: column nowrap;
min-height: inherit;
flex-grow: 1;
}
.ProseMirror > article {
.ProseMirror > article:last-of-type {
margin-bottom: auto;
}
.ProseMirror p.doc-trailing-break::after {
content: none;
}
.ProseMirror [contenteditable="false"] {
white-space: normal;
}
+7
View File
@@ -70,6 +70,7 @@ const en = {
remove_condition: 'Remove condition',
add_condition: 'Add condition',
are_you_sure_: 'Are you sure?',
close_without_saving_changes: 'Close without saving changes?',
sign_yourself: 'Sign Yourself',
set_signing_date: 'Set signing date',
signing_date: 'Signing Date',
@@ -371,6 +372,7 @@ const es = {
down: 'Abajo',
set_signing_date: 'Establecer fecha de firma',
are_you_sure_: '¿Estás seguro?',
close_without_saving_changes: '¿Cerrar sin guardar los cambios?',
sign_yourself: 'Firma tú mismo',
checked: 'Marcado',
current_date: 'Fecha actual',
@@ -652,6 +654,7 @@ const it = {
remove_condition: 'Rimuovi condizione',
add_condition: 'Aggiungi condizione',
are_you_sure_: 'Sei sicuro?',
close_without_saving_changes: 'Chiudere senza salvare le modifiche?',
sign_yourself: 'Firma te stesso',
set_signing_date: 'Imposta data di firma',
signing_date: 'Data di firma',
@@ -953,6 +956,7 @@ const pt = {
down: 'Para baixo',
set_signing_date: 'Definir data de assinatura',
are_you_sure_: 'Tem certeza?',
close_without_saving_changes: 'Fechar sem salvar as alterações?',
sign_yourself: 'Assine você mesmo',
checked: 'Marcado',
current_date: 'Data atual',
@@ -1234,6 +1238,7 @@ const fr = {
remove_condition: 'Supprimer la condition',
add_condition: 'Ajouter une condition',
are_you_sure_: 'Êtes-vous sûr ?',
close_without_saving_changes: 'Fermer sans enregistrer les modifications ?',
sign_yourself: 'Signer vous-même',
set_signing_date: 'Définir la date de signature',
signing_date: 'Date de signature',
@@ -1525,6 +1530,7 @@ const de = {
remove_condition: 'Bedingung entfernen',
add_condition: 'Bedingung hinzufügen',
are_you_sure_: 'Sind Sie sicher?',
close_without_saving_changes: 'Ohne Speichern der Änderungen schließen?',
sign_yourself: 'Selbst unterschreiben',
set_signing_date: 'Unterzeichnungsdatum festlegen',
signing_date: 'Unterzeichnungsdatum',
@@ -1816,6 +1822,7 @@ const nl = {
remove_condition: 'Voorwaarde verwijderen',
add_condition: 'Voorwaarde toevoegen',
are_you_sure_: 'Weet u het zeker?',
close_without_saving_changes: 'Sluiten zonder wijzigingen op te slaan?',
sign_yourself: 'Zelf ondertekenen',
set_signing_date: 'Ondertekeningsdatum instellen',
signing_date: 'Ondertekeningsdatum',
+8 -6
View File
@@ -30,14 +30,16 @@ class SendTestWebhookRequestJob
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
end
Faraday.post(webhook_url.url,
{
event_type: 'form.completed',
timestamp: Time.current.iso8601,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
body_json = {
event_type: 'form.completed',
timestamp: Time.current.iso8601,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json
Faraday.post(webhook_url.url, body_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT,
'X-Docuseal-Signature' => WebhookUrls::Signatures.sign(webhook_url.hmac_secret, body: body_json),
**webhook_url.secret.to_h)
end
end
+1
View File
@@ -61,6 +61,7 @@ class AccountConfig < ApplicationRecord
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
TEMPLATE_DATE_FORMATS_KEY = 'template_date_formats'
TEMPLATE_BUILDER_AUTOSAVE_KEY = 'template_builder_autosave'
POLICY_LINKS_KEY = 'policy_links'
ENABLE_MCP_KEY = 'enable_mcp'
+9 -1
View File
@@ -25,15 +25,17 @@
# account_id :bigint not null
# external_id :string
# submission_id :bigint not null
# template_id :bigint
#
# Indexes
#
# index_submitters_on_account_id_and_completed_at (account_id,completed_at) WHERE (completed_at IS NOT NULL)
# index_submitters_on_account_id_and_id (account_id,id)
# index_submitters_on_email (email)
# index_submitters_on_email_and_template_id (email,template_id) WHERE (email IS NOT NULL)
# index_submitters_on_external_id (external_id)
# index_submitters_on_slug (slug) UNIQUE
# index_submitters_on_submission_id (submission_id)
# index_submitters_on_template_id_and_id (template_id,id)
#
# Foreign Keys
#
@@ -68,6 +70,8 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) }
before_validation :set_template_id, on: :create
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
def status
@@ -124,6 +128,10 @@ class Submitter < ApplicationRecord
private
def set_template_id
self.template_id = submission&.template_id
end
def anonymize_email_events
email_events.each do |event|
event.update!(email: Digest::MD5.base64digest(event.email))
+3 -3
View File
@@ -33,10 +33,10 @@
<% end %>
<turbo-frame id="drawer"></turbo-frame>
<%= render turbo_native_app? ? 'shared/native_navbar' : 'shared/navbar' %>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
<div class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
<main class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
<%= yield %>
</div>
</main>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
<%= render 'shared/body_scripts' %>
</body>
</html>
+3 -1
View File
@@ -13,6 +13,8 @@
<%= stylesheet_pack_tag 'form', media: 'all', data: { turbo_track: 'dynamic' } %>
</head>
<body>
<%= yield %>
<main>
<%= yield %>
</main>
</body>
</html>
+4 -2
View File
@@ -13,8 +13,10 @@
<%= stylesheet_pack_tag 'application', media: 'all', data: { turbo_track: 'dynamic' } %>
</head>
<body>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
<%= yield %>
<main>
<%= yield %>
</main>
<turbo-frame id="modal"></turbo-frame>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
</body>
</html>
+2 -2
View File
@@ -22,13 +22,13 @@
<% end %>
<% if can?(:create, ::Template) && controller_name.in?(%w[templates_dashboard submissions_dashboard template_folders templates_archived submissions_archived]) %>
<% folder_name = @template_folder&.full_name %>
<native-action data-placement="create" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>"></native-action>
<native-action data-placement="create" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>"></native-action>
<native-action data-placement="create" data-icon="scan" data-native="scan" data-label="<%= t('scan') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>"></native-action>
<native-action data-placement="create" data-icon="pencil_plus">
<%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %>
</native-action>
<% unless controller_name.in?(%w[templates_archived submissions_archived]) %>
<native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>"></native-action>
<native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="upload" data-native="upload" data-label="<%= t('upload') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>" data-accept="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>"></native-action>
<native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="scan" data-native="scan" data-label="<%= t('scan') %>" data-url="<%= templates_upload_path(folder_name: folder_name) %>"></native-action>
<native-action data-placement="menu" data-menu="<%= t('new') %>" data-menu-icon="pencil_plus" data-icon="pencil_plus">
<%= link_to t('create'), new_template_path(folder_name: folder_name), data: { turbo_frame: :modal, native_modal: :medium } %>
+2 -2
View File
@@ -1,5 +1,5 @@
<%= render 'shared/navbar_warning' %>
<div class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
<nav class="max-w-6xl mb-4 mx-auto px-4 md:px-2 py-3 flex items-center justify-between">
<div class="flex items-center space-x-4">
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
<%= render 'shared/title' %>
@@ -106,4 +106,4 @@
<% end %>
</div>
<% end %>
</div>
</nav>
+2 -2
View File
@@ -93,14 +93,14 @@
</li>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<% if Docuseal.multitenant? || !turbo_native_ios? %>
<% unless turbo_native_app? %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
</li>
<% end %>
<% if Docuseal.multitenant? %>
<% if Docuseal.multitenant? && !turbo_native_app? %>
<li>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<%= t('embedding') %>
+2 -2
View File
@@ -1,4 +1,4 @@
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
@@ -32,5 +32,5 @@
</toggle-submit>
<% end %>
</div>
</main>
</div>
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
@@ -55,4 +55,4 @@
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || params.dig(:submitter, :name), email: params[:email] || params.dig(:submitter, :email), phone: params[:phone] || params.dig(:submitter, :phone) } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
<% end %>
</div>
</main>
</div>
+2 -2
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
@@ -28,4 +28,4 @@
<%= link_to t('back'), @resubmit_submitter ? submit_form_path(@resubmit_submitter.slug) : start_form_path(@template.slug, @submitter.slice(:name, :email, :phone).compact_blank), class: 'base-button' %>
</div>
</div>
</main>
</div>
+2 -2
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('share_link_is_currently_disabled')) %>
<% end %>
<main class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
<div class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
<div class="text-center w-full space-y-6">
<%= render 'start_form/banner' %>
<p class="text-xl font-semibold text-center">
@@ -27,5 +27,5 @@
<%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: start_form_path(slug: @template.slug) }, method: :post, class: 'white-button w-full' %>
</toggle-submit>
<% end %>
</main>
</div>
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
+2 -2
View File
@@ -2,7 +2,7 @@
<% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="text-center w-full space-y-6">
@@ -66,4 +66,4 @@
<%= render 'shared/attribution', link_path: '/start', account: @template.account %>
<%= render 'start_form/policy', account: @template.account %>
</div>
</main>
</div>
+13 -13
View File
@@ -11,7 +11,7 @@
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %>
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
<% with_signature_id_completed_at = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY }&.value == true %>
<main style="max-width: 1600px" class="mx-auto pl-4">
<div style="max-width: 1600px" class="mx-auto pl-4">
<% unless native_tab == 'recipients' %>
<div class="flex justify-between items-center pr-4 sticky top-0 md:relative z-10 bg-base-100 <%= 'py-1.5' unless turbo_native_app? %>">
<% unless turbo_native_app? %>
@@ -227,39 +227,39 @@
</div>
<% if submitter&.name.present? %>
<div class="flex items-center space-x-1 mt-1">
<%= svg_icon('user', class: 'w-5 h-5') %>
<span>
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
<span class="truncate">
<%= submitter&.name %>
</span>
</div>
<% end %>
<% if submitter&.email.present? %>
<div class="flex items-center space-x-1 mt-1">
<%= svg_icon('mail', class: 'w-5 h-5') %>
<span>
<%= svg_icon('mail', class: 'w-5 h-5 shrink-0') %>
<span class="truncate">
<%= submitter.email || 'N/A' %>
</span>
</div>
<% end %>
<% if submitter&.phone.present? %>
<div class="flex items-center space-x-1 mt-1">
<%= svg_icon('phone', class: 'w-5 h-5') %>
<span>
<%= svg_icon('phone', class: 'w-5 h-5 shrink-0') %>
<span class="truncate">
<%= submitter.phone %>
</span>
</div>
<% end %>
<div class="flex items-center space-x-1 mt-1">
<% if submitter&.viewer? %>
<%= svg_icon(submitter.opened_at? ? 'file_check' : 'file_unknown', class: 'w-5 h-5') %>
<%= svg_icon(submitter.opened_at? ? 'file_check' : 'file_unknown', class: 'w-5 h-5 shrink-0') %>
<% elsif submitter && !submitter.completed_at? && @submission.completed_at? %>
<%= svg_icon('writing_off', class: 'w-5 h-5') %>
<%= svg_icon('writing_off', class: 'w-5 h-5 shrink-0') %>
<% elsif @submission.expire_at? && submitter && !submitter.completed_at? %>
<%= svg_icon('clock_exclamation', class: 'w-5 h-5') %>
<%= svg_icon('clock_exclamation', class: 'w-5 h-5 shrink-0') %>
<% else %>
<%= svg_icon('writing', class: 'w-5 h-5') %>
<%= svg_icon('writing', class: 'w-5 h-5 shrink-0') %>
<% end %>
<span>
<span class="truncate">
<% if submitter&.declined_at? %>
<%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
<% elsif submitter&.viewer? %>
@@ -394,7 +394,7 @@
</label>
</toggle-visible>
<% end %>
</main>
</div>
<% unless request.headers['HTTP_X_TURBO_REQUEST_ID'] %>
<%= render 'scripts/autosize_field' %>
<% end %>
+2 -2
View File
@@ -1,5 +1,5 @@
<% completed_at = @submitter.completed_at || @submitter.submission.completed_at %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
@@ -51,5 +51,5 @@
</toggle-submit>
<% end %>
</div>
</main>
</div>
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %>
+2 -2
View File
@@ -3,7 +3,7 @@
<% I18n.with_locale(@submitter.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
<% end %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
@@ -70,4 +70,4 @@
<div><%= t('please_contact_the_requester_to_specify_your_email_for_two_factor_authentication') %></div>
<% end %>
</div>
</main>
</div>
+2 -2
View File
@@ -14,7 +14,7 @@
<% delegate_modal_id = nil %>
<% banner_html = turbo_native_app? ? '' : local_assigns[:banner_html] %>
<div style="max-height: -webkit-fill-available;">
<main id="scrollbox">
<div id="scrollbox">
<div id="sr_only_content"></div>
<div class="mx-auto block pb-72" style="max-width: 1000px">
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 transition-transform duration-300 %>
@@ -172,7 +172,7 @@
<% end %>
<% end %>
</div>
</main>
</div>
</div>
<div class="fixed bottom-0 w-full h-0 z-50">
<div class="mx-auto" style="max-width: 1000px">
+1 -1
View File
@@ -31,7 +31,7 @@
</div>
</span>
</div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>" multiple>
</div>
</label>
</file-dropzone>
+1 -1
View File
@@ -16,7 +16,7 @@
</label>
<input type="hidden" name="form_id" value="<%= form_id %>">
<convert-upload>
<input id="upload_template" name="files[]" class="hidden" type="file" accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
<input id="upload_template" name="files[]" class="hidden" type="file" accept="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>" multiple>
</convert-upload>
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
<% end %>
+3 -2
View File
@@ -7,7 +7,8 @@
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
<% end %>
<% end %>
<% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY]).to_a %>
<% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY, AccountConfig::TEMPLATE_BUILDER_AUTOSAVE_KEY]).to_a %>
<% custom_fields = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY }&.value || [] %>
<% date_formats = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_DATE_FORMATS_KEY }&.value || [] %>
<template-builder data-native-platform="<%= turbo_native_ios? ? 'ios' : (turbo_native_android? ? 'android' : '') %>" class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= custom_fields.to_json %>" data-date-formats="<%= date_formats.to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
<% autosave = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_BUILDER_AUTOSAVE_KEY }&.value != false %>
<template-builder data-native-platform="<%= turbo_native_ios? ? 'ios' : (turbo_native_android? ? 'android' : '') %>" data-accept-file-types="image/*, application/pdf, application/zip, application/json, <%= Templates::CreateAttachments.document_extensions.join(', ') %>" class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= custom_fields.to_json %>" data-date-formats="<%= date_formats.to_json %>" data-autosave="<%= autosave %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
+1 -1
View File
@@ -26,7 +26,7 @@
<%= t('share_template_with_test_mode') %>
</span>
<submit-form data-on="change">
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: true_user.account.testing_accounts) %>
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: (can?(:manage, :tenants_test) ? current_account : true_user.account).testing_accounts) %>
</submit-form>
</div>
<% end %>
+1 -1
View File
@@ -127,4 +127,4 @@
<%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_path(q: params[:q]) %>
<% end %>
<% end %>
<%= render 'shared/review_form' %>
<%= render 'shared/review_form' unless turbo_native_app? %>
@@ -28,7 +28,7 @@
</submit-form>
</form>
<% end %>
<a href="<%= edit_template_path(@template) %>" class="base-button" data-turbo="false" style="flex-shrink: 0; padding: 0px 24px;">
<a href="<%= edit_template_path(@template) %>" class="base-button" data-turbo-prefetch="false" style="flex-shrink: 0; padding: 0px 24px;">
<%= t('exit_preview') %>
</a>
</div>
@@ -194,7 +194,7 @@
<%= t('share_template_with_test_mode') %>
</span>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: true_user.account.testing_accounts) %>
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: (can?(:manage, :tenants_test) ? current_account : true_user.account).testing_accounts) %>
</submit-form>
</div>
<% end %>
+2 -2
View File
@@ -5,11 +5,11 @@
"note": "Permitted parameters are necessary for creating submitters via API"
},
{
"fingerprint": "645f2f352a813105b8b2eeadc68101d2e7ea04b8e839aa1336616be56f1108ca",
"fingerprint": "58619647d807d5bedb9499d6b1432f7f9cee385167aa97b0cb5e1b31a3db3f40",
"note": "Safe SQL"
},
{
"fingerprint": "5bcf4e6426d17b0efd98e47fde7c012f09500b9595af9cff226cf09341c023be",
"fingerprint": "8538c7cca701df19fdc3c1c6c5b7b0a1a3acacc19c840d63147553014d60ffa0",
"note": "Safe SQL"
},
{
+1
View File
@@ -9,3 +9,4 @@ autoload :OnnxRuntime, 'onnxruntime'
autoload :RQRCode, 'rqrcode'
autoload :ArabicLetterConnector, 'arabic-letter-connector/logic'
autoload :JWT, 'jwt'
autoload :WordCraft, 'wordcraft'
+1 -9
View File
@@ -25,8 +25,6 @@ Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do
resource :user, only: %i[show]
resources :attachments, only: %i[create]
resources :submitter_email_clicks, only: %i[create]
resources :submitter_form_views, only: %i[create]
resources :submitters, only: %i[index show update]
resources :submissions, only: %i[index show create update destroy] do
@@ -128,13 +126,7 @@ Rails.application.routes.draw do
controller: 'api/active_storage_blobs_proxy'
if Docuseal.multitenant?
resource :blobs_proxy_legacy, only: %i[show],
path: '/blobs/proxy/:signed_id/*filename',
controller: 'api/active_storage_blobs_proxy_legacy',
as: :rails_blob
get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service
put '/disk/:encoded_token' => 'active_storage/disk#update', as: :update_rails_disk_service
post '/direct_uploads' => 'active_storage/direct_uploads#create', as: :rails_direct_uploads
get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service if Rails.env.local?
ActiveSupport.run_load_hooks(:multitenant_routes, self)
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddTemplateIdToSubmitters < ActiveRecord::Migration[8.1]
def change
add_column :submitters, :template_id, :bigint, if_not_exists: true
end
end
@@ -0,0 +1,37 @@
# frozen_string_literal: true
class PopulateSubmitterTemplateId < ActiveRecord::Migration[8.1]
disable_ddl_transaction!
class MigrationSubmitter < ApplicationRecord
self.table_name = 'submitters'
end
class MigrationSubmission < ApplicationRecord
self.table_name = 'submissions'
end
def up
return if ENV['MULTITENANT'] == 'true'
max_id = MigrationSubmitter.maximum(:id)
return unless max_id
submission_template_id =
Arel::Nodes::Grouping.new(
MigrationSubmission.arel_table.project(MigrationSubmission.arel_table[:template_id])
.where(MigrationSubmission.arel_table[:id].eq(MigrationSubmitter.arel_table[:submission_id]))
.ast
)
(1..max_id).step(10_000) do |start_id|
MigrationSubmitter.where(template_id: nil, id: start_id...(start_id + 10_000))
.update_all(template_id: submission_template_id)
end
end
def down
nil
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddSubmittersTemplateIdIndexes < ActiveRecord::Migration[8.1]
def up
add_index :submitters, %i[email template_id], where: 'email IS NOT NULL', if_not_exists: true
add_index :submitters, %i[template_id id], if_not_exists: true
remove_index :submitters, :email, if_exists: true
end
def down
add_index :submitters, :email, if_not_exists: true
remove_index :submitters, %i[template_id id], if_exists: true
remove_index :submitters, %i[email template_id], if_exists: true
end
end
+4 -2
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_09_30_061927) do
ActiveRecord::Schema[8.1].define(version: 2026_10_25_120002) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "pg_catalog.plpgsql"
@@ -424,6 +424,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_09_30_061927) do
t.datetime "sent_at"
t.string "slug", null: false
t.bigint "submission_id", null: false
t.bigint "template_id"
t.string "timezone"
t.string "ua"
t.datetime "updated_at", null: false
@@ -431,10 +432,11 @@ ActiveRecord::Schema[8.1].define(version: 2026_09_30_061927) do
t.text "values", null: false
t.index ["account_id", "completed_at"], name: "index_submitters_on_account_id_and_completed_at", where: "(completed_at IS NOT NULL)"
t.index ["account_id", "id"], name: "index_submitters_on_account_id_and_id"
t.index ["email"], name: "index_submitters_on_email"
t.index ["email", "template_id"], name: "index_submitters_on_email_and_template_id", where: "(email IS NOT NULL)"
t.index ["external_id"], name: "index_submitters_on_external_id"
t.index ["slug"], name: "index_submitters_on_slug", unique: true
t.index ["submission_id"], name: "index_submitters_on_submission_id"
t.index ["template_id", "id"], name: "index_submitters_on_template_id_and_id"
end
create_table "template_accesses", force: :cascade do |t|
+1 -4
View File
@@ -7,10 +7,7 @@ class ApiPathConsiderJsonMiddleware
def call(env)
if env['PATH_INFO'].starts_with?('/api') &&
(!env['PATH_INFO'].ends_with?('/documents') || env['REQUEST_METHOD'] != 'POST') &&
!env['PATH_INFO'].ends_with?('/attachments') &&
!env['PATH_INFO'].ends_with?('/submitter_sms_clicks') &&
!env['PATH_INFO'].ends_with?('/submitter_email_clicks')
!env['PATH_INFO'].ends_with?('/submitter_sms_clicks')
env['CONTENT_TYPE'] = 'application/json'
end
+13 -3
View File
@@ -26,6 +26,8 @@ module ReplaceEmailVariables
DOCUMENTS_LINKS = /\{+documents\.links\}+/i
DOCUMENTS_LINK = /\{+documents\.link\}+/i
ESCAPED_BRACES = "\u{E000}\u{E001}"
EMAIL_HOST = ENV.fetch('EMAIL_HOST', nil)
module_function
@@ -86,7 +88,11 @@ module ReplaceEmailVariables
build_submitters_n_field(submitter.submission, index, :values, match[:field_name].to_s.strip)
end
replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') }
text = replace(text, SENDER_EMAIL, html_escape:) do
submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@')
end
text.tr(ESCAPED_BRACES, '{}')
end
# rubocop:enable Metrics
@@ -124,6 +130,8 @@ module ReplaceEmailVariables
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
end
elsif (mask = field.dig('preferences', 'mask').presence)
TextUtils.mask_value(value[field['uuid']].to_s, mask)
else
value[field&.dig('uuid')]
end
@@ -134,10 +142,12 @@ module ReplaceEmailVariables
def replace(text, var, html_escape: false)
text.gsub(var) do
value = yield(Regexp.last_match).to_s.tr('{}', ESCAPED_BRACES)
if html_escape
ERB::Util.html_escape(yield(Regexp.last_match))
ERB::Util.html_escape(value)
else
yield(Regexp.last_match)
value
end
end
end
+20 -7
View File
@@ -3,6 +3,8 @@
module SearchEntries
MAX_VALUE_LENGTH = 100
MUTEX = Mutex.new
UUID_REGEXP = /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
module_function
@@ -70,10 +72,10 @@ module SearchEntries
arel
else
"tsvector @@ (quote_literal(coalesce((ts_lexize('english_stem', :keyword))[1], :keyword)) || ':*')::tsquery"
"tsvector @@ (quote_literal(coalesce((ts_lexize(:dictionary, :keyword))[1], :keyword)) || ':*')::tsquery"
end
[sql, { keyword: }]
[sql, { keyword:, dictionary: default_dictionary }]
end
end
@@ -83,7 +85,7 @@ module SearchEntries
Arel.sql("ngram @@ (quote_literal(:term#{terms.size - 1}) || ':' || :weight)::tsquery")
else
Arel.sql(<<~SQL.squish)
(quote_literal(coalesce((ts_lexize('english_stem', :term#{terms.size - 1}))[1], :term#{terms.size - 1})) || ':*' || :weight)::tsquery
(quote_literal(coalesce((ts_lexize(:dictionary, :term#{terms.size - 1}))[1], :term#{terms.size - 1})) || ':*' || :weight)::tsquery
SQL
end
@@ -91,7 +93,7 @@ module SearchEntries
index = terms.index(term)
arel = Arel.sql(<<~SQL.squish)
(quote_literal(coalesce((ts_lexize('english_stem', :term#{index}))[1], :term#{index})) || ':' || :weight)::tsquery
(quote_literal(coalesce((ts_lexize(:dictionary, :term#{index}))[1], :term#{index})) || ':' || :weight)::tsquery
SQL
acc ? Arel::Nodes::InfixOperation.new('&&', arel, acc) : arel
@@ -109,7 +111,8 @@ module SearchEntries
)
end
[query.to_sql, terms.index_by.with_index { |_, index| :"term#{index}" }.merge(weight:)]
[query.to_sql,
terms.index_by.with_index { |_, index| :"term#{index}" }.merge(weight:, dictionary: default_dictionary)]
end
def build_weights_wildcard_tsquery(keyword, weight)
@@ -122,11 +125,11 @@ module SearchEntries
SQL
else
<<~SQL.squish
tsvector @@ (quote_literal(coalesce((ts_lexize('english_stem', :keyword))[1], :keyword)) || ':*' || :weight)::tsquery
tsvector @@ (quote_literal(coalesce((ts_lexize(:dictionary, :keyword))[1], :keyword)) || ':*' || :weight)::tsquery
SQL
end
[sql, { keyword:, weight: }]
[sql, { keyword:, weight:, dictionary: default_dictionary }]
end
def index_submitter(submitter)
@@ -256,4 +259,14 @@ module SearchEntries
ngrams.uniq { |e| e.sub(/':[\d,]/, "':1") }.join(' ')
end
def default_dictionary
@default_dictionary || MUTEX.synchronize do
@default_dictionary ||= SearchEntry.connection.select_value(<<~SQL.squish)
SELECT mapdict::regdictionary::text FROM pg_ts_config_map
WHERE mapcfg = get_current_ts_config() AND maptokentype = 1
ORDER BY mapseqno LIMIT 1
SQL
end
end
end
+6 -3
View File
@@ -267,7 +267,10 @@ module Submissions
is_id_verified = submission_events.any?(&:complete_verification?)
is_kba_passed = submission_events.any?(&:complete_kba?)
complete_kba_event = submission_events.find(&:complete_kba?) || SubmissionEvent.new
kba_answers = complete_kba_event.data['answers']
kba_score = " (#{kba_answers['correct']}/#{kba_answers['asked']})" if kba_answers
info_rows = [
[
@@ -297,8 +300,8 @@ module Submissions
is_id_verified && {
text: "#{I18n.t('identity_verification')}: #{I18n.t('verified')}\n"
},
is_kba_passed && {
text: "#{I18n.t('knowledge_based_authentication')}: #{I18n.t('passed')}\n"
complete_kba_event.persisted? && {
text: "#{I18n.t('knowledge_based_authentication')}: #{I18n.t('passed')}#{kba_score}\n"
},
completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" },
completed_event.data['sid'] && { text: "#{I18n.t('session_id')}: #{completed_event.data['sid']}\n" },
@@ -602,6 +602,8 @@ module Submissions
cell_valign = field.dig('preferences', 'valign').to_s.presence_in(TEXT_VALIGNS) || 'center'
cell_layouter = cell_layouters[cell_valign]
value = value.to_s
if (mask = field.dig('preferences', 'mask').presence)
value = TextUtils.mask_value(value, mask)
end
@@ -882,6 +884,8 @@ module Submissions
end
def maybe_flatten_pdf(pdf)
pdf.catalog.delete(:AcroForm) if pdf.catalog[:AcroForm].is_a?(Integer)
pdf.acro_form.create_appearances(force: true) if pdf.acro_form && pdf.acro_form[:NeedAppearances]
pdf.acro_form&.flatten
rescue HexaPDF::MissingGlyphError
+1
View File
@@ -65,6 +65,7 @@ module Submitters
submissions = submissions.where(source:) if source
Submitter
.where(template_id: template.id)
.where(submission: submissions)
.order(id: :desc)
.where(declined_at: nil)
+31 -1
View File
@@ -9,6 +9,8 @@ module Submitters
PHONE_REGEXP = /[+\d()\s-]+/
NONEDITABLE_FIELD_TYPES = %w[stamp heading strikethrough].freeze
REQUIRED_FIELD_TYPES = %w[payment kba verification].freeze
CHOICE_FIELD_TYPES = %w[radio multiple select].freeze
BOOLEAN_VALUES = [true, false].freeze
STRFTIME_MAP = {
'hour' => '%-k',
@@ -55,6 +57,7 @@ module Submitters
validate_values!(reason_field ? values.except(reason_field['uuid']) : values, submitter, params, request)
if params[:completed] == 'true'
validate_invite_submitters!(submitter)
maybe_invite_via_field(submitter, request)
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request)
@@ -504,7 +507,7 @@ module Submitters
submitter
end
def validate_value!(_value, field, _params, submitter, _request)
def validate_value!(value, field, _params, submitter, _request)
raise ValidationError, 'Missing field' unless field
raise ValidationError, 'Invalid field' if field['submitter_uuid'] != submitter.uuid
@@ -514,7 +517,34 @@ module Submitters
raise ValidationError, 'Read-only field'
end
raise ValidationError, 'Value is invalid' if field['type'] == 'checkbox' && !value.in?(BOOLEAN_VALUES)
validate_choice_value!(value, field, submitter) if field['type'].in?(CHOICE_FIELD_TYPES)
true
end
def validate_choice_value!(value, field, submitter)
return if value.blank?
raise ValidationError, 'Value is invalid' unless value.is_a?(field['type'] == 'multiple' ? Array : String)
option_names = field['options'].to_a.map.with_index do |option, index|
option['value'].presence ||
"#{I18n.t('option', locale: submitter.metadata.fetch('lang', submitter.account.locale))} #{index + 1}"
end
raise ValidationError, 'Value is invalid' unless Array.wrap(value).all? { |item| option_names.include?(item) }
end
def validate_invite_submitters!(submitter)
submission = submitter.submission
is_pending_invite = (submission.template_submitters || submission.template.submitters).any? do |s|
s['invite_by_uuid'] == submitter.uuid && submission.submitters.none? { |e| e.uuid == s['uuid'] }
end
raise ValidationError, 'Invite required' if is_pending_invite
end
end
end
+21
View File
@@ -6,6 +6,7 @@ module Templates
ZIP_CONTENT_TYPE = 'application/zip'
X_ZIP_CONTENT_TYPE = 'application/x-zip-compressed'
JSON_CONTENT_TYPE = 'application/json'
WORD_EXTENSIONS = %w[.docx .doc .odt .rtf].freeze
DOCUMENT_EXTENSIONS = %w[.docx .doc .xlsx .xls .odt .rtf].freeze
DOCUMENT_CONTENT_TYPES = %w[
@@ -134,7 +135,27 @@ module Templates
return [handle_pdf_or_image(template, file, file.read, params, extract_fields:), []]
end
if File.extname(file.original_filename.to_s).downcase.in?(WORD_EXTENSIONS)
return [handle_word_document(template, file, params), []]
end
raise InvalidFileType, "#{file.content_type}/#{dynamic}"
end
def handle_word_document(template, file, params)
pdf = WordCraft.open_buffer(file, &:to_pdf)
pdf_file = ActionDispatch::Http::UploadedFile.new(
tempfile: StringIO.new(pdf),
filename: "#{File.basename(file.original_filename, '.*')}.pdf",
type: PDF_CONTENT_TYPE
)
handle_pdf_or_image(template, pdf_file, pdf, params)
end
def document_extensions
Docuseal.advanced_formats? ? DOCUMENT_EXTENSIONS : WORD_EXTENSIONS
end
end
end
+4 -1
View File
@@ -180,8 +180,11 @@ module VerifyPdfSignature
return false if byte_range.size != 4 || byte_range.any?(&:negative?) || byte_range[0].positive?
return false if signature.signed_end > io.size
return false if byte_range[2] != byte_range[1] + (signature.contents.bytesize * 2) + 2
byte_range[2] == byte_range[1] + (signature.contents.bytesize * 2) + 2
io.seek(byte_range[1])
io.read(byte_range[2] - byte_range[1]).casecmp?("<#{signature.contents.unpack1('H*')}>")
end
def unsigned_changes?(document, io, signed_end)