From 7224a1ccec0a5f27a2f44d433379c87c706256bb Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 1 Sep 2025 13:36:55 +0300 Subject: [PATCH 01/16] adjust expire link --- .../api/active_storage_blobs_proxy_controller.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/active_storage_blobs_proxy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_controller.rb index 8df3b523..72575fff 100644 --- a/app/controllers/api/active_storage_blobs_proxy_controller.rb +++ b/app/controllers/api/active_storage_blobs_proxy_controller.rb @@ -45,16 +45,17 @@ 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) From 1e9a181e60005b4e3b4b78a323e7e6612a288199 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 1 Sep 2025 13:47:54 +0300 Subject: [PATCH 02/16] fix typo --- app/controllers/api/active_storage_blobs_proxy_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/active_storage_blobs_proxy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_controller.rb index 72575fff..a542c637 100644 --- a/app/controllers/api/active_storage_blobs_proxy_controller.rb +++ b/app/controllers/api/active_storage_blobs_proxy_controller.rb @@ -57,7 +57,7 @@ module Api 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 From f6850b54276fdb0d3b1f7f852353896b00493e09 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 1 Sep 2025 14:09:00 +0300 Subject: [PATCH 03/16] add completed submitters verification method --- app/jobs/process_submitter_completion_job.rb | 9 +++++++- app/models/completed_submitter.rb | 21 ++++++++++--------- ...fication_method_to_completed_submitters.rb | 7 +++++++ db/schema.rb | 3 ++- 4 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 db/migrate/20250901110606_add_verification_method_to_completed_submitters.rb diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index 49f79fcf..ab98895f 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.size, + 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..ae13647a 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 # 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 From 7357fa487120e60e6dba2ed8eb9fcfd162bcd47d Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 1 Sep 2025 18:19:01 +0300 Subject: [PATCH 04/16] adjust settings nav links --- app/views/shared/_settings_nav.html.erb | 4 +++- app/views/webhook_settings/show.html.erb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) 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/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' %> From 5d75ee2e4750c6f068987e161988e0e9f016c875 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 2 Sep 2025 21:17:25 +0300 Subject: [PATCH 05/16] filter export --- app/controllers/submissions_export_controller.rb | 3 +++ app/views/submissions_export/new.html.erb | 5 +++-- app/views/templates/show.html.erb | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) 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/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 %> From 9a8b72883c3cf0cab19746af485c1ad10abbf6b2 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 3 Sep 2025 12:50:16 +0300 Subject: [PATCH 06/16] fix relation --- app/models/completed_submitter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/completed_submitter.rb b/app/models/completed_submitter.rb index ae13647a..25c36e3a 100644 --- a/app/models/completed_submitter.rb +++ b/app/models/completed_submitter.rb @@ -30,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 From 54e064b0fc983de91f989ef2c4fc473558bdf5d7 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 4 Sep 2025 16:13:29 +0300 Subject: [PATCH 07/16] fix fields sort --- app/javascript/submission_form/form.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 + } }) } From 4c1ccd65bffa4f313a18ab38c2ede0c3e07b236c Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 4 Sep 2025 18:16:00 +0300 Subject: [PATCH 08/16] fix condition field value --- app/views/submissions/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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']] %> From c04bb2d7cff62875d2d84fe7e645663b6740c5ea Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 4 Sep 2025 21:19:43 +0300 Subject: [PATCH 09/16] allow to disable user 2fa --- app/controllers/users_controller.rb | 4 ++-- app/views/users/_form.html.erb | 22 +++++++++++++++------- config/locales/i18n.yml | 12 ++++++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8f433577..2e361097 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -54,7 +54,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[otp_required_for_login role] : nil))) 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 +83,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/views/users/_form.html.erb b/app/views/users/_form.html.erb index 3df7cb08..304748e5 100644 --- a/app/views/users/_form.html.erb +++ b/app/views/users/_form.html.erb @@ -1,12 +1,14 @@ <%= 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' %> @@ -17,6 +19,12 @@ <%= f.password_field :password, required: user.new_record?, class: 'base-input' %>
    <% 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? %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index c7a607f9..ef30b27b 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -24,6 +24,8 @@ en: &en thanks: Thanks private: Private select: Select + enabled: Enabled + disabled: Disabled party: Party edit_order: Edit Order expirable_file_download_links: Expirable file download links @@ -902,6 +904,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 @@ -1784,6 +1788,8 @@ es: &es range_without_total: "%{from}-%{to} eventos" it: &it + 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 @@ -2666,6 +2672,8 @@ it: &it range_without_total: "%{from}-%{to} eventi" fr: &fr + 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 @@ -3551,6 +3559,8 @@ fr: &fr range_without_total: "%{from} à %{to} événements" pt: &pt + 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 @@ -4434,6 +4444,8 @@ pt: &pt range_without_total: "%{from}-%{to} eventos" de: &de + 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 From f40f1d25dede16e373b8af2c784d6c275f62702d Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Fri, 5 Sep 2025 09:29:20 +0300 Subject: [PATCH 10/16] reveal API key with user password --- .../reveal_access_token_controller.rb | 21 ++++++++++++++++++ app/views/api_settings/index.html.erb | 19 ++++++++++++---- .../_access_token.html.erb | 2 ++ app/views/reveal_access_token/show.html.erb | 14 ++++++++++++ config/locales/i18n.yml | 18 +++++++++++++++ config/routes.rb | 1 + spec/system/api_settings_spec.rb | 22 +++++++++++++++++++ 7 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 app/controllers/reveal_access_token_controller.rb create mode 100644 app/views/reveal_access_token/_access_token.html.erb create mode 100644 app/views/reveal_access_token/show.html.erb 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/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/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/config/locales/i18n.yml b/config/locales/i18n.yml index ef30b27b..6af9db8d 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -798,6 +798,9 @@ 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. submission_sources: api: API bulk: Bulk Send @@ -1682,6 +1685,9 @@ 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. submission_sources: api: API bulk: Envío masivo @@ -2566,6 +2572,9 @@ 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. submission_sources: api: API bulk: Invio massivo @@ -3453,6 +3462,9 @@ 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. submission_sources: api: API bulk: Envoi en masse @@ -4338,6 +4350,9 @@ 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. submission_sources: api: API bulk: Envio em massa @@ -5223,6 +5238,9 @@ 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. submission_sources: api: API bulk: Massenversand diff --git a/config/routes.rb b/config/routes.rb index 76ed7af9..072faa4e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -179,6 +179,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/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 From ea2b7f20e7176f7c1b634e95cbe4de0a27d2065b Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 5 Sep 2025 14:59:20 +0300 Subject: [PATCH 11/16] count segments --- app/jobs/process_submitter_completion_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index ab98895f..a4b58a5a 100644 --- a/app/jobs/process_submitter_completion_job.rb +++ b/app/jobs/process_submitter_completion_job.rb @@ -49,7 +49,7 @@ class ProcessSubmitterCompletionJob account_id: submission.account_id, template_id: submission.template_id, source: submission.source, - sms_count: sms_events.size, + sms_count: sms_events.sum { |e| e.data['segments'] || 1 }, verification_method: complete_verification_event&.data&.dig('method'), completed_at: submitter.completed_at ) From aeea61905993910f20526db98b48aef069860a4a Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 5 Sep 2025 15:29:17 +0300 Subject: [PATCH 12/16] skip annotation --- lib/submissions/generate_result_attachments.rb | 2 +- lib/templates/build_annotations.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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://') From f7be74eb73453b26a7c570c33f19442c26d215d2 Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Fri, 5 Sep 2025 21:48:22 +0300 Subject: [PATCH 13/16] improve user password reset --- app/controllers/passwords_controller.rb | 4 ++ app/controllers/profile_controller.rb | 4 +- app/controllers/users_controller.rb | 9 ++++- app/javascript/application.js | 2 + app/javascript/elements/visible_on_input.js | 14 +++++++ app/views/profile/index.html.erb | 24 ++++++++---- config/locales/i18n.yml | 18 +++++++++ config/routes.rb | 4 +- spec/system/profile_settings_spec.rb | 43 ++++++++++++++++++++- 9 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 app/javascript/elements/visible_on_input.js diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 7ca14716..5ae3ff32 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 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/users_controller.rb b/app/controllers/users_controller.rb index 2e361097..46daba2d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class UsersController < ApplicationController - load_and_authorize_resource :user, only: %i[index edit update destroy] + load_and_authorize_resource :user, only: %i[index edit update destroy resend_reset_password] before_action :build_user, only: %i[new create] authorize_resource :user, only: %i[new create] @@ -71,6 +71,13 @@ class UsersController < ApplicationController redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_removed') end + def resend_reset_password + current_user.send_reset_password_instructions + + redirect_back fallback_location: settings_users_path, + notice: I18n.t('you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes') + end + private def role_valid?(role) diff --git a/app/javascript/application.js b/app/javascript/application.js index 7b51186d..0352f3f9 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -39,6 +39,7 @@ import RequiredCheckboxGroup from './elements/required_checkbox_group' import PageContainer from './elements/page_container' import EmailEditor from './elements/email_editor' import MountOnClick from './elements/mount_on_click' +import VisibleOnInput from './elements/visible_on_input' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -113,6 +114,7 @@ safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup) safeRegisterElement('page-container', PageContainer) safeRegisterElement('email-editor', EmailEditor) safeRegisterElement('mount-on-click', MountOnClick) +safeRegisterElement('visible-on-input', VisibleOnInput) safeRegisterElement('template-builder', class extends HTMLElement { connectedCallback () { diff --git a/app/javascript/elements/visible_on_input.js b/app/javascript/elements/visible_on_input.js new file mode 100644 index 00000000..37292173 --- /dev/null +++ b/app/javascript/elements/visible_on_input.js @@ -0,0 +1,14 @@ +export default class extends HTMLElement { + connectedCallback () { + this.input = document.getElementById(this.dataset.inputId) + + this.input.addEventListener('input', () => { + if (this.input.value.trim().length > 0) { + this.classList.remove('hidden') + } else { + this.classList.add('hidden') + this.querySelectorAll('input').forEach(input => { input.value = '' }) + } + }) + } +} diff --git a/app/views/profile/index.html.erb b/app/views/profile/index.html.erb index 6732d907..90a2475a 100644 --- a/app/views/profile/index.html.erb +++ b/app/views/profile/index.html.erb @@ -59,14 +59,24 @@ <%= 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' %> -
    + +
    + <%= 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' %> + + <%= t('dont_remember_your_current_password_click_here_to_reset_it_html', link: new_user_password_url) %> + +
    +
    + <%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %> +
    +
    <% end %> + <%= button_to nil, resend_reset_password_users_path, id: 'resend_password_button', class: 'hidden', data: { turbo_confirm: t('are_you_sure_') } %>

    <%= t('two_factor_authentication') %>

    diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 6af9db8d..07604244 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -801,6 +801,9 @@ en: &en 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.' + you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: You will receive an email with password reset instructions in a few minutes. submission_sources: api: API bulk: Bulk Send @@ -1688,6 +1691,9 @@ es: &es 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.' + you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: Recibirás un correo electrónico con las instrucciones para restablecer tu contraseña en unos minutos. submission_sources: api: API bulk: Envío masivo @@ -2575,6 +2581,9 @@ it: &it 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.' + you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: Riceverai un'email con le istruzioni per reimpostare la password entro pochi minuti. submission_sources: api: API bulk: Invio massivo @@ -3465,6 +3474,9 @@ fr: &fr 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.' + you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: Vous recevrez un e-mail avec les instructions de réinitialisation de votre mot de passe dans quelques minutes. submission_sources: api: API bulk: Envoi en masse @@ -4353,6 +4365,9 @@ pt: &pt 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.' + you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: Você receberá um e-mail com as instruções para redefinir sua senha em alguns minutos. submission_sources: api: API bulk: Envio em massa @@ -5241,6 +5256,9 @@ de: &de 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.' + you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: Sie erhalten in wenigen Minuten eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts. submission_sources: api: API bulk: Massenversand diff --git a/config/routes.rb b/config/routes.rb index 072faa4e..ecf1c3ed 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 + post :resend_reset_password, on: :collection + 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' diff --git a/spec/system/profile_settings_spec.rb b/spec/system/profile_settings_spec.rb index 3c05a664..b7dc5691 100644 --- a/spec/system/profile_settings_spec.rb +++ b/spec/system/profile_settings_spec.rb @@ -16,7 +16,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 +46,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 +56,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('You will receive an email with password reset instructions in a few minutes.') + + 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 From 284204fd7830db4017187e0eea9c67dd53a54eb2 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 6 Sep 2025 09:52:35 +0300 Subject: [PATCH 14/16] refactor, use invalid class --- app/controllers/users_controller.rb | 9 +------- .../users_send_reset_password_controller.rb | 20 +++++++++++++++++ app/javascript/application.js | 2 -- app/javascript/elements/visible_on_input.js | 14 ------------ app/views/profile/index.html.erb | 22 +++++++++---------- config/routes.rb | 2 +- 6 files changed, 33 insertions(+), 36 deletions(-) create mode 100644 app/controllers/users_send_reset_password_controller.rb delete mode 100644 app/javascript/elements/visible_on_input.js diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 46daba2d..2e361097 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class UsersController < ApplicationController - load_and_authorize_resource :user, only: %i[index edit update destroy resend_reset_password] + load_and_authorize_resource :user, only: %i[index edit update destroy] before_action :build_user, only: %i[new create] authorize_resource :user, only: %i[new create] @@ -71,13 +71,6 @@ class UsersController < ApplicationController redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_removed') end - def resend_reset_password - current_user.send_reset_password_instructions - - redirect_back fallback_location: settings_users_path, - notice: I18n.t('you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes') - end - private def role_valid?(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..e75695af --- /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('you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes') + end + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index 0352f3f9..7b51186d 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -39,7 +39,6 @@ import RequiredCheckboxGroup from './elements/required_checkbox_group' import PageContainer from './elements/page_container' import EmailEditor from './elements/email_editor' import MountOnClick from './elements/mount_on_click' -import VisibleOnInput from './elements/visible_on_input' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -114,7 +113,6 @@ safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup) safeRegisterElement('page-container', PageContainer) safeRegisterElement('email-editor', EmailEditor) safeRegisterElement('mount-on-click', MountOnClick) -safeRegisterElement('visible-on-input', VisibleOnInput) safeRegisterElement('template-builder', class extends HTMLElement { connectedCallback () { diff --git a/app/javascript/elements/visible_on_input.js b/app/javascript/elements/visible_on_input.js deleted file mode 100644 index 37292173..00000000 --- a/app/javascript/elements/visible_on_input.js +++ /dev/null @@ -1,14 +0,0 @@ -export default class extends HTMLElement { - connectedCallback () { - this.input = document.getElementById(this.dataset.inputId) - - this.input.addEventListener('input', () => { - if (this.input.value.trim().length > 0) { - this.classList.remove('hidden') - } else { - this.classList.add('hidden') - this.querySelectorAll('input').forEach(input => { input.value = '' }) - } - }) - } -} diff --git a/app/views/profile/index.html.erb b/app/views/profile/index.html.erb index 90a2475a..38461675 100644 --- a/app/views/profile/index.html.erb +++ b/app/views/profile/index.html.erb @@ -54,12 +54,10 @@

    <%= 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' %> -
    - + <%= 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' %> @@ -67,16 +65,18 @@
    <%= f.label :current_password, t('current_password'), class: 'label' %> <%= f.password_field :current_password, autocomplete: 'current-password', class: 'base-input' %> - - <%= t('dont_remember_your_current_password_click_here_to_reset_it_html', link: new_user_password_url) %> - + <% if Accounts.can_send_emails?(current_account) %> + + <%= t('dont_remember_your_current_password_click_here_to_reset_it_html', link: new_user_password_url) %> + + <% end %>
    <%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
    - +
    <% end %> - <%= button_to nil, resend_reset_password_users_path, id: 'resend_password_button', class: 'hidden', data: { turbo_confirm: t('are_you_sure_') } %> + <%= 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/config/routes.rb b/config/routes.rb index ecf1c3ed..1947b4b4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -66,7 +66,7 @@ Rails.application.routes.draw do resource :newsletter, only: %i[show update] resources :enquiries, only: %i[create] resources :users, only: %i[new create edit update destroy] do - post :resend_reset_password, on: :collection + 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] From 1245ff2cce6029a1dda1db42fe4a0155ab65e92b Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 6 Sep 2025 10:41:51 +0300 Subject: [PATCH 15/16] disable edit password --- app/controllers/users_controller.rb | 2 +- app/views/users/_form.html.erb | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2e361097..da60da91 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -54,7 +54,7 @@ class UsersController < ApplicationController @user.account = account end - if @user.update(attrs.except(*(current_user == @user ? %i[otp_required_for_login 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 diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb index 304748e5..19b9f0f2 100644 --- a/app/views/users/_form.html.erb +++ b/app/views/users/_form.html.erb @@ -14,10 +14,12 @@ <%= f.label :email, t('email'), class: 'label' %> <%= f.email_field :email, required: true, class: 'base-input' %>
    -
    - <%= 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, required: true, class: 'base-input' %> +
    + <% end %> <% if f.object != current_user %> <% if user.otp_required_for_login %>
    From 4dc6149530f57e76324c9d4437b77217b706b27f Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 6 Sep 2025 11:10:45 +0300 Subject: [PATCH 16/16] add reset password link --- app/controllers/passwords_controller.rb | 6 ++++++ app/controllers/users_controller.rb | 1 + .../users_send_reset_password_controller.rb | 2 +- app/views/profile/index.html.erb | 4 ++-- app/views/users/_form.html.erb | 10 +++++++++- config/locales/i18n.yml | 19 ++++++++++++------- spec/system/profile_settings_spec.rb | 5 ++++- spec/system/team_settings_spec.rb | 1 - 8 files changed, 35 insertions(+), 13 deletions(-) diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 5ae3ff32..60de5b12 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -20,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/users_controller.rb b/app/controllers/users_controller.rb index da60da91..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 diff --git a/app/controllers/users_send_reset_password_controller.rb b/app/controllers/users_send_reset_password_controller.rb index e75695af..f6a8af82 100644 --- a/app/controllers/users_send_reset_password_controller.rb +++ b/app/controllers/users_send_reset_password_controller.rb @@ -14,7 +14,7 @@ class UsersSendResetPasswordController < ApplicationController @user.send_reset_password_instructions redirect_back fallback_location: settings_users_path, - notice: I18n.t('you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes') + notice: I18n.t('an_email_with_password_reset_instructions_has_been_sent') end end end diff --git a/app/views/profile/index.html.erb b/app/views/profile/index.html.erb index 38461675..c5422b00 100644 --- a/app/views/profile/index.html.erb +++ b/app/views/profile/index.html.erb @@ -57,7 +57,7 @@ <%= 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' %> @@ -67,7 +67,7 @@ <%= 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', link: new_user_password_url) %> + <%= t('dont_remember_your_current_password_click_here_to_reset_it_html') %> <% end %>
    diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb index 19b9f0f2..5ceaeabc 100644 --- a/app/views/users/_form.html.erb +++ b/app/views/users/_form.html.erb @@ -13,11 +13,16 @@
    <%= 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 %>
    <% if user.new_record? && !Docuseal.multitenant? %>
    <%= f.label :password, t('password'), class: 'label' %> - <%= f.password_field :password, required: true, class: 'base-input' %> + <%= f.password_field :password, class: 'base-input' %>
    <% end %> <% if f.object != current_user %> @@ -37,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/config/locales/i18n.yml b/config/locales/i18n.yml index 07604244..18da4440 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -27,6 +27,7 @@ en: &en 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 @@ -46,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 @@ -803,7 +804,7 @@ en: &en 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.' - you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: You will receive an email with password reset instructions in a few minutes. + 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 @@ -1693,7 +1694,7 @@ es: &es 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.' - you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: Recibirás un correo electrónico con las instrucciones para restablecer tu contraseña en unos minutos. + 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 @@ -1800,6 +1801,7 @@ 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 @@ -2583,7 +2585,7 @@ it: &it 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.' - you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: Riceverai un'email con le istruzioni per reimpostare la password entro pochi minuti. + 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 @@ -2690,6 +2692,7 @@ 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 @@ -3476,7 +3479,7 @@ fr: &fr 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.' - you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: Vous recevrez un e-mail avec les instructions de réinitialisation de votre mot de passe dans quelques minutes. + 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 @@ -3583,6 +3586,7 @@ 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 @@ -4367,7 +4371,7 @@ pt: &pt 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.' - you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: Você receberá um e-mail com as instruções para redefinir sua senha em alguns minutos. + 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 @@ -4474,6 +4478,7 @@ 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 @@ -5258,7 +5263,7 @@ de: &de 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.' - you_will_receive_an_email_with_password_reset_instructions_in_a_few_minutes: Sie erhalten in wenigen Minuten eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts. + 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/spec/system/profile_settings_spec.rb b/spec/system/profile_settings_spec.rb index b7dc5691..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 @@ -79,7 +82,7 @@ RSpec.describe 'Profile Settings' do find('label', text: 'Click here').click end - expect(page).to have_content('You will receive an email with password reset instructions in a few minutes.') + expect(page).to have_content('An email with password reset instructions has been sent.') email = ActionMailer::Base.deliveries.last reset_password_url = email.body 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'