mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 15:25:16 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c1123fef63 | |||
| 4dc6149530 | |||
| 1245ff2cce | |||
| 284204fd78 | |||
| f7be74eb73 | |||
| aeea619059 | |||
| ea2b7f20e7 | |||
| f40f1d25de | |||
| c04bb2d7cf | |||
| 4c1ccd65bf | |||
| 54e064b0fc | |||
| 9a8b72883c | |||
| 5d75ee2e47 | |||
| 7357fa4871 | |||
| f6850b5427 | |||
| 1e9a181e60 | |||
| 7224a1ccec | |||
| 9b935a8180 | |||
| c4a693846e | |||
| 02c7cdcd97 | |||
| 36bee92e8e | |||
| 6542804f44 | |||
| d805fd614c |
@@ -15,6 +15,7 @@ class AccountConfigsController < ApplicationController
|
||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
|
||||
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
||||
|
||||
@@ -13,7 +13,7 @@ module Api
|
||||
def show
|
||||
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||
|
||||
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
||||
if blob_uuid.blank? || purp != 'blob'
|
||||
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
@@ -24,8 +24,9 @@ module Api
|
||||
attachment = blob.attachments.take
|
||||
|
||||
@record = attachment.record
|
||||
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
|
||||
|
||||
authorization_check!(attachment) if exp.blank?
|
||||
authorization_check!(attachment, @record, exp)
|
||||
|
||||
if request.headers['Range'].present?
|
||||
send_blob_byte_range_data blob, request.headers['Range']
|
||||
@@ -41,16 +42,22 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def authorization_check!(attachment)
|
||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
||||
!attachment.record.account.account_configs
|
||||
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||
def authorization_check!(attachment, record, exp)
|
||||
return if attachment.name == 'logo'
|
||||
return if exp.to_i >= Time.current.to_i
|
||||
return if current_user && current_ability.can?(:read, record)
|
||||
|
||||
return if is_authorized
|
||||
if exp.blank?
|
||||
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
|
||||
|
||||
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
|
||||
require_auth = configs.any? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_AUTH_KEY && c.value }
|
||||
require_ttl = configs.none? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY && c.value == false }
|
||||
|
||||
return if !require_ttl && !require_auth
|
||||
end
|
||||
|
||||
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
|
||||
|
||||
raise CanCan::AccessDenied
|
||||
end
|
||||
|
||||
@@ -18,12 +18,14 @@ module Api
|
||||
field: :completed_at
|
||||
)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submitters.map do |s|
|
||||
{
|
||||
event_type: 'form.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submitters::SerializeForWebhook.call(s)
|
||||
data: Submitters::SerializeForWebhook.call(s, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
|
||||
@@ -34,10 +34,12 @@ module Api
|
||||
associations: [:blob]
|
||||
).call
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
id: @submission.id,
|
||||
documents: documents.map do |attachment|
|
||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
@@ -14,16 +14,19 @@ module Api
|
||||
:created_by_user, :submission_events,
|
||||
template: :folder,
|
||||
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
||||
audit_trail_attachment: :blob
|
||||
audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob
|
||||
),
|
||||
field: :completed_at)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submissions.map do |s|
|
||||
{
|
||||
event_type: 'submission.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submissions::SerializeForApi.call(s, s.submitters)
|
||||
data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
|
||||
@@ -18,10 +18,12 @@ module Api
|
||||
combined_document_attachment: :blob,
|
||||
audit_trail_attachment: :blob))
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submissions.map do |s|
|
||||
Submissions::SerializeForApi.call(s, s.submitters, params,
|
||||
with_events: false, with_documents: false, with_values: false)
|
||||
with_events: false, with_documents: false, with_values: false, expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: submissions.size,
|
||||
|
||||
@@ -14,9 +14,11 @@ module Api
|
||||
documents_attachments: :blob, attachments_attachments: :blob)
|
||||
)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submitters.map do |s|
|
||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
|
||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:, expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: submitters.size,
|
||||
|
||||
@@ -26,13 +26,15 @@ module Api
|
||||
original_template: @template,
|
||||
documents: params[:documents])
|
||||
|
||||
Templates.maybe_assign_access(cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||
|
||||
SearchEntries.enqueue_reindex(cloned_template)
|
||||
|
||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,13 +24,14 @@ module Api
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: templates.map do |t|
|
||||
Templates::SerializeForApi.call(
|
||||
t,
|
||||
schema_documents.select { |e| e.record_id == t.id },
|
||||
preview_image_attachments
|
||||
)
|
||||
Templates::SerializeForApi.call(t,
|
||||
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||
preview_image_attachments:,
|
||||
expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: templates.size,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PasswordsController < Devise::PasswordsController
|
||||
# rubocop:disable Rails/LexicallyScopedActionFilter
|
||||
skip_before_action :require_no_authentication, only: %i[edit update]
|
||||
# rubocop:enable Rails/LexicallyScopedActionFilter
|
||||
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :user
|
||||
end
|
||||
@@ -16,4 +20,10 @@ class PasswordsController < Devise::PasswordsController
|
||||
Current.user = resource
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_resetting_password_path_for(_)
|
||||
new_session_path(resource_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ class ProfileController < ApplicationController
|
||||
end
|
||||
|
||||
def update_password
|
||||
if current_user.update(password_params)
|
||||
if current_user.update_with_password(password_params)
|
||||
bypass_sign_in(current_user)
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
||||
else
|
||||
@@ -31,6 +31,6 @@ class ProfileController < ApplicationController
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
params.require(:user).permit(:password, :password_confirmation, :current_password)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RevealAccessTokenController < ApplicationController
|
||||
def show
|
||||
authorize!(:manage, current_user.access_token)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:manage, current_user.access_token)
|
||||
|
||||
if current_user.valid_password?(params[:password])
|
||||
render turbo_stream: turbo_stream.replace(:access_token_container,
|
||||
partial: 'reveal_access_token/access_token',
|
||||
locals: { token: current_user.access_token.token })
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||
locals: { error_message: I18n.t('wrong_password') }),
|
||||
status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -10,11 +10,16 @@ class SubmissionsExportController < ApplicationController
|
||||
attachments_attachments: :blob })
|
||||
.order(id: :asc)
|
||||
|
||||
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
if params[:format] == 'csv'
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||
filename: "#{@template.name}.csv"
|
||||
elsif params[:format] == 'xlsx'
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||
filename: "#{@template.name}.xlsx"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,6 +17,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
||||
|
||||
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||
|
||||
Templates.maybe_assign_access(cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
||||
|
||||
@@ -69,6 +69,8 @@ class TemplatesController < ApplicationController
|
||||
@template.account = current_account
|
||||
end
|
||||
|
||||
Templates.maybe_assign_access(@template)
|
||||
|
||||
if @template.save
|
||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ class TemplatesUploadsController < ApplicationController
|
||||
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||
|
||||
Templates.maybe_assign_access(template)
|
||||
|
||||
template.save!
|
||||
|
||||
template
|
||||
|
||||
@@ -30,6 +30,7 @@ class UsersController < ApplicationController
|
||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||
end
|
||||
|
||||
@user.password = SecureRandom.hex if @user.password.blank?
|
||||
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
||||
|
||||
if @user.save
|
||||
@@ -54,7 +55,7 @@ class UsersController < ApplicationController
|
||||
@user.account = account
|
||||
end
|
||||
|
||||
if @user.update(attrs.except(current_user == @user ? :role : nil))
|
||||
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
|
||||
@@ -83,7 +84,7 @@ class UsersController < ApplicationController
|
||||
|
||||
def user_params
|
||||
if params.key?(:user)
|
||||
permitted_params = %i[email first_name last_name password archived_at]
|
||||
permitted_params = %i[email first_name last_name password archived_at otp_required_for_login]
|
||||
|
||||
permitted_params << :role if role_valid?(params.dig(:user, :role))
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UsersSendResetPasswordController < ApplicationController
|
||||
load_and_authorize_resource :user
|
||||
|
||||
LIMIT_DURATION = 10.minutes
|
||||
|
||||
def update
|
||||
authorize!(:manage, @user)
|
||||
|
||||
if @user.reset_password_sent_at && @user.reset_password_sent_at > LIMIT_DURATION.ago
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('email_has_been_sent_already')
|
||||
else
|
||||
@user.send_reset_password_instructions
|
||||
|
||||
redirect_back fallback_location: settings_users_path,
|
||||
notice: I18n.t('an_email_with_password_reset_instructions_has_been_sent')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1015,7 +1015,11 @@ export default {
|
||||
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
|
||||
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
|
||||
|
||||
return sortArea(aArea, bArea)
|
||||
if (aArea && bArea) {
|
||||
return sortArea(aArea, bArea)
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,15 @@ export default {
|
||||
return ['UL', 'I', 'EM', 'B', 'STRONG', 'P']
|
||||
},
|
||||
dom () {
|
||||
const text = this.string.replace(/(?<!\(\s*)(https?:\/\/[^\s)]+)(?!\s*\))/g, (url) => `[${url}](${url})`)
|
||||
const linkParts = this.string.split(/(https?:\/\/[^\s)]+)/g)
|
||||
|
||||
const text = linkParts.map((part, index) => {
|
||||
if (part.match(/^https?:\/\//) && !linkParts[index - 1]?.match(/\(\s*$/) && !linkParts[index + 1]?.match(/^\s*\)/)) {
|
||||
return `[${part}](${part})`
|
||||
} else {
|
||||
return part
|
||||
}
|
||||
}).join('')
|
||||
|
||||
return new DOMParser().parseFromString(snarkdown(text.replace(/\n/g, '<br>')), 'text/html')
|
||||
}
|
||||
|
||||
@@ -1265,24 +1265,31 @@ export default {
|
||||
if (!field.areas.length) {
|
||||
this.template.fields.splice(this.template.fields.indexOf(field), 1)
|
||||
|
||||
this.template.fields.forEach((f) => {
|
||||
(f.conditions || []).forEach((c) => {
|
||||
this.removeFieldConditions(field)
|
||||
}
|
||||
|
||||
this.save()
|
||||
},
|
||||
removeFieldConditions (field) {
|
||||
this.template.fields.forEach((f) => {
|
||||
if (f.conditions) {
|
||||
f.conditions.forEach((c) => {
|
||||
if (c.field_uuid === field.uuid) {
|
||||
f.conditions.splice(f.conditions.indexOf(c), 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.template.schema.forEach((item) => {
|
||||
(item.conditions || []).forEach((c) => {
|
||||
this.template.schema.forEach((item) => {
|
||||
if (item.conditions) {
|
||||
item.conditions.forEach((c) => {
|
||||
if (c.field_uuid === field.uuid) {
|
||||
item.conditions.splice(item.conditions.indexOf(c), 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.save()
|
||||
}
|
||||
})
|
||||
},
|
||||
pasteField () {
|
||||
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
|
||||
@@ -1715,8 +1722,15 @@ export default {
|
||||
})
|
||||
})
|
||||
|
||||
this.template.fields =
|
||||
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
|
||||
this.template.fields = this.template.fields.reduce((acc, f) => {
|
||||
if (removedFieldUuids.includes(f.uuid) && !f.areas?.length) {
|
||||
this.removeFieldConditions(f)
|
||||
} else {
|
||||
acc.push(f)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
this.save()
|
||||
}
|
||||
|
||||
@@ -38,12 +38,19 @@ class ProcessSubmitterCompletionJob
|
||||
|
||||
submission = submitter.submission
|
||||
|
||||
complete_verification_events, sms_events =
|
||||
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification])
|
||||
.partition { |e| e.event_type == 'complete_verification' }
|
||||
|
||||
complete_verification_event = complete_verification_events.first
|
||||
|
||||
completed_submitter.assign_attributes(
|
||||
submission_id: submitter.submission_id,
|
||||
account_id: submission.account_id,
|
||||
template_id: submission.template_id,
|
||||
source: submission.source,
|
||||
sms_count: submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count,
|
||||
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
|
||||
verification_method: complete_verification_event&.data&.dig('method'),
|
||||
completed_at: submitter.completed_at
|
||||
)
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ class AccountConfig < ApplicationRecord
|
||||
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
|
||||
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
||||
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
|
||||
DOWNLOAD_LINKS_EXPIRE_KEY = 'download_links_expire'
|
||||
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
|
||||
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
|
||||
WITH_SIGNATURE_ID = 'with_signature_id'
|
||||
|
||||
@@ -4,16 +4,17 @@
|
||||
#
|
||||
# Table name: completed_submitters
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# completed_at :datetime not null
|
||||
# sms_count :integer not null
|
||||
# source :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# submission_id :bigint not null
|
||||
# submitter_id :bigint not null
|
||||
# template_id :bigint
|
||||
# id :bigint not null, primary key
|
||||
# completed_at :datetime not null
|
||||
# sms_count :integer not null
|
||||
# source :string not null
|
||||
# verification_method :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# submission_id :bigint not null
|
||||
# submitter_id :bigint not null
|
||||
# template_id :bigint
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
@@ -29,5 +30,5 @@ class CompletedSubmitter < ApplicationRecord
|
||||
has_many :completed_documents, dependent: :destroy,
|
||||
primary_key: :submitter_id,
|
||||
foreign_key: :submitter_id,
|
||||
inverse_of: :submitter
|
||||
inverse_of: :completed_submitter
|
||||
end
|
||||
|
||||
@@ -114,16 +114,16 @@ class Submission < ApplicationRecord
|
||||
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
|
||||
end
|
||||
|
||||
def audit_trail_url
|
||||
def audit_trail_url(expires_at: nil)
|
||||
return if audit_trail.blank?
|
||||
|
||||
ActiveStorage::Blob.proxy_url(audit_trail.blob)
|
||||
ActiveStorage::Blob.proxy_url(audit_trail.blob, expires_at:)
|
||||
end
|
||||
alias audit_log_url audit_trail_url
|
||||
|
||||
def combined_document_url
|
||||
def combined_document_url(expires_at: nil)
|
||||
return if combined_document.blank?
|
||||
|
||||
ActiveStorage::Blob.proxy_url(combined_document.blob)
|
||||
ActiveStorage::Blob.proxy_url(combined_document.blob, expires_at:)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -130,6 +130,18 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY) %>
|
||||
<% if can?(:manage, account_config) %>
|
||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||
<%= f.hidden_field :key %>
|
||||
<div class="flex items-center justify-between py-2.5">
|
||||
<span>
|
||||
<%= t('expirable_file_download_links') %>
|
||||
</span>
|
||||
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY) %>
|
||||
<% if can?(:manage, account_config) %>
|
||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||
|
||||
@@ -11,10 +11,21 @@
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex w-full space-x-4">
|
||||
<% token = current_user.access_token.token %>
|
||||
<masked-input class="block w-full" data-token="<%= token %>">
|
||||
<input id="api_key" type="text" value="<%= token.sub(token[5..], '*' * token[5..].size) %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
</masked-input>
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
<% obscured_token = current_user.access_token.token.sub(token[5..], '*' * token[5..].size) %>
|
||||
<% if current_account.testing? %>
|
||||
<masked-input class="block w-full" data-token="<%= token %>">
|
||||
<input id="api_key" type="text" value="<%= obscured_token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
</masked-input>
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
<% else %>
|
||||
<a id="access_token_container" href="<%= settings_reveal_access_token_path %>" data-turbo-frame="modal" class="flex w-full space-x-4">
|
||||
<input id="api_key" type="text" value="<%= obscured_token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
<div class="base-button">
|
||||
<%= svg_icon('copy', class: 'w-6 h-6 text-white') %>
|
||||
<span class="hidden md:inline"><%= t('copy') %></span>
|
||||
</div>
|
||||
</a>
|
||||
<% end %>
|
||||
</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>
|
||||
|
||||
@@ -54,19 +54,29 @@
|
||||
<p class="text-2xl font-bold mt-8 mb-4">
|
||||
<%= t('change_password') %>
|
||||
</p>
|
||||
<%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||
<div class="form-control">
|
||||
<%= f.label :password, t('new_password'), class: 'label' %>
|
||||
<%= f.password_field :password, autocomplete: 'off', class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.label :password_confirmation, t('confirm_password'), class: 'label' %>
|
||||
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control pt-2">
|
||||
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
|
||||
<%= 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 %>
|
||||
<div class="<%= 'peer-invalid:hidden' if current_user.errors.blank? %> space-y-4 mt-4">
|
||||
<div class="form-control">
|
||||
<%= f.label :password_confirmation, t('confirm_password'), class: 'label' %>
|
||||
<%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.label :current_password, t('current_password'), class: 'label' %>
|
||||
<%= f.password_field :current_password, autocomplete: 'current-password', class: 'base-input' %>
|
||||
<% if Accounts.can_send_emails?(current_account) %>
|
||||
<span class="label-text-alt mt-1">
|
||||
<%= t('dont_remember_your_current_password_click_here_to_reset_it_html') %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<input id="api_key" type="text" value="<%= token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
@@ -0,0 +1,14 @@
|
||||
<%= render 'shared/turbo_modal', title: t('reveal_api_key') do %>
|
||||
<%= form_tag settings_reveal_access_token_path, enctype: 'multipart/form-data', data: { turbo_frame: :_top } do %>
|
||||
<div class="form-control">
|
||||
<%= label_tag :password, t('enter_your_password_to_reveal_the_api_key'), class: 'label' %>
|
||||
<%= password_field_tag :password, nil, class: 'base-input', autocomplete: 'current-password', required: true, autofocus: true, placeholder: t('password') %>
|
||||
<% if local_assigns[:error_message].present? %>
|
||||
<span class="label-text-alt text-red-400 mt-1"><%= local_assigns[:error_message] %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form-control mt-4">
|
||||
<%= submit_tag t('submit'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -56,6 +56,8 @@
|
||||
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
|
||||
</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' %>
|
||||
@@ -70,7 +72,7 @@
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
|
||||
<% 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: { prefetch: false } do %>
|
||||
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
<%= render 'submissions/annotation', annot: %>
|
||||
<% end %>
|
||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||
<% value = values[field['uuid']].presence || (field['default_value'] != '{{date}}' && field['readonly'] == true && field['conditions'].blank? && field['default_value'].present? ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||
<% value ||= field['default_value'] if field['type'] == 'heading' %>
|
||||
<% next if value.blank? %>
|
||||
<% submitter = submitters_index[field['submitter_uuid']] %>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<% filter_params = params.permit(:q, *Submissions::Filter::ALLOWED_PARAMS) %>
|
||||
<%= render 'shared/turbo_modal', title: t('export'), close_after_submit: false do %>
|
||||
<div class="space-y-2">
|
||||
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx }, method: :get, data: { turbo_frame: :_top } do %>
|
||||
<%= button_to template_submissions_export_index_path(@template), params: { format: :xlsx, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>
|
||||
<div class="flex items-center p-4 text-left rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
|
||||
<div class="enabled">
|
||||
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
|
||||
@@ -14,7 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= button_to template_submissions_export_index_path(@template), params: { format: :csv }, method: :get, data: { turbo_frame: :_top } do %>
|
||||
<%= button_to template_submissions_export_index_path(@template), params: { format: :csv, **filter_params }, method: :get, data: { turbo_frame: :_top } do %>
|
||||
<div class="flex items-center text-left p-4 rounded-2xl border border-neutral-300 hover:cursor-pointer hover:bg-neutral hover:text-gray-300">
|
||||
<div class="enabled">
|
||||
<%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<% if params[:q].present? || params[:status].present? || filter_params.present? || @pagy.pages > 1 %>
|
||||
<%= render 'shared/search_input', title_selector: 'h2' %>
|
||||
<% end %>
|
||||
<%= link_to new_template_submissions_export_path(@template), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
|
||||
<%= link_to new_template_submissions_export_path(@template, params.permit(:q, *Submissions::Filter::ALLOWED_PARAMS)), class: 'hidden md:flex btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('download', class: 'w-6 h-6 stroke-2') %>
|
||||
<span><%= t('export') %></span>
|
||||
<% end %>
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
<%= form_for user, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
|
||||
<div class="space-y-2">
|
||||
<div class="form-control">
|
||||
<%= f.label :first_name, t('first_name'), class: 'label' %>
|
||||
<%= f.text_field :first_name, required: true, class: 'base-input', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.label :last_name, t('last_name'), class: 'label' %>
|
||||
<%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %>
|
||||
<div class="flex space-x-4">
|
||||
<div class="w-full">
|
||||
<%= f.label :first_name, t('first_name'), class: 'label' %>
|
||||
<%= f.text_field :first_name, required: true, class: 'base-input w-full', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<%= f.label :last_name, t('last_name'), class: 'label' %>
|
||||
<%= f.text_field :last_name, required: true, class: 'base-input w-full', dir: 'auto' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.label :email, t('email'), class: 'label' %>
|
||||
<%= f.email_field :email, required: true, class: 'base-input' %>
|
||||
<% if user.persisted? && Accounts.can_send_emails?(current_account) %>
|
||||
<span class="label-text-alt mt-2 mx-1">
|
||||
<%= t('click_here_to_send_a_reset_password_email_html') %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.label :password, t('password'), class: 'label' %>
|
||||
<%= f.password_field :password, required: user.new_record?, class: 'base-input' %>
|
||||
</div>
|
||||
<% if user.new_record? && !Docuseal.multitenant? %>
|
||||
<div class="form-control">
|
||||
<%= f.label :password, t('password'), class: 'label' %>
|
||||
<%= f.password_field :password, class: 'base-input' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if f.object != current_user %>
|
||||
<% if user.otp_required_for_login %>
|
||||
<div class="form-control">
|
||||
<%= f.label :otp_required_for_login, t('two_factor_authentication'), class: 'label' %>
|
||||
<%= f.select :otp_required_for_login, [[t('enabled'), true], [t('disabled'), false]], { include_blank: false }, class: 'base-select' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render 'role_select', f: %>
|
||||
<% end %>
|
||||
<% if local_assigns[:extra_fields_html].present? %>
|
||||
@@ -27,3 +42,6 @@
|
||||
<%= f.button button_title, class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if user.persisted? %>
|
||||
<%= button_to nil, user_send_reset_password_path(user), id: 'resend_password_button', method: :put, class: 'hidden', data: { turbo_confirm: t('are_you_sure_'), turbo_frame: :_top } %>
|
||||
<% end %>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
||||
<% if params[:action] == 'index' %>
|
||||
<% if params[:action] == 'index' && (current_user == true_user || current_account.testing?) %>
|
||||
<%= render 'shared/test_mode_toggle' %>
|
||||
<% end %>
|
||||
<% if @webhook_url.persisted? && params[:action] == 'index' %>
|
||||
|
||||
+66
-1
@@ -24,8 +24,12 @@ en: &en
|
||||
thanks: Thanks
|
||||
private: Private
|
||||
select: Select
|
||||
enabled: Enabled
|
||||
disabled: Disabled
|
||||
party: Party
|
||||
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Click here</label> to send a reset password email.'
|
||||
edit_order: Edit Order
|
||||
expirable_file_download_links: Expirable file download links
|
||||
invite_form_fields: Invite form fields
|
||||
default_parties: Default parties
|
||||
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
|
||||
@@ -36,13 +40,14 @@ en: &en
|
||||
bcc_recipients: BCC recipients
|
||||
resend_pending: Re-send pending
|
||||
always_enforce_signing_order: Always enforce the signing order
|
||||
create_templates_with_private_access_by_default: Create templates with private access by default
|
||||
ensure_unique_recipients: Ensure unique recipients
|
||||
edit_per_party: Edit per party
|
||||
reply_to: Reply to
|
||||
pending_by_me: Pending by me
|
||||
partially_completed: Partially completed
|
||||
require_phone_2fa_to_open: Require phone 2FA to open
|
||||
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: The sender has requested a two factor authentication via one time password sent to your <b>%{phone}</b> phone number.
|
||||
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: The sender has requested two-factor authentication via a one-time password sent to your <b>%{phone}</b> phone number.
|
||||
send_verification_code: Send verification code
|
||||
code_has_been_resent: Code has been re-sent
|
||||
invalid_code: Invalid code
|
||||
@@ -794,6 +799,12 @@ en: &en
|
||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" has been completed by <strong>{submission.submitters}</strong>'
|
||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "<strong>{template.name}</strong>" in the email attachments.'
|
||||
you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "<strong>{template.name}</strong>".'
|
||||
reveal_api_key: Reveal API Key
|
||||
enter_your_password_to_reveal_the_api_key: Enter your password to reveal the API key
|
||||
wrong_password: Wrong password.
|
||||
current_password: Current password
|
||||
dont_remember_your_current_password_click_here_to_reset_it_html: 'Don''t remember your current password? <label class="link" for="resend_password_button">Click here</label> to reset it.'
|
||||
an_email_with_password_reset_instructions_has_been_sent: An email with password reset instructions has been sent.
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Bulk Send
|
||||
@@ -900,6 +911,10 @@ en: &en
|
||||
range_without_total: "%{from}-%{to} events"
|
||||
|
||||
es: &es
|
||||
enabled: Habilitado
|
||||
disabled: Deshabilitado
|
||||
expirable_file_download_links: Enlaces de descarga de archivos con vencimiento
|
||||
create_templates_with_private_access_by_default: Crear plantillas con acceso privado por defecto
|
||||
party: Parte
|
||||
edit_order: Edita Pedido
|
||||
select: Seleccionar
|
||||
@@ -1674,6 +1689,12 @@ es: &es
|
||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" ha sido completado por <strong>{submission.submitters}</strong>'
|
||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, revisa la copia de tu "<strong>{template.name}</strong>" en los archivos adjuntos del correo electrónico.'
|
||||
you_have_been_invited_to_sign_the_template_name_html: 'Has sido invitado a firmar el "<strong>{template.name}</strong>".'
|
||||
reveal_api_key: Revelar clave API
|
||||
enter_your_password_to_reveal_the_api_key: Introduce tu contraseña para revelar la clave API
|
||||
wrong_password: Contraseña incorrecta.
|
||||
current_password: Contraseña actual
|
||||
dont_remember_your_current_password_click_here_to_reset_it_html: '¿No recuerdas tu contraseña actual? <label class="link" for="resend_password_button">Haz clic aquí</label> para restablecerla.'
|
||||
an_email_with_password_reset_instructions_has_been_sent: Se enviará un correo electrónico con las instrucciones para restablecer tu contraseña en unos minutos.
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envío masivo
|
||||
@@ -1780,6 +1801,11 @@ es: &es
|
||||
range_without_total: "%{from}-%{to} eventos"
|
||||
|
||||
it: &it
|
||||
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Clicca qui</label> per inviare una email per reimpostare la password.'
|
||||
enabled: Abilitato
|
||||
disabled: Disabilitato
|
||||
expirable_file_download_links: Link di download di file con scadenza
|
||||
create_templates_with_private_access_by_default: Crea modelli con accesso privato per impostazione predefinita
|
||||
party: Parte
|
||||
edit_order: Modifica Ordine
|
||||
select: Seleziona
|
||||
@@ -2554,6 +2580,12 @@ it: &it
|
||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" è stato completato da <strong>{submission.submitters}</strong>'
|
||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Per favore, controlla la copia del tuo "<strong>{template.name}</strong>" negli allegati dell''email.'
|
||||
you_have_been_invited_to_sign_the_template_name_html: 'Sei stato invitato a firmare il "<strong>{template.name}</strong>".'
|
||||
reveal_api_key: Mostra chiave API
|
||||
enter_your_password_to_reveal_the_api_key: Inserisci la tua password per mostrare la chiave API
|
||||
wrong_password: Password errata.
|
||||
current_password: Password attuale
|
||||
dont_remember_your_current_password_click_here_to_reset_it_html: 'Non ricordi la tua password attuale? <label class="link" for="resend_password_button">Clicca qui</label> per reimpostarla.'
|
||||
an_email_with_password_reset_instructions_has_been_sent: Un'email con le istruzioni per reimpostare la password ti è stata inviata e arriverà entro pochi minuti.
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Invio massivo
|
||||
@@ -2660,6 +2692,11 @@ it: &it
|
||||
range_without_total: "%{from}-%{to} eventi"
|
||||
|
||||
fr: &fr
|
||||
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Cliquez ici</label> pour envoyer un e-mail de réinitialisation du mot de passe.'
|
||||
enabled: Activé
|
||||
disabled: Désactivé
|
||||
expirable_file_download_links: Liens de téléchargement de fichiers expirables
|
||||
create_templates_with_private_access_by_default: Créer des modèles avec un accès privé par défaut
|
||||
party: Partie
|
||||
edit_order: Modifier la commande
|
||||
select: Sélectionner
|
||||
@@ -3437,6 +3474,12 @@ fr: &fr
|
||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" a été complété par <strong>{submission.submitters}</strong>'
|
||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Veuillez vérifier la copie de votre "<strong>{template.name}</strong>" dans les pièces jointes de l’e-mail.'
|
||||
you_have_been_invited_to_sign_the_template_name_html: 'Vous avez été invité à signer le "<strong>{template.name}</strong>".'
|
||||
reveal_api_key: Révéler la clé API
|
||||
enter_your_password_to_reveal_the_api_key: Entrez votre mot de passe pour révéler la clé API
|
||||
wrong_password: Mot de passe incorrect.
|
||||
current_password: Mot de passe actuel
|
||||
dont_remember_your_current_password_click_here_to_reset_it_html: 'Vous ne vous souvenez plus de votre mot de passe actuel ? <label class="link" for="resend_password_button">Cliquez ici</label> pour le réinitialiser.'
|
||||
an_email_with_password_reset_instructions_has_been_sent: Un e-mail contenant les instructions pour réinitialiser votre mot de passe vous sera envoyé dans quelques minutes.
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envoi en masse
|
||||
@@ -3543,6 +3586,11 @@ fr: &fr
|
||||
range_without_total: "%{from} à %{to} événements"
|
||||
|
||||
pt: &pt
|
||||
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Clique aqui</label> para enviar um e-mail de redefinição de senha.'
|
||||
enabled: Ativado
|
||||
disabled: Desativado
|
||||
expirable_file_download_links: Links de download de arquivos com expiração
|
||||
create_templates_with_private_access_by_default: Criar modelos com acesso privado por padrão
|
||||
party: Parte
|
||||
edit_order: Edita Pedido
|
||||
select: Selecionar
|
||||
@@ -4318,6 +4366,12 @@ pt: &pt
|
||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" foi concluído por <strong>{submission.submitters}</strong>'
|
||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, verifique a cópia do seu "<strong>{template.name}</strong>" nos anexos do e-mail.'
|
||||
you_have_been_invited_to_sign_the_template_name_html: 'Você foi convidado a assinar o "<strong>{template.name}</strong>".'
|
||||
reveal_api_key: Revelar chave API
|
||||
enter_your_password_to_reveal_the_api_key: Insira sua senha para revelar a chave API
|
||||
wrong_password: Senha incorreta.
|
||||
current_password: Senha atual
|
||||
dont_remember_your_current_password_click_here_to_reset_it_html: 'Não se lembra da sua senha atual? <label class="link" for="resend_password_button">Clique aqui</label> para redefini-la.'
|
||||
an_email_with_password_reset_instructions_has_been_sent: Um e-mail com instruções para redefinir sua senha será enviado em alguns minutos.
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Envio em massa
|
||||
@@ -4424,6 +4478,11 @@ pt: &pt
|
||||
range_without_total: "%{from}-%{to} eventos"
|
||||
|
||||
de: &de
|
||||
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Klicken Sie hier</label>, um eine E-Mail zum Zurücksetzen des Passworts zu senden.'
|
||||
enabled: Aktiviert
|
||||
disabled: Deaktiviert
|
||||
expirable_file_download_links: Ablaufbare Datei-Download-Links
|
||||
create_templates_with_private_access_by_default: Vorlagen standardmäßig mit privatem Zugriff erstellen
|
||||
party: Partei
|
||||
edit_order: Bestellung bearbeiten
|
||||
select: Auswählen
|
||||
@@ -5199,6 +5258,12 @@ de: &de
|
||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" wurde von <strong>{submission.submitters}</strong> abgeschlossen'
|
||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Bitte prüfen Sie die Kopie Ihres "<strong>{template.name}</strong>" in den E-Mail-Anhängen.'
|
||||
you_have_been_invited_to_sign_the_template_name_html: 'Du wurdest eingeladen, "<strong>{template.name}</strong>" zu unterschreiben.'
|
||||
reveal_api_key: API-Schlüssel anzeigen
|
||||
enter_your_password_to_reveal_the_api_key: Gib dein Passwort ein, um den API-Schlüssel anzuzeigen
|
||||
wrong_password: Falsches Passwort.
|
||||
current_password: Aktuelles Passwort
|
||||
dont_remember_your_current_password_click_here_to_reset_it_html: 'Sie erinnern sich nicht an Ihr aktuelles Passwort? <label class="link" for="resend_password_button">Klicken Sie hier</label>, um es zurückzusetzen.'
|
||||
an_email_with_password_reset_instructions_has_been_sent: Eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts wurde Ihnen in wenigen Minuten zugesendet.
|
||||
submission_sources:
|
||||
api: API
|
||||
bulk: Massenversand
|
||||
|
||||
+4
-1
@@ -65,7 +65,9 @@ Rails.application.routes.draw do
|
||||
resources :setup, only: %i[index create]
|
||||
resource :newsletter, only: %i[show update]
|
||||
resources :enquiries, only: %i[create]
|
||||
resources :users, only: %i[new create edit update destroy]
|
||||
resources :users, only: %i[new create edit update destroy] do
|
||||
resource :send_reset_password, only: %i[update], controller: 'users_send_reset_password'
|
||||
end
|
||||
resource :user_signature, only: %i[edit update destroy]
|
||||
resource :user_initials, only: %i[edit update destroy]
|
||||
resources :submissions_archived, only: %i[index], path: 'submissions/archived'
|
||||
@@ -179,6 +181,7 @@ Rails.application.routes.draw do
|
||||
defaults: { status: :integration }
|
||||
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
||||
resources :api, only: %i[index create], controller: 'api_settings'
|
||||
resource :reveal_access_token, only: %i[show create], controller: 'reveal_access_token'
|
||||
resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
|
||||
post :resend
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PopulateExpireLinkConfigs < ActiveRecord::Migration[8.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
class MigrationAccount < ActiveRecord::Base
|
||||
self.table_name = 'accounts'
|
||||
end
|
||||
|
||||
class MigrationAccountConfig < ActiveRecord::Base
|
||||
self.table_name = 'account_configs'
|
||||
|
||||
serialize :value, coder: JSON
|
||||
end
|
||||
|
||||
def up
|
||||
MigrationAccount.find_each do |account|
|
||||
config = MigrationAccountConfig.find_or_initialize_by(key: 'download_links_expire', account_id: account.id)
|
||||
|
||||
next if config.persisted?
|
||||
|
||||
config.value = false
|
||||
config.save!
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddVerificationMethodToCompletedSubmitters < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :completed_submitters, :verification_method, :string
|
||||
end
|
||||
end
|
||||
+2
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_07_18_121133) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_09_01_110606) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "btree_gin"
|
||||
enable_extension "plpgsql"
|
||||
@@ -117,6 +117,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_18_121133) do
|
||||
t.datetime "completed_at", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "verification_method"
|
||||
t.index ["account_id"], name: "index_completed_submitters_on_account_id"
|
||||
t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true
|
||||
end
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Accounts
|
||||
LINK_EXPIRES_AT = 40.minutes
|
||||
|
||||
module_function
|
||||
|
||||
def create_duplicate(account)
|
||||
@@ -185,4 +187,11 @@ module Accounts
|
||||
rescue TZInfo::InvalidTimezoneIdentifier
|
||||
'UTC'
|
||||
end
|
||||
|
||||
def link_expires_at(account)
|
||||
return if AccountConfig.find_or_initialize_by(account: account,
|
||||
key: AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY).value == false
|
||||
|
||||
LINK_EXPIRES_AT.from_now
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,6 +102,7 @@ module ReplaceEmailVariables
|
||||
return unless submitter
|
||||
|
||||
value = submitter.try(field_name)
|
||||
expires_at = nil
|
||||
|
||||
if value_name
|
||||
field = (submission.template_fields || submission.template.fields).find { |e| e['name'] == value_name }
|
||||
@@ -114,7 +115,11 @@ module ReplaceEmailVariables
|
||||
|
||||
attachment = submitter.attachments.find { |e| e.uuid == attachment_uuid }
|
||||
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
||||
if attachment
|
||||
expires_at ||= Accounts.link_expires_at(Account.new(id: submission.account_id))
|
||||
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
|
||||
end
|
||||
else
|
||||
value[field&.dig('uuid')]
|
||||
end
|
||||
|
||||
+1
-1
@@ -239,7 +239,7 @@ module Submissions
|
||||
|
||||
item['conditions'].each_with_object([]) do |condition, acc|
|
||||
result =
|
||||
if fields_index[condition['field_uuid']]['submitter_uuid'] == include_submitter_uuid
|
||||
if fields_index.dig(condition['field_uuid'], 'submitter_uuid') == include_submitter_uuid
|
||||
submitter_conditions_acc << condition if submitter_conditions_acc
|
||||
|
||||
true
|
||||
|
||||
@@ -391,8 +391,8 @@ module Submissions
|
||||
composer.formatted_text_box(
|
||||
Array.wrap(value).map do |uuid|
|
||||
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
||||
link =
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||
|
||||
link = r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
|
||||
|
||||
{ link:, text: "#{attachment.filename}\n", style: :link }
|
||||
end,
|
||||
@@ -489,6 +489,10 @@ module Submissions
|
||||
padding: [5, 0, 0, 8],
|
||||
position: :float, text_align: :left)
|
||||
end
|
||||
|
||||
def r
|
||||
Rails.application.routes.url_helpers
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,8 +6,8 @@ module Submissions
|
||||
|
||||
module_function
|
||||
|
||||
def call(submissions, format: :csv)
|
||||
rows = build_table_rows(submissions)
|
||||
def call(submissions, format: :csv, expires_at: nil)
|
||||
rows = build_table_rows(submissions, expires_at:)
|
||||
|
||||
if format.to_sym == :csv
|
||||
rows_to_csv(rows)
|
||||
@@ -57,7 +57,7 @@ module Submissions
|
||||
headers.map { |key| row.find { |e| e[:name] == key }&.dig(:value) }
|
||||
end
|
||||
|
||||
def build_table_rows(submissions)
|
||||
def build_table_rows(submissions, expires_at: nil)
|
||||
submissions.preload(submitters: [attachments_attachments: :blob, documents_attachments: :blob])
|
||||
.find_each.map do |submission|
|
||||
submission_data = []
|
||||
@@ -69,7 +69,7 @@ module Submissions
|
||||
|
||||
submission_data += build_submission_data(submitter, submitter_name, submitters_count)
|
||||
|
||||
submission_data += submitter_formatted_fields(submitter).map do |field|
|
||||
submission_data += submitter_formatted_fields(submitter, expires_at:).map do |field|
|
||||
{
|
||||
name: column_name(field[:name], submitter_name, submitters_count),
|
||||
value: field[:value]
|
||||
@@ -81,7 +81,7 @@ module Submissions
|
||||
submission_data += submitter.documents.map.with_index(1) do |attachment, index|
|
||||
{
|
||||
name: "#{I18n.t('document')} #{index}",
|
||||
value: ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||
value: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -123,7 +123,7 @@ module Submissions
|
||||
submitters_count > 1 ? "#{submitter_name} - #{name}" : name
|
||||
end
|
||||
|
||||
def submitter_formatted_fields(submitter)
|
||||
def submitter_formatted_fields(submitter, expires_at: nil)
|
||||
fields = submitter.submission.template_fields || submitter.submission.template.fields
|
||||
|
||||
template_fields = fields.select { |f| f['submitter_uuid'] == submitter.uuid }
|
||||
@@ -142,11 +142,13 @@ module Submissions
|
||||
value =
|
||||
if template_field_type.in?(%w[image signature])
|
||||
attachment = attachments_index[submitter_value]
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
||||
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
|
||||
elsif template_field_type == 'file'
|
||||
Array.wrap(submitter_value).compact_blank.filter_map do |e|
|
||||
attachment = attachments_index[e]
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
||||
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
|
||||
end
|
||||
else
|
||||
submitter_value
|
||||
|
||||
@@ -201,13 +201,15 @@ module Submissions
|
||||
|
||||
attachments_data_cache = {}
|
||||
|
||||
return pdfs_index if submitter.submission.template_fields.blank?
|
||||
submission = submitter.submission
|
||||
|
||||
with_headings = find_last_submitter(submitter.submission, submitter:).blank? if with_headings.nil?
|
||||
return pdfs_index if submission.template_fields.blank?
|
||||
|
||||
with_headings = find_last_submitter(submission, submitter:).blank? if with_headings.nil?
|
||||
|
||||
locale = submitter.metadata.fetch('lang', account.locale)
|
||||
|
||||
submitter.submission.template_fields.each do |field|
|
||||
submission.template_fields.each do |field|
|
||||
next if field['type'] == 'heading' && !with_headings
|
||||
next if field['submitter_uuid'] != submitter.uuid && field['type'] != 'heading'
|
||||
|
||||
@@ -224,7 +226,7 @@ module Submissions
|
||||
|
||||
page[:Annots] ||= []
|
||||
page[:Annots] = page[:Annots].try(:reject) do |e|
|
||||
next if e.is_a?(Integer)
|
||||
next if e.is_a?(Integer) || e.is_a?(Symbol)
|
||||
|
||||
e.present? && e[:A] && e[:A][:URI].to_s.starts_with?('file:///docuseal_field')
|
||||
end || page[:Annots]
|
||||
@@ -476,7 +478,7 @@ module Submissions
|
||||
height_diff - (height_diff.zero? ? diff : 0)
|
||||
],
|
||||
A: { Type: :Action, S: :URI,
|
||||
URI: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
||||
URI: r.submissions_preview_url(submission.slug, **Docuseal.default_url_options) }
|
||||
}
|
||||
)
|
||||
|
||||
@@ -747,11 +749,15 @@ module Submissions
|
||||
def maybe_rotate_pdf(pdf)
|
||||
return pdf if pdf.pages.size > MAX_PAGE_ROTATE
|
||||
|
||||
is_pages_rotated = pdf.pages.root[:Rotate].present? && pdf.pages.root[:Rotate] != 0
|
||||
|
||||
pdf.pages.root[:Rotate] = 0 if is_pages_rotated
|
||||
|
||||
is_rotated = pdf.pages.filter_map do |page|
|
||||
page.rotate(0, flatten: true) if page[:Rotate] != 0
|
||||
end.present?
|
||||
|
||||
return pdf unless is_rotated
|
||||
return pdf if !is_rotated && !is_pages_rotated
|
||||
|
||||
io = StringIO.new
|
||||
|
||||
@@ -873,7 +879,7 @@ module Submissions
|
||||
end
|
||||
end
|
||||
|
||||
def h
|
||||
def r
|
||||
Rails.application.routes.url_helpers
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,6 @@ module Submissions
|
||||
module SerializeForApi
|
||||
SERIALIZE_PARAMS = {
|
||||
only: %i[id name slug source submitters_order expire_at created_at updated_at archived_at],
|
||||
methods: %i[audit_log_url combined_document_url],
|
||||
include: {
|
||||
submitters: { only: %i[id] },
|
||||
template: { only: %i[id name external_id created_at updated_at],
|
||||
@@ -15,11 +14,13 @@ module Submissions
|
||||
|
||||
module_function
|
||||
|
||||
def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true)
|
||||
def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true,
|
||||
expires_at: Accounts.link_expires_at(Account.new(id: submission.account_id)))
|
||||
submitters ||= submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
||||
|
||||
serialized_submitters = submitters.map do |submitter|
|
||||
Submitters::SerializeForApi.call(submitter, with_documents:, with_events: false, with_values:, params:)
|
||||
Submitters::SerializeForApi.call(submitter, with_documents:, with_events: false, with_values:, params:,
|
||||
expires_at:)
|
||||
end
|
||||
|
||||
json = submission.as_json(SERIALIZE_PARAMS)
|
||||
@@ -30,8 +31,6 @@ module Submissions
|
||||
json['submission_events'] = Submitters::SerializeForApi.serialize_events(submission.submission_events)
|
||||
end
|
||||
|
||||
json['combined_document_url'] ||= maybe_build_combined_url(submitters, submission, params)
|
||||
|
||||
if submitters.all?(&:completed_at?)
|
||||
last_submitter = submitters.max_by(&:completed_at)
|
||||
|
||||
@@ -39,10 +38,16 @@ module Submissions
|
||||
json['documents'] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
|
||||
end
|
||||
|
||||
json['audit_log_url'] = submission.audit_log_url(expires_at:)
|
||||
json['combined_document_url'] = submission.combined_document_url(expires_at:)
|
||||
json['combined_document_url'] ||= maybe_build_combined_url(submitters, submission, params, expires_at:)
|
||||
|
||||
json['status'] = 'completed'
|
||||
json['completed_at'] = last_submitter.completed_at.as_json
|
||||
else
|
||||
json['documents'] = [] if with_documents
|
||||
json['audit_log_url'] = nil
|
||||
json['combined_document_url'] = nil
|
||||
json['status'] = build_status(submission, submitters)
|
||||
json['completed_at'] = nil
|
||||
end
|
||||
@@ -60,7 +65,7 @@ module Submissions
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_build_combined_url(submitters, submission, params)
|
||||
def maybe_build_combined_url(submitters, submission, params, expires_at: nil)
|
||||
return unless submitters.all?(&:completed_at?)
|
||||
|
||||
attachment = submission.combined_document_attachment
|
||||
@@ -71,7 +76,7 @@ module Submissions
|
||||
attachment = Submissions::GenerateCombinedAttachment.call(submitter)
|
||||
end
|
||||
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) if attachment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@ module Submitters
|
||||
module_function
|
||||
|
||||
def call(submitter, with_template: false, with_events: false, with_documents: true, with_urls: false,
|
||||
with_values: true, params: {})
|
||||
with_values: true, params: {}, expires_at: Accounts.link_expires_at(Account.new(id: submitter.account_id)))
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [submitter],
|
||||
associations: if with_documents
|
||||
@@ -24,15 +24,19 @@ module Submitters
|
||||
additional_attrs = {}
|
||||
|
||||
if params[:include].to_s.include?('fields')
|
||||
additional_attrs['fields'] = SerializeForWebhook.build_fields_array(submitter)
|
||||
additional_attrs['fields'] = SerializeForWebhook.build_fields_array(submitter, expires_at:)
|
||||
end
|
||||
|
||||
if with_template
|
||||
additional_attrs['template'] = submitter.submission.template.as_json(only: %i[id name created_at updated_at])
|
||||
end
|
||||
|
||||
additional_attrs['values'] = SerializeForWebhook.build_values_array(submitter) if with_values
|
||||
additional_attrs['documents'] = SerializeForWebhook.build_documents_array(submitter) if with_documents
|
||||
additional_attrs['values'] = SerializeForWebhook.build_values_array(submitter, expires_at:) if with_values
|
||||
|
||||
if with_documents
|
||||
additional_attrs['documents'] = SerializeForWebhook.build_documents_array(submitter, expires_at:)
|
||||
end
|
||||
|
||||
additional_attrs['preferences'] = submitter.preferences.except('default_values')
|
||||
additional_attrs['submission_events'] = serialize_events(submitter.submission_events) if with_events
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ module Submitters
|
||||
|
||||
module_function
|
||||
|
||||
def call(submitter)
|
||||
def call(submitter, expires_at: Accounts.link_expires_at(Account.new(id: submitter.account_id)))
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [submitter], associations: [documents_attachments: :blob, attachments_attachments: :blob]
|
||||
).call
|
||||
|
||||
values = build_values_array(submitter)
|
||||
documents = build_documents_array(submitter)
|
||||
values = build_values_array(submitter, expires_at:)
|
||||
documents = build_documents_array(submitter, expires_at:)
|
||||
|
||||
submission = submitter.submission
|
||||
|
||||
@@ -32,7 +32,7 @@ module Submitters
|
||||
'preferences' => submitter.preferences.except('default_values'),
|
||||
'values' => values,
|
||||
'documents' => documents,
|
||||
'audit_log_url' => submitter.submission.audit_log_url,
|
||||
'audit_log_url' => submitter.submission.audit_log_url(expires_at:),
|
||||
'submission_url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options),
|
||||
'template' => submission.template.as_json(
|
||||
only: %i[id name external_id created_at updated_at],
|
||||
@@ -40,15 +40,15 @@ module Submitters
|
||||
),
|
||||
'submission' => {
|
||||
'id' => submission.id,
|
||||
'audit_log_url' => submission.audit_log_url,
|
||||
'combined_document_url' => submission.combined_document_url,
|
||||
'audit_log_url' => submission.audit_log_url(expires_at:),
|
||||
'combined_document_url' => submission.combined_document_url(expires_at:),
|
||||
'status' => build_submission_status(submission),
|
||||
'url' => r.submissions_preview_url(submission.slug, **Docuseal.default_url_options),
|
||||
'created_at' => submission.created_at.as_json
|
||||
})
|
||||
end
|
||||
|
||||
def build_values_array(submitter)
|
||||
def build_values_array(submitter, expires_at: nil)
|
||||
fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || []
|
||||
attachments_index = submitter.attachments.index_by(&:uuid)
|
||||
submitter_field_counters = Hash.new { 0 }
|
||||
@@ -64,13 +64,13 @@ module Submitters
|
||||
|
||||
next if !submitter.values.key?(field['uuid']) && !submitter.completed_at?
|
||||
|
||||
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index)
|
||||
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index, expires_at:)
|
||||
|
||||
{ 'field' => field_name, 'value' => value }
|
||||
end
|
||||
end
|
||||
|
||||
def build_fields_array(submitter)
|
||||
def build_fields_array(submitter, expires_at: nil)
|
||||
fields = submitter.submission.template_fields.presence || submitter.submission&.template&.fields || []
|
||||
attachments_index = submitter.attachments.index_by(&:uuid)
|
||||
submitter_field_counters = Hash.new { 0 }
|
||||
@@ -86,7 +86,7 @@ module Submitters
|
||||
|
||||
next if !submitter.values.key?(field['uuid']) && !submitter.completed_at?
|
||||
|
||||
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index)
|
||||
value = fetch_field_value(field, submitter.values[field['uuid']], attachments_index, expires_at:)
|
||||
|
||||
{ 'name' => field_name, 'uuid' => field['uuid'], 'value' => value, 'readonly' => field['readonly'] == true }
|
||||
end
|
||||
@@ -104,26 +104,26 @@ module Submitters
|
||||
end
|
||||
end
|
||||
|
||||
def build_documents_array(submitter)
|
||||
def build_documents_array(submitter, expires_at: nil)
|
||||
submitter.documents.map do |attachment|
|
||||
{ 'name' => attachment.filename.base, 'url' => rails_storage_proxy_url(attachment) }
|
||||
{ 'name' => attachment.filename.base, 'url' => rails_storage_proxy_url(attachment, expires_at:) }
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_field_value(field, value, attachments_index)
|
||||
def fetch_field_value(field, value, attachments_index, expires_at: nil)
|
||||
if field['type'].in?(%w[image signature initials stamp payment])
|
||||
rails_storage_proxy_url(attachments_index[value])
|
||||
rails_storage_proxy_url(attachments_index[value], expires_at:)
|
||||
elsif field['type'] == 'file'
|
||||
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e]) }
|
||||
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e], expires_at:) }
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def rails_storage_proxy_url(attachment)
|
||||
def rails_storage_proxy_url(attachment, expires_at: nil)
|
||||
return if attachment.blank?
|
||||
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:)
|
||||
end
|
||||
|
||||
def r
|
||||
|
||||
@@ -37,6 +37,10 @@ module Templates
|
||||
hash
|
||||
end
|
||||
|
||||
def maybe_assign_access(_template)
|
||||
nil
|
||||
end
|
||||
|
||||
def search(current_user, templates, keyword)
|
||||
if Docuseal.fulltext_search?
|
||||
fulltext_search(current_user, templates, keyword)
|
||||
|
||||
@@ -10,6 +10,7 @@ module Templates
|
||||
pdf.pages.flat_map.with_index do |page, index|
|
||||
(page[:Annots] || []).filter_map do |annot|
|
||||
next if annot.blank?
|
||||
next if annot.is_a?(Integer) || annot.is_a?(Symbol)
|
||||
next if annot[:A].blank? || annot[:A][:URI].blank?
|
||||
next unless annot[:Subtype] == :Link
|
||||
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')
|
||||
|
||||
@@ -14,7 +14,8 @@ module Templates
|
||||
|
||||
module_function
|
||||
|
||||
def call(template, schema_documents = template.schema_documents.preload(:blob), preview_image_attachments = nil)
|
||||
def call(template, schema_documents: template.schema_documents.preload(:blob), preview_image_attachments: nil,
|
||||
expires_at: Accounts.link_expires_at(Account.new(id: template.account_id)))
|
||||
json = template.as_json(SERIALIZE_PARAMS)
|
||||
|
||||
preview_image_attachments ||=
|
||||
@@ -40,8 +41,8 @@ module Templates
|
||||
{
|
||||
'id' => attachment.id,
|
||||
'uuid' => attachment.uuid,
|
||||
'url' => ActiveStorage::Blob.proxy_url(attachment.blob),
|
||||
'preview_image_url' => first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob),
|
||||
'url' => ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:),
|
||||
'preview_image_url' => first_page_blob && ActiveStorage::Blob.proxy_url(first_page_blob, expires_at:),
|
||||
'filename' => attachment.filename
|
||||
}
|
||||
end
|
||||
|
||||
@@ -8,6 +8,10 @@ describe 'Templates API' do
|
||||
let(:folder) { create(:template_folder, account:) }
|
||||
let(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } }
|
||||
|
||||
before do
|
||||
allow(Accounts).to receive(:link_expires_at).and_return(Accounts::LINK_EXPIRES_AT)
|
||||
end
|
||||
|
||||
describe 'GET /api/templates' do
|
||||
it 'returns a list of templates' do
|
||||
templates = [
|
||||
@@ -211,8 +215,8 @@ describe 'Templates API' do
|
||||
{
|
||||
id: template.documents.first.id,
|
||||
uuid: template.documents.first.uuid,
|
||||
url: ActiveStorage::Blob.proxy_url(attachment.blob),
|
||||
preview_image_url: ActiveStorage::Blob.proxy_url(first_page_blob),
|
||||
url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: Accounts::LINK_EXPIRES_AT),
|
||||
preview_image_url: ActiveStorage::Blob.proxy_url(first_page_blob, expires_at: Accounts::LINK_EXPIRES_AT),
|
||||
filename: 'sample-document.pdf'
|
||||
}
|
||||
],
|
||||
@@ -235,7 +239,7 @@ describe 'Templates API' do
|
||||
folder_name: folder.name,
|
||||
source: 'native',
|
||||
external_id: template.external_id,
|
||||
application_key: template.external_id # Backward compatibility
|
||||
application_key: template.external_id
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -14,4 +14,26 @@ RSpec.describe 'API Settings' do
|
||||
token = user.access_token.token
|
||||
expect(page).to have_field('X-Auth-Token', with: token.sub(token[5..], '*' * token[5..].size))
|
||||
end
|
||||
|
||||
it 'reveals API key with correct password' do
|
||||
find('#api_key').click
|
||||
|
||||
within('.modal') do
|
||||
fill_in 'password', with: user.password
|
||||
click_button 'Submit'
|
||||
end
|
||||
|
||||
expect(page).to have_field('X-Auth-Token', with: user.access_token.token)
|
||||
end
|
||||
|
||||
it 'shows error with incorrect password' do
|
||||
find('#api_key').click
|
||||
|
||||
within('.modal') do
|
||||
fill_in 'password', with: 'wrong_password'
|
||||
click_button 'Submit'
|
||||
end
|
||||
|
||||
expect(page).to have_content('Wrong password')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,9 @@ RSpec.describe 'Profile Settings' do
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
|
||||
allow(Accounts).to receive(:can_send_emails?).and_return(true)
|
||||
|
||||
visit settings_profile_index_path
|
||||
end
|
||||
|
||||
@@ -16,7 +19,6 @@ RSpec.describe 'Profile Settings' do
|
||||
|
||||
expect(page).to have_content('Change Password')
|
||||
expect(page).to have_field('user[password]')
|
||||
expect(page).to have_field('user[password_confirmation]')
|
||||
end
|
||||
|
||||
context 'when changes contact information' do
|
||||
@@ -47,6 +49,7 @@ RSpec.describe 'Profile Settings' do
|
||||
it 'updates password' do
|
||||
fill_in 'New password', with: 'newpassword'
|
||||
fill_in 'Confirm new password', with: 'newpassword'
|
||||
fill_in 'Current password', with: 'password'
|
||||
|
||||
all(:button, 'Update')[1].click
|
||||
|
||||
@@ -56,10 +59,51 @@ RSpec.describe 'Profile Settings' do
|
||||
it 'does not update if password confirmation does not match' do
|
||||
fill_in 'New password', with: 'newpassword'
|
||||
fill_in 'Confirm new password', with: 'newpassword1'
|
||||
fill_in 'Current password', with: 'password'
|
||||
|
||||
all(:button, 'Update')[1].click
|
||||
|
||||
expect(page).to have_content("Password confirmation doesn't match Password")
|
||||
end
|
||||
|
||||
it 'does not update if current password is incorrect' do
|
||||
fill_in 'New password', with: 'newpassword'
|
||||
fill_in 'Confirm new password', with: 'newpassword'
|
||||
fill_in 'Current password', with: 'wrongpassword'
|
||||
|
||||
all(:button, 'Update')[1].click
|
||||
|
||||
expect(page).to have_content('Current password is invalid')
|
||||
end
|
||||
|
||||
it 'resets password and signs in with new password', sidekiq: :inline do
|
||||
fill_in 'New password', with: 'newpassword'
|
||||
accept_confirm('Are you sure?') do
|
||||
find('label', text: 'Click here').click
|
||||
end
|
||||
|
||||
expect(page).to have_content('An email with password reset instructions has been sent.')
|
||||
|
||||
email = ActionMailer::Base.deliveries.last
|
||||
reset_password_url = email.body
|
||||
.encoded[/href="([^"]+)"/, 1]
|
||||
.sub(%r{https?://(.*?)/}, "#{Capybara.current_session.server.base_url}/")
|
||||
|
||||
visit reset_password_url
|
||||
|
||||
fill_in 'New password', with: 'new_strong_password'
|
||||
fill_in 'Confirm new password', with: 'new_strong_password'
|
||||
click_button 'Change my password'
|
||||
|
||||
expect(page).to have_content('Your password has been changed successfully. You are now signed in.')
|
||||
|
||||
visit new_user_session_path
|
||||
|
||||
fill_in 'Email', with: user.email
|
||||
fill_in 'Password', with: 'new_strong_password'
|
||||
click_button 'Sign In'
|
||||
|
||||
expect(page).to have_content('Signed in successfully')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -115,7 +115,6 @@ RSpec.describe 'Team Settings' do
|
||||
fill_in 'First name', with: 'Adam'
|
||||
fill_in 'Last name', with: 'Meier'
|
||||
fill_in 'Email', with: 'adam.meier@example.com'
|
||||
fill_in 'Password', with: 'new_password'
|
||||
|
||||
expect do
|
||||
click_button 'Submit'
|
||||
|
||||
Reference in New Issue
Block a user