Merge from docusealco/wip

pull/381/merge 2.1.4
Alex Turchyn 2 months ago committed by GitHub
commit c1123fef63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -45,18 +45,19 @@ module Api
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)
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
if exp.blank?
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
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 }
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
return if !require_ttl && !require_auth
end
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
raise CanCan::AccessDenied
end

@ -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,6 +10,9 @@ 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'

@ -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
}
})
}

@ -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
)

@ -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

@ -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' %>

@ -24,7 +24,10 @@ 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
@ -44,7 +47,7 @@ en: &en
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
@ -796,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
@ -902,6 +911,8 @@ 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
@ -1678,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
@ -1784,6 +1801,9 @@ 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
@ -2560,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
@ -2666,6 +2692,9 @@ 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
@ -3445,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 le-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
@ -3551,6 +3586,9 @@ 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
@ -4328,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
@ -4434,6 +4478,9 @@ 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
@ -5211,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

@ -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,7 @@
# frozen_string_literal: true
class AddVerificationMethodToCompletedSubmitters < ActiveRecord::Migration[8.0]
def change
add_column :completed_submitters, :verification_method, :string
end
end

@ -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_08_31_125322) 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_08_31_125322) 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

@ -226,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]

@ -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,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'

Loading…
Cancel
Save