diff --git a/app/controllers/api/active_storage_blobs_proxy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_controller.rb index 8df3b523..a542c637 100644 --- a/app/controllers/api/active_storage_blobs_proxy_controller.rb +++ b/app/controllers/api/active_storage_blobs_proxy_controller.rb @@ -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 diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 7ca14716..60de5b12 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -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 diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb index 668cd140..a1ee71bf 100644 --- a/app/controllers/profile_controller.rb +++ b/app/controllers/profile_controller.rb @@ -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 diff --git a/app/controllers/reveal_access_token_controller.rb b/app/controllers/reveal_access_token_controller.rb new file mode 100644 index 00000000..c8959afd --- /dev/null +++ b/app/controllers/reveal_access_token_controller.rb @@ -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 diff --git a/app/controllers/submissions_export_controller.rb b/app/controllers/submissions_export_controller.rb index c12a2e6e..bfbb7359 100644 --- a/app/controllers/submissions_export_controller.rb +++ b/app/controllers/submissions_export_controller.rb @@ -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' diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8f433577..39cbc223 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -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)) diff --git a/app/controllers/users_send_reset_password_controller.rb b/app/controllers/users_send_reset_password_controller.rb new file mode 100644 index 00000000..f6a8af82 --- /dev/null +++ b/app/controllers/users_send_reset_password_controller.rb @@ -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 diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index 8161907c..d58c04d8 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -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 + } }) } diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index 49f79fcf..a4b58a5a 100644 --- a/app/jobs/process_submitter_completion_job.rb +++ b/app/jobs/process_submitter_completion_job.rb @@ -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 ) diff --git a/app/models/completed_submitter.rb b/app/models/completed_submitter.rb index 6ada62f3..25c36e3a 100644 --- a/app/models/completed_submitter.rb +++ b/app/models/completed_submitter.rb @@ -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 diff --git a/app/views/api_settings/index.html.erb b/app/views/api_settings/index.html.erb index c5a761a8..57f6fa16 100644 --- a/app/views/api_settings/index.html.erb +++ b/app/views/api_settings/index.html.erb @@ -11,10 +11,21 @@
<% token = current_user.access_token.token %> - - - - <%= 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? %> + + + + <%= 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 %> + + +
+ <%= svg_icon('copy', class: 'w-6 h-6 text-white') %> + +
+
+ <% end %>
<%= 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_') } %>
diff --git a/app/views/profile/index.html.erb b/app/views/profile/index.html.erb index 6732d907..c5422b00 100644 --- a/app/views/profile/index.html.erb +++ b/app/views/profile/index.html.erb @@ -54,19 +54,29 @@

<%= t('change_password') %>

- <%= form_for current_user, url: update_password_settings_profile_index_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> -
- <%= f.label :password, t('new_password'), class: 'label' %> - <%= f.password_field :password, autocomplete: 'off', class: 'base-input' %> -
-
- <%= f.label :password_confirmation, t('confirm_password'), class: 'label' %> - <%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %> -
-
- <%= 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 %> +
+
+ <%= f.label :password_confirmation, t('confirm_password'), class: 'label' %> + <%= f.password_field :password_confirmation, autocomplete: 'off', class: 'base-input' %> +
+
+ <%= 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) %> + + <%= t('dont_remember_your_current_password_click_here_to_reset_it_html') %> + + <% end %> +
+
+ <%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %> +
<% 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_') } %>

<%= t('two_factor_authentication') %>

diff --git a/app/views/reveal_access_token/_access_token.html.erb b/app/views/reveal_access_token/_access_token.html.erb new file mode 100644 index 00000000..960faf7f --- /dev/null +++ b/app/views/reveal_access_token/_access_token.html.erb @@ -0,0 +1,2 @@ + +<%= 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') %> diff --git a/app/views/reveal_access_token/show.html.erb b/app/views/reveal_access_token/show.html.erb new file mode 100644 index 00000000..4ee8c5fb --- /dev/null +++ b/app/views/reveal_access_token/show.html.erb @@ -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 %> +
+ <%= 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? %> + <%= local_assigns[:error_message] %> + <% end %> +
+
+ <%= submit_tag t('submit'), class: 'base-button' %> +
+ <% end %> +<% end %> diff --git a/app/views/shared/_settings_nav.html.erb b/app/views/shared/_settings_nav.html.erb index 32964656..64fc1c9c 100644 --- a/app/views/shared/_settings_nav.html.erb +++ b/app/views/shared/_settings_nav.html.erb @@ -56,6 +56,8 @@ <%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %> <% end %> + <% end %> + <% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %> <% if can?(:read, WebhookUrl) %>
  • <%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %> @@ -70,7 +72,7 @@ <% end %>
  • <% 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?) %>
  • <%= 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 %> diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index eb1999e1..eeb779e5 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -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']] %> diff --git a/app/views/submissions_export/new.html.erb b/app/views/submissions_export/new.html.erb index 93a33c6e..7b2df9b6 100644 --- a/app/views/submissions_export/new.html.erb +++ b/app/views/submissions_export/new.html.erb @@ -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 %>
    - <%= 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 %>
    <%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %> @@ -14,7 +15,7 @@
    <% 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 %>
    <%= svg_icon('download', class: 'w-12 h-12 stroke-2 mr-2') %> diff --git a/app/views/templates/show.html.erb b/app/views/templates/show.html.erb index c749ef1f..82ffb69e 100644 --- a/app/views/templates/show.html.erb +++ b/app/views/templates/show.html.erb @@ -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') %> <%= t('export') %> <% end %> diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb index 3df7cb08..5ceaeabc 100644 --- a/app/views/users/_form.html.erb +++ b/app/views/users/_form.html.erb @@ -1,22 +1,37 @@ <%= form_for user, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
    -
    - <%= f.label :first_name, t('first_name'), class: 'label' %> - <%= f.text_field :first_name, required: true, class: 'base-input', dir: 'auto' %> -
    -
    - <%= f.label :last_name, t('last_name'), class: 'label' %> - <%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %> +
    +
    + <%= f.label :first_name, t('first_name'), class: 'label' %> + <%= f.text_field :first_name, required: true, class: 'base-input w-full', dir: 'auto' %> +
    +
    + <%= f.label :last_name, t('last_name'), class: 'label' %> + <%= f.text_field :last_name, required: true, class: 'base-input w-full', dir: 'auto' %> +
    <%= 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) %> + + <%= t('click_here_to_send_a_reset_password_email_html') %> + + <% end %>
    -
    - <%= f.label :password, t('password'), class: 'label' %> - <%= f.password_field :password, required: user.new_record?, class: 'base-input' %> -
    + <% if user.new_record? && !Docuseal.multitenant? %> +
    + <%= f.label :password, t('password'), class: 'label' %> + <%= f.password_field :password, class: 'base-input' %> +
    + <% end %> <% if f.object != current_user %> + <% if user.otp_required_for_login %> +
    + <%= 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' %> +
    + <% end %> <%= render 'role_select', f: %> <% end %> <% if local_assigns[:extra_fields_html].present? %> @@ -27,3 +42,6 @@ <%= f.button button_title, class: 'base-button' %>
    <% 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 %> diff --git a/app/views/webhook_settings/show.html.erb b/app/views/webhook_settings/show.html.erb index 89c27570..1b9a53ad 100644 --- a/app/views/webhook_settings/show.html.erb +++ b/app/views/webhook_settings/show.html.erb @@ -4,7 +4,7 @@

    Webhook

    - <% 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' %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index c7a607f9..18da4440 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -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: ' 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 %{phone} 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 %{phone} 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: '"{template.name}" has been completed by {submission.submitters}' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "{template.name}" in the email attachments.' you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "{template.name}".' + 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? 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: '"{template.name}" ha sido completado por {submission.submitters}' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, revisa la copia de tu "{template.name}" en los archivos adjuntos del correo electrónico.' you_have_been_invited_to_sign_the_template_name_html: 'Has sido invitado a firmar el "{template.name}".' + 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? 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: ' 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: '"{template.name}" è stato completato da {submission.submitters}' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Per favore, controlla la copia del tuo "{template.name}" negli allegati dell''email.' you_have_been_invited_to_sign_the_template_name_html: 'Sei stato invitato a firmare il "{template.name}".' + 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? 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: ' 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: '"{template.name}" a été complété par {submission.submitters}' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Veuillez vérifier la copie de votre "{template.name}" 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 "{template.name}".' + 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 ? 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: ' 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: '"{template.name}" foi concluído por {submission.submitters}' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, verifique a cópia do seu "{template.name}" nos anexos do e-mail.' you_have_been_invited_to_sign_the_template_name_html: 'Você foi convidado a assinar o "{template.name}".' + 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? 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: ', 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: '"{template.name}" wurde von {submission.submitters} abgeschlossen' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Bitte prüfen Sie die Kopie Ihres "{template.name}" in den E-Mail-Anhängen.' you_have_been_invited_to_sign_the_template_name_html: 'Du wurdest eingeladen, "{template.name}" 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? , 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 diff --git a/config/routes.rb b/config/routes.rb index 76ed7af9..1947b4b4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20250901110606_add_verification_method_to_completed_submitters.rb b/db/migrate/20250901110606_add_verification_method_to_completed_submitters.rb new file mode 100644 index 00000000..31d48047 --- /dev/null +++ b/db/migrate/20250901110606_add_verification_method_to_completed_submitters.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index bb59b018..9b5ee95b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 4cad84bf..ae85f346 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -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] diff --git a/lib/templates/build_annotations.rb b/lib/templates/build_annotations.rb index b654198b..9a933f99 100644 --- a/lib/templates/build_annotations.rb +++ b/lib/templates/build_annotations.rb @@ -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://') diff --git a/spec/system/api_settings_spec.rb b/spec/system/api_settings_spec.rb index a14ef36d..6f796add 100644 --- a/spec/system/api_settings_spec.rb +++ b/spec/system/api_settings_spec.rb @@ -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 diff --git a/spec/system/profile_settings_spec.rb b/spec/system/profile_settings_spec.rb index 3c05a664..9cce6088 100644 --- a/spec/system/profile_settings_spec.rb +++ b/spec/system/profile_settings_spec.rb @@ -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 diff --git a/spec/system/team_settings_spec.rb b/spec/system/team_settings_spec.rb index 01ce568f..693f31df 100644 --- a/spec/system/team_settings_spec.rb +++ b/spec/system/team_settings_spec.rb @@ -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'