From 7537d05d1f23ee3824e9ccce5a9bf5046edadff6 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 28 Sep 2026 15:30:09 +0300 Subject: [PATCH 01/34] fix builder resize --- app/javascript/template_builder/builder.vue | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 1ee213493..9ed5d3205 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -1644,14 +1644,14 @@ export default { this.redoStack = [] this.$nextTick(() => { - this.onWindowResize() + this.resizeObserver = new ResizeObserver(this.onResize) + this.resizeObserver.observe(this.$el.getRootNode().querySelector('div[data-v-app]')) }) document.addEventListener('keyup', this.onKeyUp) document.addEventListener('template-builder:apply-revision', this.onApplyRevisionEvent) window.addEventListener('keydown', this.onKeyDown) - window.addEventListener('resize', this.onWindowResize) window.addEventListener('dragleave', this.onWindowDragLeave) this.$nextTick(() => { @@ -1675,7 +1675,7 @@ export default { document.removeEventListener('template-builder:apply-revision', this.onApplyRevisionEvent) window.removeEventListener('keydown', this.onKeyDown) - window.removeEventListener('resize', this.onWindowResize) + this.resizeObserver.disconnect() window.removeEventListener('dragleave', this.onWindowDragLeave) }, beforeUpdate () { @@ -2443,13 +2443,15 @@ export default { this.save() } }, - onWindowResize (e) { + onResize () { const breakpointLg = 1024 const breakpointMd = 768 const width = this.$el.getRootNode().querySelector('div[data-v-app]').offsetWidth - this.isBreakpointLg = width < breakpointLg - this.isBreakpointMd = width < breakpointMd + if (width) { + this.isBreakpointLg = width < breakpointLg + this.isBreakpointMd = width < breakpointMd + } }, onPagesWheel (event) { const isFastScroll = Math.abs(event.deltaY) > 1 From a5c6f4e40c463a34e32e5ab89426d1bded53710d Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 28 Sep 2026 15:32:26 +0300 Subject: [PATCH 02/34] update gem --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5dc77e9f3..a1472da2d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -497,7 +497,7 @@ GEM ruby-vips (2.3.0) ffi (~> 1.12) logger - rubyzip (3.2.2) + rubyzip (3.7.0) securerandom (0.4.1) semantic_range (3.1.1) shakapacker (9.7.0) From db8e5efbd50ce4e9a45d32abe6f34167475d62b6 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 28 Sep 2026 17:54:47 +0300 Subject: [PATCH 03/34] adjust i18n --- README.md | 2 +- config/locales/i18n.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 64a917ade..e2f6e8b92 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d - Files storage on disk or AWS S3, Google Storage, Azure Cloud - Automatic PDF eSignature - PDF signature verification -- Users management +- User management - Mobile-optimized - 7 UI languages with signing available in 14 languages - API and Webhooks for integrations diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 436d58683..adc5073c7 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -683,7 +683,7 @@ en: &en thirty_days: 30 days free: Free unlimited_documents_storage: Unlimited documents storage - users_management: Users management + user_management: User management ten_field_types: 10 field types multiple_signers: Multiple signers automatic_esignature: Automatic eSignature @@ -1840,7 +1840,7 @@ es: &es thirty_days: 30 días free: Gratis unlimited_documents_storage: Almacenamiento ilimitado - users_management: Gestión de usuarios + user_management: Gestión de usuarios ten_field_types: 10 tipos de campos multiple_signers: Múltiples firmantes automatic_esignature: Firma electrónica automática @@ -3000,7 +3000,7 @@ it: &it thirty_days: 30 giorni free: Gratuito unlimited_documents_storage: Archiviazione illimitata di documenti - users_management: Gestione utenti + user_management: Gestione utenti ten_field_types: 10 tipi di campi multiple_signers: Firmatari multipli automatic_esignature: Firma elettronica automatica @@ -4146,7 +4146,7 @@ fr: &fr thirty_days: 30 jours free: Gratuit unlimited_documents_storage: Stockage de documents illimité - users_management: Gestion des utilisateurs + user_management: Gestion des utilisateurs ten_field_types: 10 types de champs multiple_signers: Plusieurs signataires automatic_esignature: Signature électronique automatique @@ -5302,7 +5302,7 @@ pt: &pt thirty_days: 30 dias free: Gratuito unlimited_documents_storage: Armazenamento ilimitado - users_management: Gerenciamento de usuários + user_management: Gerenciamento de usuários ten_field_types: 10 tipos de campo multiple_signers: Múltiplos signatários automatic_esignature: Assinatura eletrônica automática @@ -6461,7 +6461,7 @@ de: &de thirty_days: 30 Tage free: Kostenlos unlimited_documents_storage: Unbegrenzter Dokumentenspeicher - users_management: Benutzerverwaltung + user_management: Benutzerverwaltung ten_field_types: 10 Feldtypen multiple_signers: Mehrere Unterzeichner automatic_esignature: Automatische E‑Signatur @@ -8148,7 +8148,7 @@ nl: &nl thirty_days: 30 dagen free: Gratis unlimited_documents_storage: Onbeperkte documentopslag - users_management: Gebruikersbeheer + user_management: Gebruikersbeheer ten_field_types: 10 veldtypen multiple_signers: Meerdere ondertekenaars automatic_esignature: Automatische e-handtekening From 4190544edc95d398da3b4df6fc842f805785822a Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 28 Sep 2026 23:28:13 +0300 Subject: [PATCH 04/34] use ivar --- .../submit_form_prefill_attachments_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/submit_form_prefill_attachments_controller.rb b/app/controllers/submit_form_prefill_attachments_controller.rb index d56dc2a45..dbd720c6a 100644 --- a/app/controllers/submit_form_prefill_attachments_controller.rb +++ b/app/controllers/submit_form_prefill_attachments_controller.rb @@ -4,19 +4,19 @@ class SubmitFormPrefillAttachmentsController < ActionController::API include ActiveStorage::SetCurrent def create - submitter = Submitter.find_by!(slug: params[:submit_form_slug]) + @submitter = Submitter.find_by!(slug: params[:submit_form_slug]) - unless can_upload?(submitter) + unless can_upload?(@submitter) return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content end uuid, slug = ApplicationRecord.signed_id_verifier.verify(params[:prefill_token], purpose: :prefill_attachment) - raise ActiveRecord::RecordNotFound if slug != submitter.slug + raise ActiveRecord::RecordNotFound if slug != @submitter.slug source_attachment = ActiveStorage::Attachment.find_by!(uuid:) - attachment = submitter.attachments_attachments.find_or_create_by!(blob_id: source_attachment.blob_id) + attachment = @submitter.attachments_attachments.find_or_create_by!(blob_id: source_attachment.blob_id) render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type]) end From ce9ab691ec3a3ae0a16d8b5d5b32bf64ddbf09d1 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 30 Sep 2026 12:57:48 +0300 Subject: [PATCH 05/34] fix document metadata --- app/controllers/submit_form_metadata_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/submit_form_metadata_controller.rb b/app/controllers/submit_form_metadata_controller.rb index 55c95c830..32faba108 100644 --- a/app/controllers/submit_form_metadata_controller.rb +++ b/app/controllers/submit_form_metadata_controller.rb @@ -17,12 +17,12 @@ class SubmitFormMetadataController < ApplicationController submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] } end - ActiveRecord::Associations::Preloader.new(records: documents, associations: %i[blob record]).call + ActiveRecord::Associations::Preloader.new(records: documents, associations: :blob).call text_runs = documents.to_h do |document| [ document.uuid, - DocumentMetadatas.find_or_create_for_document(document, account_id: document.record.account_id).text_runs + DocumentMetadatas.find_or_create_for_document(document, account_id: submission.account_id).text_runs ] end From acce6195b093b86caee1f434e69d6496935b69a1 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 30 Sep 2026 13:38:47 +0300 Subject: [PATCH 06/34] rescue validation errors --- app/controllers/api/submissions_controller.rb | 2 +- app/controllers/api/submitters_controller.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index e55cbe03d..a00f1d543 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -98,7 +98,7 @@ module Api render json: build_create_json(submissions) rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError, - DownloadUtils::UnableToDownload => e + Submitters::SubmitValues::ValidationError, DownloadUtils::UnableToDownload => e Rollbar.warning(e) if defined?(Rollbar) render json: { error: e.message }, status: :unprocessable_content diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index 555dc92dd..313f84fc1 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -78,7 +78,8 @@ module Api render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true, with_events: false, params:) - rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e + rescue Submitters::NormalizeValues::BaseError, Submitters::SubmitValues::ValidationError, + DownloadUtils::UnableToDownload => e Rollbar.warning(e) if defined?(Rollbar) render json: { error: e.message }, status: :unprocessable_content From 91147d2f48a6dbb0c399ff253bf8f1f6ecd05ed5 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 30 Sep 2026 19:22:41 +0300 Subject: [PATCH 07/34] fix verify pdf --- app/views/verify_pdf_signature/_result.html.erb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/views/verify_pdf_signature/_result.html.erb b/app/views/verify_pdf_signature/_result.html.erb index 73dabae28..560e1860c 100644 --- a/app/views/verify_pdf_signature/_result.html.erb +++ b/app/views/verify_pdf_signature/_result.html.erb @@ -35,10 +35,12 @@ <%= svg_icon('user', class: 'w-5 h-5 inline') %> <%= signature.reason %> -
- <%= svg_icon('calendar', class: 'w-5 h-5 inline') %> - <%= l(signature.signing_time.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %> -
+ <% if signature.signing_time %> +
+ <%= svg_icon('calendar', class: 'w-5 h-5 inline') %> + <%= l(signature.signing_time.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %> +
+ <% end %>
<%= svg_icon('certificate', class: 'w-5 h-5 inline') %> <%= signature.common_name %> From 0905e7d21559b0ef14d8d2de4bf01b4dff384992 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 30 Sep 2026 19:23:58 +0300 Subject: [PATCH 08/34] reduce limit --- app/views/template_folders/show.html.erb | 2 +- app/views/templates_dashboard/index.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/template_folders/show.html.erb b/app/views/template_folders/show.html.erb index 5f03dc8dc..0d463c693 100644 --- a/app/views/template_folders/show.html.erb +++ b/app/views/template_folders/show.html.erb @@ -82,7 +82,7 @@
<% templates_order_select_html = capture do %> <% if params[:q].blank? && @pagy.pages > 1 && !can?(:manage, :countless) %> - <%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000, selected_order: %> + <%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 1_000, selected_order: %> <% end %> <% end %> <%= render 'shared/pagination', pagy: @pagy, items_name: @templates.present? ? 'templates' : 'template_folders', right_additional_html: templates_order_select_html %> diff --git a/app/views/templates_dashboard/index.html.erb b/app/views/templates_dashboard/index.html.erb index 65c121375..d54056c5e 100644 --- a/app/views/templates_dashboard/index.html.erb +++ b/app/views/templates_dashboard/index.html.erb @@ -52,7 +52,7 @@ <% end %> <% templates_order_select_html = capture do %> <% if params[:q].blank? && @pagy.pages > 1 && !can?(:manage, :countless) %> - <%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000, selected_order: %> + <%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 1_000, selected_order: %> <% end %> <% end %> <% if @template_folders.present? || @show_default_folder || @show_shared_folder %> From a847e7aa9cc9b754200ecb84436d5340af2d2dd4 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 30 Sep 2026 19:56:12 +0300 Subject: [PATCH 09/34] custom otp email --- app/mailers/submitter_mailer.rb | 10 +++++++++- app/mailers/template_mailer.rb | 12 +++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb index 3a8c191bb..f8e59714c 100644 --- a/app/mailers/submitter_mailer.rb +++ b/app/mailers/submitter_mailer.rb @@ -185,8 +185,16 @@ class SubmitterMailer < ApplicationMailer assign_message_metadata('otp_verification_email', submitter) + from = + if Docuseal.multitenant? && + AccountConfig.exists?(account_id: submitter.account_id, key: 'custom_otp_email', value: true) + from_address_for_submitter(submitter) + else + default_params[:from] + end + I18n.with_locale(locale || submitter.account.locale) do - mail(to: submitter.email, subject: I18n.t('email_verification')) + mail(to: submitter.email, from:, subject: I18n.t('email_verification')) end end diff --git a/app/mailers/template_mailer.rb b/app/mailers/template_mailer.rb index bef49ba83..c92fb62f6 100644 --- a/app/mailers/template_mailer.rb +++ b/app/mailers/template_mailer.rb @@ -9,6 +9,16 @@ class TemplateMailer < ApplicationMailer assign_message_metadata('otp_verification_email', template) - mail(to: email, subject: I18n.t('email_verification')) + from = + if Docuseal.multitenant? && + AccountConfig.exists?(account_id: template.account_id, key: 'custom_otp_email', value: true) + put_metadata('from_user_id' => template.author_id) + + template.author.friendly_name + else + default_params[:from] + end + + mail(to: email, from:, subject: I18n.t('email_verification')) end end From e187644716dc4cfa1afc695213097623b76f5812 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 30 Sep 2026 11:56:04 +0300 Subject: [PATCH 10/34] refactor otp --- .../submit_form_email_2fas_controller.rb | 6 +- app/mailers/submitter_mailer.rb | 3 +- app/mailers/template_mailer.rb | 2 +- app/models/otp_challenge.rb | 21 +++++++ .../20260930061927_create_otp_challenges.rb | 17 +++++ db/schema.rb | 13 +++- lib/email_verification_codes.rb | 27 -------- lib/otp_challenges.rb | 63 +++++++++++++++++++ lib/submitters.rb | 6 +- 9 files changed, 121 insertions(+), 37 deletions(-) create mode 100644 app/models/otp_challenge.rb create mode 100644 db/migrate/20260930061927_create_otp_challenges.rb delete mode 100644 lib/email_verification_codes.rb create mode 100644 lib/otp_challenges.rb diff --git a/app/controllers/submit_form_email_2fas_controller.rb b/app/controllers/submit_form_email_2fas_controller.rb index 511daaf65..6f95e7544 100644 --- a/app/controllers/submit_form_email_2fas_controller.rb +++ b/app/controllers/submit_form_email_2fas_controller.rb @@ -10,12 +10,10 @@ class SubmitFormEmail2fasController < ApplicationController def create RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true) - RateLimit.call("verify-2fa-code-1h-#{@submitter.id}", limit: 10, ttl: 1.hour, enabled: true) - RateLimit.call("verify-2fa-code-1d-#{@submitter.id}", limit: 20, ttl: 1.day, enabled: true) - value = [@submitter.email.downcase.strip, @submitter.slug].join(':') + value = [@submitter.email.downcase.squish, @submitter.slug].join(':') - if EmailVerificationCodes.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value) + if OtpChallenges.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value, purpose: 'form_email_2fa') SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email }) cookies.encrypted[:email_2fa_slug] = diff --git a/app/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb index f8e59714c..3a3c1e990 100644 --- a/app/mailers/submitter_mailer.rb +++ b/app/mailers/submitter_mailer.rb @@ -181,7 +181,8 @@ class SubmitterMailer < ApplicationMailer def otp_verification_email(submitter, locale: nil) @current_account = submitter.account @submitter = submitter - @otp_code = EmailVerificationCodes.generate([submitter.email.downcase.strip, submitter.slug].join(':')) + @otp_code = OtpChallenges.generate([submitter.email.downcase.squish, submitter.slug].join(':'), + purpose: 'form_email_2fa') assign_message_metadata('otp_verification_email', submitter) diff --git a/app/mailers/template_mailer.rb b/app/mailers/template_mailer.rb index c92fb62f6..881d0fb4f 100644 --- a/app/mailers/template_mailer.rb +++ b/app/mailers/template_mailer.rb @@ -5,7 +5,7 @@ class TemplateMailer < ApplicationMailer @current_account = template.account @template = template - @otp_code = EmailVerificationCodes.generate([email.downcase.strip, template.slug].join(':')) + @otp_code = OtpChallenges.generate([email.downcase.squish, template.slug].join(':'), purpose: 'form_email_2fa') assign_message_metadata('otp_verification_email', template) diff --git a/app/models/otp_challenge.rb b/app/models/otp_challenge.rb new file mode 100644 index 000000000..f4e35dbdf --- /dev/null +++ b/app/models/otp_challenge.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: otp_challenges +# +# id :bigint not null, primary key +# attempts :integer default(0), not null +# expire_at :datetime not null +# key :string not null +# nonce :string not null +# verified_at :datetime +# version :integer not null +# created_at :datetime not null +# +# Indexes +# +# index_otp_challenges_on_key_and_version (key,version) UNIQUE +# +class OtpChallenge < ApplicationRecord +end diff --git a/db/migrate/20260930061927_create_otp_challenges.rb b/db/migrate/20260930061927_create_otp_challenges.rb new file mode 100644 index 000000000..7876486a3 --- /dev/null +++ b/db/migrate/20260930061927_create_otp_challenges.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateOtpChallenges < ActiveRecord::Migration[8.1] + def change + create_table :otp_challenges do |t| + t.string :key, null: false + t.integer :version, null: false + t.string :nonce, null: false + t.integer :attempts, null: false, default: 0 + t.datetime :expire_at, null: false + t.datetime :verified_at + t.datetime :created_at, null: false + + t.index %i[key version], unique: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f159c4a97..58fd3dc2d 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.1].define(version: 2026_08_19_091500) do +ActiveRecord::Schema[8.1].define(version: 2026_09_30_061927) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "pg_catalog.plpgsql" @@ -322,6 +322,17 @@ ActiveRecord::Schema[8.1].define(version: 2026_08_19_091500) do t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end + create_table "otp_challenges", force: :cascade do |t| + t.integer "attempts", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "expire_at", null: false + t.string "key", null: false + t.string "nonce", null: false + t.datetime "verified_at" + t.integer "version", null: false + t.index ["key", "version"], name: "index_otp_challenges_on_key_and_version", unique: true + end + create_table "search_entries", force: :cascade do |t| t.bigint "account_id", null: false t.datetime "created_at", null: false diff --git a/lib/email_verification_codes.rb b/lib/email_verification_codes.rb deleted file mode 100644 index 8fdfaacca..000000000 --- a/lib/email_verification_codes.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module EmailVerificationCodes - DRIFT_BEHIND = 5.minutes - - module_function - - def generate(value) - totp = ROTP::TOTP.new(build_totp_secret(value)) - - totp.at(Time.current) - end - - def verify(code, value) - totp = ROTP::TOTP.new(build_totp_secret(value)) - - totp.verify(code, drift_behind: DRIFT_BEHIND) - end - - def build_totp_secret(value) - ROTP::Base32.encode( - Digest::SHA1.digest( - [Rails.application.secret_key_base, 'form_email_2fa', value].join(':') - ) - ) - end -end diff --git a/lib/otp_challenges.rb b/lib/otp_challenges.rb new file mode 100644 index 000000000..078199519 --- /dev/null +++ b/lib/otp_challenges.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module OtpChallenges + CODE_TTL = 10.minutes + REUSE_MIN_TTL = 5.minutes + MAX_ATTEMPTS = 20 + MAX_TOTAL_ATTEMPTS = 100 + ATTEMPTS_WINDOW = 23.hours + + module_function + + def generate(value, purpose:) + key = build_key(value, purpose) + latest_challenge = OtpChallenge.where(key:).order(version: :desc).first + + otp_challenge = + if latest_challenge && !latest_challenge.verified_at? && latest_challenge.expire_at > REUSE_MIN_TTL.from_now + latest_challenge + else + version = latest_challenge ? latest_challenge.version + 1 : 1 + + OtpChallenge.create_with(nonce: SecureRandom.base58(16), expire_at: CODE_TTL.from_now) + .create_or_find_by!(key:, version:) + end + + build_hotp(value, purpose, otp_challenge.nonce).at(0) + end + + def verify(code, value, purpose:) + challenges = OtpChallenge.where(key: build_key(value, purpose)) + + recent_challenges = challenges.where(expire_at: ATTEMPTS_WINDOW.ago..).order(version: :desc).to_a + + latest_challenge = recent_challenges.first + + return false if latest_challenge.nil? || latest_challenge.verified_at? || latest_challenge.expire_at.past? + + raise RateLimit::LimitApproached if recent_challenges.sum(&:attempts) >= MAX_ATTEMPTS + raise RateLimit::LimitApproached if challenges.sum(:attempts) >= MAX_TOTAL_ATTEMPTS + + if build_hotp(value, purpose, latest_challenge.nonce).verify(code.to_s, 0) + OtpChallenge.where(id: latest_challenge.id, verified_at: nil).update_all(verified_at: Time.current) == 1 + else + latest_challenge.increment!(:attempts) + + OtpChallenge.current_transaction.after_rollback { OtpChallenge.update_counters(latest_challenge.id, attempts: 1) } + + false + end + end + + def build_key(value, purpose) + Digest::SHA256.hexdigest([purpose, value].join(':')) + end + + def build_hotp(value, purpose, nonce) + ROTP::HOTP.new( + ROTP::Base32.encode( + Digest::SHA1.digest([Rails.application.secret_key_base, purpose, value, nonce].join(':')) + ) + ) + end +end diff --git a/lib/submitters.rb b/lib/submitters.rb index b7a7a7dd1..4dcd1ebd0 100644 --- a/lib/submitters.rb +++ b/lib/submitters.rb @@ -280,12 +280,12 @@ module Submitters email_digest = Digest::MD5.base64digest(submitter.email.downcase.squish) RateLimit.call("verify-2fa-code-#{email_digest}", limit: 2, ttl: 45.seconds, enabled: true) - RateLimit.call("verify-2fa-code-1h-#{email_digest}", limit: 10, ttl: 1.hour, enabled: true) - RateLimit.call("verify-2fa-code-1d-#{email_digest}", limit: 20, ttl: 1.day, enabled: true) link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':') - raise InvalidOtp, I18n.t(:invalid_code) unless EmailVerificationCodes.verify(otp, link_2fa_key) + unless OtpChallenges.verify(otp.to_s.gsub(/\D/, ''), link_2fa_key, purpose: 'form_email_2fa') + raise InvalidOtp, I18n.t(:invalid_code) + end true end From 78b0ce9828510a86953018db3fe5a0cd86155f28 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 1 Oct 2026 19:36:22 +0300 Subject: [PATCH 11/34] add iban validation --- app/javascript/submission_form/i18n.js | 14 ++++++++++++++ app/javascript/submission_form/text_step.vue | 19 +++++++++++++++++++ .../template_builder/field_settings.vue | 1 + app/javascript/template_builder/i18n.js | 7 +++++++ 4 files changed, 41 insertions(+) diff --git a/app/javascript/submission_form/i18n.js b/app/javascript/submission_form/i18n.js index 1da0bd06c..e349e56ce 100644 --- a/app/javascript/submission_form/i18n.js +++ b/app/javascript/submission_form/i18n.js @@ -13,6 +13,7 @@ const en = { must_be_valid_email: 'Must be a valid email address', must_be_valid_url: 'Must be a valid URL (starting with http:// or https://)', must_be_valid_zip: 'Must be a valid ZIP code (XXXXX or XXXXX-XXXX)', + must_be_valid_iban: 'Must be a valid IBAN', must_contain_numbers_only: 'Must contain numbers only', must_contain_letters_only: 'Must contain letters only', complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.', @@ -132,6 +133,7 @@ const es = { must_be_valid_email: 'Debe ser una dirección de correo electrónico válida', must_be_valid_url: 'Debe ser una URL válida (que comience con http:// o https://)', must_be_valid_zip: 'Debe ser un ZIP válido (XXXXX o XXXXX-XXXX)', + must_be_valid_iban: 'Debe ser un IBAN válido', must_contain_numbers_only: 'Debe contener solo números', must_contain_letters_only: 'Debe contener solo letras', complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.', @@ -251,6 +253,7 @@ const it = { must_be_valid_email: 'Deve essere un indirizzo email valido', must_be_valid_url: 'Deve essere un URL valido (che inizia con http:// o https://)', must_be_valid_zip: 'Deve essere uno ZIP valido (XXXXX o XXXXX-XXXX)', + must_be_valid_iban: 'Deve essere un IBAN valido', must_contain_numbers_only: 'Deve contenere solo numeri', must_contain_letters_only: 'Deve contenere solo lettere', complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.", @@ -370,6 +373,7 @@ const de = { must_be_valid_email: 'Muss eine gültige E-Mail-Adresse sein', must_be_valid_url: 'Muss eine gültige URL sein (beginnend mit http:// oder https://)', must_be_valid_zip: 'Muss eine gültige ZIP sein (XXXXX oder XXXXX-XXXX)', + must_be_valid_iban: 'Muss eine gültige IBAN sein', must_contain_numbers_only: 'Darf nur Zahlen enthalten', must_contain_letters_only: 'Darf nur Buchstaben enthalten', complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.', @@ -489,6 +493,7 @@ const fr = { must_be_valid_email: 'Doit être une adresse e-mail valide', must_be_valid_url: 'Doit être une URL valide (commençant par http:// ou https://)', must_be_valid_zip: 'Doit être un ZIP valide (XXXXX ou XXXXX-XXXX)', + must_be_valid_iban: 'Doit être un IBAN valide', must_contain_numbers_only: 'Doit contenir uniquement des chiffres', must_contain_letters_only: 'Doit contenir uniquement des lettres', complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.", @@ -608,6 +613,7 @@ const pl = { must_be_valid_email: 'Musi być prawidłowym adresem e-mail', must_be_valid_url: 'Musi być prawidłowym adresem URL (zaczynającym się od http:// lub https://)', must_be_valid_zip: 'Musi być prawidłowym ZIP (XXXXX lub XXXXX-XXXX)', + must_be_valid_iban: 'Musi być prawidłowym numerem IBAN', must_contain_numbers_only: 'Może zawierać tylko cyfry', must_contain_letters_only: 'Może zawierać tylko litery', complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.', @@ -727,6 +733,7 @@ const uk = { must_be_valid_email: 'Має бути дійсною адресою електронної пошти', must_be_valid_url: 'Має бути дійсним URL (починаючи з http:// або https://)', must_be_valid_zip: 'Має бути дійсним ZIP (XXXXX або XXXXX-XXXX)', + must_be_valid_iban: 'Має бути дійсним IBAN', must_contain_numbers_only: 'Має містити лише цифри', must_contain_letters_only: 'Має містити лише літери', complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.", @@ -846,6 +853,7 @@ const cs = { must_be_valid_email: 'Musí být platná e-mailová adresa', must_be_valid_url: 'Musí být platná URL (začínající http:// nebo https://)', must_be_valid_zip: 'Musí být platný ZIP (XXXXX nebo XXXXX-XXXX)', + must_be_valid_iban: 'Musí být platný IBAN', must_contain_numbers_only: 'Musí obsahovat pouze čísla', must_contain_letters_only: 'Musí obsahovat pouze písmena', complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.', @@ -965,6 +973,7 @@ const pt = { must_be_valid_email: 'Deve ser um endereço de e-mail válido', must_be_valid_url: 'Deve ser uma URL válida (começando com http:// ou https://)', must_be_valid_zip: 'Deve ser um ZIP válido (XXXXX ou XXXXX-XXXX)', + must_be_valid_iban: 'Deve ser um IBAN válido', must_contain_numbers_only: 'Deve conter apenas números', must_contain_letters_only: 'Deve conter apenas letras', complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.', @@ -1084,6 +1093,7 @@ const he = { must_be_valid_email: 'חייבת להיות כתובת אימייל חוקית', must_be_valid_url: 'חייבת להיות כתובת URL חוקית (המתחילה ב-http:// או https://)', must_be_valid_zip: 'חייב להיות ZIP חוקי (XXXXX או XXXXX-XXXX)', + must_be_valid_iban: 'חייב להיות IBAN חוקי', must_contain_numbers_only: 'חייב להכיל מספרים בלבד', must_contain_letters_only: 'חייב להכיל אותיות בלבד', complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.', @@ -1203,6 +1213,7 @@ const nl = { must_be_valid_email: 'Moet een geldig e-mailadres zijn', must_be_valid_url: 'Moet een geldige URL zijn (beginnend met http:// of https://)', must_be_valid_zip: 'Moet een geldige ZIP zijn (XXXXX of XXXXX-XXXX)', + must_be_valid_iban: 'Moet een geldig IBAN zijn', must_contain_numbers_only: 'Mag alleen cijfers bevatten', must_contain_letters_only: 'Mag alleen letters bevatten', complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.', @@ -1322,6 +1333,7 @@ const ar = { must_be_valid_email: 'يجب أن يكون عنوان بريد إلكتروني صالحًا', must_be_valid_url: 'يجب أن يكون عنوان URL صالحًا (يبدأ بـ http:// أو https://)', must_be_valid_zip: 'يجب أن يكون ZIP صالحًا (XXXXX أو XXXXX-XXXX)', + must_be_valid_iban: 'يجب أن يكون IBAN صالحًا', must_contain_numbers_only: 'يجب أن يحتوي على أرقام فقط', must_contain_letters_only: 'يجب أن يحتوي على أحرف فقط', complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.', @@ -1441,6 +1453,7 @@ const ko = { must_be_valid_email: '유효한 이메일 주소여야 합니다', must_be_valid_url: '유효한 URL이어야 합니다 (http:// 또는 https://로 시작)', must_be_valid_zip: '유효한 ZIP이어야 합니다 (XXXXX 또는 XXXXX-XXXX)', + must_be_valid_iban: '유효한 IBAN이어야 합니다', must_contain_numbers_only: '숫자만 포함해야 합니다', must_contain_letters_only: '문자만 포함해야 합니다', complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.', @@ -1560,6 +1573,7 @@ const ja = { must_be_valid_email: '有効なメールアドレスである必要があります', must_be_valid_url: '有効なURLである必要があります (http://またはhttps://で始まる)', must_be_valid_zip: '有効なZIPである必要があります (XXXXX または XXXXX-XXXX)', + must_be_valid_iban: '有効なIBANである必要があります', must_contain_numbers_only: '数字のみを含む必要があります', must_contain_letters_only: '文字のみを含む必要があります', complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。', diff --git a/app/javascript/submission_form/text_step.vue b/app/javascript/submission_form/text_step.vue index c8695c268..dbb9e3275 100644 --- a/app/javascript/submission_form/text_step.vue +++ b/app/javascript/submission_form/text_step.vue @@ -34,6 +34,7 @@