Compare commits

..
27 Commits
Author SHA1 Message Date
Alex Turchyn 5764b4dac1 Merge from docusealco/wip 2025-03-10 13:13:01 +02:00
Pete Matsyburka 3affd67d24 add font field preferences 2025-03-09 12:46:28 +02:00
Pete Matsyburka d5a98a0f9e web concurrency auto 2025-03-08 16:11:11 +02:00
Pete Matsyburka 9629cc7226 make remember configurable 2025-03-05 20:44:32 +02:00
Pete Matsyburka 06415ad869 add routes hook 2025-03-05 18:29:39 +02:00
Pete Matsyburka 3f872da435 fix mask 2025-03-05 18:07:38 +02:00
Pete Matsyburka bc1d5a9825 add font type 2025-03-05 12:12:16 +02:00
Pete Matsyburka 72c8ed9a00 update gem 2025-03-04 22:17:01 +02:00
Pete Matsyburka 3fec433846 fix builder editable 2025-03-04 19:46:55 +02:00
Pete Matsyburka 3b572702d7 add mask size 2025-03-04 01:02:18 +02:00
Alex Turchyn febdc0b48d add specs for masked fields 2025-03-03 22:43:27 +02:00
Pete Matsyburka 8773bb2f62 disable cfr toggle 2025-03-03 13:21:10 +02:00
Pete Matsyburka 55ad70fabc fix builder default date format 2025-03-03 11:24:38 +02:00
Pete Matsyburka 23dcd62672 add field mask 2025-03-03 00:18:56 +02:00
Alex Turchyn 42371d7ce3 Merge from docusealco/wip 2025-03-02 15:37:12 +02:00
Pete Matsyburka 4116746be3 destroy email events 2025-02-28 12:12:02 +02:00
Pete Matsyburka b2b2856ac0 handle required pdf fields 2025-02-27 21:37:32 +02:00
Alex Turchyn b65bca213f add brakeman to CI 2025-02-27 18:43:57 +02:00
Pete Matsyburka 7a72e2cbb7 allow http 2025-02-27 10:59:32 +02:00
Pete Matsyburka 3a73398a1e fix email 2025-02-27 10:01:33 +02:00
Pete Matsyburka cce20d9230 fix rubocop 2025-02-25 14:39:11 +02:00
Pete Matsyburka 8b72c91951 add console audit tables 2025-02-25 13:16:11 +02:00
Pete Matsyburka b0d8e47a75 fix annotate model 2025-02-25 13:10:33 +02:00
Pete Matsyburka 85824faa2b https webhooks 2025-02-25 10:33:16 +02:00
Pete Matsyburka 8a507339af add brakeman 2025-02-25 00:54:05 +02:00
Pete Matsyburka 4636849118 fix permitted params 2025-02-24 23:59:30 +02:00
Pete Matsyburka cc52cae9fc fix not found page 2025-02-24 14:19:48 +02:00
37 changed files with 751 additions and 116 deletions

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
---
:position: before
:position_in_additional_file_patterns: before
:position_in_class: before
:position_in_factory: before
:position_in_fixture: before
:position_in_routes: before
:position_in_serializer: before
:position_in_test: before
:classified_sort: true
:exclude_controllers: true
:exclude_factories: true
:exclude_fixtures: false
:exclude_helpers: true
:exclude_scaffolds: true
:exclude_serializers: false
:exclude_sti_subclasses: false
:exclude_tests: false
:force: false
:format_markdown: false
:format_rdoc: false
:format_yard: false
:frozen: false
:ignore_model_sub_dir: false
:ignore_unknown_models: false
:include_version: false
:show_check_constraints: false
:show_complete_foreign_keys: false
:show_foreign_keys: true
:show_indexes: true
:simple_indexes: false
:sort: false
:timestamp: false
:trace: false
:with_comment: true
:with_column_comments: true
:with_table_comments: true
:active_admin: false
:command:
:debug: false
:hide_default_column_types: ''
:hide_limit_column_types: ''
:ignore_columns:
:ignore_routes:
:models: true
:routes: false
:skip_on_db_migrate: false
:target_action: :do_annotations
:wrapper:
:wrapper_close:
:wrapper_open:
:classes_default_to_s: []
:additional_file_patterns: []
:model_dir:
- app/models
:require: []
:root_dir:
- ''
+27
View File
@@ -77,6 +77,33 @@ jobs:
run: | run: |
./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js" ./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js"
brakeman:
name: Brakeman
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.1
- name: Cache gems
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Install gems
run: |
gem install bundler
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
sudo apt-get install libvips
- name: Run Brakeman
run: bundle exec brakeman -q --exit-on-warn
rspec: rspec:
name: RSpec name: RSpec
runs-on: ubuntu-latest runs-on: ubuntu-latest
+3 -1
View File
@@ -46,7 +46,6 @@ gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'
group :development, :test do group :development, :test do
gem 'annotate'
gem 'better_html' gem 'better_html'
gem 'bullet' gem 'bullet'
gem 'debug' gem 'debug'
@@ -63,6 +62,9 @@ group :development, :test do
end end
group :development do group :development do
gem 'annotaterb'
gem 'brakeman', require: false
gem 'foreman', require: false
gem 'letter_opener_web' gem 'letter_opener_web'
gem 'web-console' gem 'web-console'
end end
+8 -5
View File
@@ -74,9 +74,7 @@ GEM
uri (>= 0.13.1) uri (>= 0.13.1)
addressable (2.8.7) addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 7.0)
annotate (2.6.5) annotaterb (4.14.0)
activerecord (>= 2.3.0)
rake (>= 0.8.7)
arabic-letter-connector (0.1.1) arabic-letter-connector (0.1.1)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
@@ -120,6 +118,8 @@ GEM
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.4) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (7.0.0)
racc
builder (3.3.0) builder (3.3.0)
bullet (8.0.0) bullet (8.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@@ -229,6 +229,7 @@ GEM
ffi (1.17.1-arm64-darwin) ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-linux-gnu) ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl) ffi (1.17.1-x86_64-linux-musl)
foreman (0.88.1)
geom2d (0.4.1) geom2d (0.4.1)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
@@ -558,7 +559,7 @@ GEM
unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4) unicode-emoji (4.0.4)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
uri (1.0.2) uri (1.0.3)
useragent (0.16.11) useragent (0.16.11)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
@@ -588,13 +589,14 @@ PLATFORMS
x86_64-linux-musl x86_64-linux-musl
DEPENDENCIES DEPENDENCIES
annotate annotaterb
arabic-letter-connector arabic-letter-connector
aws-sdk-s3 aws-sdk-s3
aws-sdk-secretsmanager aws-sdk-secretsmanager
azure-storage-blob azure-storage-blob
better_html better_html
bootsnap bootsnap
brakeman
bullet bullet
cancancan cancancan
capybara capybara
@@ -610,6 +612,7 @@ DEPENDENCIES
faker faker
faraday faraday
faraday-follow_redirects faraday-follow_redirects
foreman
google-cloud-storage google-cloud-storage
hexapdf hexapdf
image_processing image_processing
@@ -39,7 +39,7 @@ class AccountConfigsController < ApplicationController
end end
def account_config_params def account_config_params
params.required(:account_config).permit!.tap do |attrs| params.required(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0]) attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end end
end end
@@ -39,7 +39,7 @@ class NotificationsSettingsController < ApplicationController
end end
def email_config_params def email_config_params
params.require(:account_config).permit!.tap do |attrs| params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS]) attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
end end
end end
@@ -50,7 +50,7 @@ class PersonalizationSettingsController < ApplicationController
end end
def account_config_params def account_config_params
attrs = params.require(:account_config).permit! attrs = params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] })
return attrs if attrs[:value].is_a?(String) return attrs if attrs[:value].is_a?(String)
@@ -20,7 +20,7 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug]) @submission ||= Submission.find_by!(slug: params[:slug])
raise ActionController::RoutingError if @submission.account.archived_at? raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
if !@submission.submitters.all?(&:completed_at?) && !signature_valid && if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
(!current_user || !current_ability.can?(:read, @submission)) (!current_user || !current_ability.can?(:read, @submission))
+1 -1
View File
@@ -26,7 +26,7 @@ class UserConfigsController < ApplicationController
end end
def user_config_params def user_config_params
params.required(:user_config).permit!.tap do |attrs| params.required(:user_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0]) attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end end
end end
+7 -5
View File
@@ -45,17 +45,16 @@ class UsersController < ApplicationController
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo? return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at)) attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
if params.dig(:user, :account_id).present? if params.dig(:user, :account_id).present?
account = Account.accessible_by(current_ability).find(params[:user][:account_id]) account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
authorize!(:manage, account) authorize!(:manage, account)
@user.account = account @user.account = account
end end
if @user.update(attrs) if @user.update(attrs.except(current_user == @user ? :role : nil))
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated') redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
@@ -84,8 +83,11 @@ class UsersController < ApplicationController
def user_params def user_params
if params.key?(:user) if params.key?(:user)
params.require(:user).permit(:email, :first_name, :last_name, :password, permitted_params = %i[email first_name last_name password archived_at]
:role, :archived_at, :account_id)
permitted_params << :role if role_valid?(params.dig(:user, :role))
params.require(:user).permit(permitted_params)
else else
{} {}
end end
+22 -6
View File
@@ -3,7 +3,7 @@
class="field-area flex absolute lg:text-base -outline-offset-1" class="field-area flex absolute lg:text-base -outline-offset-1"
dir="auto" dir="auto"
:style="computedStyle" :style="computedStyle"
:class="{ 'font-mono': field.preferences?.font === 'Courier', 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" :class="{ 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
> >
<div <div
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid" v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -168,7 +168,7 @@
<div <div
v-else-if="field.type === 'cells'" v-else-if="field.type === 'cells'"
class="w-full flex items-center" class="w-full flex items-center"
:class="{ 'justify-end': field.preferences?.align === 'right' }" :class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
> >
<div <div
v-for="(char, index) in modelValue" v-for="(char, index) in modelValue"
@@ -184,7 +184,7 @@
ref="textContainer" ref="textContainer"
dir="auto" dir="auto"
class="flex items-center px-0.5 w-full" class="flex items-center px-0.5 w-full"
:class="alignClasses[field.preferences?.align]" :class="{ ...alignClasses, ...fontClasses }"
> >
<span <span
v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0" v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0"
@@ -326,10 +326,26 @@ export default {
} }
}, },
alignClasses () { alignClasses () {
if (!this.field.preferences) {
return {}
}
return { return {
center: 'text-center', 'text-center': this.field.preferences.align === 'center',
left: 'text-left', 'text-left': this.field.preferences.align === 'left',
right: 'text-right' 'text-right': this.field.preferences.align === 'right'
}
},
fontClasses () {
if (!this.field.preferences) {
return {}
}
return {
'font-mono': this.field.preferences.font === 'Courier',
'font-serif': this.field.preferences.font === 'Times',
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
} }
}, },
option () { option () {
+32 -5
View File
@@ -141,6 +141,7 @@
:with-required="false" :with-required="false"
:with-areas="false" :with-areas="false"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]" @scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
@@ -161,7 +162,7 @@
ref="touchValueTarget" ref="touchValueTarget"
class="flex items-center h-full w-full" class="flex items-center h-full w-full"
dir="auto" dir="auto"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? (alignClasses[field.preferences?.align] || '') : 'justify-center']" :class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center']"
@click="focusValueInput" @click="focusValueInput"
> >
<span <span
@@ -253,6 +254,17 @@
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
</Teleport> </Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport <Teleport
v-if="isShowConditionsModal" v-if="isShowConditionsModal"
:to="modalContainerEl" :to="modalContainerEl"
@@ -283,6 +295,7 @@ import FieldType from './field_type'
import Field from './field' import Field from './field'
import FieldSettings from './field_settings' import FieldSettings from './field_settings'
import FormulaModal from './formula_modal' import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal' import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal' import DescriptionModal from './description_modal'
import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue' import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue'
@@ -295,6 +308,7 @@ export default {
IconCheck, IconCheck,
FieldSettings, FieldSettings,
FormulaModal, FormulaModal,
FontModal,
IconDotsVertical, IconDotsVertical,
DescriptionModal, DescriptionModal,
ConditionsModal, ConditionsModal,
@@ -352,6 +366,7 @@ export default {
data () { data () {
return { return {
isShowFormulaModal: false, isShowFormulaModal: false,
isShowFontModal: false,
isShowConditionsModal: false, isShowConditionsModal: false,
isContenteditable: false, isContenteditable: false,
isSettingsFocus: false, isSettingsFocus: false,
@@ -386,11 +401,19 @@ export default {
defaultName () { defaultName () {
return this.buildDefaultName(this.field, this.template.fields) return this.buildDefaultName(this.field, this.template.fields)
}, },
alignClasses () { fontClasses () {
if (!this.field.preferences) {
return {}
}
return { return {
center: 'justify-center', 'justify-center': this.field.preferences.align === 'center',
left: 'justify-start', 'justify-start': this.field.preferences.align === 'left',
right: 'justify-end' 'justify-end': this.field.preferences.align === 'right',
'font-mono': this.field.preferences.font === 'Courier',
'font-serif': this.field.preferences.font === 'Times',
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
} }
}, },
optionIndexText () { optionIndexText () {
@@ -483,6 +506,10 @@ export default {
this.$el.getRootNode().activeElement.blur() this.$el.getRootNode().activeElement.blur()
}, },
maybeToggleDefaultValue () { maybeToggleDefaultValue () {
if (!this.editable) {
return
}
if (['text', 'number'].includes(this.field.type)) { if (['text', 'number'].includes(this.field.type)) {
this.isContenteditable = true this.isContenteditable = true
+6 -3
View File
@@ -1316,6 +1316,10 @@ export default {
} }
}, },
onDropfield (area) { onDropfield (area) {
if (!this.editable) {
return
}
const field = this.fieldsDragFieldRef.value || { const field = this.fieldsDragFieldRef.value || {
name: '', name: '',
uuid: v4(), uuid: v4(),
@@ -1338,9 +1342,8 @@ export default {
} }
if (field.type === 'date') { if (field.type === 'date') {
field.preferences = { field.preferences ||= {}
format: this.defaultDateFormat field.preferences.format ||= this.defaultDateFormat
}
} }
} }
+15
View File
@@ -127,6 +127,7 @@
:editable="editable" :editable="editable"
:background-color="dropdownBgColor" :background-color="dropdownBgColor"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@set-draw="$emit('set-draw', $event)" @set-draw="$emit('set-draw', $event)"
@@ -231,6 +232,17 @@
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
</Teleport> </Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport <Teleport
v-if="isShowConditionsModal" v-if="isShowConditionsModal"
:to="modalContainerEl" :to="modalContainerEl"
@@ -261,6 +273,7 @@ import FieldType from './field_type'
import PaymentSettings from './payment_settings' import PaymentSettings from './payment_settings'
import FieldSettings from './field_settings' import FieldSettings from './field_settings'
import FormulaModal from './formula_modal' import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal' import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal' import DescriptionModal from './description_modal'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue' import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
@@ -275,6 +288,7 @@ export default {
PaymentSettings, PaymentSettings,
IconNewSection, IconNewSection,
FormulaModal, FormulaModal,
FontModal,
DescriptionModal, DescriptionModal,
ConditionsModal, ConditionsModal,
IconRouteAltLeft, IconRouteAltLeft,
@@ -305,6 +319,7 @@ export default {
isNameFocus: false, isNameFocus: false,
showPaymentModal: false, showPaymentModal: false,
isShowFormulaModal: false, isShowFormulaModal: false,
isShowFontModal: false,
isShowConditionsModal: false, isShowConditionsModal: false,
isShowDescriptionModal: false, isShowDescriptionModal: false,
renderDropdown: false renderDropdown: false
@@ -330,6 +330,19 @@
v-if="field.type != 'stamp'" v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5" class="pb-0.5 mt-0.5"
> >
<li v-if="field.type == 'text'">
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-font')"
>
<IconTypography
width="18"
/>
<span class="text-sm">
{{ t('font') }}
</span>
</label>
</li>
<li <li
v-if="field.type != 'stamp'" v-if="field.type != 'stamp'"
> >
@@ -427,7 +440,7 @@
</template> </template>
<script> <script>
import { IconRouteAltLeft, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue' import { IconRouteAltLeft, IconTypography, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue'
export default { export default {
name: 'FieldSettings', name: 'FieldSettings',
@@ -438,6 +451,7 @@ export default {
IconRouteAltLeft, IconRouteAltLeft,
IconCopy, IconCopy,
IconNewSection, IconNewSection,
IconTypography,
IconX IconX
}, },
inject: ['template', 'save', 't'], inject: ['template', 'save', 't'],
@@ -472,7 +486,7 @@ export default {
default: true default: true
} }
}, },
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'remove-area'], emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area'],
data () { data () {
return { return {
} }
@@ -0,0 +1,290 @@
<template>
<div
class="modal modal-open items-start !animate-none overflow-y-auto"
>
<div
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
{{ t('font') }} - {{ field.name || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
class="text-xl"
@click.prevent="$emit('close')"
>&times;</a>
</div>
<div class="mt-4">
<div>
<div class="flex items-center space-x-1.5">
<span>
<div class="dropdown">
<label
tabindex="0"
class="base-input flex items-center justify-between items-center"
style="height: 32px; padding-right: 0; width: 120px"
:class="fonts.find((f) => f.value === preferences.font)?.class"
>
<span style="margin-top: 1px">
{{ preferences.font || 'Default' }}
</span>
<IconChevronDown
class="ml-2 mr-2 mt-0.5"
width="18"
height="18"
/>
</label>
<div
tabindex="0"
class="dropdown-content p-0 mt-1 block z-10 menu shadow bg-white border border-base-300 rounded-md w-52"
>
<div
v-for="(font, index) in fonts"
:key="index"
:value="font.value"
:class="{ 'bg-base-300': preferences.font == font.value, [font.class]: true }"
class="hover:bg-base-300 px-2 py-1.5 cursor-pointer"
@click="[font.value ? preferences.font = font.value : delete preferences.font, closeDropdown()]"
>
{{ font.label }}
</div>
</div>
</div>
</span>
<span class="relative">
<select
class="select input-bordered bg-white select-sm text-center pl-2"
style="font-size: 16px; width: 86px; text-align-last: center;"
@change="$event.target.value ? preferences.font_size = parseInt($event.target.value) : delete preferences.font_size"
>
<option
:selected="!preferences.font_size"
value=""
>
Auto
</option>
<option
v-for="size in sizes"
:key="size"
:value="size"
:selected="size === preferences.font_size"
>
{{ size }}
</option>
</select>
<span
class="border-l pl-1.5 absolute bg-white absolute bottom-0 pointer-events-none text-sm h-5"
style="right: 13px; top: 7px"
>
pt
</span>
</span>
<span>
<div
class="join"
style="height: 32px"
>
<button
v-for="(type, index) in types"
:key="index"
class="btn btn-sm join-item bg-white input-bordered hover:border-base-content/20 hover:bg-base-200/50 px-2"
:class="{ '!bg-base-300': preferences.font_type?.includes(type.value) }"
@click="setFontType(type.value)"
>
<component :is="type.icon" />
</button>
</div>
</span>
<span>
<div
class="join"
style="height: 32px"
>
<button
v-for="(align, index) in aligns"
:key="index"
class="btn btn-sm join-item bg-white input-bordered hover:border-base-content/20 hover:bg-base-200/50 px-2"
:class="{ '!bg-base-300': preferences.align === align.value }"
@click="align.value && preferences.align != align.value ? preferences.align = align.value : delete preferences.align"
>
<component :is="align.icon" />
</button>
</div>
</span>
<span>
<select
class="input input-bordered bg-white input-sm text-lg rounded-md"
style="-webkit-appearance: none; -moz-appearance: none; text-indent: 0px; text-overflow: ''; padding: 0px 6px; height: 32px"
@change="$event.target.value ? preferences.color = $event.target.value : delete preferences.color"
>
<option
v-for="(color, index) in colors"
:key="index"
:value="color.value"
:selected="color.value == preferences.color"
>
{{ color.label }}
</option>
</select>
</span>
</div>
</div>
<div class="mt-4">
<div
class="flex items-center border border-base-content/20 rounded-xl bg-white px-4 h-16"
:style="{
color: preferences.color || 'black',
fontSize: (preferences.font_size || 12) + 'pt',
}"
:class="textClasses"
>
<span
contenteditable="true"
class="outline-none"
>
{{ field.default_value || field.name || buildDefaultName(field, template.fields) }}
</span>
</div>
</div>
<div class="mt-4">
<button
class="base-button w-full"
@click.prevent="saveAndClose"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { IconChevronDown, IconBold, IconItalic, IconAlignLeft, IconAlignRight, IconAlignCenter } from '@tabler/icons-vue'
export default {
name: 'FontModal',
components: {
IconChevronDown
},
inject: ['t', 'save', 'template'],
props: {
field: {
type: Object,
required: true
},
editable: {
type: Boolean,
required: false,
default: true
},
buildDefaultName: {
type: Function,
required: true
}
},
emits: ['close'],
data () {
return {
preferences: {}
}
},
computed: {
fonts () {
return [
{ value: null, label: 'Default' },
{ value: 'Times', label: 'Times', class: 'font-serif' },
{ value: 'Courier', label: 'Courier', class: 'font-mono' }
]
},
types () {
return [
{ icon: IconBold, value: 'bold' },
{ icon: IconItalic, value: 'italic' }
]
},
aligns () {
return [
{ icon: IconAlignLeft, value: 'left' },
{ icon: IconAlignCenter, value: 'center' },
{ icon: IconAlignRight, value: 'right' }
]
},
sizes () {
return [...Array(23).keys()].map(i => i + 6)
},
colors () {
return [
{ label: '⬛', value: 'black' },
{ label: '🟦', value: 'blue' },
{ label: '🟥', value: 'red' }
]
},
textClasses () {
return {
'font-mono': this.preferences.font === 'Courier',
'font-serif': this.preferences.font === 'Times',
'justify-center': this.preferences.align === 'center',
'justify-start': this.preferences.align === 'left',
'justify-end': this.preferences.align === 'right',
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
}
},
keys () {
return ['font_type', 'font_size', 'color', 'align', 'font']
}
},
created () {
this.preferences = this.keys.reduce((acc, key) => {
acc[key] = this.field.preferences?.[key]
return acc
}, {})
},
methods: {
closeDropdown () {
this.$el.getRootNode().activeElement.blur()
},
setFontType (value) {
if (value === 'bold') {
if (this.preferences.font_type === 'bold') {
delete this.preferences.font_type
} else if (this.preferences.font_type === 'italic') {
this.preferences.font_type = 'bold_italic'
} else if (this.preferences.font_type === 'bold_italic') {
this.preferences.font_type = 'italic'
} else {
this.preferences.font_type = value
}
}
if (value === 'italic') {
if (this.preferences.font_type === 'italic') {
delete this.preferences.font_type
} else if (this.preferences.font_type === 'bold') {
this.preferences.font_type = 'bold_italic'
} else if (this.preferences.font_type === 'bold_italic') {
this.preferences.font_type = 'bold'
} else {
this.preferences.font_type = value
}
}
},
saveAndClose () {
this.field.preferences ||= {}
this.keys.forEach((key) => delete this.field.preferences[key])
Object.assign(this.field.preferences, this.preferences)
this.save()
this.$emit('close')
}
}
}
</script>
+6
View File
@@ -1,4 +1,5 @@
const en = { const en = {
font: 'Font',
party: 'Party', party: 'Party',
method: 'Method', method: 'Method',
reorder_fields: 'Reorder fields', reorder_fields: 'Reorder fields',
@@ -161,6 +162,7 @@ const en = {
} }
const es = { const es = {
fuente: 'Fuente',
party: 'Parte', party: 'Parte',
method: 'Método', method: 'Método',
reorder_fields: 'Reordenar campos', reorder_fields: 'Reordenar campos',
@@ -323,6 +325,7 @@ const es = {
} }
const it = { const it = {
font: 'Carattere',
party: 'Parte', party: 'Parte',
method: 'Metodo', method: 'Metodo',
reorder_fields: 'Riordina i campi', reorder_fields: 'Riordina i campi',
@@ -485,6 +488,7 @@ const it = {
} }
const pt = { const pt = {
fonte: 'Fonte',
party: 'Parte', party: 'Parte',
method: 'Método', method: 'Método',
reorder_fields: 'Reorganizar campos', reorder_fields: 'Reorganizar campos',
@@ -647,6 +651,7 @@ const pt = {
} }
const fr = { const fr = {
font: 'Police',
party: 'Partie', party: 'Partie',
method: 'Méthode', method: 'Méthode',
reorder_fields: 'Réorganiser les champs', reorder_fields: 'Réorganiser les champs',
@@ -809,6 +814,7 @@ const fr = {
} }
const de = { const de = {
font: 'Schriftart',
party: 'Partei', party: 'Partei',
method: 'Verfahren', method: 'Verfahren',
reorder_fields: 'Felder neu anordnen', reorder_fields: 'Felder neu anordnen',
+11
View File
@@ -55,6 +55,7 @@ class Submitter < ApplicationRecord
has_many_attached :attachments has_many_attached :attachments
has_many_attached :preview_documents has_many_attached :preview_documents
has_many :template_accesses, through: :template has_many :template_accesses, through: :template
has_many :email_events, as: :emailable, dependent: (Docuseal.multitenant? ? nil : :destroy)
has_many :document_generation_events, dependent: :destroy has_many :document_generation_events, dependent: :destroy
has_many :submission_events, dependent: :destroy has_many :submission_events, dependent: :destroy
@@ -63,6 +64,8 @@ class Submitter < ApplicationRecord
scope :completed, -> { where.not(completed_at: nil) } scope :completed, -> { where.not(completed_at: nil) }
after_destroy :anonymize_email_events, if: -> { Docuseal.multitenant? }
def status def status
if declined_at? if declined_at?
'declined' 'declined'
@@ -108,4 +111,12 @@ class Submitter < ApplicationRecord
fields.any? { |f| f['submitter_uuid'] == uuid && signature_field_types.include?(f['type']) } fields.any? { |f| f['submitter_uuid'] == uuid && signature_field_types.include?(f['type']) }
end end
end end
private
def anonymize_email_events
email_events.each do |event|
event.update!(email: Digest::MD5.base64digest(event.email))
end
end
end end
+2 -2
View File
@@ -63,7 +63,7 @@
<span> <span>
<%= t('add_signature_id_to_the_documents') %> <%= t('add_signature_id_to_the_documents') %>
</span> </span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %> <%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()', disabled: can?(:manage, :cfr) %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
@@ -75,7 +75,7 @@
<span> <span>
<%= t('require_signing_reason') %> <%= t('require_signing_reason') %>
</span> </span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %> <%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()', disabled: can?(:manage, :cfr) %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
+2 -1
View File
@@ -1,7 +1,8 @@
<% align = field.dig('preferences', 'align') %> <% align = field.dig('preferences', 'align') %>
<% color = field.dig('preferences', 'color') %> <% color = field.dig('preferences', 'color') %>
<% font = field.dig('preferences', 'font') %> <% font = field.dig('preferences', 'font') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>"> <% font_type = field.dig('preferences', 'font_type') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %> <% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden"> <div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%"> <div class="flex-grow flex overflow-hidden" style="min-height: 50%">
+27 -7
View File
@@ -19,7 +19,7 @@
<%= svg_icon('external_link', class: 'w-6 h-6') %> <%= svg_icon('external_link', class: 'w-6 h-6') %>
<span class="hidden md:inline"><%= t('audit_log') %></span> <span class="hidden md:inline"><%= t('audit_log') %></span>
</a> </a>
<% else %> <% elsif signed_in? %>
<%= link_to submission_events_path(@submission), class: 'white-button', data: { turbo_frame: :modal } do %> <%= link_to submission_events_path(@submission), class: 'white-button', data: { turbo_frame: :modal } do %>
<%= svg_icon('logs', class: 'w-6 h-6') %> <%= svg_icon('logs', class: 'w-6 h-6') %>
<span class="hidden md:block"><%= t('event_log') %></span> <span class="hidden md:block"><%= t('event_log') %></span>
@@ -105,7 +105,18 @@
<% value = values[field['uuid']] %> <% value = values[field['uuid']] %>
<% value ||= field['default_value'] if field['type'] == 'heading' %> <% value ||= field['default_value'] if field['type'] == 'heading' %>
<% next if value.blank? %> <% next if value.blank? %>
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> <% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
<span class="group">
<span class="hidden group-hover:inline">
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
</span>
<span class="group-hover:hidden">
<%= render 'submissions/value', area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
</span>
</span>
<% else %>
<%= render 'submissions/value', area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
<% end %>
<% end %> <% end %>
</div> </div>
</div> </div>
@@ -232,12 +243,21 @@
</div> </div>
<% elsif field['type'] == 'checkbox' %> <% elsif field['type'] == 'checkbox' %>
<%= svg_icon('check', class: 'w-6 h-6') %> <%= svg_icon('check', class: 'w-6 h-6') %>
<% elsif field['type'] == 'number' %>
<%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
<% elsif field['type'] == 'date' %>
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
<% else %> <% else %>
<div class="whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div> <% if field['type'] == 'number' %>
<% value = NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
<% elsif field['type'] == 'date' %>
<% value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
<% end %>
<% if (mask = field.dig('preferences', 'mask').presence) %>
<% if signed_in? && can?(:read, @submission) %>
<div class="[&:not(:hover)]:after:content-[attr(data-value)] block whitespace-pre-wrap group" data-value="<%= Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') %>"><span class="hidden group-hover:block"><%= Array.wrap(value).join(', ') %></span></div>
<% else %>
<div class="whitespace-pre-wrap"><%= Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') %></div>
<% end %>
<% else %>
<div class="whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
<% end %>
<% end %> <% end %>
</div> </div>
</div> </div>
+1 -1
View File
@@ -58,7 +58,7 @@
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %> <% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %>
<% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %> <% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %> <% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
<%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value:, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %> <%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %>
<% end %> <% end %>
</div> </div>
</div> </div>
+8
View File
@@ -0,0 +1,8 @@
{
"ignored_warnings": [
{
"fingerprint": "25f4ce5fee1e1180fa1919dc4ee78db3ab3457a956e4679503aa745771a43836",
"note": "Permitted parameters are necessary for creating submitters via API"
}
]
}
+1 -1
View File
@@ -187,7 +187,7 @@ Devise.setup do |config|
# ==> Configuration for :rememberable # ==> Configuration for :rememberable
# The time the user will be remembered without asking for credentials again. # The time the user will be remembered without asking for credentials again.
config.remember_for = 2.years config.remember_for = ENV.fetch('SESSION_REMEMBER_DAYS', '730').to_i.days
# Invalidates all the remember me tokens when the user signs out. # Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = true config.expire_all_remember_me_on_sign_out = true
+8 -2
View File
@@ -33,8 +33,14 @@ environment ENV.fetch('RAILS_ENV', 'development')
# the concurrency of the application would be max `threads` * `workers`. # the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support # Workers do not work on JRuby or Windows (both of which do not support
# processes). # processes).
#
workers ENV.fetch('WEB_CONCURRENCY', 0) if ENV['WEB_CONCURRENCY_AUTO'] == 'true'
require 'etc'
workers Etc.nprocessors
else
workers ENV.fetch('WEB_CONCURRENCY', 0)
end
# Use the `preload_app!` method when specifying a `workers` number. # Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code # This directive tells Puma to first boot the application and load code
+2
View File
@@ -122,6 +122,8 @@ Rails.application.routes.draw do
get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service 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 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 post '/direct_uploads' => 'active_storage/direct_uploads#create', as: :rails_direct_uploads
ActiveSupport.run_load_hooks(:multitenant_routes, self)
end end
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
@@ -0,0 +1,37 @@
# frozen_string_literal: true
class CreateConsole1984Tables < ActiveRecord::Migration[7.0]
def change
create_table :console1984_sessions do |t|
t.text :reason
t.references :user, null: false, index: false
t.timestamps
t.index :created_at
t.index %i[user_id created_at]
end
create_table :console1984_users do |t|
t.string :username, null: false
t.timestamps
t.index [:username]
end
create_table :console1984_commands do |t|
t.text :statements
t.references :sensitive_access
t.references :session, null: false, index: false
t.timestamps
t.index %i[session_id created_at sensitive_access_id], name: 'on_session_and_sensitive_chronologically'
end
create_table :console1984_sensitive_accesses do |t|
t.text :justification
t.references :session, null: false
t.timestamps
end
end
end
+35 -1
View File
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2024_12_07_172237) do ActiveRecord::Schema[8.0].define(version: 2025_02_25_111255) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -112,6 +112,40 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_07_172237) do
t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true
end end
create_table "console1984_commands", force: :cascade do |t|
t.text "statements"
t.bigint "sensitive_access_id"
t.bigint "session_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["sensitive_access_id"], name: "index_console1984_commands_on_sensitive_access_id"
t.index ["session_id", "created_at", "sensitive_access_id"], name: "on_session_and_sensitive_chronologically"
end
create_table "console1984_sensitive_accesses", force: :cascade do |t|
t.text "justification"
t.bigint "session_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["session_id"], name: "index_console1984_sensitive_accesses_on_session_id"
end
create_table "console1984_sessions", force: :cascade do |t|
t.text "reason"
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["created_at"], name: "index_console1984_sessions_on_created_at"
t.index ["user_id", "created_at"], name: "index_console1984_sessions_on_user_id_and_created_at"
end
create_table "console1984_users", force: :cascade do |t|
t.string "username", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["username"], name: "index_console1984_users_on_username"
end
create_table "document_generation_events", force: :cascade do |t| create_table "document_generation_events", force: :cascade do |t|
t.bigint "submitter_id", null: false t.bigint "submitter_id", null: false
t.string "event_name", null: false t.string "event_name", null: false
+19 -1
View File
@@ -3,10 +3,28 @@
module SendWebhookRequest module SendWebhookRequest
USER_AGENT = 'DocuSeal.com Webhook' USER_AGENT = 'DocuSeal.com Webhook'
LOCALHOSTS = %w[0.0.0.0 127.0.0.1 localhost].freeze
HttpsError = Class.new(StandardError)
LocalhostError = Class.new(StandardError)
module_function module_function
def call(webhook_url, event_type:, data:) def call(webhook_url, event_type:, data:)
Faraday.post(webhook_url.url) do |req| uri = begin
URI(webhook_url.url)
rescue URI::Error
Addressable::URI.parse(webhook_url.url).normalize
end
if Docuseal.multitenant?
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' &&
!AccountConfig.exists?(key: :allow_http,
account_id: webhook_url.account_id)
raise LocalhostError, "Can't send to localhost." if uri.host.in?(LOCALHOSTS)
end
Faraday.post(uri) do |req|
req.headers['Content-Type'] = 'application/json' req.headers['Content-Type'] = 'application/json'
req.headers['User-Agent'] = USER_AGENT req.headers['User-Agent'] = USER_AGENT
req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present? req.headers.merge!(webhook_url.secret.to_h) if webhook_url.secret.present?
+1
View File
@@ -107,6 +107,7 @@ module Submissions
def normalize_email(email) def normalize_email(email)
return if email.blank? return if email.blank?
return if email.is_a?(Numeric)
return email.downcase if email.to_s.include?(',') || return email.downcase if email.to_s.include?(',') ||
email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) || email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
+5 -4
View File
@@ -359,13 +359,14 @@ module Submissions
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale) value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
end end
if field['type'] == 'number' value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
value = NumberUtils.format_number(value,
field.dig('preferences', 'format'))
end
value = value.join(', ') if value.is_a?(Array) value = value.join(', ') if value.is_a?(Array)
if (mask = field.dig('preferences', 'mask').presence)
value = TextUtils.mask_value(value, mask)
end
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }], composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left, text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left,
padding: [0, 0, 10, 0]) padding: [0, 0, 10, 0])
+42 -6
View File
@@ -4,12 +4,29 @@ module Submissions
module GenerateResultAttachments module GenerateResultAttachments
FONT_SIZE = 11 FONT_SIZE = 11
FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf' FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf'
FONT_BOLD_PATH = '/fonts/GoNotoKurrent-Bold.ttf'
FONT_NAME = if File.exist?(FONT_PATH) FONT_NAME = if File.exist?(FONT_PATH)
FONT_PATH FONT_PATH
else else
'Helvetica' 'Helvetica'
end end
FONT_BOLD_NAME = if File.exist?(FONT_BOLD_PATH)
FONT_BOLD_PATH
else
'Helvetica'
end
FONT_ITALIC_NAME = 'Helvetica'
FONT_BOLD_ITALIC_NAME = 'Helvetica'
FONT_VARIANS = {
none: FONT_NAME,
bold: FONT_BOLD_NAME,
italic: FONT_ITALIC_NAME,
bold_italic: FONT_BOLD_ITALIC_NAME
}.freeze
SIGN_REASON = 'Signed by %<name>s with DocuSeal.com' SIGN_REASON = 'Signed by %<name>s with DocuSeal.com'
RTL_REGEXP = TextUtils::RTL_REGEXP RTL_REGEXP = TextUtils::RTL_REGEXP
@@ -18,12 +35,15 @@ module Submissions
TEXT_TOP_MARGIN = 1 TEXT_TOP_MARGIN = 1
MAX_PAGE_ROTATE = 20 MAX_PAGE_ROTATE = 20
COURIER_FONT = 'Courier'
A4_SIZE = [595, 842].freeze A4_SIZE = [595, 842].freeze
TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING' TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING'
DEFAULT_FONTS = %w[Times Helvetica Courier].freeze DEFAULT_FONTS = %w[Times Helvetica Courier].freeze
FONTS_LINE_HEIGHT = {
'Times' => 1.4,
'Helvetica' => 1.4,
'Courier' => 1.6
}.freeze
MISSING_GLYPH_REPLACE = { MISSING_GLYPH_REPLACE = {
'▪' => '-', '▪' => '-',
@@ -192,8 +212,16 @@ module Submissions
fill_color = field.dig('preferences', 'color').presence fill_color = field.dig('preferences', 'color').presence
font_name = field.dig('preferences', 'font') font_name = field.dig('preferences', 'font')
font_variant = (field.dig('preferences', 'font_type').presence || 'none').to_sym
font_name = FONT_NAME unless font_name.in?(DEFAULT_FONTS) font_name = FONT_NAME unless font_name.in?(DEFAULT_FONTS)
font = pdf.fonts.add(font_name)
if font_variant != :none && font_name == FONT_NAME
font_name = FONT_VARIANS[font_variant]
font_variant = nil unless font_name.in?(DEFAULT_FONTS)
end
font = pdf.fonts.add(font_name, variant: font_variant)
value = submitter.values[field['uuid']] value = submitter.values[field['uuid']]
value = field['default_value'] if field['type'] == 'heading' value = field['default_value'] if field['type'] == 'heading'
@@ -391,6 +419,10 @@ module Submissions
when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? } when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? }
cell_width = area['cell_w'] * width cell_width = area['cell_w'] * width
if (mask = field.dig('preferences', 'mask').presence)
value = TextUtils.mask_value(value, mask)
end
chars = TextUtils.maybe_rtl_reverse(value).chars chars = TextUtils.maybe_rtl_reverse(value).chars
chars = chars.reverse if field.dig('preferences', 'align') == 'right' chars = chars.reverse if field.dig('preferences', 'align') == 'right'
@@ -440,8 +472,12 @@ module Submissions
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', ')) value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
if (mask = field.dig('preferences', 'mask').presence)
value = TextUtils.mask_value(value, mask)
end
text_params = { font:, fill_color:, font_size: } text_params = { font:, fill_color:, font_size: }
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1)
text = HexaPDF::Layout::TextFragment.create(value, **text_params) text = HexaPDF::Layout::TextFragment.create(value, **text_params)
@@ -450,7 +486,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1 if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text_params[:font_size] = (font_size / 1.4).to_i text_params[:font_size] = (font_size / 1.4).to_i
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1)
text = HexaPDF::Layout::TextFragment.create(value, **text_params) text = HexaPDF::Layout::TextFragment.create(value, **text_params)
@@ -461,7 +497,7 @@ module Submissions
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1 if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
text_params[:font_size] = (font_size / 1.9).to_i text_params[:font_size] = (font_size / 1.9).to_i
text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1)
text = HexaPDF::Layout::TextFragment.create(value, **text_params) text = HexaPDF::Layout::TextFragment.create(value, **text_params)
+10
View File
@@ -0,0 +1,10 @@
# frozen_string_literal: true
# This rake task was added by annotate_rb gem.
# Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this
if Rails.env.development? && ENV['ANNOTATERB_SKIP_ON_DB_TASKS'].nil?
require 'annotate_rb'
AnnotateRb::Core.load_rake_tasks
end
-57
View File
@@ -1,57 +0,0 @@
# frozen_string_literal: true
if Rails.env.development?
require 'annotate'
task set_annotation_options: :environment do
Annotate.set_defaults(
'active_admin' => 'false',
'additional_file_patterns' => [],
'routes' => 'false',
'models' => 'true',
'position_in_routes' => 'before',
'position_in_class' => 'before',
'position_in_test' => 'before',
'position_in_fixture' => 'before',
'position_in_factory' => 'before',
'position_in_serializer' => 'before',
'show_foreign_keys' => 'true',
'show_complete_foreign_keys' => 'false',
'show_indexes' => 'true',
'simple_indexes' => 'false',
'model_dir' => 'app/models',
'root_dir' => '',
'include_version' => 'false',
'require' => '',
'exclude_tests' => 'true',
'exclude_fixtures' => 'true',
'exclude_factories' => 'true',
'exclude_serializers' => 'false',
'exclude_scaffolds' => 'true',
'exclude_controllers' => 'true',
'exclude_helpers' => 'true',
'exclude_sti_subclasses' => 'false',
'ignore_model_sub_dir' => 'false',
'ignore_columns' => nil,
'ignore_routes' => nil,
'ignore_unknown_models' => 'false',
'hide_limit_column_types' => 'integer,bigint,boolean',
'hide_default_column_types' => 'json,jsonb,hstore',
'skip_on_db_migrate' => 'false',
'format_bare' => 'true',
'format_rdoc' => 'false',
'format_yard' => 'false',
'format_markdown' => 'false',
'sort' => 'false',
'force' => 'false',
'frozen' => 'false',
'classified_sort' => 'true',
'trace' => 'false',
'wrapper_open' => nil,
'wrapper_close' => nil,
'with_comment' => 'true'
)
end
Annotate.load_tasks
end
+1 -1
View File
@@ -101,7 +101,7 @@ module Templates
{ {
uuid: SecureRandom.uuid, uuid: SecureRandom.uuid,
required: false, required: field.flags.include?(:required),
preferences: {}, preferences: {},
areas:, areas:,
**field_properties **field_properties
+20
View File
@@ -2,6 +2,8 @@
module TextUtils module TextUtils
RTL_REGEXP = /[\p{Hebrew}\p{Arabic}]/ RTL_REGEXP = /[\p{Hebrew}\p{Arabic}]/
MASK_REGEXP = /[^\s\-_\[\]\(\)\+\?\.\,]/
MASK_SYMBOL = 'X'
module_function module_function
@@ -13,6 +15,24 @@ module TextUtils
false false
end end
def mask_value(text, unmask_size = 0)
if unmask_size.is_a?(Numeric) && !unmask_size.zero? && unmask_size.abs < text.length
if unmask_size.negative?
[
text.first(text.length + unmask_size).gsub(MASK_REGEXP, MASK_SYMBOL),
text.last(-unmask_size)
].join
elsif unmask_size.positive?
[
text.first(unmask_size),
text.last(text.length - unmask_size).gsub(MASK_REGEXP, MASK_SYMBOL)
].join
end
else
text.to_s.gsub(MASK_REGEXP, MASK_SYMBOL)
end
end
def maybe_rtl_reverse(text) def maybe_rtl_reverse(text)
if text.match?(RTL_REGEXP) if text.match?(RTL_REGEXP)
TwitterCldr::Shared::Bidi TwitterCldr::Shared::Bidi
+24
View File
@@ -772,6 +772,30 @@ RSpec.describe 'Signing Form', type: :system do
end end
end end
context 'when the masked field' do
let(:template) { create(:template, submitter_count: 2, account:, author:, only_field_types: %w[text]) }
let(:submission) { create(:submission, template: template) }
let!(:first_submitter) { create(:submitter, submission:, uuid: template.submitters[0]['uuid'], account:) }
let!(:second_submitter) { create(:submitter, submission:, uuid: template.submitters[1]['uuid'], account:) }
it 'shows the masked value instead of the real value' do
field = submission.template_fields.find do |f|
f['name'] == 'First Name' && f['submitter_uuid'] == first_submitter.uuid
end
field['preferences']['mask'] = true
submission.save!
visit submit_form_path(slug: first_submitter.slug)
fill_in 'First Name', with: 'Jahn'
click_button 'Complete'
visit submit_form_path(slug: second_submitter.slug)
expect(page).to have_content('XXXX')
end
end
it 'sends completed email' do it 'sends completed email' do
template = create(:template, account:, author:, only_field_types: %w[text signature]) template = create(:template, account:, author:, only_field_types: %w[text signature])
submission = create(:submission, template:) submission = create(:submission, template:)