Merge from docusealco/wip

master 2.2.5
Alex Turchyn 1 week ago committed by GitHub
commit 94d1fb7dcb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -9,7 +9,7 @@ RUN apk --no-cache add fontforge wget && \
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/1.0.0/model_704_int8.onnx" && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/1.1.0/model_704_int8.onnx" && \
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
mkdir -p /pdfium-linux && \
tar -xzf pdfium-linux.tgz -C /pdfium-linux
@ -90,7 +90,7 @@ COPY --from=download /model.onnx /app/tmp/model.onnx
COPY --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts
RUN bundle exec bootsnap precompile --gemfile app/ lib/
RUN bundle exec bootsnap precompile -j 1 --gemfile app/ lib/
WORKDIR /data/docuseal
ENV WORKDIR=/data/docuseal

@ -37,7 +37,7 @@ module Api
rescue Submitters::MaliciousFileExtension => e
Rollbar.error(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
render json: { error: e.message }, status: :unprocessable_content
end
def build_new_cookie_signatures_json(submitter, attachment)

@ -5,6 +5,8 @@ class PasswordsController < Devise::PasswordsController
skip_before_action :require_no_authentication, only: %i[edit update]
# rubocop:enable Rails/LexicallyScopedActionFilter
around_action :with_browser_locale
class Current < ActiveSupport::CurrentAttributes
attribute :user
end

@ -16,7 +16,9 @@ class StartFormController < ApplicationController
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def show
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
raise ActionController::RoutingError, I18n.t('not_found')
end
if @template.shared_link?
@submitter = @template.submissions.new(account_id: @template.account_id)

@ -10,6 +10,7 @@ class SubmissionEventsController < ApplicationController
'api_complete_form' => 'check',
'send_reminder_email' => 'mail_forward',
'send_2fa_sms' => '2fa',
'send_2fa_email' => '2fa',
'send_sms' => 'send',
'phone_verified' => 'phone_check',
'email_verified' => 'email_check',

@ -53,8 +53,11 @@ class SubmissionsPreviewController < ApplicationController
def use_signature?(submission)
return false if current_user && can?(:read, submission)
return true if submission.submitters.any? { |e| e.preferences['require_phone_2fa'] }
return true if submission.submitters.any? do |e|
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
end
return true if submission.template&.preferences&.dig('require_phone_2fa')
return true if submission.template&.preferences&.dig('require_email_2fa')
!submission_valid_ttl?(submission)
end

@ -17,6 +17,7 @@ class SubmitFormController < ApplicationController
submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :email_2fa if require_email_2fa?(@submitter)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@ -47,6 +48,11 @@ class SubmitFormController < ApplicationController
end
def update
if require_email_2fa?(@submitter)
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
status: :unprocessable_content
end
if @submitter.completed_at?
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
end
@ -77,6 +83,8 @@ class SubmitFormController < ApplicationController
def completed
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
end
def success; end
@ -109,4 +117,12 @@ class SubmitFormController < ApplicationController
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
end
def require_email_2fa?(submitter)
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
submitter.preferences['require_email_2fa'] != true
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
true
end
end

@ -0,0 +1,52 @@
# frozen_string_literal: true
class SubmitFormEmail2fasController < ApplicationController
around_action :with_browser_locale
skip_before_action :authenticate_user!
skip_authorization_check
before_action :load_submitter
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def create
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
value = [@submitter.email.downcase.strip, @submitter.slug].join(':')
if EmailVerificationCodes.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value)
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
cookies.encrypted[:email_2fa_slug] =
{ value: @submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
redirect_to submit_form_path(@submitter.slug)
else
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:invalid_code)
end
rescue RateLimit::LimitApproached
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
end
def update
if @submitter.submission_events.where(event_type: 'send_2fa_email').exists?(created_at: 15.seconds.ago..)
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:rate_limit_exceeded)
end
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {}
redirect_to submit_form_path(@submitter.slug, status: :sent), **redir_params
rescue RateLimit::LimitApproached
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
end
def load_submitter
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
end
end

@ -54,7 +54,7 @@ class TemplatesPreferencesController < ApplicationController
documents_copy_email_attach_documents documents_copy_email_reply_to
completed_notification_email_attach_documents
completed_redirect_url validate_unique_submitters
require_all_submitters submitters_order require_phone_2fa
require_all_submitters submitters_order require_phone_2fa require_email_2fa
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +

@ -54,7 +54,8 @@ class UsersController < ApplicationController
def update
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
attrs = user_params.compact_blank
attrs = attrs.merge(user_params.slice(:archived_at)) if current_ability.can?(:create, @user)
if params.dig(:user, :account_id).present?
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))

@ -133,6 +133,12 @@
name="_method"
type="hidden"
>
<input
v-if="validate === false"
value="false"
name="validate"
type="hidden"
>
<div class="md:mt-4">
<div v-if="['cells', 'text'].includes(currentField.type)">
<TextStep
@ -351,8 +357,6 @@
class="base-checkbox !h-7 !w-7"
:required="field.required"
:checked="!!values[field.uuid]"
@invalid="$event.target.setCustomValidity(t('please_check_the_box_to_continue'))"
@change="$event.target.setCustomValidity($event.target.validity.valueMissing ? t('please_check_the_box_to_continue') : '')"
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
>
<span
@ -718,6 +722,11 @@ export default {
required: false,
default: false
},
validate: {
type: Boolean,
required: false,
default: true
},
withDisclosure: {
type: Boolean,
required: false,

@ -49,7 +49,12 @@
</li>
</template>
</template>
<template v-else>
<div
v-if="fieldTypes.length && submitterDefaultFields.length"
class="bg-base-300"
style="height: 1px; margin-top: 1px; margin-bottom: 1px"
/>
<template v-if="fieldTypes.length || !submitterDefaultFields.length">
<template
v-for="(icon, type) in fieldIconsSorted"
:key="type"

@ -4,7 +4,9 @@ class ProcessSubmissionExpiredJob
include Sidekiq::Job
def perform(params = {})
submission = Submission.find(params['submission_id'])
submission = Submission.find_by(id: params['submission_id'])
return unless submission
return if submission.archived_at?
return if submission.template&.archived_at?

@ -8,8 +8,13 @@ class SendFormCompletedWebhookRequestJob
MAX_ATTEMPTS = 12
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submitter = Submitter.find_by(id: params['submitter_id'])
return unless submitter
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i

@ -8,8 +8,13 @@ class SendFormDeclinedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submitter = Submitter.find_by(id: params['submitter_id'])
return unless submitter
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i

@ -8,8 +8,13 @@ class SendFormStartedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submitter = Submitter.find_by(id: params['submitter_id'])
return unless submitter
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i

@ -8,8 +8,13 @@ class SendFormViewedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submitter = Submitter.find_by(id: params['submitter_id'])
return unless submitter
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i

@ -8,8 +8,13 @@ class SendSubmissionArchivedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submission = Submission.find_by(id: params['submission_id'])
return unless submission
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i

@ -8,8 +8,13 @@ class SendSubmissionCompletedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submission = Submission.find_by(id: params['submission_id'])
return unless submission
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i

@ -8,8 +8,13 @@ class SendSubmissionCreatedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submission = Submission.find_by(id: params['submission_id'])
return unless submission
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i

@ -8,8 +8,13 @@ class SendSubmissionExpiredWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submission = Submission.find_by(id: params['submission_id'])
return unless submission
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i

@ -0,0 +1,15 @@
# frozen_string_literal: true
class SendSubmitterVerificationEmailJob
include Sidekiq::Job
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
SubmitterMailer.otp_verification_email(submitter).deliver_now!
SubmissionEvent.create!(submitter_id: params['submitter_id'],
event_type: 'send_2fa_email',
data: { email: submitter.email })
end
end

@ -8,8 +8,13 @@ class SendTemplateCreatedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
template = Template.find(params['template_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
template = Template.find_by(id: params['template_id'])
return unless template
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i

@ -8,8 +8,13 @@ class SendTemplateUpdatedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
template = Template.find(params['template_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
template = Template.find_by(id: params['template_id'])
return unless template
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i

@ -8,10 +8,13 @@ class SendTestWebhookRequestJob
USER_AGENT = 'DocuSeal.com Webhook'
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submitter = Submitter.find_by(id: params['submitter_id'])
return unless webhook_url && submitter
return unless submitter
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
Faraday.post(webhook_url.url,
{

@ -144,6 +144,17 @@ class SubmitterMailer < ApplicationMailer
end
end
def otp_verification_email(submitter)
@submitter = submitter
@otp_code = EmailVerificationCodes.generate([submitter.email.downcase.strip, submitter.slug].join(':'))
assign_message_metadata('otp_verification_email', submitter)
I18n.with_locale(submitter.account.locale) do
mail(to: submitter.email, subject: I18n.t('email_verification'))
end
end
private
def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil)

@ -48,6 +48,7 @@ class SubmissionEvent < ApplicationRecord
send_reminder_email: 'send_reminder_email',
send_sms: 'send_sms',
send_2fa_sms: 'send_2fa_sms',
send_2fa_email: 'send_2fa_email',
open_email: 'open_email',
click_email: 'click_email',
click_sms: 'click_sms',

@ -1,5 +1,10 @@
<p>Hello <%= @resource.email %>!</p>
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>
<p><%= @resource.first_name.present? ? t('hello_name', name: @resource.first_name) : t('hi_there') %>,</p>
<p><%= t('you_requested_to_reset_your_password_use_the_link_below_to_continue') %>:</p>
<p><%= link_to t('change_my_password'), edit_password_url(@resource, reset_password_token: @token) %></p>
<p><%= t('your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one') %></p>
<p><%= t('if_you_didnt_request_this_you_can_ignore_this_email') %></p>
<p>
<%= t('thanks') %>,<br>
<%= Docuseal.product_name %>
</p>
<% content_for(:remove_attribution, true) %>

@ -29,7 +29,7 @@
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
<submitters-autocomplete data-field="email">
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('email')} (#{t('optional')})" %>" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>">
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: (local_assigns[:require_email_2fa] == true ? t(:email) : "#{t('email')} (#{t('optional')})"), value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "detailed_email_#{item['uuid']}", required: local_assigns[:require_email_2fa] == true %>
</linked-input>
</submitters-autocomplete>
<% has_phone_field = true %>

@ -1,6 +1,7 @@
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
<% only_detailed = require_phone_2fa || prefillable_fields.present? %>
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? %>
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
<% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
@ -25,7 +26,7 @@
</div>
<% end %>
<div id="detailed" class="<%= 'hidden' unless only_detailed %>">
<%= render 'detailed_form', template: @template, require_phone_2fa:, prefillable_fields: %>
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields: %>
</div>
<div id="list" class="hidden">
<%= render 'list_form', template: @template %>

@ -0,0 +1,72 @@
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
<% I18n.with_locale(@submitter.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: @submitter.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 dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<% last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last %>
<% if last_submitter %>
<p dir="auto" class="text-sm">
<%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.to_date, format: :long)) %>
</p>
<% end %>
</div>
</div>
</div>
</div>
<% if @submitter.email.present? %>
<div>
<%= t('the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html', email: TextUtils.mask_email(@submitter.email, 2)) %>
</div>
<% if params[:status] == 'sent' || params[:status] == 'error' %>
<%= form_for '', url: submit_form_email_2fa_path, method: :post, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
<div dir="auto" class="form-control !mt-0">
<%= f.hidden_field :submitter_slug, value: @submitter.slug %>
<%= 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>
<% end %>
<% if flash[:notice] %>
<%= flash[:notice] %>
<% end %>
</span>
<span>
<label for="resend_code" id="resend_label" class="link"><%= t(:re_send_email) %></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), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug, resend: true }, method: :put, id: 'resend_code', class: 'hidden' %>
<% else %>
<% if params[:t] %>
<fetch-form data-onload="true">
<%= button_to '', api_submitter_email_clicks_path, params: { submitter_slug: @submitter.slug, t: params[:t] }, method: :post, class: 'hidden' %>
</fetch-form>
<% end %>
<toggle-submit dir="auto" class="form-control">
<%= button_to button_title(title: t('send_verification_code')), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug }, method: :put, class: 'base-button w-full' %>
</toggle-submit>
<% end %>
<% else %>
<div><%= t('please_contact_the_requester_to_specify_your_email_for_two_factor_authentication') %></div>
<% end %>
</div>
</div>

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

@ -67,7 +67,7 @@
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message, :require_phone_2fa).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body')), @template.preferences['require_phone_2fa'] == true) do |ff| %>
<%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message, :require_phone_2fa, :require_email_2fa).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body')), @template.preferences['require_phone_2fa'] == true, @template.preferences['require_email_2fa'] == true) do |ff| %>
<div class="form-control mb-2">
<%= ff.label :completed_redirect_url, t('redirect_on_completion_url'), class: 'label' %>
<%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %>
@ -81,6 +81,14 @@
</div>
<% end %>
<%= render 'templates_preferences/form_fields', ff: %>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= t('require_email_2fa_to_open') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :require_email_2fa, { checked: ff.object.require_email_2fa == true, class: 'toggle' }, 'true', 'false' %>
</submit-form>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>

@ -3,12 +3,19 @@
<%= render 'shared/turbo_modal_large', title: t('share_link') do %>
<div class="mt-2 mb-4 px-5">
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
<label for="template_shared_link" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('enable_shared_link') %></span>
<submit-form data-on="change" class="flex">
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
</submit-form>
</label>
<% if @template.preferences&.dig('require_email_2fa') || @template.preferences&.dig('require_phone_2fa') %>
<label for="template_shared_link" class="tooltip tooltip-bottom flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300" data-tip="<%= t(:templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link) %>">
<span><%= t('enable_shared_link') %></span>
<%= check_box_tag 'shared_link', 'true', false, disabled: true, class: 'toggle' %>
</label>
<% else %>
<label for="template_shared_link" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('enable_shared_link') %></span>
<submit-form data-on="change" class="flex">
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
</submit-form>
</label>
<% end %>
<div class="flex gap-2 mt-3">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
<check-on-click data-element-id="template_shared_link">
@ -31,48 +38,50 @@
</div>
</div>
<% end %>
<div class="collapse collapse-arrow join-item mt-4">
<input id="accordion_checkbox" type="checkbox" name="accordion" class="min-h-0">
<div class="collapse-title min-h-0 after:!right-3 p-1">
<%= t('advanced_settings') %>
</div>
<div class="collapse-content !p-0">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 pt-4 border-t border-base-300' }, data: { close_on_submit: false } do |f| %>
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<label class="pl-1"><%= t('link_form_fields') %></label>
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
<% %w[name email phone].each do |field| %>
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-14 border-base-300 py-3.5 border rounded-xl' do %>
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
<span class="font-medium w-full text-center"><%= t(field) %></span>
<% if @template.preferences&.dig('require_email_2fa') != true && @template.preferences&.dig('require_phone_2fa') != true %>
<div class="collapse collapse-arrow join-item mt-4">
<input id="accordion_checkbox" type="checkbox" name="accordion" class="min-h-0">
<div class="collapse-title min-h-0 after:!right-3 p-1">
<%= t('advanced_settings') %>
</div>
<div class="collapse-content !p-0">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 pt-4 border-t border-base-300' }, data: { close_on_submit: false } do |f| %>
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<label class="pl-1"><%= t('link_form_fields') %></label>
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
<% %w[name email phone].each do |field| %>
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-14 border-base-300 py-3.5 border rounded-xl' do %>
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
<span class="font-medium w-full text-center"><%= t(field) %></span>
<% end %>
<% end %>
<% end %>
</required-checkbox-group>
<% end %>
<% if multiple_submitters && enough_defined_submitters %>
<div class="collapse collapse-arrow join-item border border-base-300 mt-4">
<input type="checkbox" name="accordion" class="min-h-0">
<div class="collapse-title py-4 min-h-0">
<%= t('default_parties') %>
</div>
<div class="collapse-content !pb-0">
<%= render 'templates_preferences/recipients', template: @template, close_on_submit: false, with_toggles: false, with_submission_requester: false %>
</required-checkbox-group>
<% end %>
<% if multiple_submitters && enough_defined_submitters %>
<div class="collapse collapse-arrow join-item border border-base-300 mt-4">
<input type="checkbox" name="accordion" class="min-h-0">
<div class="collapse-title py-4 min-h-0">
<%= t('default_parties') %>
</div>
<div class="collapse-content !pb-0">
<%= render 'templates_preferences/recipients', template: @template, close_on_submit: false, with_toggles: false, with_submission_requester: false %>
</div>
</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-4' }, 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 mt-4 h-14 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
</submit-form>
</label>
<% 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-4' }, 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 mt-4 h-14 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
</submit-form>
</label>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
</div>
</div>
</div>
<% end %>
<% end %>

@ -12,15 +12,24 @@
<% end %>
</h1>
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
<% if params[:status].blank? && can?(:create, User.new(account: current_account)) %>
<%= render 'users/extra_buttons' %>
<% if content_for(:add_user_button) %>
<%= content_for(:add_user_button) %>
<% else %>
<%= link_to new_user_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_user') %></span>
<% if params[:status].blank? %>
<% if can?(:create, User.new(account: current_account)) %>
<%= render 'users/extra_buttons' %>
<% if content_for(:add_user_button) %>
<%= content_for(:add_user_button) %>
<% else %>
<%= link_to new_user_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_user') %></span>
<% end %>
<% end %>
<% else %>
<div class="tooltip" data-tip="<%= t('contact_your_administrator_to_add_new_users') %>">
<%= link_to '#', class: 'btn btn-primary btn-md gap-2 w-full md:w-fit btn-disabled', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_user') %></span>
<% end %>
</div>
<% end %>
<% end %>
</div>
@ -79,7 +88,7 @@
<%= t('remove') %>
<% end %>
<% end %>
<% if params[:status] == 'archived' && can?(:manage, user) && user != current_user && user.archived_at? %>
<% if params[:status] == 'archived' && can?(:create, user) && user != current_user && user.archived_at? %>
<%= button_to user_path(user), method: :put, params: { user: { archived_at: nil } }, class: 'btn btn-outline btn-xs', title: t('unarchive'), data: { turbo_confirm: t('are_you_sure_') } do %>
<%= t('unarchive') %>
<% end %>

@ -0,0 +1,9 @@
# frozen_string_literal: true
ActiveSupport.on_load(:active_job) do
ActiveJob::LogSubscriber.class_eval do
def args_info(_job)
''
end
end
end

@ -17,7 +17,10 @@ module Devise
assign_message_metadata(action, record)
initialize_from_record(record)
mail(headers_for(action, opts), &)
I18n.with_locale(record.account.locale) do
mail(headers_for(action, opts), &)
end
end
end
end

@ -25,6 +25,7 @@ en: &en
upload_a_new_document: Upload a New Document
billing: Billing
hi_there: Hi there
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Templates that require email or phone 2FA cannot be used via a shared link.
pro: Pro
thanks: Thanks
private: Private
@ -32,6 +33,7 @@ en: &en
enabled: Enabled
disabled: Disabled
party: Party
make_owner: Make Owner
use_direct_file_attachment_links_in_the_documents: Use direct file attachment links in the documents
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Click here</label> to send a reset password email.'
edit_order: Edit Order
@ -861,11 +863,19 @@ en: &en
reports: Reports
completed_submissions: Completed submissions
sms: SMS
require_email_2fa_to_open: Require email 2FA to open
verification_required_refresh_the_page_and_pass_2fa: Verification required, refresh the page and pass 2FA.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: The sender has requested two-factor authentication via a one-time password sent to your <b>%{email}</b> email.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Please contact the requester to specify your email for two-factor authentication.
rate_limit_exceeded: Rate limit exceeded
a_confirmation_email_has_been_sent_to_the_new_email_address: A confirmation email has been sent to the new email address.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} email address is awaiting confirmation. Follow the link in the email to confirm."
please_confirm_your_email_address_using_the_link_below_: 'Please confirm your email address using the link below:'
confirm_email: Confirm email
unconfirmed: Unconfirmed
you_requested_to_reset_your_password_use_the_link_below_to_continue: You requested to reset your password. Use the link below to continue
if_you_didnt_request_this_you_can_ignore_this_email: "If you didn't request this, please ignore this email."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Your password won't change until you open the link above and set a new one."
devise:
confirmations:
confirmed: Your email address has been successfully confirmed.
@ -887,6 +897,7 @@ en: &en
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
send_sms_to_html: '<b>SMS sent</b> to %{submitter_name}'
send_2fa_sms_to_html: '<b>Verification SMS sent</b> to %{submitter_name}'
send_2fa_email_to_html: '<b>Verification email sent</b> to %{submitter_name}'
open_email_by_html: '<b>Email opened</b> by %{submitter_name}'
click_email_by_html: '<b>Email link clicked</b> by %{submitter_name}'
click_sms_by_html: '<b>SMS link clicked</b> by %{submitter_name}'
@ -980,6 +991,8 @@ en: &en
range_without_total: "%{from}-%{to} events"
es: &es
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Las plantillas que requieren autenticación en dos pasos por correo electrónico o teléfono no se pueden usar mediante un enlace compartido.
make_owner: Hacer propietario
billing: Facturación
add_from_google_drive: Agregar desde Google Drive
or_add_from: O agregar desde
@ -1821,11 +1834,19 @@ es: &es
reports: Informes
completed_submissions: Envíos completados
sms: SMS
require_email_2fa_to_open: Requerir 2FA por correo electrónico para abrir
verification_required_refresh_the_page_and_pass_2fa: Verificación requerida, actualiza la página y completa la autenticación de dos factores.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: El remitente ha solicitado autenticación de dos factores mediante una contraseña de un solo uso enviada a tu correo electrónico <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Por favor, contacta al remitente para especificar tu correo electrónico para la autenticación de dos factores.
rate_limit_exceeded: Límite de velocidad excedido
a_confirmation_email_has_been_sent_to_the_new_email_address: Se ha enviado un correo electrónico de confirmación a la nueva dirección de correo electrónico.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} está pendiente de confirmación. Sigue el enlace en el correo para confirmarla."
please_confirm_your_email_address_using_the_link_below_: 'Por favor, confirma tu dirección de correo electrónico utilizando el enlace a continuación:'
confirm_email: Confirmar correo
unconfirmed: No confirmado
you_requested_to_reset_your_password_use_the_link_below_to_continue: Solicitaste restablecer tu contraseña. Usa el enlace a continuación para continuar.
if_you_didnt_request_this_you_can_ignore_this_email: "Si no solicitaste esto, puedes ignorar este correo electrónico."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Tu contraseña no cambiará hasta que abras el enlace anterior y establezcas una nueva."
devise:
confirmations:
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
@ -1847,6 +1868,7 @@ es: &es
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
send_sms_to_html: '<b>SMS enviado</b> a %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS de verificación enviado</b> a %{submitter_name}'
send_2fa_email_to_html: '<b>Correo electrónico de verificación enviado</b> a %{submitter_name}'
open_email_by_html: '<b>Correo electrónico abierto</b> por %{submitter_name}'
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}'
@ -1940,6 +1962,8 @@ es: &es
range_without_total: "%{from}-%{to} eventos"
it: &it
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: I modelli che richiedono 2FA via email o telefono non possono essere utilizzati tramite un link condiviso.
make_owner: Rendi proprietario
billing: Fatturazione
add_from_google_drive: Aggiungi da Google Drive
or_add_from: Oppure aggiungi da
@ -2782,11 +2806,19 @@ it: &it
reports: Rapporti
completed_submissions: Invii completati
sms: SMS
require_email_2fa_to_open: Richiedi 2FA email per aprire
verification_required_refresh_the_page_and_pass_2fa: Verifica richiesta, aggiorna la pagina e completa l'autenticazione a due fattori.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: Il mittente ha richiesto l'autenticazione a due fattori tramite una password monouso inviata al tuo indirizzo email <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Si prega di contattare il mittente per specificare il tuo indirizzo email per l'autenticazione a due fattori.
rate_limit_exceeded: Limite di velocità superato
a_confirmation_email_has_been_sent_to_the_new_email_address: È stata inviata un'email di conferma al nuovo indirizzo email.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} è in attesa di conferma. Segui il link nell'email per confermare."
please_confirm_your_email_address_using_the_link_below_: 'Conferma il tuo indirizzo email utilizzando il link qui sotto:'
confirm_email: Conferma email
unconfirmed: Non confermato
you_requested_to_reset_your_password_use_the_link_below_to_continue: Hai richiesto di reimpostare la tua password. Usa il link qui sotto per continuare.
if_you_didnt_request_this_you_can_ignore_this_email: "Se non hai richiesto questo, puoi ignorare questa email."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "La tua password non cambierà finché non apri il link sopra e ne imposti una nuova."
devise:
confirmations:
confirmed: Il tuo indirizzo email è stato confermato con successo.
@ -2808,6 +2840,7 @@ it: &it
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
send_sms_to_html: '<b>SMS inviato</b> a %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS di verifica inviato</b> a %{submitter_name}'
send_2fa_email_to_html: '<b>Email di verifica inviata</b> a %{submitter_name}'
open_email_by_html: '<b>E-mail aperta</b> da %{submitter_name}'
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}"
@ -2901,6 +2934,8 @@ it: &it
range_without_total: "%{from}-%{to} eventi"
fr: &fr
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Les modèles nécessitant une authentification à deux facteurs par e-mail ou téléphone ne peuvent pas être utilisés via un lien partagé.
make_owner: Rendre propriétaire
billing: Facturation
add_from_google_drive: Ajouter depuis Google Drive
or_add_from: Ou ajouter depuis
@ -3739,11 +3774,19 @@ fr: &fr
reports: Rapports
completed_submissions: Soumissions terminées
sms: SMS
require_email_2fa_to_open: Exiger la 2FA par email pour ouvrir
verification_required_refresh_the_page_and_pass_2fa: Vérification requise, actualisez la page et effectuez l'authentification à deux facteurs.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: L'expéditeur a demandé l'authentification à deux facteurs par mot de passe à usage unique envoyé à votre adresse e-mail <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Veuillez contacter l'expéditeur pour spécifier votre adresse e-mail pour l'authentification à deux facteurs.
rate_limit_exceeded: Limite de débit dépassée
a_confirmation_email_has_been_sent_to_the_new_email_address: Un e-mail de confirmation a été envoyé à la nouvelle adresse e-mail.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} est en attente de confirmation. Suivez le lien dans l'e-mail pour la confirmer."
please_confirm_your_email_address_using_the_link_below_: 'Veuillez confirmer votre adresse e-mail en utilisant le lien ci-dessous :'
confirm_email: "Confirmer l'e-mail"
unconfirmed: Non confirmé
you_requested_to_reset_your_password_use_the_link_below_to_continue: Vous avez demandé à réinitialiser votre mot de passe. Utilisez le lien ci-dessous pour continuer.
if_you_didnt_request_this_you_can_ignore_this_email: "Si vous n'avez pas fait cette demande, veuillez ignorer cet e-mail."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Votre mot de passe ne changera pas tant que vous naurez pas ouvert le lien ci-dessus et défini un nouveau mot de passe."
devise:
confirmations:
confirmed: Votre adresse e-mail a été confirmée avec succès.
@ -3765,6 +3808,7 @@ fr: &fr
send_reminder_email_to_html: "<b>Email de rappel envoyé</b> à %{submitter_name}"
send_sms_to_html: "<b>SMS envoyé</b> à %{submitter_name}"
send_2fa_sms_to_html: "<b>SMS de vérification envoyé</b> à %{submitter_name}"
send_2fa_email_to_html: "<b>Email de vérification envoyé</b> à %{submitter_name}"
open_email_by_html: "<b>Email ouvert</b> par %{submitter_name}"
click_email_by_html: "<b>Lien email cliqué</b> par %{submitter_name}"
click_sms_by_html: "<b>Lien SMS cliqué</b> par %{submitter_name}"
@ -3858,6 +3902,8 @@ fr: &fr
range_without_total: "%{from}-%{to} événements"
pt: &pt
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Modelos que exigem 2FA por e-mail ou telefone não podem ser usados por meio de um link compartilhado.
make_owner: Tornar proprietário
billing: Pagamentos
add_from_google_drive: Adicionar do Google Drive
or_add_from: Ou adicionar de
@ -4699,11 +4745,19 @@ pt: &pt
reports: Relatórios
completed_submissions: Envios concluídos
sms: SMS
require_email_2fa_to_open: Exigir 2FA por e-mail para abrir
verification_required_refresh_the_page_and_pass_2fa: Verificação necessária, atualize a página e conclua a autenticação de dois fatores.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: O remetente solicitou autenticação de dois fatores por meio de uma senha de uso único enviada para o seu e-mail <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Por favor, entre em contato com o remetente para especificar seu e-mail para autenticação de dois fatores.
rate_limit_exceeded: Limite de taxa excedido
a_confirmation_email_has_been_sent_to_the_new_email_address: Um e-mail de confirmação foi enviado para o novo endereço de e-mail.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} está aguardando confirmação. Siga o link enviado para esse endereço de e-mail para confirmar."
please_confirm_your_email_address_using_the_link_below_: 'Por favor, confirme seu endereço de e-mail usando o link abaixo:'
confirm_email: Confirmar e-mail
unconfirmed: Não confirmado
you_requested_to_reset_your_password_use_the_link_below_to_continue: Você solicitou a redefinição da sua senha. Use o link abaixo para continuar.
if_you_didnt_request_this_you_can_ignore_this_email: "Se você não solicitou isso, pode ignorar este e-mail."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Sua senha não será alterada até que você abra o link acima e defina uma nova."
devise:
confirmations:
confirmed: Seu endereço de e-mail foi confirmado com sucesso.
@ -4725,6 +4779,7 @@ pt: &pt
send_reminder_email_to_html: '<b>Email de lembrete enviado</b> para %{submitter_name}'
send_sms_to_html: '<b>SMS enviado</b> para %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS de verificação enviado</b> para %{submitter_name}'
send_2fa_email_to_html: '<b>Email de verificação enviado</b> para %{submitter_name}'
open_email_by_html: '<b>E-mail aberto</b> por %{submitter_name}'
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}'
@ -4818,6 +4873,8 @@ pt: &pt
range_without_total: "%{from}-%{to} eventos"
de: &de
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Vorlagen, die eine Zwei-Faktor-Authentifizierung per E-Mail oder Telefon erfordern, können nicht über einen geteilten Link verwendet werden.
make_owner: Eigentümer machen
billing: Abrechnung
add_from_google_drive: Aus Google Drive hinzufügen
or_add_from: Oder hinzufügen aus
@ -5659,11 +5716,19 @@ de: &de
reports: Berichte
completed_submissions: Abgeschlossene Übermittlungen
sms: SMS
require_email_2fa_to_open: E-Mail-2FA erforderlich, um zu öffnen
verification_required_refresh_the_page_and_pass_2fa: Verifikation erforderlich. Aktualisieren Sie die Seite und führen Sie die Zwei-Faktor-Authentifizierung durch.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: Der Absender hat die Zwei-Faktor-Authentifizierung per Einmalpasswort angefordert, das an Ihre E-Mail-Adresse <b>%{email}</b> gesendet wurde.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Bitte wenden Sie sich an den Absender, um Ihre E-Mail-Adresse für die Zwei-Faktor-Authentifizierung anzugeben.
rate_limit_exceeded: Limit überschritten
a_confirmation_email_has_been_sent_to_the_new_email_address: Eine Bestätigungs-E-Mail wurde an die neue E-Mail-Adresse gesendet.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} wartet auf Bestätigung. Folgen Sie dem Link in der E-Mail, um sie zu bestätigen."
please_confirm_your_email_address_using_the_link_below_: 'Bitte bestätigen Sie Ihre E-Mail-Adresse über den folgenden Link:'
confirm_email: E-Mail bestätigen
unconfirmed: Unbestätigt
you_requested_to_reset_your_password_use_the_link_below_to_continue: Sie haben angefordert, Ihr Passwort zurückzusetzen. Verwenden Sie den untenstehenden Link, um fortzufahren.
if_you_didnt_request_this_you_can_ignore_this_email: "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail ignorieren."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Ihr Passwort wird erst geändert, wenn Sie den obigen Link öffnen und ein neues festlegen."
devise:
confirmations:
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
@ -5685,6 +5750,7 @@ de: &de
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
send_sms_to_html: '<b>SMS gesendet</b> an %{submitter_name}'
send_2fa_sms_to_html: '<b>Verifizierungs-SMS gesendet</b> an %{submitter_name}'
send_2fa_email_to_html: '<b>Verifizierungs-E-Mail gesendet</b> an %{submitter_name}'
open_email_by_html: '<b>E-Mail geöffnet</b> von %{submitter_name}'
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}'
@ -5817,6 +5883,7 @@ pl:
completed_on_time: 'Zakończono %{time}'
document_has_been_signed_already: 'Dokument został już podpisany'
form_has_been_submitted_already: Formularz został już przesłany
re_send_email: Wyślij wiadomość e-mail ponownie
send_copy_to_email: Wyślij kopię na Email
sending: Wysyłanie
resubmit: Prześlij ponownie
@ -5867,6 +5934,10 @@ pl:
please_reply_to_this_email_if_you_didnt_request_this: Odpowiedz na ten e-mail, jeśli nie prosiłeś o to.
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: Twoje konto użytkownika zostało zarchiwizowane. Skontaktuj się z administratorem, aby przywrócić dostęp do konta.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: Nie udało się uzyskać dostępu do Twojego adresu e-mail. Może to być spowodowane literówką w adresie lub niedostępnością skrzynki. Skontaktuj się z support@docuseal.com, aby się zalogować.
verification_required_refresh_the_page_and_pass_2fa: Weryfikacja wymagana, odśwież stronę i przejdź uwierzytelnienie dwuskładnikowe.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: Nadawca zażądał uwierzytelniania dwuskładnikowego za pośrednictwem hasła jednorazowego wysłanego na Twój adres e-mail <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Skontaktuj się z nadawcą, aby podać swój adres e-mail do uwierzytelniania dwuskładnikowego.
rate_limit_exceeded: Przekroczono limit
uk:
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
@ -5908,6 +5979,7 @@ uk:
completed_on_time: 'Завершено %{time}'
document_has_been_signed_already: 'Документ уже був підписаний'
form_has_been_submitted_already: Форма вже була подана
re_send_email: Надіслати e-mail знову
send_copy_to_email: Надіслати копію на Email
sending: Надсилання
resubmit: Подати знову
@ -5958,6 +6030,10 @@ uk:
please_reply_to_this_email_if_you_didnt_request_this: Відповідайте на цей лист, якщо ви цього не запитували.
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: Ваш обліковий запис було архівовано. Зверніться до адміністратора, щоб відновити доступ.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: Не вдалося отримати доступ до вашої електронної пошти. Це може статися, якщо була допущена помилка в адресі або ваша скринька недоступна. Зверніться до support@docuseal.com, щоб увійти.
verification_required_refresh_the_page_and_pass_2fa: Необхідна верифікація, оновіть сторінку та пройдіть двофакторну автентифікацію.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: Відправник запросив двофакторну автентифікацію за допомогою одноразового пароля, відправленого на вашу електронну пошту <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Будь ласка, зв'яжіться з відправником, щоб вказати вашу електронну пошту для двофакторної автентифікації.
rate_limit_exceeded: Перевищено ліміт
cs:
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
@ -5999,6 +6075,7 @@ cs:
completed_on_time: 'Dokončeno %{time}'
document_has_been_signed_already: 'Dokument již byl podepsán'
form_has_been_submitted_already: Formulář již byl odeslán
re_send_email: Znovu odeslat e-mail
send_copy_to_email: Odeslat kopii na Email
sending: Odesílání
resubmit: Odeslat znovu
@ -6049,6 +6126,10 @@ cs:
please_reply_to_this_email_if_you_didnt_request_this: Odpovězte na tento e-mail, pokud jste o to nežádali.
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: Váš uživatelský účet byl archivován. Kontaktujte svého administrátora pro obnovení přístupu.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: Nepodařilo se dosáhnout na váš e-mail. To se může stát, pokud je v adrese překlep nebo vaše schránka není dostupná. Kontaktujte support@docuseal.com pro přihlášení.
verification_required_refresh_the_page_and_pass_2fa: Ověření vyžadováno. Aktualizujte stránku a dokončete dvoufaktorové ověření.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: Odesílatel požádal o dvoufaktorové ověření pomocí jednorázového hesla odeslaného na vaši e-mailovou adresu <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Prosím kontaktujte odesílatele a uveďte svůj e-mail pro dvoufaktorové ověření.
rate_limit_exceeded: Překročena hranice
he:
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
@ -6090,6 +6171,7 @@ he:
completed_on_time: 'הושלם ב-%{time}'
document_has_been_signed_already: 'המסמך כבר נחתם'
form_has_been_submitted_already: הטופס כבר נשלח
re_send_email: שלח דוא"ל שוב
send_copy_to_email: שלח עותק לדוא"ל
sending: שולח
resubmit: שלח מחדש
@ -6140,8 +6222,14 @@ he:
please_reply_to_this_email_if_you_didnt_request_this: 'השב למייל זה אם לא ביקשת זאת.'
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: החשבון שלך הועבר לארכיון. פנה למנהל המערכת כדי לשחזר את הגישה.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: לא ניתן היה לגשת לדוא"ל שלך. ייתכן שזה קרה עקב שגיאת כתיב בכתובת או אם תיבת הדואר אינה זמינה. אנא פנה ל־support@docuseal.com כדי להתחבר.
verification_required_refresh_the_page_and_pass_2fa: נדרשת אימות. רענן את הדף והשלם אימות דו-שלבי.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: השולח ביקש אימות דו-שלבי באמצעות סיסמה חד-פעמית שנשלחה לכתובת הדוא"ל שלך <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: אנא פנה לשולח וציין את כתובת הדוא"ל שלך לאימות דו-שלבי.
rate_limit_exceeded: חריגה ממגבלת
nl: &nl
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Sjablonen waarvoor e-mail- of telefoon-2FA vereist is, kunnen niet via een gedeelde link worden gebruikt.
make_owner: Eigenaar maken
billing: Facturatie
add_from_google_drive: Toevoegen vanuit Google Drive
or_add_from: Of toevoegen vanuit
@ -6980,11 +7068,19 @@ nl: &nl
reports: Rapporten
completed_submissions: Voltooide inzendingen
sms: SMS
require_email_2fa_to_open: E-mail 2FA vereisen om te openen
verification_required_refresh_the_page_and_pass_2fa: Verificatie vereist. Vernieuw de pagina en voer twee-factor-authenticatie uit.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: De afzender heeft twee-factor-authenticatie aangevraagd via een eenmalig wachtwoord dat naar uw e-mailadres <b>%{email}</b> is verzonden.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Neem alstublieft contact op met de afzender om uw e-mailadres op te geven voor twee-factor-authenticatie.
rate_limit_exceeded: Snelheidslimiet overschreden
a_confirmation_email_has_been_sent_to_the_new_email_address: Er is een bevestigingsmail verzonden naar het nieuwe e-mailadres.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} wacht op bevestiging. Volg de link in de e-mail om te bevestigen."
please_confirm_your_email_address_using_the_link_below_: 'Bevestig je e-mailadres via de onderstaande link:'
confirm_email: E-mailadres bevestigen
unconfirmed: Onbevestigd
you_requested_to_reset_your_password_use_the_link_below_to_continue: Je hebt gevraagd je wachtwoord te resetten. Gebruik de onderstaande link om verder te gaan.
if_you_didnt_request_this_you_can_ignore_this_email: "Als je dit niet hebt aangevraagd, kun je deze e-mail negeren."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Je wachtwoord wordt niet gewijzigd totdat je de bovenstaande link opent en een nieuw wachtwoord instelt."
devise:
confirmations:
confirmed: Je e-mailadres is succesvol bevestigd.
@ -7006,6 +7102,7 @@ nl: &nl
send_reminder_email_to_html: "<b>Herinneringsmail verzonden</b> naar %{submitter_name}"
send_sms_to_html: "<b>SMS verzonden</b> naar %{submitter_name}"
send_2fa_sms_to_html: "<b>Verificatie-SMS verzonden</b> naar %{submitter_name}"
send_2fa_email_to_html: "<b>Verificatie-e-mail verzonden</b> naar %{submitter_name}"
open_email_by_html: "<b>E-mail geopend</b> door %{submitter_name}"
click_email_by_html: "<b>E-maillink aangeklikt</b> door %{submitter_name}"
click_sms_by_html: "<b>SMS-link aangeklikt</b> door %{submitter_name}"
@ -7137,6 +7234,7 @@ ar:
completed_on_time: 'تم إكماله في %{time}'
document_has_been_signed_already: 'تم توقيع الوثيقة بالفعل'
form_has_been_submitted_already: تم تقديم الاستمارة بالفعل
re_send_email: إعادة إرسال البريد الإلكتروني
send_copy_to_email: إرسال نسخة إلى البريد الإلكتروني
sending: جارٍ الإرسال
resubmit: إعادة التقديم
@ -7188,6 +7286,10 @@ ar:
please_reply_to_this_email_if_you_didnt_request_this: 'يرجى الرد على هذا البريد إذا لم تطلب ذلك.'
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: تم أرشفة حسابك. يرجى التواصل مع المسؤول لاستعادة الوصول إلى حسابك.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: تعذّر الوصول إلى بريدك الإلكتروني. قد يحدث هذا في حال وجود خطأ في العنوان أو إذا كانت صندوق البريد غير متاح. يرجى التواصل مع support@docuseal.com لتسجيل الدخول.
verification_required_refresh_the_page_and_pass_2fa: مطلوب التحقق. قم بتحديث الصفحة وأكمل المصادقة الثنائية.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: طلب المرسل المصادقة الثنائية عبر كلمة مرور لمرة واحدة مرسلة إلى عنوان بريدك الإلكتروني <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: يرجى الاتصال بالمرسل لتحديد عنوان بريدك الإلكتروني للمصادقة الثنائية.
rate_limit_exceeded: تم تجاوز الحد المسموح به
ko:
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
@ -7228,6 +7330,7 @@ ko:
completed_on_time: '%{time}에 완료됨'
document_has_been_signed_already: '문서가 이미 서명되었습니다'
form_has_been_submitted_already: 양식이 이미 제출되었습니다.
re_send_email: 이메일 다시 보내기
send_copy_to_email: 이메일로 사본 보내기
sending: 전송 중
resubmit: 다시 제출
@ -7279,6 +7382,10 @@ ko:
please_reply_to_this_email_if_you_didnt_request_this: 요청하지 않았다면 이 이메일에 회신하세요.
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: 사용자 계정이 보관되었습니다. 계정 접근을 복원하려면 관리자에게 문의하세요.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: 이메일에 접근할 수 없습니다. 주소에 오타가 있거나 메일박스가 사용 불가능한 경우 발생할 수 있습니다. 로그인하려면 support@docuseal.com에 문의하세요.
verification_required_refresh_the_page_and_pass_2fa: 인증이 필요합니다. 페이지를 새로 고치고 2단계 인증을 완료하세요.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: 발신자가 <b>%{email}</b> 이메일 주소로 전송된 일회용 비밀번호를 통해 2단계 인증을 요청했습니다.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: 2단계 인증을 위해 이메일 주소를 지정하려면 발신자에게 문의하세요.
rate_limit_exceeded: 속도 제한 초과
ja:
require_phone_2fa_to_open: 電話による2段階認証が必要です
@ -7319,6 +7426,7 @@ ja:
completed_on_time: '%{time} に完了'
document_has_been_signed_already: ドキュメントはすでに署名されています
form_has_been_submitted_already: フォームはすでに送信されています
re_send_email: メールを再度送信
send_copy_to_email: メールにコピーを送信
sending: 送信中
resubmit: 再送信
@ -7370,6 +7478,10 @@ ja:
please_reply_to_this_email_if_you_didnt_request_this: このリクエストを行っていない場合は、このメールに返信してください。
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: あなたのユーザーアカウントはアーカイブされました。アクセスを復元するには管理者に連絡してください。
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: メールにアクセスできませんでした。アドレスの入力ミスやメールボックスが利用できない場合に発生することがあります。ログインするには support@docuseal.com に連絡してください。
verification_required_refresh_the_page_and_pass_2fa: 認証が必要です。ページを更新して2段階認証を完了してください。
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: 送信者は、<b>%{email}</b> メールアドレスに送信されたワンタイムパスワードによる2段階認証を要求しました。
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: 2段階認証用にメールアドレスを指定するために、送信者にお問い合わせください。
rate_limit_exceeded: レート制限を超えました
en-US:
<<: *en

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

@ -250,7 +250,7 @@ module Submissions
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? }
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.email_verified? }
is_phone_verified =
submission.template_fields.any? do |e|

@ -24,7 +24,7 @@ module TextUtils
text.to_s.gsub(TRANSLITERATION_REGEXP) { |e| TRANSLITERATIONS[e] }
end
def mask_value(text, unmask_size = 0)
def mask_value(text, unmask_size = 0, mask_symbol = MASK_SYMBOL)
if unmask_size.is_a?(Numeric) && !unmask_size.zero? && unmask_size.abs < text.length
if unmask_size.negative?
[
@ -34,14 +34,34 @@ module TextUtils
elsif unmask_size.positive?
[
text.first(unmask_size),
text.last(text.length - unmask_size).gsub(MASK_REGEXP, MASK_SYMBOL)
text.last(text.length - unmask_size).gsub(MASK_REGEXP, mask_symbol)
].join
end
else
text.to_s.gsub(MASK_REGEXP, MASK_SYMBOL)
text.to_s.gsub(MASK_REGEXP, mask_symbol)
end
end
def mask_email(email, unmask_size = 2)
return email if email.exclude?('@')
local, domain = email.split('@', 2)
return email if local.blank? || domain.blank?
masked_local = mask_value(local, unmask_size, '*')
domain_parts = domain.split('.')
if domain_parts.blank? || domain_parts[0].blank?
masked_domain = mask_value(domain, 1, '*')
else
domain_parts[0] = mask_value(domain_parts[0], 1, '*')
masked_domain = domain_parts.join('.')
end
"#{masked_local}@#{masked_domain}"
end
def maybe_rtl_reverse(text)
if text.match?(RTL_REGEXP)
TwitterCldr::Shared::Bidi

@ -0,0 +1,9 @@
# frozen_string_literal: true
class DeviseMailerPreview < ActionMailer::Preview
def reset_password_instructions
user = User.first
token = user.send(:set_reset_password_token)
Devise::Mailer.reset_password_instructions(user, token)
end
end

@ -22,4 +22,8 @@ class SubmitterMailerPreview < ActionMailer::Preview
SubmitterMailer.documents_copy_email(submitter)
end
def otp_verification_email
SubmitterMailer.otp_verification_email(Submitter.last)
end
end

@ -1130,4 +1130,76 @@ RSpec.describe 'Signing Form' do
end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1)
end
end
context 'when the 2FA email verification is enabled', sidekiq: :inline do
let(:template) { create(:template, account:, author:, only_field_types: %w[text]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
before do
template.update(preferences: { require_email_2fa: true })
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
value: GenerateCertificate.call.transform_values(&:to_pem))
end
it 'completes the form if the one-time password is filled correctly' do
visit submit_form_path(slug: submitter.slug)
click_button 'Send verification code'
email = ActionMailer::Base.deliveries.last
one_time_code = email.body.encoded[%r{<b>(\d{6})</b>}, 1]
fill_in 'one_time_code', with: one_time_code
click_button 'Submit'
fill_in 'First Name', with: 'Mary'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'First Name')).to eq 'Mary'
end
it "doesn't complete the form if the one-time code is invalid" do
visit submit_form_path(slug: submitter.slug)
click_button 'Send verification code'
fill_in 'one_time_code', with: '123456'
click_button 'Submit'
expect(page).to have_content 'Invalid code'
end
it 'completes the form after resending the one time code' do
visit submit_form_path(slug: submitter.slug)
click_button 'Send verification code'
find('#resend_label').click
email = ActionMailer::Base.deliveries.last
one_time_code = email.body.encoded[%r{<b>(\d{6})</b>}, 1]
fill_in 'one_time_code', with: one_time_code
click_button 'Submit'
fill_in 'First Name', with: 'Mary'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'First Name')).to eq 'Mary'
end
end
end

Loading…
Cancel
Save