Branding and new features

pull/606/head
Usman Sarwar 4 weeks ago
parent b3e72f0726
commit 10ecb1d417

@ -2,7 +2,7 @@
source 'https://rubygems.org'
ruby '3.4.2'
ruby '~> 3.2.0'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'aws-sdk-s3', require: false

@ -685,7 +685,7 @@ DEPENDENCIES
webmock
RUBY VERSION
ruby 3.4.2p28
ruby 3.2.2p53
BUNDLED WITH
2.5.3

@ -0,0 +1,32 @@
# frozen_string_literal: true
class AccountLogosController < ApplicationController
before_action :load_account
authorize_resource :account
def update
file = params[:file]
return redirect_to settings_personalization_path, alert: I18n.t('file_is_missing') if file.blank?
@account.logo.attach(
io: file.open,
filename: file.original_filename,
content_type: file.content_type
)
redirect_to settings_personalization_path, notice: I18n.t('logo_has_been_uploaded')
end
def destroy
@account.logo.purge
redirect_to settings_personalization_path, notice: I18n.t('logo_has_been_uploaded')
end
private
def load_account
@account = current_account
end
end

@ -11,8 +11,21 @@ class ConsoleRedirectController < ApplicationController
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
if request.path == '/sign_up'
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premises"
end
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
# In development, if console URL is localhost and doesn't exist, redirect to cloud URL instead
if Rails.env.development? && Docuseal::CONSOLE_URL.include?('localhost') && !Docuseal.multitenant?
if params[:redir].to_s.include?('/on_premises')
return redirect_to 'https://console.docuseal.com/on_premises', allow_other_host: true
elsif params[:redir].to_s.include?('/plans')
return redirect_to 'https://console.docuseal.com/plans', allow_other_host: true
end
end
auth = JsonWebToken.encode(uuid: true_user.uuid,
scope: :console,
exp: 1.minute.from_now.to_i)
@ -20,7 +33,8 @@ class ConsoleRedirectController < ApplicationController
redir_uri = Addressable::URI.parse(params[:redir])
path = redir_uri.path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
redirect_to "#{Docuseal::CONSOLE_URL}#{path}?#{{ **redir_uri&.query_values, 'auth' => auth }.to_query}",
query_values = redir_uri&.query_values || {}
redirect_to "#{Docuseal::CONSOLE_URL}#{path}?#{{ **query_values, 'auth' => auth }.to_query}",
allow_other_host: true
end
end

@ -7,7 +7,7 @@ class SubmissionsArchivedController < ApplicationController
@submissions = @submissions.left_joins(:template)
@submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:template_accesses, :created_by_user, template: :author)
.preload(:created_by_user, template: :author)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)

@ -8,7 +8,7 @@ class SubmissionsDashboardController < ApplicationController
@submissions = @submissions.where(archived_at: nil)
.where(templates: { archived_at: nil })
.preload(:template_accesses, :created_by_user, template: :author)
.preload(:created_by_user, template: :author)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)

@ -11,7 +11,6 @@ class TemplateFoldersController < ApplicationController
def show
@templates = Template.active.accessible_by(current_ability)
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
.preload(:author, :template_accesses)
@template_folders =
@template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
@ -22,6 +21,7 @@ class TemplateFoldersController < ApplicationController
if @templates.exists?
@templates = Templates.search(current_user, @templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, selected_order)
@templates = @templates.preload(:author, :template_accesses)
limit =
if @template_folders.size < 4

@ -19,7 +19,7 @@ class TemplatesController < ApplicationController
submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
@pagy, @submissions = pagy_auto(submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end

@ -155,7 +155,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)),
backgroundColor: '#faf7f5',
backgroundColor: '#FFFFFF',
locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true',
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
@ -171,9 +171,10 @@ safeRegisterElement('template-builder', class extends HTMLElement {
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
withConditions: this.dataset.withConditions === 'true',
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
withAddPageButton: true,
withReplaceAndCloneUpload: true,
withDownload: true,
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
caurrencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
showTourStartForm: this.dataset.showTourStartForm === 'true'
})

@ -4,6 +4,11 @@
@import "tailwindcss/components";
@import "tailwindcss/utilities";
body {
@apply antialiased text-base-content;
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
}
a[href],
input[type='checkbox'],
input[type='submit'],
@ -31,50 +36,92 @@ button[disabled] .enabled, button.btn-disabled .enabled {
display: none;
}
.input-bordered {
@apply border-base-content/20;
/* Form controls - enterprise-grade inputs */
.input-bordered,
.select-bordered,
.textarea-bordered {
@apply border-base-content/15 transition-all duration-200;
}
.select-bordered {
@apply border-base-content/20;
.input-bordered:focus,
.select-bordered:focus,
.textarea-bordered:focus {
@apply border-primary outline-none ring-2 ring-primary/20;
}
.textarea-bordered {
@apply border-base-content/20;
.input-bordered:hover:not(:focus):not(:disabled),
.textarea-bordered:hover:not(:focus):not(:disabled) {
@apply border-base-content/25;
}
.btn {
@apply no-animation;
.base-input {
@apply input input-bordered bg-white h-11 px-4 text-base rounded-lg font-normal transition-all duration-200;
}
.base-input {
@apply input input-bordered bg-white;
.base-input::placeholder {
@apply text-base-content/50;
}
.base-input:disabled,
.base-input[readonly] {
@apply bg-base-200 cursor-not-allowed opacity-90;
}
.base-textarea {
@apply textarea textarea-bordered bg-white rounded-3xl;
@apply textarea textarea-bordered bg-white rounded-lg px-4 py-3 text-base font-normal transition-all duration-200 min-h-[100px];
}
.base-select {
@apply select base-input w-full font-normal;
}
/* Form labels */
.form-control .label {
@apply mb-1.5;
}
.form-control .label .label-text {
@apply font-medium text-base-content text-sm;
}
.form-control .label-text-alt {
@apply text-base-content/70 text-sm mt-1;
}
/* Buttons - clear hierarchy with transitions */
.btn {
@apply transition-all duration-200 font-medium;
}
.base-button {
@apply btn btn-neutral text-white text-base;
@apply btn btn-primary text-primary-content text-base px-6 h-11 rounded-lg hover:opacity-90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-2;
}
.white-button {
@apply btn btn-outline text-base bg-white border-2;
@apply btn btn-outline text-base bg-white border-2 border-base-300 rounded-lg px-6 h-11 hover:bg-base-200 hover:border-base-content/20 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-primary/20 focus:ring-offset-2 transition-all duration-200;
}
.btn-neutral,
.btn-primary {
@apply transition-all duration-200 hover:opacity-90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-offset-2;
}
.btn-primary {
@apply focus:ring-primary/30;
}
.btn-neutral {
@apply focus:ring-neutral/20;
}
.base-checkbox {
@apply checkbox rounded bg-white checkbox-sm no-animation;
@apply checkbox rounded bg-white checkbox-sm no-animation transition-colors duration-200;
}
.base-radio {
@apply radio bg-white radio-sm no-animation;
}
.base-select {
@apply select base-input w-full font-normal;
}
.tooltip-bottom-end:before {
transform: translateX(-95%);
top: var(--tooltip-offset);
@ -117,10 +164,10 @@ button[disabled] .enabled, button.btn-disabled .enabled {
.autocomplete {
background: white;
z-index: 1000;
font: 16px/25px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font: inherit;
overflow: auto;
box-sizing: border-box;
@apply border border-base-300 mt-1 rounded-md;
@apply border border-base-300 mt-1 rounded-lg shadow-soft-lg;
}
.autocomplete * {
@ -128,16 +175,16 @@ button[disabled] .enabled, button.btn-disabled .enabled {
}
.autocomplete > div {
@apply px-2 py-1.5 font-normal;
@apply px-3 py-2.5 font-normal transition-colors duration-150;
}
.autocomplete .group {
background: #eee;
@apply bg-base-200;
}
.autocomplete > div:hover:not(.group),
.autocomplete > div.selected {
@apply bg-base-300;
@apply bg-base-200;
cursor: pointer;
}

@ -1,13 +1,16 @@
export default class extends HTMLElement {
connectedCallback () {
this.button.addEventListener('click', () => {
this.button.addEventListener('click', (e) => {
const expirationDate = new Date()
expirationDate.setFullYear(expirationDate.getFullYear() + 10)
const expires = expirationDate.toUTCString()
document.cookie = this.dataset.key + '=' + this.dataset.value + '; expires=' + expires + '; path=/'
const form = this.closest('form')
if (form) {
e.preventDefault()
form.requestSubmit(this.button)
}
})
}

@ -4,8 +4,8 @@ export default actionable(class extends HTMLElement {
connectedCallback () {
document.body.classList.add('overflow-hidden')
this.addEventListener('click', this.onClick)
document.addEventListener('keyup', this.onEscKey)
document.addEventListener('turbo:before-cache', this.close)
if (this.dataset.closeAfterSubmit !== 'false') {
@ -16,11 +16,22 @@ export default actionable(class extends HTMLElement {
disconnectedCallback () {
document.body.classList.remove('overflow-hidden')
this.removeEventListener('click', this.onClick)
document.removeEventListener('keyup', this.onEscKey)
document.removeEventListener('turbo:submit-end', this.onSubmit)
document.removeEventListener('turbo:before-cache', this.close)
}
onClick = (e) => {
const isCloseButton = e.target.closest('[data-turbo-modal-close]')
const isOutsideContent = !e.target.closest('[data-turbo-modal-content]')
if (isCloseButton || isOutsideContent) {
e.preventDefault()
e.stopPropagation()
this.close()
}
}
onSubmit = (e) => {
if (e.detail.success && e.detail?.formSubmission?.formElement?.dataset?.closeOnSubmit !== 'false') {
this.close()

@ -31,36 +31,36 @@ button[disabled] .enabled, button.btn-disabled .enabled {
display: none;
}
.input-bordered {
@apply border-base-content/20;
.input-bordered,
.select-bordered,
.textarea-bordered {
@apply border-base-content/15 transition-all duration-200;
}
.select-bordered {
@apply border-base-content/20;
.input-bordered:focus,
.select-bordered:focus,
.textarea-bordered:focus {
@apply border-primary outline-none ring-2 ring-primary/20;
}
.textarea-bordered {
@apply border-base-content/20;
.base-input {
@apply input input-bordered bg-white h-11 px-4 text-base rounded-lg font-normal transition-all duration-200;
}
.base-textarea {
@apply textarea textarea-bordered bg-white rounded-3xl;
@apply textarea textarea-bordered bg-white rounded-lg px-4 py-3 text-base font-normal transition-all duration-200 min-h-[100px];
}
.btn {
@apply no-animation;
}
.base-input {
@apply input input-bordered bg-white;
@apply transition-all duration-200 font-medium;
}
.base-button {
@apply btn btn-neutral text-white text-base;
@apply btn btn-primary text-primary-content text-base px-6 h-11 rounded-lg hover:opacity-90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-2;
}
.white-button {
@apply btn btn-outline text-base bg-white border-2;
@apply btn btn-outline text-base bg-white border-2 border-base-300 rounded-lg px-6 h-11 hover:bg-base-200 hover:border-base-content/20 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-primary/20 focus:ring-offset-2 transition-all duration-200;
}
.base-checkbox {

@ -3,10 +3,10 @@
class="flex absolute lg:text-base -outline-offset-1 field-area"
dir="auto"
:style="[computedStyle, fontStyle]"
:class="{ '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, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
:class="{ 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable && field.type !== 'redact', 'border border-red-100': !isActive && submittable && field.type !== 'redact', 'bg-opacity-80': !isActive && !isValueSet && submittable && field.type !== 'redact', 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable && field.type !== 'redact', '!bg-black !opacity-100 border-0': field.type === 'redact' }"
>
<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' && field.type !== 'redact' && submittable && !area.option_uuid"
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
>
<span
@ -104,6 +104,11 @@
class="object-contain mx-auto"
:src="initials.url"
>
<div
v-else-if="field.type === 'redact'"
class="absolute inset-0 bg-black pointer-events-none"
aria-hidden="true"
/>
<div
v-else-if="(field.type === 'file' || field.type === 'payment') && attachments.length"
class="px-0.5 flex flex-col justify-center"
@ -224,6 +229,12 @@
>
{{ formatNumber(modelValue, field.preferences?.format) }}
</span>
<div
v-else-if="field.type === 'redact'"
class="w-full h-full bg-black"
>
<!-- Redacted area - solid black overlay hides underlying text during signing -->
</div>
<span
v-else-if="field.type === 'strikethrough'"
class="w-full h-full flex items-center justify-center"
@ -384,6 +395,7 @@ export default {
text: this.t('text'),
signature: this.t('signature'),
initials: this.t('initials'),
redact: this.t('redact'),
date: this.t('date'),
number: this.t('number'),
image: this.t('image'),
@ -447,6 +459,7 @@ export default {
return {
text: IconTextSize,
signature: IconWritingSign,
redact: IconRubberStamp,
date: IconCalendarEvent,
number: IconSquareNumber1,
image: IconPhoto,

@ -14,8 +14,8 @@
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
/>
<FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])"
:values="readonlyConditionalFieldValues"
:steps="readonlyDisplaySteps"
:values="readonlyDisplayFieldValues"
:submitter="submitter"
:attachments-index="attachmentsIndex"
:submittable="false"
@ -1010,6 +1010,20 @@ export default {
readonlyConditionalFields () {
return this.readonlyFields.filter((f) => f.conditions?.length)
},
readonlyDisplaySteps () {
const conditional = this.readonlyConditionalFields.map((e) => [e])
const conditionalUuids = new Set(this.readonlyConditionalFields.flat().map((f) => f.uuid))
const redactOnly = this.readonlyFields
.filter((f) => f.type === 'redact' && !conditionalUuids.has(f.uuid))
.map((f) => [f])
return [...conditional, ...redactOnly]
},
readonlyDisplayFieldValues () {
const redactValues = this.readonlyFields
.filter((f) => f.type === 'redact')
.reduce((acc, f) => { acc[f.uuid] = ''; return acc }, {})
return { ...this.readonlyConditionalFieldValues, ...redactValues }
},
readonlyFields () {
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
},

@ -1406,6 +1406,11 @@ export default {
field.readonly = true
}
if (type === 'redact') {
field.readonly = true
field.required = false
}
if (type === 'datenow') {
field.type = 'date'
field.readonly = true
@ -1455,6 +1460,11 @@ export default {
field.readonly = true
}
if (type === 'redact') {
field.readonly = true
field.required = false
}
if (type === 'date') {
field.preferences = {
format: this.defaultDateFormat
@ -2088,7 +2098,7 @@ export default {
field.default_value = '{{date}}'
}
if (['stamp', 'heading', 'strikethrough'].includes(field.type)) {
if (['stamp', 'heading', 'strikethrough', 'redact'].includes(field.type)) {
field.readonly = true
if (field.type === 'strikethrough') {

@ -51,7 +51,7 @@
</template>
<script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough, IconUserScan } from '@tabler/icons-vue'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough, IconUserScan, IconBarrierBlock } from '@tabler/icons-vue'
export default {
name: 'FiledTypeDropdown',
@ -98,6 +98,7 @@ export default {
return {
heading: this.t('heading'),
strikethrough: this.t('strikeout'),
redact: this.t('redact'),
text: this.t('text'),
signature: this.t('signature'),
initials: this.t('initials'),
@ -120,6 +121,7 @@ export default {
},
fieldLabels () {
return {
redact: this.t('redact_field'),
text: this.t('text_field'),
signature: this.t('signature_field'),
initials: this.t('initials_field'),
@ -143,6 +145,7 @@ export default {
return {
heading: IconHeading,
strikethrough: IconStrikethrough,
redact: IconBarrierBlock,
text: IconTextSize,
signature: IconWritingSign,
initials: IconLetterCaseUpper,

@ -134,6 +134,7 @@ const en = {
payment: 'Payment',
phone: 'Phone',
text_field: 'Text Field',
redact_field: 'Redact Field',
signature_field: 'Signature Field',
initials_field: 'Initials Field',
date_field: 'Date Field',

@ -4,12 +4,12 @@
#
# Table name: access_tokens
#
# id :bigint not null, primary key
# sha256 :text not null
# id :integer not null, primary key
# sha256 :string not null
# token :text not null
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
# user_id :integer not null
#
# Indexes
#
@ -18,7 +18,7 @@
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
# user_id (user_id => users.id)
#
class AccessToken < ApplicationRecord
TOKEN_LENGTH = 43

@ -4,7 +4,7 @@
#
# Table name: accounts
#
# id :bigint not null, primary key
# id :integer not null, primary key
# archived_at :datetime
# locale :string not null
# name :string not null
@ -50,6 +50,7 @@ class Account < ApplicationRecord
has_many :testing_accounts, through: :account_testing_accounts, source: :linked_account
has_many :active_users, -> { active }, dependent: :destroy,
inverse_of: :account, class_name: 'User'
has_one_attached :logo
attribute :timezone, :string, default: 'UTC'
attribute :locale, :string, default: 'en-US'

@ -4,11 +4,11 @@
#
# Table name: account_accesses
#
# id :bigint not null, primary key
# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# user_id :bigint not null
# account_id :integer not null
# user_id :integer not null
#
# Indexes
#
@ -16,7 +16,7 @@
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# account_id (account_id => accounts.id)
#
class AccountAccess < ApplicationRecord
belongs_to :account

@ -4,12 +4,12 @@
#
# Table name: account_configs
#
# id :bigint not null, primary key
# id :integer not null, primary key
# key :string not null
# value :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# account_id :integer not null
#
# Indexes
#
@ -18,7 +18,7 @@
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# account_id (account_id => accounts.id)
#
class AccountConfig < ApplicationRecord
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'

@ -4,12 +4,12 @@
#
# Table name: account_linked_accounts
#
# id :bigint not null, primary key
# id :integer not null, primary key
# account_type :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# linked_account_id :bigint not null
# account_id :integer not null
# linked_account_id :integer not null
#
# Indexes
#
@ -19,8 +19,8 @@
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (linked_account_id => accounts.id)
# account_id (account_id => accounts.id)
# linked_account_id (linked_account_id => accounts.id)
#
class AccountLinkedAccount < ApplicationRecord
belongs_to :account

@ -4,7 +4,7 @@
#
# Table name: completed_documents
#
# id :bigint not null, primary key
# id :integer not null, primary key
# sha256 :string not null
# created_at :datetime not null
# updated_at :datetime not null

@ -4,7 +4,7 @@
#
# Table name: completed_submitters
#
# id :bigint not null, primary key
# id :integer not null, primary key
# completed_at :datetime not null
# is_first :boolean
# sms_count :integer not null
@ -19,9 +19,9 @@
#
# Indexes
#
# index_completed_submitters_account_id_completed_at_is_first (account_id,completed_at) WHERE (is_first = true)
# index_completed_submitters_account_id_completed_at_is_first (account_id,completed_at) WHERE is_first = TRUE
# index_completed_submitters_on_account_id_and_completed_at (account_id,completed_at)
# index_completed_submitters_on_submission_id (submission_id) UNIQUE WHERE (is_first = true)
# index_completed_submitters_on_submission_id (submission_id) UNIQUE WHERE is_first = TRUE
# index_completed_submitters_on_submitter_id (submitter_id) UNIQUE
#
class CompletedSubmitter < ApplicationRecord

@ -4,20 +4,20 @@
#
# Table name: document_generation_events
#
# id :bigint not null, primary key
# id :integer not null, primary key
# event_name :string not null
# created_at :datetime not null
# updated_at :datetime not null
# submitter_id :bigint not null
# submitter_id :integer not null
#
# Indexes
#
# index_document_generation_events_on_submitter_id (submitter_id)
# index_document_generation_events_on_submitter_id_and_event_name (submitter_id,event_name) UNIQUE WHERE ((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))
# index_document_generation_events_on_submitter_id_and_event_name (submitter_id,event_name) UNIQUE WHERE event_name IN ('start', 'complete')
#
# Foreign Keys
#
# fk_rails_... (submitter_id => submitters.id)
# submitter_id (submitter_id => submitters.id)
#
class DocumentGenerationEvent < ApplicationRecord
belongs_to :submitter

@ -4,7 +4,7 @@
#
# Table name: email_events
#
# id :bigint not null, primary key
# id :integer not null, primary key
# data :text not null
# email :string not null
# emailable_type :string not null
@ -12,21 +12,21 @@
# event_type :string not null
# tag :string not null
# created_at :datetime not null
# account_id :bigint not null
# emailable_id :bigint not null
# account_id :integer not null
# emailable_id :integer not null
# message_id :string not null
#
# Indexes
#
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
# index_email_events_on_email (email)
# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'permanent_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))
# index_email_events_on_email_event_types (email) WHERE event_type IN ('bounce', 'soft_bounce', 'permanent_bounce', 'complaint', 'soft_complaint')
# index_email_events_on_emailable (emailable_type,emailable_id)
# index_email_events_on_message_id (message_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# account_id (account_id => accounts.id)
#
class EmailEvent < ApplicationRecord
belongs_to :emailable, polymorphic: true

@ -4,15 +4,15 @@
#
# Table name: email_messages
#
# id :bigint not null, primary key
# id :integer not null, primary key
# body :text not null
# sha1 :string not null
# subject :text not null
# uuid :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# account_id :integer not null
# author_id :integer not null
#
# Indexes
#
@ -22,8 +22,8 @@
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (author_id => users.id)
# account_id (account_id => accounts.id)
# author_id (author_id => users.id)
#
class EmailMessage < ApplicationRecord
belongs_to :author, class_name: 'User'

@ -4,12 +4,12 @@
#
# Table name: encrypted_configs
#
# id :bigint not null, primary key
# id :integer not null, primary key
# key :string not null
# value :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# account_id :integer not null
#
# Indexes
#
@ -18,7 +18,7 @@
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# account_id (account_id => accounts.id)
#
class EncryptedConfig < ApplicationRecord
CONFIG_KEYS = [

@ -4,12 +4,12 @@
#
# Table name: encrypted_user_configs
#
# id :bigint not null, primary key
# id :integer not null, primary key
# key :string not null
# value :text not null
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
# user_id :integer not null
#
# Indexes
#
@ -18,7 +18,7 @@
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
# user_id (user_id => users.id)
#
class EncryptedUserConfig < ApplicationRecord
belongs_to :user

@ -4,7 +4,7 @@
#
# Table name: lock_events
#
# id :bigint not null, primary key
# id :integer not null, primary key
# event_name :string not null
# key :string not null
# created_at :datetime not null
@ -12,7 +12,7 @@
#
# Indexes
#
# index_lock_events_on_event_name_and_key (event_name,key) UNIQUE WHERE ((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))
# index_lock_events_on_event_name_and_key (event_name,key) UNIQUE WHERE event_name IN ('start', 'complete')
# index_lock_events_on_key (key)
#
class LockEvent < ApplicationRecord

@ -4,13 +4,13 @@
#
# Table name: submissions
#
# id :bigint not null, primary key
# id :integer not null, primary key
# archived_at :datetime
# expire_at :datetime
# name :text
# preferences :text not null
# slug :string not null
# source :text not null
# source :string not null
# submitters_order :string not null
# template_fields :text
# template_schema :text
@ -19,23 +19,23 @@
# variables_schema :text
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# created_by_user_id :bigint
# template_id :bigint
# account_id :integer not null
# created_by_user_id :integer
# template_id :integer
#
# Indexes
#
# index_submissions_on_account_id_and_id (account_id,id)
# index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL)
# index_submissions_on_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE (archived_at IS NOT NULL)
# index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE archived_at IS NULL
# index_submissions_on_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE archived_at IS NOT NULL
# index_submissions_on_created_by_user_id (created_by_user_id)
# index_submissions_on_slug (slug) UNIQUE
# index_submissions_on_template_id (template_id)
#
# Foreign Keys
#
# fk_rails_... (created_by_user_id => users.id)
# fk_rails_... (template_id => templates.id)
# created_by_user_id (created_by_user_id => users.id)
# template_id (template_id => templates.id)
#
class Submission < ApplicationRecord
belongs_to :template, optional: true

@ -4,15 +4,15 @@
#
# Table name: submission_events
#
# id :bigint not null, primary key
# id :integer not null, primary key
# data :text not null
# event_timestamp :datetime not null
# event_type :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint
# submission_id :bigint not null
# submitter_id :bigint
# account_id :integer
# submission_id :integer not null
# submitter_id :integer
#
# Indexes
#
@ -20,13 +20,13 @@
# index_submission_events_on_created_at (created_at)
# index_submission_events_on_submission_id (submission_id)
# index_submission_events_on_submitter_id (submitter_id)
# index_submissions_events_on_sms_event_types (account_id,created_at) WHERE ((event_type)::text = ANY ((ARRAY['send_sms'::character varying, 'send_2fa_sms'::character varying])::text[]))
# index_submissions_events_on_sms_event_types (account_id,created_at) WHERE event_type IN ('send_sms', 'send_2fa_sms')
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (submission_id => submissions.id)
# fk_rails_... (submitter_id => submitters.id)
# account_id (account_id => accounts.id)
# submission_id (submission_id => submissions.id)
# submitter_id (submitter_id => submitters.id)
#
class SubmissionEvent < ApplicationRecord
belongs_to :submission

@ -4,7 +4,7 @@
#
# Table name: submitters
#
# id :bigint not null, primary key
# id :integer not null, primary key
# completed_at :datetime
# declined_at :datetime
# email :string
@ -22,9 +22,9 @@
# values :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# account_id :integer not null
# external_id :string
# submission_id :bigint not null
# submission_id :integer not null
#
# Indexes
#
@ -37,7 +37,7 @@
#
# Foreign Keys
#
# fk_rails_... (submission_id => submissions.id)
# submission_id (submission_id => submissions.id)
#
class Submitter < ApplicationRecord
belongs_to :submission

@ -4,7 +4,7 @@
#
# Table name: templates
#
# id :bigint not null, primary key
# id :integer not null, primary key
# archived_at :datetime
# fields :text not null
# name :string not null
@ -17,16 +17,16 @@
# variables_schema :text
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# account_id :integer not null
# author_id :integer not null
# external_id :string
# folder_id :bigint not null
# folder_id :integer not null
#
# Indexes
#
# index_templates_on_account_id (account_id)
# index_templates_on_account_id_and_folder_id_and_id (account_id,folder_id,id) WHERE (archived_at IS NULL)
# index_templates_on_account_id_and_id_archived (account_id,id) WHERE (archived_at IS NOT NULL)
# index_templates_on_account_id_and_folder_id_and_id (account_id,folder_id,id) WHERE archived_at IS NULL
# index_templates_on_account_id_and_id_archived (account_id,id) WHERE archived_at IS NOT NULL
# index_templates_on_author_id (author_id)
# index_templates_on_external_id (external_id)
# index_templates_on_folder_id (folder_id)
@ -34,9 +34,9 @@
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (author_id => users.id)
# fk_rails_... (folder_id => template_folders.id)
# account_id (account_id => accounts.id)
# author_id (author_id => users.id)
# folder_id (folder_id => template_folders.id)
#
class Template < ApplicationRecord
DEFAULT_SUBMITTER_NAME = 'First Party'

@ -4,11 +4,11 @@
#
# Table name: template_accesses
#
# id :bigint not null, primary key
# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# template_id :bigint not null
# user_id :bigint not null
# template_id :integer not null
# user_id :integer not null
#
# Indexes
#
@ -16,7 +16,7 @@
#
# Foreign Keys
#
# fk_rails_... (template_id => templates.id)
# template_id (template_id => templates.id)
#
class TemplateAccess < ApplicationRecord
ADMIN_USER_ID = -1

@ -4,14 +4,14 @@
#
# Table name: template_folders
#
# id :bigint not null, primary key
# id :integer not null, primary key
# archived_at :datetime
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# parent_folder_id :bigint
# account_id :integer not null
# author_id :integer not null
# parent_folder_id :integer
#
# Indexes
#
@ -21,9 +21,9 @@
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (author_id => users.id)
# fk_rails_... (parent_folder_id => template_folders.id)
# account_id (account_id => accounts.id)
# author_id (author_id => users.id)
# parent_folder_id (parent_folder_id => template_folders.id)
#
class TemplateFolder < ApplicationRecord
DEFAULT_NAME = 'Default'

@ -4,12 +4,12 @@
#
# Table name: template_sharings
#
# id :bigint not null, primary key
# id :integer not null, primary key
# ability :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# template_id :bigint not null
# account_id :integer not null
# template_id :integer not null
#
# Indexes
#
@ -18,7 +18,7 @@
#
# Foreign Keys
#
# fk_rails_... (template_id => templates.id)
# template_id (template_id => templates.id)
#
class TemplateSharing < ApplicationRecord
ALL_ID = -1

@ -4,7 +4,7 @@
#
# Table name: users
#
# id :bigint not null, primary key
# id :integer not null, primary key
# archived_at :datetime
# confirmation_sent_at :datetime
# confirmation_token :string
@ -32,7 +32,7 @@
# uuid :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# account_id :integer not null
#
# Indexes
#
@ -44,7 +44,7 @@
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# account_id (account_id => accounts.id)
#
class User < ApplicationRecord
ROLES = [

@ -4,12 +4,12 @@
#
# Table name: user_configs
#
# id :bigint not null, primary key
# id :integer not null, primary key
# key :string not null
# value :text not null
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
# user_id :integer not null
#
# Indexes
#
@ -18,7 +18,7 @@
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
# user_id (user_id => users.id)
#
class UserConfig < ApplicationRecord
SIGNATURE_KEY = 'signature'

@ -4,7 +4,7 @@
#
# Table name: webhook_attempts
#
# id :bigint not null, primary key
# id :integer not null, primary key
# attempt :integer not null
# response_body :text
# response_status_code :integer not null

@ -4,7 +4,7 @@
#
# Table name: webhook_events
#
# id :bigint not null, primary key
# id :integer not null, primary key
# event_type :string not null
# record_type :string not null
# status :string not null
@ -17,7 +17,7 @@
#
# Indexes
#
# index_webhook_events_error (webhook_url_id,id) WHERE ((status)::text = 'error'::text)
# index_webhook_events_error (webhook_url_id,id) WHERE status = 'error'
# index_webhook_events_on_uuid_and_webhook_url_id (uuid,webhook_url_id) UNIQUE
# index_webhook_events_on_webhook_url_id_and_id (webhook_url_id,id)
#

@ -4,14 +4,14 @@
#
# Table name: webhook_urls
#
# id :bigint not null, primary key
# id :integer not null, primary key
# events :text not null
# secret :text not null
# sha1 :string not null
# url :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# account_id :integer not null
#
# Indexes
#
@ -20,7 +20,7 @@
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# account_id (account_id => accounts.id)
#
class WebhookUrl < ApplicationRecord
EVENTS = %w[

@ -1,9 +1,9 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">
<div class="max-w-4xl mx-auto space-y-8">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">
<%= t('account') %>
</h1>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('company_name', default: 'Company') %></h2>
<%= form_for '', url: settings_account_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= fields_for current_account do |ff| %>
<div class="form-control">
@ -41,9 +41,10 @@
</div>
<% end %>
<% end %>
</section>
<% if can?(:manage, AccountConfig) %>
<div class="px-1 mt-8">
<h2 class="text-2xl font-bold mb-2">
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5">
<%= t('preferences') %>
</h2>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::FORCE_MFA) %>
@ -260,19 +261,17 @@
<%= button_to params[:reindex] == 'true' ? t('reindex') : t('build_search_index'), settings_search_entries_reindex_index_path, method: :post, class: 'btn btn-sm btn-neutral text-white px-4' %>
</div>
<% end %>
</div>
</section>
<% end %>
<%= render 'compliances' %>
<%= render 'integrations' %>
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
<div class="px-1 mt-8">
<h2 class="text-2xl font-bold mb-2">
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft border-error/30">
<h2 class="text-lg font-semibold text-error mb-5">
<%= t('danger_zone') %>
</h2>
<%= button_to button_title(title: t('delete_my_account')), settings_account_path, class: 'btn btn-outline btn-error block', data: { turbo_confirm: t('you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue') }, method: :delete, id: :account_delete_button %>
</div>
</section>
<% end %>
</div>
<div class="w-0 md:w-52"></div>
</div>
<%= render 'shared/app_tour', type: 'account' %>

@ -1,12 +1,9 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="flex-grow">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
<h1 class="text-4xl font-bold">API</h1>
<div class="max-w-4xl mx-auto space-y-8">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">API</h1>
<%= render 'shared/test_mode_toggle' %>
</div>
<div class="card bg-base-200">
<div class="card-body p-6">
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
<div class="flex flex-col md:flex-row gap-4">
<div class="flex w-full space-x-4">
@ -29,10 +26,9 @@
</div>
<%= button_to button_title(title: t('rotate'), disabled_with: t('rotate'), icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: t('remove_existing_api_token_and_generated_a_new_one_are_you_sure_') } %>
</div>
</div>
</div>
<div class="space-y-4 mt-4">
<div class="collapse collapse-plus bg-base-200 px-1">
</section>
<div class="space-y-4">
<div class="collapse collapse-plus bg-base-100 rounded-xl border border-base-300 shadow-soft px-1">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
@ -68,7 +64,7 @@
</div>
</div>
</div>
<div class="collapse collapse-plus bg-base-200 px-1">
<div class="collapse collapse-plus bg-base-100 rounded-xl border border-base-300 shadow-soft px-1">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
@ -94,7 +90,7 @@
</div>
</div>
</div>
<div class="collapse collapse-plus bg-base-200 px-1">
<div class="collapse collapse-plus bg-base-100 rounded-xl border border-base-300 shadow-soft px-1">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
@ -121,4 +117,3 @@
<%= link_to t('open_full_api_reference'), "#{Docuseal::PRODUCT_URL}/docs/api", class: 'btn btn-warning text-base mt-4 px-8', target: '_blank', rel: 'noopener' %>
</div>
</div>
</div>

@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">Email SMTP</h1>
<div class="max-w-4xl mx-auto space-y-8">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">Email SMTP</h1>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('smtp_configuration', default: 'SMTP Configuration') %></h2>
<% value = @encrypted_config.value || {} %>
<%= form_for @encrypted_config, url: settings_email_index_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.fields_for :value do |ff| %>
@ -57,6 +57,5 @@
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
</div>
<% end %>
</div>
<div class="w-0 md:w-52"></div>
</section>
</div>

@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
.error-page {
background-color: #FAF7F4;
color: #2E2F30;
background-color: #FFFFFF;
color: #263854;
text-align: center;
font-family: arial, sans-serif;
margin: 0;
@ -15,21 +15,21 @@
.error-page h1 {
font-weight: 700;
font-size: 10em;
color: #3BBFF7;
color: #54B0E8;
margin: 0 0 0.1em;
}
.error-page h2 {
font-weight: 600;
font-size: 1.75em;
color: #291434;
color: #263854;
margin: 0.5em 0;
}
.error-page a.btn {
display: inline-block;
width: auto;
background-color: #291434;
background-color: #263854;
padding: 1em 1.5em;
border-radius: 25px;
color: #ffffff;

@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
.error-page {
background-color: #FAF7F4;
color: #2E2F30;
background-color: #FFFFFF;
color: #263854;
text-align: center;
font-family: arial, sans-serif;
margin: 0;
@ -15,21 +15,21 @@
.error-page h1 {
font-weight: 700;
font-size: 10em;
color: #FABE22;
color: #1FE0B3;
margin: 0 0 0.1em;
}
.error-page h2 {
font-weight: 600;
font-size: 1.75em;
color: #291434;
color: #263854;
margin: 0.5em 0;
}
.error-page a.btn {
display: inline-block;
width: auto;
background-color: #291434;
background-color: #263854;
padding: 1em 1.5em;
border-radius: 25px;
color: #ffffff;

@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
.error-page {
background-color: #FAF7F4;
color: #2E2F30;
background-color: #FFFFFF;
color: #263854;
text-align: center;
font-family: arial, sans-serif;
margin: 0;
@ -15,21 +15,21 @@
.error-page h1 {
font-weight: 700;
font-size: 10em;
color: #F97272;
color: #4E87C8;
margin: 0 0 0.1em;
}
.error-page h2 {
font-weight: 600;
font-size: 1.75em;
color: #291434;
color: #263854;
margin: 0.5em 0;
}
.error-page a.btn {
display: inline-block;
width: auto;
background-color: #291434;
background-color: #263854;
padding: 1em 1.5em;
border-radius: 25px;
color: #ffffff;

@ -1,10 +1,10 @@
<div class="flex-wrap space-y-4 md:flex md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="md:flex-grow">
<div class="max-w-xl">
<h1 class="text-4xl font-bold mb-4">
<div class="max-w-4xl mx-auto space-y-8">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">
<%= t('pdf_signature') %>
</h1>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<div class="max-w-xl">
<h2 class="text-lg font-semibold text-base-content mb-4"><%= t('verify_signed_pdf') %></h2>
<div id="result">
<p class="mb-2">
<%= t('upload_signed_pdf_file_to_validate_its_signature_') %>
@ -40,8 +40,10 @@
</file-dropzone>
<% end %>
</div>
<div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">
</section>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft overflow-hidden">
<div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4">
<h2 class="text-lg font-semibold text-base-content">
<%= t('signing_certificates') %>
</h2>
<% if can?(:create, @encrypted_config) %>
@ -105,11 +107,12 @@
</tbody>
</table>
</div>
</section>
<% encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY) %>
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %>
<div class="flex-grow max-w-xl">
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<div class="flex justify-between items-end mb-4">
<h2 class="text-lg font-semibold text-base-content">
<%= t('timestamp_server') %>
</h2>
</div>
@ -131,13 +134,13 @@
<%= f.button button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button' %>
</div>
<% end %>
</div>
</section>
<% end %>
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'single') %>
<% if can?(:manage, account_config) %>
<div class="px-1 mt-8 max-w-xl">
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<div class="flex justify-between items-end mb-4">
<h2 class="text-lg font-semibold text-base-content">
<%= t('preferences') %>
</h2>
</div>
@ -182,7 +185,6 @@
</div>
<% end %>
<% end %>
</div>
</section>
<% end %>
</div>
</div>

@ -2,3 +2,6 @@
<%= content_for(:html_title) || (signed_in? ? 'DocuSeal' : 'DocuSeal | Open Source Document Signing') %>
</title>
<%= render 'shared/meta' %>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">

@ -18,7 +18,7 @@
<% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %>
</head>
<body>
<body class="min-h-screen bg-base-100">
<% if params[:modal].present? %>
<% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
<% if url_params[:action] == 'new' %>
@ -29,7 +29,7 @@
<turbo-frame id="drawer"></turbo-frame>
<%= render 'shared/navbar' %>
<% if flash.present? %><%= render 'shared/flash' %><% end %>
<div class="max-w-6xl mx-auto px-4 md:px-2 mb-8">
<div class="max-w-7xl mx-auto px-4 md:px-6 py-8 md:py-10">
<%= yield %>
</div>
<%= render 'shared/body_scripts' %>

@ -1,9 +1,9 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">
<div class="max-w-4xl mx-auto space-y-8">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">
<%= t('email_notifications') %>
</h1>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('notification_preferences', default: 'Notification Preferences') %></h2>
<div class="mt-2 mb-1">
<% user_config = UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::RECEIVE_COMPLETED_EMAIL) %>
<% if can?(:manage, user_config) %>
@ -21,13 +21,12 @@
<% end %>
</div>
<%= render 'bcc_form', config: @bcc_config %>
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">
</section>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5">
<%= t('sign_request_email_reminders') %>
</h2>
</div>
<%= render 'reminder_banner' %>
<%= render 'reminder_form', config: @reminder_config %>
</div>
<div class="w-0 md:w-52"></div>
</section>
</div>

@ -1 +1,42 @@
<%= render 'logo_placeholder' %>
<% if current_account.logo.attached? %>
<div class="mb-4">
<div class="flex items-center space-x-4">
<div class="w-32 h-32 border border-base-300 rounded-md flex items-center justify-center bg-base-200">
<%= image_tag current_account.logo, class: 'max-w-full max-h-full object-contain' %>
</div>
<div>
<p class="text-sm text-base-content/70 mb-2">Current Logo</p>
<%= form_for '', url: account_logo_path, method: :delete, data: { turbo_confirm: 'Are you sure?' }, html: { class: 'inline' } do |f| %>
<%= f.button 'Remove Logo', class: 'btn btn-sm btn-ghost' %>
<% end %>
</div>
</div>
</div>
<% end %>
<%= form_for '', url: account_logo_path, method: :patch, html: { enctype: 'multipart/form-data', class: 'space-y-4' } do |f| %>
<file-dropzone data-submit-on-upload="true" class="w-full">
<label for="logo_file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed cursor-pointer">
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center p-2">
<div class="flex flex-col items-center text-center">
<span data-target="file-dropzone.icon">
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
</span>
<span data-target="file-dropzone.loading" class="hidden">
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
</span>
<div class="font-medium mb-1">
<%= t('upload_logo') %>
</div>
<div class="text-xs">
<%= t('click_to_upload_or_drag_and_drop_html') %>
</div>
</div>
<input id="logo_file" name="file" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/png,image/jpeg,image/jpg,image/svg+xml">
</div>
</label>
</file-dropzone>
<div class="form-control">
<%= f.button button_title(title: t('upload_logo'), disabled_with: t('saving')), class: 'base-button' %>
</div>
<% end %>

@ -1,27 +1,29 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<p class="text-4xl font-bold mb-4">
<%= t('email_templates') %>
</p>
<div class="max-w-4xl mx-auto space-y-8">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">
<%= t('personalization') %>
</h1>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('email_templates') %></h2>
<div class="space-y-4">
<%= render 'signature_request_email_form' %>
<%= render 'documents_copy_email_form' %>
<%= render 'submitter_completed_email_form' %>
</div>
<p class="text-4xl font-bold mb-4 mt-8">
<%= t('company_logo') %>
</p>
</section>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('company_logo') %></h2>
<%= render 'logo_form' %>
<p class="text-4xl font-bold mb-4 mt-8">
<%= t('submission_form') %>
</p>
</section>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('submission_form') %></h2>
<div class="space-y-4">
<%= render 'form_completed_message_form' %>
<%= render 'form_completed_button_form' %>
<%= render 'form_policy_links_form' %>
</div>
</section>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('form_customization', default: 'Form Customization') %></h2>
<%= render 'form_customization_settings' %>
</div>
<div class="w-0 md:w-52"></div>
</section>
</div>

@ -1,9 +1,9 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">
<div class="max-w-4xl mx-auto space-y-8">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">
<%= t('profile') %>
</h1>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('personal_details', default: 'Personal Details') %></h2>
<%= form_for current_user, url: update_contact_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<div class="grid md:grid-cols-2 gap-4">
<div class="form-control">
@ -28,10 +28,10 @@
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
</div>
<% end %>
</section>
<%= render 'email_configs' %>
<p class="text-2xl font-bold mt-8 mb-4">
<%= t('signature') %>
</p>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('signature') %></h2>
<% signature = UserConfigs.load_signature(current_user) %>
<% if signature %>
<div class="flex justify-center mb-4 relative">
@ -42,9 +42,9 @@
<a href="<%= edit_user_signature_path %>" data-turbo-frame="modal" class="base-button w-full">
<%= t('update_signature') %>
</a>
<p class="text-2xl font-bold mt-8 mb-4">
<%= t('initials') %>
</p>
</section>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('initials') %></h2>
<% initials = UserConfigs.load_initials(current_user) %>
<% if initials %>
<div class="flex justify-center mb-4 relative">
@ -55,10 +55,10 @@
<a href="<%= edit_user_initials_path %>" data-turbo-frame="modal" class="base-button w-full">
<%= t('update_initials') %>
</a>
</section>
<% if true_user == current_user && !current_account.testing? %>
<p class="text-2xl font-bold mt-8 mb-4">
<%= t('change_password') %>
</p>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('change_password') %></h2>
<%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off' } do |f| %>
<%= f.label :password, t('new_password'), class: 'label' %>
<%= f.password_field :password, autocomplete: 'off', class: 'base-input peer w-full', required: true %>
@ -82,9 +82,9 @@
</div>
<% end %>
<%= button_to nil, user_send_reset_password_path(current_user), id: 'resend_password_button', method: :put, class: 'hidden', data: { turbo_confirm: t('are_you_sure_') } %>
<p class="text-2xl font-bold mt-8 mb-4">
<%= t('two_factor_authentication') %>
</p>
</section>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('two_factor_authentication') %></h2>
<div class="space-y-4">
<% if current_user.otp_required_for_login %>
<p class="flex items-center space-x-1">
@ -104,7 +104,6 @@
<a href="<%= new_mfa_setup_path %>" data-turbo-frame="modal" class="base-button w-full !px-8">🔒 <%= t('set_up_2fa') %></a>
<% end %>
</div>
</section>
<% end %>
</div>
<div class="w-0 md:w-52"></div>
</div>

@ -20,6 +20,6 @@
"orientation": "any",
"description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
"categories": ["productivity", "utilities"],
"theme_color": "#FAF7F4",
"background_color": "#FAF7F4"
"theme_color": "#FFFFFF",
"background_color": "#FFFFFF"
}

@ -1,6 +1,6 @@
<div id="flash" class="absolute top-0 w-full h-0 z-20">
<div class="max-w-xl mx-auto mt-1.5">
<div class="px-4 py-3 rounded-2xl bg-base-200 flex items-center justify-between mx-4 md:mx-0">
<div id="flash" class="absolute top-0 w-full h-0 z-20 animate-fade-in">
<div class="max-w-xl mx-auto mt-3">
<div class="px-4 py-3.5 rounded-lg bg-base-100 border border-base-300 shadow-soft-lg flex items-center justify-between mx-4 md:mx-0">
<div class="flex items-center space-x-3">
<% if flash[:notice] %>
<%= svg_icon('info_circle', class: 'stroke-info flex-none w-6 h-6') %>

@ -27,4 +27,4 @@
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<meta name="theme-color" content="#faf7f5">
<meta name="theme-color" content="#FFFFFF">

@ -1,9 +1,116 @@
<%= 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">
<div class="flex items-center space-x-4">
<header class="w-full border-b border-base-300 bg-base-100/95 backdrop-blur-sm sticky top-0 z-10 transition-shadow duration-200">
<div class="max-w-7xl mx-auto px-4 md:px-6 py-3.5 md:py-4 flex items-center justify-between">
<div class="flex items-center space-x-8">
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
<%= render 'shared/title' %>
</a>
<% if signed_in? %>
<nav class="hidden md:flex items-center space-x-6">
<% is_templates = request.path == root_path || request.path.start_with?('/templates') || request.path.start_with?('/folders') %>
<% is_submissions = request.path.start_with?('/submissions') %>
<% is_settings = request.path.start_with?('/settings') %>
<%= link_to root_path, class: "font-medium text-base transition-colors duration-200 #{is_templates ? 'text-primary' : 'text-base-content hover:text-primary'}" do %>
<%= t('templates') %>
<% end %>
<%= link_to submissions_path, class: "font-medium text-base transition-colors duration-200 #{is_submissions ? 'text-primary' : 'text-base-content hover:text-primary'}" do %>
<%= t('submissions') %>
<% end %>
<div class="dropdown dropdown-hover">
<label tabindex="0" class="font-medium text-base transition-colors duration-200 <%= is_settings ? 'text-primary' : 'text-base-content hover:text-primary' %> cursor-pointer">
<%= t('settings') %>
<%= svg_icon('chevron_down', class: 'w-4 h-4 inline-block ml-1') %>
</label>
<ul tabindex="0" class="z-10 dropdown-content p-1.5 mt-2 shadow-soft-lg menu bg-base-100 rounded-lg min-w-[200px] border border-base-300 animate-fade-in-up">
<li><%= link_to t('profile'), settings_profile_index_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %></li>
<li><%= link_to t('account'), settings_account_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %></li>
<% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<li>
<%= link_to t('email'), settings_email_index_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user %>
<li>
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %>
</li>
<% end %>
<% end %>
<% if can?(:read, AccountConfig) %>
<li>
<%= link_to t('notifications'), settings_notifications_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
<li>
<%= link_to t('e_signature'), settings_esign_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %>
</li>
<% end %>
<% if can?(:read, AccountConfig) %>
<li>
<%= link_to t('personalization'), settings_personalization_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %>
</li>
<% end %>
<% if can?(:read, User) %>
<li>
<%= link_to t('users'), settings_users_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %>
</li>
<% end %>
<% if Docuseal.demo? || !Docuseal.multitenant? %>
<% if can?(:read, AccessToken) %>
<li>
<%= link_to 'API', settings_api_index_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %>
</li>
<% end %>
<% end %>
<% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %>
<% if can?(:read, WebhookUrl) %>
<li>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %>
</li>
<% end %>
<% end %>
<%= render 'shared/settings_nav_extra' %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<li>
<%= content_for(:pro_link) || link_to(Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3', data: { turbo: false }) do %>
<%= t('plans') %>
<span class="badge badge-warning"><%= t('pro') %></span>
<% end %>
</li>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<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 rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3', data: { turbo: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
</li>
<% if Docuseal.multitenant? %>
<li>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3', data: { turbo: false } do %>
<%= t('embedding') %>
<% end %>
</li>
<% end %>
<% if (!Docuseal.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base rounded-md hover:bg-base-200 transition-colors duration-150 py-2.5 px-3' %>
</li>
<% end %>
<%= render 'shared/settings_nav_extra2' %>
<% end %>
</ul>
</div>
</nav>
<% end %>
<span class="hidden sm:inline">
<%= render 'shared/github' if request.path.starts_with?('/settings') %>
</span>
@ -20,14 +127,13 @@
<% else %>
<div class="flex items-center justify-center space-x-4 mr-1">
<%= render 'shared/navbar_buttons' %>
<%= link_to t('settings'), settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg', id: 'account_settings_button' %>
</div>
<% end %>
<div class="dropdown dropdown-end">
<label tabindex="0" class="cursor-pointer bg-base-content text-purple-300 rounded-full p-2 w-9 justify-center flex">
<label tabindex="0" class="cursor-pointer bg-neutral text-neutral-content rounded-full p-2 w-9 h-9 justify-center items-center flex font-semibold text-sm transition-all duration-200 hover:opacity-90 active:scale-95">
<span class="text-sm align-text-top"><%= current_user.initials %></span>
</label>
<ul tabindex="0" class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box min-w-[160px] text-right">
<ul tabindex="0" class="z-10 dropdown-content p-1.5 mt-2 shadow-soft-lg menu text-base bg-base-100 rounded-lg min-w-[180px] text-right border border-base-300 animate-fade-in-up">
<li>
<%= link_to settings_profile_index_path, class: 'flex items-center' do %>
<%= svg_icon('adjustments', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
@ -99,4 +205,4 @@
<% end %>
</div>
<% end %>
</div>
</header>

@ -7,66 +7,66 @@
</li>
<li></li>
<li>
<%= link_to t('profile'), settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
<%= link_to t('profile'), settings_profile_index_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<li>
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
<%= link_to t('account'), settings_account_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<li>
<%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %>
<%= link_to t('email'), settings_email_index_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user %>
<li>
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
<%= link_to 'SMS', settings_sms_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<% end %>
<% end %>
<% if can?(:read, AccountConfig) %>
<li>
<%= link_to t('notifications'), settings_notifications_path, class: 'text-base hover:bg-base-300' %>
<%= link_to t('notifications'), settings_notifications_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
<li>
<%= link_to t('e_signature'), settings_esign_path, class: 'text-base hover:bg-base-300' %>
<%= link_to t('e_signature'), settings_esign_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<% end %>
<% if can?(:read, AccountConfig) %>
<li>
<%= link_to t('personalization'), settings_personalization_path, class: 'text-base hover:bg-base-300' %>
<%= link_to t('personalization'), settings_personalization_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<% end %>
<% if can?(:read, User) %>
<li>
<%= link_to t('users'), settings_users_path, class: 'text-base hover:bg-base-300' %>
<%= link_to t('users'), settings_users_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<% end %>
<%= render 'shared/settings_nav_extra' %>
<% if Docuseal.demo? || !Docuseal.multitenant? %>
<% if can?(:read, AccessToken) %>
<li>
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
<%= link_to 'API', settings_api_index_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<% end %>
<% end %>
<% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %>
<% if can?(:read, WebhookUrl) %>
<li>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<% end %>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<li>
<%= content_for(:pro_link) || link_to(Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { turbo: false }) do %>
<%= content_for(:pro_link) || link_to(Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5', data: { turbo: false }) do %>
<%= t('plans') %>
<span class="badge badge-warning"><%= t('pro') %></span>
<% end %>
@ -74,20 +74,20 @@
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<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 %>
<%= 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 rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5', data: { turbo: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
</li>
<% if Docuseal.multitenant? %>
<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 %>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5', data: { turbo: false } do %>
<%= t('embedding') %>
<% end %>
</li>
<% end %>
<% if (!Docuseal.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base rounded-lg hover:bg-base-200 transition-colors duration-150 py-2.5' %>
</li>
<% end %>
<%= render 'shared/settings_nav_extra2' %>

@ -1,18 +1,18 @@
<turbo-frame id="drawer">
<turbo-modal class="modal modal-open items-start !animate-none justify-end overflow-y-auto !justify-normal md:!justify-end" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="bg-base-100 min-h-screen max-h-screen relative w-full relative">
<div class="turbo-modal-backdrop absolute inset-0 cursor-pointer z-0" data-turbo-modal-close aria-hidden="true"></div>
<div class="bg-base-100 min-h-screen max-h-screen relative w-full relative z-10" data-turbo-modal-content>
<% if local_assigns[:title] %>
<div class="flex justify-between bg-base-100 py-2 px-4 items-center border-b pb-2 font-medium">
<span>
<%= local_assigns[:title] %>
</span>
<a href="#" class="text-xl" data-action="click:turbo-modal#close">&times;</a>
<button type="button" class="text-xl hover:opacity-70" data-turbo-modal-close aria-label="Close">&times;</button>
</div>
<% else %>
<span data-action="click:turbo-modal#close" class="absolute btn border border-base-100 bg-base-100 hover:bg-base-300 hover:border-base-300 btn-primary !text-base btn-sm btn-circle" style="left: -40px; top: 6px">
<button type="button" data-turbo-modal-close class="absolute btn border border-base-100 bg-base-100 hover:bg-base-300 hover:border-base-300 btn-primary !text-base btn-sm btn-circle" style="left: -40px; top: 6px" aria-label="Close">
&times;
</span>
</button>
<% end %>
<div class="w-screen md:w-[620px] overflow-y-auto" style="max-height: calc(100vh - <%= local_assigns[:title] ? '45px' : '0px' %>)">
<%= yield %>

@ -1,13 +1,13 @@
<turbo-frame id="modal">
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
<div class="turbo-modal-backdrop absolute inset-0 cursor-pointer z-0" data-turbo-modal-close aria-hidden="true"></div>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none relative z-10" data-turbo-modal-content>
<% if local_assigns[:title] %>
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
<%= local_assigns[:title] %>
</span>
<a href="#" class="text-xl" data-action="click:turbo-modal#close">&times;</a>
<button type="button" class="text-xl hover:opacity-70" data-turbo-modal-close aria-label="Close">&times;</button>
</div>
<% end %>
<div>

@ -1,13 +1,13 @@
<turbo-frame id="modal">
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto !justify-normal md:!justify-center" data-close-after-submit="<%= local_assigns.key?(:close_after_submit) ? local_assigns[:close_after_submit] : true %>">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="bg-base-100 min-h-screen max-h-screen md:min-h-fit md:mt-3 md:rounded-2xl relative w-full relative overflow-y-auto">
<div class="turbo-modal-backdrop absolute inset-0 cursor-pointer z-0" data-turbo-modal-close aria-hidden="true"></div>
<div class="bg-base-100 min-h-screen max-h-screen md:min-h-fit md:mt-3 md:rounded-2xl relative w-full relative overflow-y-auto z-10" data-turbo-modal-content>
<% if local_assigns[:title] %>
<div class="flex justify-between bg-base-100 py-2 px-5 items-center border-b pb-2 font-medium mt-0.5">
<span>
<%= local_assigns[:title] %>
</span>
<a href="#" class="text-xl" data-action="click:turbo-modal#close">&times;</a>
<button type="button" class="text-xl hover:opacity-70" data-turbo-modal-close aria-label="Close">&times;</button>
</div>
<% end %>
<div class="w-full md:w-[590px] overflow-y-auto" style="max-height: calc(100vh - 14px - 45px)">

@ -1,8 +1,6 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">SMS</h1>
<div class="max-w-4xl mx-auto space-y-8">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">SMS</h1>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<%= render 'placeholder' %>
</div>
<div class="w-0 md:w-52"></div>
</section>
</div>

@ -1,8 +1,6 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">SAML SSO</h1>
<div class="max-w-4xl mx-auto space-y-8">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">SAML SSO</h1>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<%= render 'placeholder' %>
</div>
<div class="w-0 md:w-52"></div>
</section>
</div>

@ -1,9 +1,9 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">
<div class="max-w-4xl mx-auto space-y-8">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">
<%= t('storage') %>
</h1>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 class="text-lg font-semibold text-base-content mb-5"><%= t('storage_provider', default: 'Storage Provider') %></h2>
<% value = @encrypted_config.value || { 'service' => 'disk' } %>
<% configs = value['configs'] || {} %>
<%= form_for @encrypted_config, url: settings_storage_index_path, method: :post, html: { autocomplete: 'off', class: 'w-full' } do |f| %>
@ -34,6 +34,5 @@
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
</div>
<% end %>
</div>
<div class="w-0 md:w-52"></div>
</section>
</div>

@ -10,8 +10,8 @@
</div>
<%= render 'templates/embedding', template: %>
<div class="mt-4">
<a href="#" class="link text-center block" data-action="click:turbo-modal#close">
<button type="button" class="link text-center block w-full" data-turbo-modal-close>
<%= t('close') %>
</a>
</button>
</div>
</div>

@ -1,8 +1,6 @@
<div class="flex-wrap space-y-4 md:flex md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="md:flex-grow">
<div class="flex flex-col md:flex-row md:flex-wrap gap-2 md:justify-between md:items-end mb-4 min-h-12">
<h1 class="text-4xl font-bold">
<div class="max-w-6xl mx-auto space-y-8">
<div class="flex flex-col md:flex-row md:flex-wrap gap-2 md:justify-between md:items-end min-h-12">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">
<% if params[:status] == 'archived' %>
<%= t('archived_users') %>
<% elsif params[:status] == 'integration' %>
@ -34,8 +32,9 @@
<% end %>
</div>
</div>
<section class="bg-base-100 rounded-xl border border-base-300 overflow-hidden shadow-soft">
<div class="overflow-x-auto">
<table class="table w-full table-lg rounded-b-none overflow-hidden">
<table class="table w-full table-lg overflow-hidden">
<thead class="bg-base-200">
<tr class="text-neutral uppercase">
<th>
@ -99,6 +98,7 @@
</tbody>
</table>
</div>
</section>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'users', left_additional_html: render('bottom_links') %>
<% else %>
@ -107,4 +107,3 @@
</div>
<% end %>
</div>
</div>

@ -40,7 +40,7 @@
<% end %>
<% else %>
<li class="ml-7">
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center btn-info bg-blue-50" style="left: -12px;">
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center btn-info bg-base-200" style="left: -12px;">
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %>
</span>
<p class="leading-none text-base-content/60 pt-1">

@ -8,7 +8,7 @@
<%= webhook_event.webhook_attempts.max_by(&:id)&.response_status_code if local_assigns[:with_status] %>
</div>
<% elsif webhook_event.status == 'pending' %>
<div class="btn btn-outline btn-xs btn-info bg-blue-50 gap-1">
<div class="btn btn-outline btn-xs btn-info bg-base-200 gap-1">
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0 stroke-2') %>
<%= webhook_event.webhook_attempts.max_by(&:id)&.response_status_code if local_assigns[:with_status] %>
</div>

@ -1,8 +1,6 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="flex-grow min-w-0">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
<h1 class="text-4xl font-bold">Webhooks</h1>
<div class="max-w-4xl mx-auto space-y-8">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">Webhooks</h1>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<%= render 'shared/test_mode_toggle' %>
<% if @webhook_url.persisted? %>
@ -15,8 +13,8 @@
</div>
<div class="space-y-4">
<% @webhook_urls.each do |webhook_url| %>
<%= link_to settings_webhook_path(webhook_url), class: 'card bg-base-200' do %>
<div class="card-body p-6 min-w-0">
<%= link_to settings_webhook_path(webhook_url), class: 'block bg-base-100 rounded-xl border border-base-300 p-6 shadow-soft hover:border-primary/30 transition-colors' do %>
<div class="min-w-0">
<p class="flex items-center space-x-1">
<%= svg_icon('world', class: 'w-6 h-6 shrink-0') %>
<span class="text-xl font-semibold truncate"><%= webhook_url.url %></span>
@ -31,4 +29,3 @@
<% end %>
</div>
</div>
</div>

@ -1,8 +1,6 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="flex-grow">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
<h1 class="text-4xl font-bold">Webhook</h1>
<div class="max-w-4xl mx-auto space-y-8">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-base-content">Webhook</h1>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<% if params[:action] == 'index' && (current_user == true_user || current_account.testing?) %>
<%= render 'shared/test_mode_toggle' %>
@ -15,8 +13,7 @@
<% end %>
</div>
</div>
<div class="card bg-base-200">
<div class="card-body p-6">
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end md:relative">
<%= label_tag :url, 'Webhook URL', class: 'text-sm font-semibold' %>
<% if @webhook_url.persisted? %>
@ -79,11 +76,10 @@
</div>
<% end %>
<% end %>
</div>
</div>
</section>
<% if @webhook_events.present? || params[:status].present? %>
<div class="mt-6">
<h2 id="log" class="text-3xl font-bold"><%= t('events_log') %></h2>
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<h2 id="log" class="text-lg font-semibold text-base-content mb-4"><%= t('events_log') %></h2>
<div class="tabs border-b mt-4">
<%= link_to t('all'), url_for(params.to_unsafe_h.except(:status)), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
<%= link_to t('succeeded'), url_for(params.to_unsafe_h.merge(status: 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
@ -122,10 +118,10 @@
</div>
</div>
<% end %>
</div>
</section>
<% elsif (submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last) && can?(:read, submitter) %>
<div class="space-y-4 mt-4">
<div class="collapse collapse-open bg-base-200 px-1">
<section class="bg-base-100 rounded-xl border border-base-300 p-6 md:p-8 shadow-soft">
<div class="collapse collapse-open bg-base-100 rounded-xl border border-base-300 shadow-soft px-1">
<div class="p-4 text-xl font-medium">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center md:h-8">
<span>
@ -145,7 +141,6 @@
</div>
</div>
</div>
</div>
</section>
<% end %>
</div>
</div>

@ -4,12 +4,16 @@ default: &default
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15).to_i + ENV.fetch('SIDEKIQ_THREADS', 5).to_i %>
development:
<<: *default
database: docuseal_dev
adapter: sqlite3
database: db/development.sqlite3
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15).to_i + ENV.fetch('SIDEKIQ_THREADS', 5).to_i %>
timeout: 5000
test:
<<: *default
database: docuseal_test
adapter: sqlite3
database: db/test.sqlite3
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15).to_i + ENV.fetch('SIDEKIQ_THREADS', 5).to_i %>
timeout: 5000
production:
<% if !ENV['DATABASE_HOST'].to_s.empty? %>

@ -65,6 +65,7 @@ Rails.application.routes.draw do
end
resource :user_signature, only: %i[edit update destroy]
resource :user_initials, only: %i[edit update destroy]
resource :account_logo, only: %i[update destroy], controller: 'account_logos'
resources :submissions_archived, only: %i[index], path: 'submissions/archived'
resources :submissions, only: %i[index], controller: 'submissions_dashboard'
resources :submissions, only: %i[show destroy] do
@ -75,6 +76,7 @@ Rails.application.routes.draw do
resources :console_redirect, only: %i[index]
resources :upgrade, only: %i[index], controller: 'console_redirect'
resources :manage, only: %i[index], controller: 'console_redirect'
get 'sign_up' => 'console_redirect#index'
resource :testing_account, only: %i[show destroy]
resources :testing_api_settings, only: %i[index]
resources :submitters_autocomplete, only: %i[index]

@ -11,14 +11,10 @@
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "plpgsql"
create_table "access_tokens", force: :cascade do |t|
t.bigint "user_id", null: false
t.integer "user_id", null: false
t.text "token", null: false
t.text "sha256", null: false
t.string "sha256", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["sha256"], name: "index_access_tokens_on_sha256", unique: true
@ -26,15 +22,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
end
create_table "account_accesses", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "user_id", null: false
t.integer "account_id", null: false
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "user_id"], name: "index_account_accesses_on_account_id_and_user_id", unique: true
end
create_table "account_configs", force: :cascade do |t|
t.bigint "account_id", null: false
t.integer "account_id", null: false
t.string "key", null: false
t.text "value", null: false
t.datetime "created_at", null: false
@ -44,8 +40,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
end
create_table "account_linked_accounts", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "linked_account_id", null: false
t.integer "account_id", null: false
t.integer "linked_account_id", null: false
t.text "account_type", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@ -119,16 +115,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
t.datetime "updated_at", null: false
t.string "verification_method"
t.boolean "is_first"
t.index ["account_id", "completed_at"], name: "index_completed_submitters_account_id_completed_at_is_first", where: "(is_first = true)"
t.index ["account_id", "completed_at"], name: "index_completed_submitters_account_id_completed_at_is_first", where: "is_first = TRUE"
t.index ["account_id", "completed_at"], name: "index_completed_submitters_on_account_id_and_completed_at"
t.index ["submission_id"], name: "index_completed_submitters_on_submission_id", unique: true, where: "(is_first = true)"
t.index ["submission_id"], name: "index_completed_submitters_on_submission_id", unique: true, where: "is_first = TRUE"
t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true
end
create_table "console1984_commands", force: :cascade do |t|
t.text "statements"
t.bigint "sensitive_access_id"
t.bigint "session_id", null: false
t.integer "sensitive_access_id"
t.integer "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"
@ -137,7 +133,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
create_table "console1984_sensitive_accesses", force: :cascade do |t|
t.text "justification"
t.bigint "session_id", null: false
t.integer "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"
@ -145,7 +141,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
create_table "console1984_sessions", force: :cascade do |t|
t.text "reason"
t.bigint "user_id", null: false
t.integer "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"
@ -160,18 +156,18 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
end
create_table "document_generation_events", force: :cascade do |t|
t.bigint "submitter_id", null: false
t.integer "submitter_id", null: false
t.string "event_name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["submitter_id", "event_name"], name: "index_document_generation_events_on_submitter_id_and_event_name", unique: true, where: "((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))"
t.index ["submitter_id", "event_name"], name: "index_document_generation_events_on_submitter_id_and_event_name", unique: true, where: "event_name IN ('start', 'complete')"
t.index ["submitter_id"], name: "index_document_generation_events_on_submitter_id"
end
create_table "email_events", force: :cascade do |t|
t.bigint "account_id", null: false
t.integer "account_id", null: false
t.string "emailable_type", null: false
t.bigint "emailable_id", null: false
t.integer "emailable_id", null: false
t.string "message_id", null: false
t.string "tag", null: false
t.string "event_type", null: false
@ -181,15 +177,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
t.datetime "created_at", null: false
t.index ["account_id", "event_datetime"], name: "index_email_events_on_account_id_and_event_datetime"
t.index ["email"], name: "index_email_events_on_email"
t.index ["email"], name: "index_email_events_on_email_event_types", where: "((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'permanent_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))"
t.index ["email"], name: "index_email_events_on_email_event_types", where: "event_type IN ('bounce', 'soft_bounce', 'permanent_bounce', 'complaint', 'soft_complaint')"
t.index ["emailable_type", "emailable_id"], name: "index_email_events_on_emailable"
t.index ["message_id"], name: "index_email_events_on_message_id"
end
create_table "email_messages", force: :cascade do |t|
t.string "uuid", null: false
t.bigint "author_id", null: false
t.bigint "account_id", null: false
t.integer "author_id", null: false
t.integer "account_id", null: false
t.text "subject", null: false
t.text "body", null: false
t.string "sha1", null: false
@ -201,7 +197,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
end
create_table "encrypted_configs", force: :cascade do |t|
t.bigint "account_id", null: false
t.integer "account_id", null: false
t.string "key", null: false
t.text "value", null: false
t.datetime "created_at", null: false
@ -211,7 +207,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
end
create_table "encrypted_user_configs", force: :cascade do |t|
t.bigint "user_id", null: false
t.integer "user_id", null: false
t.string "key", null: false
t.text "value", null: false
t.datetime "created_at", null: false
@ -225,13 +221,13 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
t.string "event_name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["event_name", "key"], name: "index_lock_events_on_event_name_and_key", unique: true, where: "((event_name)::text = ANY ((ARRAY['start'::character varying, 'complete'::character varying])::text[]))"
t.index ["event_name", "key"], name: "index_lock_events_on_event_name_and_key", unique: true, where: "event_name IN ('start', 'complete')"
t.index ["key"], name: "index_lock_events_on_key"
end
create_table "oauth_access_grants", force: :cascade do |t|
t.bigint "resource_owner_id", null: false
t.bigint "application_id", null: false
t.integer "resource_owner_id", null: false
t.integer "application_id", null: false
t.string "token", null: false
t.integer "expires_in", null: false
t.text "redirect_uri", null: false
@ -244,8 +240,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
end
create_table "oauth_access_tokens", force: :cascade do |t|
t.bigint "resource_owner_id"
t.bigint "application_id", null: false
t.integer "resource_owner_id"
t.integer "application_id", null: false
t.string "token", null: false
t.string "refresh_token"
t.integer "expires_in"
@ -271,33 +267,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
end
create_table "search_entries", force: :cascade do |t|
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "account_id", null: false
t.tsvector "tsvector", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.tsvector "ngram"
t.index ["account_id", "ngram"], name: "index_search_entries_on_account_id_ngram_submission", where: "((record_type)::text = 'Submission'::text)", using: :gin
t.index ["account_id", "ngram"], name: "index_search_entries_on_account_id_ngram_submitter", where: "((record_type)::text = 'Submitter'::text)", using: :gin
t.index ["account_id", "ngram"], name: "index_search_entries_on_account_id_ngram_template", where: "((record_type)::text = 'Template'::text)", using: :gin
t.index ["account_id", "tsvector"], name: "index_search_entries_on_account_id_tsvector_submission", where: "((record_type)::text = 'Submission'::text)", using: :gin
t.index ["account_id", "tsvector"], name: "index_search_entries_on_account_id_tsvector_submitter", where: "((record_type)::text = 'Submitter'::text)", using: :gin
t.index ["account_id", "tsvector"], name: "index_search_entries_on_account_id_tsvector_template", where: "((record_type)::text = 'Template'::text)", using: :gin
t.index ["record_id", "record_type"], name: "index_search_entries_on_record_id_and_record_type", unique: true
end
create_table "submission_events", force: :cascade do |t|
t.bigint "submission_id", null: false
t.bigint "submitter_id"
t.integer "submission_id", null: false
t.integer "submitter_id"
t.text "data", null: false
t.string "event_type", null: false
t.datetime "event_timestamp", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "account_id"
t.index ["account_id", "created_at"], name: "index_submissions_events_on_sms_event_types", where: "((event_type)::text = ANY ((ARRAY['send_sms'::character varying, 'send_2fa_sms'::character varying])::text[]))"
t.integer "account_id"
t.index ["account_id", "created_at"], name: "index_submissions_events_on_sms_event_types", where: "event_type IN ('send_sms', 'send_2fa_sms')"
t.index ["account_id"], name: "index_submission_events_on_account_id"
t.index ["created_at"], name: "index_submission_events_on_created_at"
t.index ["submission_id"], name: "index_submission_events_on_submission_id"
@ -305,8 +284,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
end
create_table "submissions", force: :cascade do |t|
t.bigint "template_id"
t.bigint "created_by_user_id"
t.integer "template_id"
t.integer "created_by_user_id"
t.datetime "archived_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@ -317,21 +296,21 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
t.string "submitters_order", null: false
t.string "slug", null: false
t.text "preferences", null: false
t.bigint "account_id", null: false
t.integer "account_id", null: false
t.datetime "expire_at"
t.text "name"
t.text "variables_schema"
t.text "variables"
t.index ["account_id", "id"], name: "index_submissions_on_account_id_and_id"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id", where: "(archived_at IS NULL)"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id_archived", where: "(archived_at IS NOT NULL)"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id", where: "archived_at IS NULL"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id_archived", where: "archived_at IS NOT NULL"
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
t.index ["slug"], name: "index_submissions_on_slug", unique: true
t.index ["template_id"], name: "index_submissions_on_template_id"
end
create_table "submitters", force: :cascade do |t|
t.bigint "submission_id", null: false
t.integer "submission_id", null: false
t.string "uuid", null: false
t.string "email"
t.string "slug", null: false
@ -348,7 +327,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
t.string "external_id"
t.text "preferences", null: false
t.text "metadata", null: false
t.bigint "account_id", null: false
t.integer "account_id", null: false
t.datetime "declined_at"
t.string "timezone"
t.index ["account_id", "id"], name: "index_submitters_on_account_id_and_id"
@ -360,8 +339,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
end
create_table "template_accesses", force: :cascade do |t|
t.bigint "template_id", null: false
t.bigint "user_id", null: false
t.integer "template_id", null: false
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["template_id", "user_id"], name: "index_template_accesses_on_template_id_and_user_id", unique: true
@ -369,20 +348,20 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
create_table "template_folders", force: :cascade do |t|
t.string "name", null: false
t.bigint "author_id", null: false
t.bigint "account_id", null: false
t.integer "author_id", null: false
t.integer "account_id", null: false
t.datetime "archived_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "parent_folder_id"
t.integer "parent_folder_id"
t.index ["account_id"], name: "index_template_folders_on_account_id"
t.index ["author_id"], name: "index_template_folders_on_author_id"
t.index ["parent_folder_id"], name: "index_template_folders_on_parent_folder_id"
end
create_table "template_sharings", force: :cascade do |t|
t.bigint "template_id", null: false
t.bigint "account_id", null: false
t.integer "template_id", null: false
t.integer "account_id", null: false
t.string "ability", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@ -396,19 +375,19 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
t.text "schema", null: false
t.text "fields", null: false
t.text "submitters", null: false
t.bigint "author_id", null: false
t.bigint "account_id", null: false
t.integer "author_id", null: false
t.integer "account_id", null: false
t.datetime "archived_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "source", null: false
t.bigint "folder_id", null: false
t.integer "folder_id", null: false
t.string "external_id"
t.text "preferences", null: false
t.boolean "shared_link", default: false, null: false
t.text "variables_schema"
t.index ["account_id", "folder_id", "id"], name: "index_templates_on_account_id_and_folder_id_and_id", where: "(archived_at IS NULL)"
t.index ["account_id", "id"], name: "index_templates_on_account_id_and_id_archived", where: "(archived_at IS NOT NULL)"
t.index ["account_id", "folder_id", "id"], name: "index_templates_on_account_id_and_folder_id_and_id", where: "archived_at IS NULL"
t.index ["account_id", "id"], name: "index_templates_on_account_id_and_id_archived", where: "archived_at IS NOT NULL"
t.index ["account_id"], name: "index_templates_on_account_id"
t.index ["author_id"], name: "index_templates_on_author_id"
t.index ["external_id"], name: "index_templates_on_external_id"
@ -417,7 +396,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
end
create_table "user_configs", force: :cascade do |t|
t.bigint "user_id", null: false
t.integer "user_id", null: false
t.string "key", null: false
t.text "value", null: false
t.datetime "created_at", null: false
@ -432,7 +411,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
t.string "email", null: false
t.string "role", null: false
t.string "encrypted_password", null: false
t.bigint "account_id", null: false
t.integer "account_id", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
@ -483,12 +462,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["uuid", "webhook_url_id"], name: "index_webhook_events_on_uuid_and_webhook_url_id", unique: true
t.index ["webhook_url_id", "id"], name: "index_webhook_events_error", where: "((status)::text = 'error'::text)"
t.index ["webhook_url_id", "id"], name: "index_webhook_events_error", where: "status = 'error'"
t.index ["webhook_url_id", "id"], name: "index_webhook_events_on_webhook_url_id_and_id"
end
create_table "webhook_urls", force: :cascade do |t|
t.bigint "account_id", null: false
t.integer "account_id", null: false
t.text "url", null: false
t.text "events", null: false
t.string "sha1", null: false

@ -0,0 +1,2 @@
DATABASE_URL= # keep empty to use sqlite or specify postgresql database URL
SECRET_KEY_BASE=001f0f350ae2b0d7898d11cd9fd25cc2f193990470d9fd4b7feafa7d363794c55902d56ebd75a079f75e57b9706388ce9279f1db2153a0a63588bb867cb7371c

@ -62,7 +62,8 @@ module Docuseal
end
def advanced_formats?
multitenant?
# Enable advanced formats (DOCX, etc.) in development or if multitenant
Rails.env.development? || multitenant?
end
def demo?

@ -8,6 +8,8 @@ module RateLimit
module_function
def call(key, limit:, ttl:, enabled: Docuseal.multitenant?)
# Disable rate limiting in development
return true if Rails.env.development?
return true unless enabled
value = STORE.increment(key, 1, expires_in: ttl)

@ -196,6 +196,8 @@ module Submissions
with_submitter_timezone:,
with_file_links:,
with_signature_id_reason:)
rasterize_redacted_pages(submitter.submission, pdfs_index)
end
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
@ -269,7 +271,7 @@ module Submissions
layouter = HexaPDF::Layout::TextLayouter.new(text_valign:, text_align:, font:, font_size:)
next if Array.wrap(value).compact_blank.blank?
next if Array.wrap(value).compact_blank.blank? && field['type'] != 'redact'
if is_flatten
begin
@ -632,6 +634,15 @@ module Submissions
c.stroke
end
end
when 'redact'
area_x = area['x'] * width
area_y = area['y'] * height
area_w = area['w'] * width
area_h = area['h'] * height
canvas.fill_color('black')
.rectangle(area_x, height - area_y - area_h, area_w, area_h)
.fill
else
if field['type'] == 'date'
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), locale)
@ -905,6 +916,81 @@ module Submissions
HexaPDF::Document.new(io:)
end
REDACT_RENDER_SCALE = 2
def pages_with_redactions(submission)
fields = submission.template_fields || submission.template.fields
acc = Hash.new { |h, k| h[k] = [] }
fields.each do |field|
next unless field['type'] == 'redact'
field.fetch('areas', []).each do |area|
next unless area['attachment_uuid'].present? && area['page'].present?
acc[area['attachment_uuid']] << area['page']
end
end
acc.transform_values { |a| a.uniq.sort.reverse }
end
def rasterize_redacted_pages(submission, pdfs_index)
redacted_by_uuid = pages_with_redactions(submission)
pdfs_index.each_key do |attachment_uuid|
page_indices = redacted_by_uuid[attachment_uuid]
next if page_indices.blank?
pdf = pdfs_index[attachment_uuid]
next unless pdf.is_a?(HexaPDF::Document)
pdfs_index[attachment_uuid] = build_pdf_with_rasterized_redactions(pdf, page_indices)
end
end
def build_pdf_with_rasterized_redactions(original_pdf, redacted_page_indices)
redacted_set = redacted_page_indices.to_set
io = StringIO.new
original_pdf.write(io, incremental: false, validate: false)
pdf_bytes = io.string
result = HexaPDF::Document.new
original_pdf.pages.each_with_index do |page, page_index|
page_to_add = if redacted_set.include?(page_index)
rasterized_page_from_pdf_bytes(pdf_bytes, page_index, page.box.width, page.box.height)
end
page_to_add ||= page
result.pages.insert(page_index, result.import(page_to_add))
end
result
rescue Pdfium::PdfiumError, Vips::Error => e
Rollbar.error(e) if defined?(Rollbar)
original_pdf
end
def rasterized_page_from_pdf_bytes(pdf_bytes, page_index, page_width_pt, page_height_pt)
pdfium_doc = Pdfium::Document.open_bytes(pdf_bytes)
pdfium_page = pdfium_doc.get_page(page_index)
data, render_width, render_height = pdfium_page.render_to_bitmap(scale: REDACT_RENDER_SCALE)
pdfium_page.close
pdfium_doc.close
vips_image = Vips::Image.new_from_memory(data, render_width, render_height, 4, :uchar)
vips_image = vips_image.copy(interpretation: :srgb)
png_data = vips_image.write_to_buffer('.png')
new_doc = HexaPDF::Document.new
new_page = new_doc.pages.add
new_page.box.width = page_width_pt
new_page.box.height = page_height_pt
new_page.canvas.image(
StringIO.new(png_data),
at: [0, 0],
width: page_width_pt,
height: page_height_pt
)
new_page
end
def sign_reason(name)
format(SIGN_REASON, name:)
end

@ -106,8 +106,14 @@ module Submitters
end
end
def load_logo(_submitter)
def load_logo(submitter)
# Use account logo if available, otherwise use default
account = submitter.submission.account
if account.logo.attached?
StringIO.new(account.logo.download)
else
PdfIcons.stamp_logo_io
end
end
end
end

@ -29,10 +29,12 @@ module Templates
end
end
def handle_pdf_or_image(template, file, document_data = nil, params = {}, extract_fields: false)
def handle_pdf_or_image(template, file, document_data = nil, params = {}, extract_fields: false, content_type_override: nil, filename_override: nil)
document_data ||= file.read
content_type = content_type_override || file.content_type
filename = filename_override || file.original_filename
if file.content_type == PDF_CONTENT_TYPE
if content_type == PDF_CONTENT_TYPE
document_data = maybe_decrypt_pdf_or_raise(document_data, params)
annotations =
@ -43,13 +45,13 @@ module Templates
blob = ActiveStorage::Blob.create_and_upload!(
io: StringIO.new(document_data),
filename: file.original_filename,
filename: filename,
metadata: {
identified: file.content_type == PDF_CONTENT_TYPE,
analyzed: file.content_type == PDF_CONTENT_TYPE,
identified: content_type == PDF_CONTENT_TYPE,
analyzed: content_type == PDF_CONTENT_TYPE,
pdf: { annotations: }.compact_blank, sha256:
}.compact_blank,
content_type: file.content_type
content_type: content_type
)
document = template.documents.create!(blob:)
@ -106,7 +108,65 @@ module Templates
return handle_pdf_or_image(template, file, file.read, params, extract_fields:)
end
# Handle document types (DOCX, DOC, XLSX, etc.) by converting to PDF
if DOCUMENT_CONTENT_TYPES.include?(file.content_type)
pdf_data = convert_document_to_pdf(file)
if pdf_data
# Process the converted PDF with PDF content type and filename
pdf_filename = File.basename(file.original_filename, '.*') + '.pdf'
return handle_pdf_or_image(template, file, pdf_data, params, extract_fields: extract_fields, content_type_override: PDF_CONTENT_TYPE, filename_override: pdf_filename)
else
raise InvalidFileType, "Unable to convert #{file.content_type} to PDF. Please install LibreOffice (brew install --cask libreoffice on macOS or apt-get install libreoffice on Linux) or convert the document to PDF manually."
end
end
raise InvalidFileType, file.content_type
end
def convert_document_to_pdf(file)
# Try to use LibreOffice to convert document to PDF
libreoffice_path = find_libreoffice
return nil unless libreoffice_path
# Create a temporary file for the input document
input_temp = Tempfile.new(['input', File.extname(file.original_filename)])
input_temp.binmode
file.rewind
input_temp.write(file.read)
input_temp.close
output_dir = Dir.mktmpdir
output_file = File.join(output_dir, File.basename(file.original_filename, '.*') + '.pdf')
begin
# Use LibreOffice headless mode to convert to PDF
success = system(libreoffice_path, '--headless', '--convert-to', 'pdf', '--outdir', output_dir, input_temp.path, out: File::NULL, err: File::NULL)
if success && File.exist?(output_file)
pdf_data = File.binread(output_file)
return pdf_data
end
rescue StandardError => e
Rails.logger.warn("Document conversion failed: #{e.message}")
ensure
input_temp.unlink if input_temp
FileUtils.rm_rf(output_dir) if Dir.exist?(output_dir)
end
nil
end
def find_libreoffice
# Check common LibreOffice installation paths
paths = [
'/Applications/LibreOffice.app/Contents/MacOS/soffice', # macOS
'/usr/bin/libreoffice', # Linux
'/usr/local/bin/libreoffice', # Linux alternative
`which libreoffice`.strip, # System PATH
`which soffice`.strip # Alternative command name
].compact.reject(&:empty?)
paths.find { |path| File.executable?(path) }
end
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

@ -1,4 +1,32 @@
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif']
},
animation: {
'fade-in': 'fadeIn 0.2s ease-out',
'fade-in-up': 'fadeInUp 0.25s ease-out'
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' }
},
fadeInUp: {
'0%': { opacity: '0', transform: 'translateY(4px)' },
'100%': { opacity: '1', transform: 'translateY(0)' }
}
},
boxShadow: {
'soft': '0 2px 8px rgba(38, 56, 84, 0.06)',
'soft-lg': '0 4px 16px rgba(38, 56, 84, 0.08)',
'soft-xl': '0 8px 24px rgba(38, 56, 84, 0.1)',
'focus-ring': '0 0 0 3px rgba(31, 224, 179, 0.25)',
'focus-ring-neutral': '0 0 0 3px rgba(38, 56, 84, 0.15)'
}
}
},
plugins: [
require('daisyui')
],
@ -7,17 +35,24 @@ module.exports = {
{
docuseal: {
'color-scheme': 'light',
primary: '#e4e0e1',
secondary: '#ef9fbc',
accent: '#eeaf3a',
neutral: '#291334',
'base-100': '#faf7f5',
'base-200': '#efeae6',
'base-300': '#e7e2df',
'base-content': '#291334',
'--rounded-btn': '1.9rem',
primary: '#1FE0B3',
'primary-content': '#263854',
secondary: '#54B0E8',
'secondary-content': '#FFFFFF',
accent: '#4E87C8',
'accent-content': '#FFFFFF',
neutral: '#263854',
'neutral-content': '#FFFFFF',
'base-100': '#FFFFFF',
'base-200': '#f0f4f8',
'base-300': '#e2e8f0',
'base-content': '#263854',
info: '#54B0E8',
'info-content': '#FFFFFF',
'--rounded-btn': '0.5rem',
'--tab-border': '2px',
'--tab-radius': '.5rem'
'--tab-radius': '.5rem',
'--rounded-box': '0.75rem'
}
}
]

Loading…
Cancel
Save