shared link 2fa
This commit is contained in:
committed by
Pete Matsyburka
parent
931addedbe
commit
a61c3e798d
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user