shared link 2fa

pull/502/head
Alex Turchyn 4 months ago committed by Pete Matsyburka
parent 931addedbe
commit a61c3e798d

@ -63,7 +63,7 @@ RSpec/MultipleExpectations:
Max: 25
RSpec/ExampleLength:
Max: 50
Max: 500
RSpec/MultipleMemoizedHelpers:
Max: 15

@ -6,12 +6,15 @@ class StartFormController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
around_action :with_browser_locale, only: %i[show completed]
around_action :with_browser_locale, only: %i[show update completed]
before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_resubmit_submitter, only: :update
before_action :load_template
before_action :authorize_start!, only: :update
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def show
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
@ -20,6 +23,7 @@ class StartFormController < ApplicationController
.submitters.new(account_id: @template.account_id,
uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid'])
render :email_verification if params[:email_verification]
else
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
@ -49,17 +53,10 @@ class StartFormController < ApplicationController
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
if @submitter.errors.blank? && @submitter.save
if is_new_record
WebhookUrls.enqueue_events(@submitter.submission, 'submission.created')
SearchEntries.enqueue_reindex(@submitter)
if @submitter.submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
'submission_id' => @submitter.submission_id)
end
end
if @template.preferences['shared_link_2fa'] == true
handle_require_2fa(@submitter, is_new_record:)
elsif @submitter.errors.blank? && @submitter.save
enqueue_new_submitter_jobs(@submitter) if is_new_record
redirect_to submit_form_path(@submitter.slug)
else
@ -89,6 +86,16 @@ class StartFormController < ApplicationController
private
def enqueue_new_submitter_jobs(submitter)
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
SearchEntries.enqueue_reindex(submitter)
return unless submitter.submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(submitter.submission.expire_at, 'submission_id' => submitter.submission_id)
end
def load_resubmit_submitter
@resubmit_submitter =
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
@ -123,7 +130,7 @@ class StartFormController < ApplicationController
.order(id: :desc)
.where(declined_at: nil)
.where(external_id: nil)
.where(ip: [nil, request.remote_ip])
.where(template.preferences['shared_link_2fa'] == true ? {} : { ip: [nil, request.remote_ip] })
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(find_params)
@ -197,4 +204,39 @@ class StartFormController < ApplicationController
I18n.t('not_found')
end
end
def handle_require_2fa(submitter, is_new_record:)
return render :show, status: :unprocessable_entity if submitter.errors.present?
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
if cookies.encrypted[:email_2fa_slug] == submitter.slug || is_otp_verified
if submitter.save
enqueue_new_submitter_jobs(submitter) if is_new_record
if is_otp_verified
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
cookies.encrypted[:email_2fa_slug] =
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
end
redirect_to submit_form_path(submitter.slug)
else
render :show, status: :unprocessable_entity
end
else
Submitters.send_shared_link_email_verification_code(submitter, request:)
render :email_verification
end
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
alert: e.message
rescue RateLimit::LimitApproached
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
alert: I18n.t(:too_many_attempts)
end
end

@ -0,0 +1,31 @@
# frozen_string_literal: true
class StartFormEmail2faSendController < ApplicationController
around_action :with_browser_locale
skip_before_action :authenticate_user!
skip_authorization_check
def create
@template = Template.find_by!(slug: params[:slug])
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(**submitter_params, account_id: @template.account_id)
Submitters.send_shared_link_email_verification_code(@submitter, request:)
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
**redir_params
rescue Submitters::UnableToSendCode => e
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
alert: e.message
end
private
def submitter_params
params.require(:submitter).permit(:name, :email, :phone)
end
end

@ -12,6 +12,7 @@ class SubmissionEventsController < ApplicationController
'send_2fa_sms' => '2fa',
'send_sms' => 'send',
'phone_verified' => 'phone_check',
'email_verified' => 'email_check',
'click_sms' => 'hand_click',
'decline_form' => 'x',
'start_verification' => 'player_play',

@ -9,6 +9,7 @@ class SubmitFormController < ApplicationController
before_action :load_submitter, only: %i[show update completed]
before_action :maybe_render_locked_page, only: :show
before_action :maybe_require_link_2fa, only: %i[show update]
CONFIG_KEYS = [].freeze
@ -50,7 +51,7 @@ class SubmitFormController < ApplicationController
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
end
if @submitter.template&.archived_at? || @submitter.submission.archived_at?
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
end
@ -80,6 +81,15 @@ class SubmitFormController < ApplicationController
private
def maybe_require_link_2fa
return if @submitter.submission.source != 'link'
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
redirect_to start_form_path(@submitter.submission.template.slug)
end
def maybe_render_locked_page
return render :archived if @submitter.submission.template&.archived_at? ||
@submitter.submission.archived_at? ||

@ -26,7 +26,7 @@ class TemplatesPreferencesController < ApplicationController
completed_notification_email_attach_documents
completed_redirect_url validate_unique_submitters
require_all_submitters submitters_order require_phone_2fa
default_expire_at_duration
default_expire_at_duration shared_link_2fa
default_expire_at
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +

@ -0,0 +1,13 @@
# frozen_string_literal: true
class TemplateMailer < ApplicationMailer
def otp_verification_email(template, email:)
@template = template
@otp_code = EmailVerificationCodes.generate([email.downcase.strip, template.slug].join(':'))
assign_message_metadata('otp_verification_email', template)
mail(to: email, subject: I18n.t('email_verification'))
end
end

@ -47,6 +47,7 @@ class SubmissionEvent < ApplicationRecord
click_email: 'click_email',
click_sms: 'click_sms',
phone_verified: 'phone_verified',
email_verified: 'email_verified',
start_form: 'start_form',
start_verification: 'start_verification',
complete_verification: 'complete_verification',

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 19h-6a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v6" />
<path d="M3 7l9 6l9 -6" />
<path d="M15 19l2 2l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 420 B

@ -0,0 +1,53 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %>
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<%= render 'start_form/banner' %>
</div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div>
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
<p dir="auto" class="text-sm"><%= t('invited_by_html', name: @template.account.name) %></p>
</div>
</div>
</div>
</div>
<div>
<%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %>
</div>
<%= form_for '', url: start_form_path(@template.slug), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
<div dir="auto" class="form-control !mt-0">
<%= f.hidden_field 'submitter[name]', value: params[:name] || @submitter&.name %>
<%= f.hidden_field 'submitter[email]', value: params[:email] || @submitter&.email %>
<%= f.hidden_field 'submitter[phone]', value: params[:phone] || @submitter&.phone %>
<%= f.text_field :one_time_code, required: true, class: 'base-input text-center', placeholder: 'XXX-XXX' %>
<div class="flex justify-between items-center mt-1">
<span>
<% if flash[:alert] %>
<span class="text-red-500">
<%= flash[:alert] %>
</span>
<% elsif flash[:notice] %>
<%= flash[:notice] %>
<% end %>
</span>
<span>
<label for="resend_code" id="resend_label" class="link"><%= t(:re_send_code) %></label>
</span>
</div>
</div>
<toggle-submit dir="auto" class="form-control">
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
</toggle-submit>
<% end %>
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || @submitter&.name, email: params[:email] || @submitter&.email, phone: params[:phone] || @submitter&.phone } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
</div>
</div>

@ -0,0 +1,3 @@
<p><%= t('your_verification_code_to_access_the_name', name: @template.name) %></p>
<p><b><%= @otp_code %></b></p>
<p><%= t('please_reply_to_this_email_if_you_didnt_request_this') %></p>

@ -41,5 +41,15 @@
</div>
</div>
<% end %>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:shared_link_2fa).new(@template.preferences['shared_link_2fa'] == true) do |ff| %>
<label for="template_preferences_shared_link_2fa" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</label>
<% end %>
<% end %>
<% end %>
</div>
<% end %>

@ -769,6 +769,15 @@ en: &en
there_are_no_events: There are no events
resend: Resend
next_attempt_in_time_in_words: Next attempt in %{time_in_words}
request_email_otp_verification_with_shared_link: Request email OTP verification with shared link
sms_rate_limit_exceeded: SMS rate limit exceeded
invalid_phone_number: Invalid phone number
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Please contact the requester to specify your phone number for two-factor authentication.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: We've sent a one-time verification code to your email address. Please enter the code below to continue.
re_send_code: Re-send Code
email_verification: Email verification
your_verification_code_to_access_the_name: 'Your verification code to access the "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Please reply to this email if you didn't request this.
submission_sources:
api: API
bulk: Bulk Send
@ -786,6 +795,7 @@ en: &en
click_email_by_html: '<b>Email link clicked</b> by %{submitter_name}'
click_sms_by_html: '<b>SMS link clicked</b> by %{submitter_name}'
phone_verified_by_html: '<b>Phone verified</b> by %{submitter_name}'
email_verified_by_html: '<b>Email verified</b> by %{submitter_name}'
start_form_by_html: '<b>Submission started</b> by %{submitter_name}'
view_form_by_html: '<b>Form viewed</b> by %{submitter_name}'
invite_party_by_html: '<b>Invited</b> %{invited_submitter_name} by %{submitter_name}'
@ -1624,6 +1634,15 @@ es: &es
there_are_no_events: No hay eventos
resend: Reenviar
next_attempt_in_time_in_words: Próximo intento en %{time_in_words}
request_email_otp_verification_with_shared_link: Solicitar verificación OTP por correo electrónico con enlace compartido
sms_rate_limit_exceeded: Límite de SMS excedido
invalid_phone_number: Número de teléfono inválido
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Contacte al solicitante para especificar su número para la autenticación de dos factores.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Enviamos un código de verificación único a su correo electrónico. Ingréselo a continuación para continuar.
re_send_code: Reenviar código
email_verification: Verificación por correo electrónico
your_verification_code_to_access_the_name: 'Su código de verificación para acceder a "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Por favor, responda este correo si no solicitó esto.
submission_sources:
api: API
bulk: Envío masivo
@ -1641,6 +1660,7 @@ es: &es
click_email_by_html: '<b>Enlace del correo electrónico clicado</b> por %{submitter_name}'
click_sms_by_html: '<b>Enlace del SMS clicado</b> por %{submitter_name}'
phone_verified_by_html: '<b>Teléfono verificado</b> por %{submitter_name}'
email_verified_by_html: '<b>Correo electrónico verificado</b> por %{submitter_name}'
start_form_by_html: '<b>Envío iniciado</b> por %{submitter_name}'
view_form_by_html: '<b>Formulario visto</b> por %{submitter_name}'
invite_party_by_html: '<b>Invitado</b> %{invited_submitter_name} por %{submitter_name}'
@ -2477,6 +2497,16 @@ it: &it
there_are_no_events: Nessun evento
resend: Invia di nuovo
next_attempt_in_time_in_words: Prossimo tentativo tra %{time_in_words}
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: "Questo modello ha più parti, il che impedisce l'uso di un link di condivisione perché non è chiaro quale parte sia responsabile di campi specifici. Per risolvere, definisci i dettagli predefiniti della parte."
request_email_otp_verification_with_shared_link: "Richiedi la verifica OTP tramite email con link condiviso"
sms_rate_limit_exceeded: Limite SMS superato
invalid_phone_number: Numero di telefono non valido
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Contatta il richiedente per specificare il tuo numero per l'autenticazione a due fattori.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Abbiamo inviato un codice di verifica una tantum alla tua email. Inseriscilo qui sotto per continuare.
re_send_code: Invia di nuovo il codice
email_verification: Verifica email
your_verification_code_to_access_the_name: 'Il tuo codice per accedere a "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Rispondi a questa email se non hai richiesto questo.
submission_sources:
api: API
bulk: Invio massivo
@ -2494,6 +2524,7 @@ it: &it
click_email_by_html: "<b>Link dell'e-mail cliccato</b> da %{submitter_name}"
click_sms_by_html: "<b>Link dell'SMS cliccato</b> da %{submitter_name}"
phone_verified_by_html: '<b>Telefono verificato</b> da %{submitter_name}'
email_verified_by_html: '<b>Email verificata</b> da %{submitter_name}'
start_form_by_html: '<b>Invio iniziato</b> da %{submitter_name}'
view_form_by_html: '<b>Modulo visualizzato</b> da %{submitter_name}'
invite_party_by_html: '<b>Invitato</b> %{invited_submitter_name} da %{submitter_name}'
@ -3333,6 +3364,16 @@ fr: &fr
there_are_no_events: Aucun événement
resend: Renvoyer
next_attempt_in_time_in_words: Nouvelle tentative dans %{time_in_words}
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: "Ce modèle contient plusieurs parties, ce qui empêche l'utilisation d'un lien de partage car il n'est pas clair quelle partie est responsable de certains champs. Pour résoudre cela, définissez les détails de la partie par défaut."
request_email_otp_verification_with_shared_link: "Demander une vérification OTP par e-mail avec un lien de partage"
sms_rate_limit_exceeded: Limite de SMS dépassée
invalid_phone_number: Numéro de téléphone invalide
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Veuillez contacter l'expéditeur pour spécifier votre numéro pour l'authentification à deux facteurs.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Un code de vérification unique a été envoyé à votre adresse email. Veuillez le saisir ci-dessous pour continuer.
re_send_code: Renvoyer le code
email_verification: Vérification de l'email
your_verification_code_to_access_the_name: 'Votre code pour accéder à "%{name}" :'
please_reply_to_this_email_if_you_didnt_request_this: Veuillez répondre à cet email si vous n'avez pas fait cette demande.
submission_sources:
api: API
bulk: Envoi en masse
@ -3350,6 +3391,7 @@ fr: &fr
click_email_by_html: "<b>Lien de l'e-mail cliqué</b> par %{submitter_name}"
click_sms_by_html: '<b>Lien du SMS cliqué</b> par %{submitter_name}'
phone_verified_by_html: '<b>Téléphone vérifié</b> par %{submitter_name}'
email_verified_by_html: '<b>Email vérifié</b> par %{submitter_name}'
start_form_by_html: '<b>Soumission commencée</b> par %{submitter_name}'
view_form_by_html: '<b>Formulaire consulté</b> par %{submitter_name}'
invite_party_by_html: '<b>Invité</b> %{invited_submitter_name} par %{submitter_name}'
@ -4188,6 +4230,15 @@ pt: &pt
there_are_no_events: Nenhum evento
resend: Reenviar
next_attempt_in_time_in_words: Próxima tentativa em %{time_in_words}
request_email_otp_verification_with_shared_link: Solicitar verificação de OTP por e-mail com link compartilhado
sms_rate_limit_exceeded: Limite de SMS excedido
invalid_phone_number: Número de telefone inválido
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Entre em contato com o solicitante para especificar seu número para autenticação de dois fatores.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Enviamos um código de verificação único para seu e-mail. Insira-o abaixo para continuar.
re_send_code: Reenviar código
email_verification: Verificação de e-mail
your_verification_code_to_access_the_name: 'Seu código de verificação para acessar "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Responda a este e-mail se você não solicitou isso.
submission_sources:
api: API
bulk: Envio em massa
@ -4205,6 +4256,7 @@ pt: &pt
click_email_by_html: '<b>Link do e-mail clicado</b> por %{submitter_name}'
click_sms_by_html: '<b>Link do SMS clicado</b> por %{submitter_name}'
phone_verified_by_html: '<b>Telefone verificado</b> por %{submitter_name}'
email_verified_by_html: '<b>Email verificado</b> por %{submitter_name}'
start_form_by_html: '<b>Submissão iniciada</b> por %{submitter_name}'
view_form_by_html: '<b>Formulário visualizado</b> por %{submitter_name}'
invite_party_by_html: '<b>Convidado</b> %{invited_submitter_name} por %{submitter_name}'
@ -5044,6 +5096,15 @@ de: &de
there_are_no_events: Keine Ereignisse vorhanden
resend: Erneut senden
next_attempt_in_time_in_words: Nächster Versuch in %{time_in_words}
request_email_otp_verification_with_shared_link: Fordern Sie die E-Mail-OTP-Verifizierung mit Freigabelink an
sms_rate_limit_exceeded: SMS-Limit überschritten
invalid_phone_number: Ungültige Telefonnummer
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Kontaktieren Sie den Absender, um Ihre Telefonnummer für die Zwei-Faktor-Authentifizierung anzugeben.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Wir haben einen einmaligen Verifizierungscode an Ihre E-Mail-Adresse gesendet. Bitte geben Sie ihn unten ein, um fortzufahren.
re_send_code: Code erneut senden
email_verification: E-Mail-Verifizierung
your_verification_code_to_access_the_name: 'Ihr Verifizierungscode für den Zugriff auf "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Antworten Sie auf diese E-Mail, wenn Sie dies nicht angefordert haben.
submission_sources:
api: API
bulk: Massenversand
@ -5061,6 +5122,7 @@ de: &de
click_email_by_html: '<b>E-Mail-Link angeklickt</b> von %{submitter_name}'
click_sms_by_html: '<b>SMS-Link angeklickt</b> von %{submitter_name}'
phone_verified_by_html: '<b>Telefon verifiziert</b> von %{submitter_name}'
email_verified_by_html: '<b>Email verifiziert</b> von %{submitter_name}'
start_form_by_html: '<b>Einreichung gestartet</b> von %{submitter_name}'
view_form_by_html: '<b>Formular angesehen</b> von %{submitter_name}'
invite_party_by_html: '<b>Eingeladen</b> %{invited_submitter_name} von %{submitter_name}'
@ -5227,6 +5289,15 @@ pl:
select_data_residency: Wybierz lokalizację danych
company_name: Nazwa firmy
optional: opcjonalne
submit: Prześlij
sms_rate_limit_exceeded: Przekroczono limit wiadomości SMS
invalid_phone_number: Nieprawidłowy numer telefonu
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Skontaktuj się z nadawcą, aby podać numer telefonu do uwierzytelniania dwuskładnikowego.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Wysłaliśmy jednorazowy kod weryfikacyjny na Twój adres e-mail. Wprowadź go poniżej, aby kontynuować.
re_send_code: Wyślij ponownie kod
email_verification: Weryfikacja e-mail
your_verification_code_to_access_the_name: 'Twój kod weryfikacyjny do uzyskania dostępu do "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Odpowiedz na ten e-mail, jeśli nie prosiłeś o to.
uk:
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
@ -5307,6 +5378,15 @@ uk:
select_data_residency: Виберіть місце зберігання даних
company_name: Назва компанії
optional: необов’язково
submit: Надіслати
sms_rate_limit_exceeded: Перевищено ліміт SMS
invalid_phone_number: Невірний номер телефону
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Зв’яжіться з відправником, щоб вказати номер телефону для двофакторної автентифікації.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Ми надіслали одноразовий код підтвердження на вашу електронну пошту. Введіть його нижче, щоб продовжити.
re_send_code: Надіслати код повторно
email_verification: Підтвердження електронної пошти
your_verification_code_to_access_the_name: 'Ваш код доступу до "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Відповідайте на цей лист, якщо ви цього не запитували.
cs:
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
@ -5387,6 +5467,15 @@ cs:
select_data_residency: Vyberte umístění dat
company_name: Název společnosti
optional: volitelné
submit: Odeslat
sms_rate_limit_exceeded: Překročena hranice SMS
invalid_phone_number: Neplatné telefonní číslo
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Kontaktujte odesílatele kvůli zadání vašeho čísla pro dvoufázové ověření.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Poslali jsme jednorázový ověřovací kód na váš e-mail. Zadejte ho níže pro pokračování.
re_send_code: Znovu odeslat kód
email_verification: Ověření e-mailu
your_verification_code_to_access_the_name: 'Váš ověřovací kód pro přístup k "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Odpovězte na tento e-mail, pokud jste o to nežádali.
he:
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
@ -5467,6 +5556,15 @@ he:
select_data_residency: בחר מיקום נתונים
company_name: שם החברה
optional: אופציונלי
submit: 'שלח'
sms_rate_limit_exceeded: 'חריגה ממגבלת SMS'
invalid_phone_number: 'מספר טלפון לא תקין'
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 'פנה לשולח כדי לציין את מספרך לאימות דו-שלבי.'
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: 'שלחנו קוד אימות חד-פעמי לדוא"ל שלך. הזן את הקוד למטה כדי להמשיך.'
re_send_code: 'שלח קוד מחדש'
email_verification: 'אימות דוא"ל'
your_verification_code_to_access_the_name: 'קוד האימות שלך לגישה ל-%{name}:'
please_reply_to_this_email_if_you_didnt_request_this: 'השב למייל זה אם לא ביקשת זאת.'
nl:
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
@ -5547,6 +5645,15 @@ nl:
select_data_residency: Selecteer gegevenslocatie
company_name: Bedrijfsnaam
optional: optioneel
submit: Verzenden
sms_rate_limit_exceeded: SMS-limiet overschreden
invalid_phone_number: Ongeldig telefoonnummer
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Neem contact op met de aanvrager om uw nummer voor twee-factor-authenticatie op te geven.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: We hebben een eenmalige verificatiecode naar je e-mailadres gestuurd. Voer de code hieronder in om verder te gaan.
re_send_code: Code opnieuw verzenden
email_verification: E-mailverificatie
your_verification_code_to_access_the_name: 'Je verificatiecode voor toegang tot "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Reageer op deze e-mail als je dit niet hebt aangevraagd.
ar:
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
@ -5627,6 +5734,15 @@ ar:
select_data_residency: اختر موقع تخزين البيانات
company_name: اسم الشركة
optional: اختياري
submit: 'إرسال'
sms_rate_limit_exceeded: 'تم تجاوز حد رسائل SMS'
invalid_phone_number: 'رقم الهاتف غير صالح'
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 'يرجى الاتصال بالمرسل لتحديد رقم هاتفك للمصادقة الثنائية.'
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: 'أرسلنا رمز تحقق لمرة واحدة إلى بريدك الإلكتروني. الرجاء إدخاله أدناه للمتابعة.'
re_send_code: 'إعادة إرسال الرمز'
email_verification: 'التحقق من البريد الإلكتروني'
your_verification_code_to_access_the_name: 'رمز التحقق الخاص بك للوصول إلى "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: 'يرجى الرد على هذا البريد إذا لم تطلب ذلك.'
ko:
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
@ -5707,6 +5823,15 @@ ko:
select_data_residency: 데이터 저장 위치 선택
company_name: 회사 이름
optional: 선택 사항
submit: 제출
sms_rate_limit_exceeded: SMS 제한 초과
invalid_phone_number: 잘못된 전화번호입니다
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 이중 인증을 위해 전화번호를 지정하려면 요청자에게 문의하세요.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: 일회용 인증 코드를 이메일로 보냈습니다. 계속하려면 아래에 입력하세요.
re_send_code: 코드 재전송
email_verification: 이메일 인증
your_verification_code_to_access_the_name: '"%{name}"에 액세스하기 위한 인증 코드:'
please_reply_to_this_email_if_you_didnt_request_this: 요청하지 않았다면 이 이메일에 회신하세요.
ja:
require_phone_2fa_to_open: 電話による2段階認証が必要です
@ -5787,6 +5912,15 @@ ja:
select_data_residency: データ保存場所を選択
company_name: 会社名
optional: 任意
submit: 送信
sms_rate_limit_exceeded: SMSの送信制限を超えました
invalid_phone_number: 無効な電話番号です
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 二要素認証のため、リクエスターに電話番号を指定してください。
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: ワンタイム認証コードをメールアドレスに送信しました。続行するには、以下にコードを入力してください。
re_send_code: コードを再送信
email_verification: メール認証
your_verification_code_to_access_the_name: '"%{name}"へのアクセスコード:'
please_reply_to_this_email_if_you_didnt_request_this: このリクエストを行っていない場合は、このメールに返信してください。
en-US:
<<: *en

@ -133,6 +133,7 @@ Rails.application.routes.draw do
end
resource :resubmit_form, controller: 'start_form', only: :update
resources :start_form_email_2fa_send, only: :create
resources :submit_form, only: %i[], path: '' do
get :success, on: :collection

@ -0,0 +1,27 @@
# 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, value].join(':')
)
)
end
end

@ -244,11 +244,18 @@ module Submissions
click_email_event =
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.click_email? }
verify_email_event =
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.phone_verified? }
is_phone_verified =
submission.template_fields.any? do |e|
e['type'] == 'phone' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
end
verify_phone_event =
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.phone_verified? }
is_id_verified =
submission.template_fields.any? do |e|
e['type'] == 'verification' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
@ -270,10 +277,10 @@ module Submissions
[
composer.document.layout.formatted_text_box(
[
submitter.email && click_email_event && {
submitter.email && (click_email_event || verify_email_event) && {
text: "#{I18n.t('email_verification')}: #{I18n.t('verified')}\n"
},
submitter.phone && is_phone_verified && {
submitter.phone && (is_phone_verified || verify_phone_event) && {
text: "#{I18n.t('phone_verification')}: #{I18n.t('verified')}\n"
},
is_id_verified && {

@ -11,6 +11,9 @@ module Submitters
'values' => 'D'
}.freeze
UnableToSendCode = Class.new(StandardError)
InvalidOtp = Class.new(StandardError)
module_function
def search(current_user, submitters, keyword)
@ -194,4 +197,27 @@ module Submitters
"#{filename}.#{blob.filename.extension}"
end
def send_shared_link_email_verification_code(submitter, request:)
RateLimit.call("send-otp-code-#{request.remote_ip}", limit: 2, ttl: 45.seconds, enabled: true)
TemplateMailer.otp_verification_email(submitter.submission.template, email: submitter.email).deliver_later!
rescue RateLimit::LimitApproached
Rollbar.warning("Limit verification code for template: #{submitter.submission.template.id}") if defined?(Rollbar)
raise UnableToSendCode, I18n.t('too_many_attempts')
end
def verify_link_otp!(otp, submitter)
return false if otp.blank?
RateLimit.call("verify-2fa-code-#{Digest::MD5.base64digest(submitter.email)}",
limit: 2, ttl: 45.seconds, 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)
true
end
end

@ -0,0 +1,9 @@
# frozen_string_literal: true
class TemplateMailerPreview < ActionMailer::Preview
def otp_verification_email
template = Template.active.last
TemplateMailer.otp_verification_email(template, email: 'john.doe@example.com')
end
end

@ -161,7 +161,6 @@ RSpec.describe 'Signing Form' do
expect(field_value(submitter, 'Cell code')).to eq '123'
end
# rubocop:disable RSpec/ExampleLength
it 'completes the form when name, email, and phone are required' do
template.update(preferences: { link_form_fields: %w[email name phone] })
@ -250,7 +249,93 @@ RSpec.describe 'Signing Form' do
expect(field_value(submitter, 'Attachment')).to be_present
expect(field_value(submitter, 'Cell code')).to eq '123'
end
# rubocop:enable RSpec/ExampleLength
it 'completes the form when identity verification with a 2FA code is enabled', sidekiq: :inline do
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
value: GenerateCertificate.call.transform_values(&:to_pem))
template.update(preferences: { link_form_fields: %w[email name], shared_link_2fa: true })
visit start_form_path(slug: template.slug)
fill_in 'Email', with: 'john.dou@example.com'
fill_in 'Name', with: 'John Doe'
expect do
click_button 'Start'
end.to change { ActionMailer::Base.deliveries.count }.by(1)
email = ActionMailer::Base.deliveries.last
code = email.body.encoded[%r{<b>(.*?)</b>}, 1]
fill_in 'one_time_code', with: code
click_button 'Submit'
fill_in 'First Name', with: 'John'
click_button 'next'
fill_in 'Birthday', with: I18n.l(20.years.ago, format: '%Y-%m-%d')
click_button 'next'
check 'Do you agree?'
click_button 'next'
choose 'Boy'
click_button 'next'
draw_canvas
click_button 'next'
fill_in 'House number', with: '123'
click_button 'next'
%w[Red Blue].each { |color| check color }
click_button 'next'
select 'Male', from: 'Gender'
click_button 'next'
draw_canvas
click_button 'next'
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
click_button 'next'
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf'))
click_button 'next'
fill_in 'Cell code', with: '123'
click_on 'Complete'
expect(page).to have_button('Download')
expect(page).to have_content('Document has been signed!')
submitter = template.submissions.last.submitters.last
expect(submitter.email).to eq('john.dou@example.com')
expect(submitter.name).to eq('John Doe')
expect(submitter.ip).to eq('127.0.0.1')
expect(submitter.ua).to be_present
expect(submitter.opened_at).to be_present
expect(submitter.completed_at).to be_present
expect(submitter.declined_at).to be_nil
expect(field_value(submitter, 'First Name')).to eq 'John'
expect(field_value(submitter, 'Birthday')).to eq 20.years.ago.strftime('%Y-%m-%d')
expect(field_value(submitter, 'Do you agree?')).to be_truthy
expect(field_value(submitter, 'First child')).to eq 'Boy'
expect(field_value(submitter, 'Signature')).to be_present
expect(field_value(submitter, 'House number')).to eq 123
expect(field_value(submitter, 'Colors')).to contain_exactly('Red', 'Blue')
expect(field_value(submitter, 'Gender')).to eq 'Male'
expect(field_value(submitter, 'Initials')).to be_present
expect(field_value(submitter, 'Avatar')).to be_present
expect(field_value(submitter, 'Attachment')).to be_present
expect(field_value(submitter, 'Cell code')).to eq '123'
end
end
context 'when the submitter form link is opened' do

Loading…
Cancel
Save