Compare commits

..

20 Commits

Author SHA1 Message Date
Alex Turchyn 33ca930055
Merge from docusealco/wip
1 month ago
Pete Matsyburka 3b396f8421 fix redirect
1 month ago
Pete Matsyburka aa77e3a8d3 adjust port check
1 month ago
Pete Matsyburka 75316d8d87 fix draw custom field
1 month ago
Pete Matsyburka a999109a5c add port check
1 month ago
Pete Matsyburka bdd33c7d6b fix spec
1 month ago
Pete Matsyburka 61c5ee22a0 hide form credentials
1 month ago
Pete Matsyburka 12c5b909e0 use cancan
1 month ago
Pete Matsyburka 871ef6dda6 add fetch options
1 month ago
Pete Matsyburka 40052a2d7c use query_params
1 month ago
Pete Matsyburka 347be0137d refactor 2fa
1 month ago
Pete Matsyburka fe6baba8bf fix erb lint
1 month ago
Pete Matsyburka ed8c313bd4 timestamp controller multitenant
1 month ago
Pete Matsyburka ca0acb34d6 use url for open modal
1 month ago
Pete Matsyburka 34ea639c25 escape wildcard query
1 month ago
Pete Matsyburka 680ab9dbed raise invalid param
1 month ago
Pete Matsyburka 9377766e52 fix url download
1 month ago
Pete Matsyburka 7a2c37454e update favicon
1 month ago
Pete Matsyburka 8f6f418f54 fix font size
1 month ago
Pete Matsyburka 9b7745c565 hide resend on archived
1 month ago

@ -27,20 +27,18 @@ class SubmissionsDownloadController < ApplicationController
Submissions::EnsureResultGenerated.call(last_submitter)
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
if !signature_valid && !current_user_submitter?(last_submitter)
return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
return head :not_found
if last_submitter.completed_at < TTL.ago
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
end
if params[:combined] == 'true'
url = build_combined_url(@submitter)
if url
render json: [url]
else
head :not_found
end
respond_with_combined(last_submitter)
else
render json: build_urls(last_submitter)
end
@ -48,8 +46,18 @@ class SubmissionsDownloadController < ApplicationController
private
def respond_with_combined(submitter)
url = build_combined_url(submitter)
if url
render json: [url]
else
head :not_found
end
end
def current_user_submitter?(submitter)
current_user && current_user.account.submitters.exists?(id: submitter.id)
current_user && current_ability.can?(:read, submitter)
end
def build_urls(submitter)

@ -9,15 +9,15 @@ 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]
before_action :maybe_require_link_2fa, only: %i[show]
CONFIG_KEYS = [].freeze
def show
submission = @submitter.submission
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
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)
@ -48,7 +48,7 @@ class SubmitFormController < ApplicationController
end
def update
if require_email_2fa?(@submitter)
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
status: :unprocessable_content
end
@ -84,7 +84,9 @@ 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)
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
redirect_to submit_form_path(params[:submit_form_slug])
end
def success; end
@ -92,10 +94,7 @@ 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
return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request)
redirect_to start_form_path(@submitter.submission.template.slug)
end
@ -117,12 +116,4 @@ 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

@ -11,7 +11,9 @@ class SubmitFormDeclineController < ApplicationController
submitter.completed_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template&.archived_at?
submitter.submission.template&.archived_at? ||
!Submitters::AuthorizedForForm.call(submitter, current_user,
request)
ApplicationRecord.transaction do
submitter.update!(declined_at: Time.current)

@ -17,7 +17,8 @@ class SubmitFormDownloadController < ApplicationController
@submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false)
value: false) ||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
last_completed_submitter = @submitter.submission.submitters
.where.not(id: @submitter.id)

@ -12,7 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
return redirect_to submit_form_path(@submitter.slug)
end

@ -45,7 +45,8 @@ class SubmitFormInviteController < ApplicationController
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at?
!submitter.submission.template&.archived_at? &&
Submitters::AuthorizedForForm.call(submitter, current_user, request)
end
def filter_invite_submitters(submitter, key = 'invite_by_uuid')

@ -7,10 +7,12 @@ class SubmitFormValuesController < ApplicationController
def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return render json: {} if submitter.completed_at? || submitter.declined_at?
return render json: {} if submitter.submission.template&.archived_at? ||
return render json: {} if submitter.completed_at? ||
submitter.declined_at? ||
submitter.submission.template&.archived_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired?
submitter.submission.expired? ||
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
value = submitter.values[params['field_uuid']]
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?

@ -56,7 +56,7 @@ class TemplatesUploadsController < ApplicationController
def create_file_params_from_url
tempfile = Tempfile.new
tempfile.binmode
tempfile.write(DownloadUtils.call(params[:url]).body)
tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
tempfile.rewind
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?

@ -161,6 +161,11 @@ export default {
required: false,
default: false
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
},
completedButton: {
type: Object,
required: false,
@ -214,7 +219,10 @@ export default {
download () {
this.isDownloading = true
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, {
method: 'GET',
...this.fetchOptions
}).then(async (response) => {
if (response.ok) {
const urls = await response.json()
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)

@ -530,6 +530,7 @@
v-else-if="isInvite"
:submitters="inviteSubmitters"
:optional-submitters="optionalInviteSubmitters"
:fetch-options="fetchOptions"
:submitter-slug="submitterSlug"
:authenticity-token="authenticityToken"
:url="baseUrl + submitPath + '/invite'"
@ -543,6 +544,7 @@
:has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))"
:has-multiple-documents="hasMultipleDocuments"
:completed-button="completedRedirectUrl ? {} : completedButton"
:fetch-options="fetchOptions"
:completed-message="completedRedirectUrl ? {} : completedMessage"
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
:with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun"
@ -678,6 +680,11 @@ export default {
required: false,
default: () => []
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
},
optionalInviteSubmitters: {
type: Array,
required: false,
@ -1467,7 +1474,8 @@ export default {
} else {
return fetch(this.baseUrl + this.submitPath, {
method: 'POST',
body: formData || new FormData(this.$refs.form)
body: formData || new FormData(this.$refs.form),
...this.fetchOptions
}).then((response) => {
if (response.status === 200) {
currentFieldUuids.forEach((fieldUuid) => {

@ -78,6 +78,11 @@ export default {
type: Array,
required: true
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
},
optionalSubmitters: {
type: Array,
required: false,
@ -108,7 +113,8 @@ export default {
return fetch(this.url, {
method: 'POST',
body: new FormData(this.$refs.form)
body: new FormData(this.$refs.form),
...this.fetchOptions
}).then((response) => {
if (response.status === 200) {
this.$emit('success')

@ -261,7 +261,7 @@ export default {
},
computed: {
isSelectMode () {
return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField
return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField && !this.drawCustomField
},
pageSelectedAreas () {
if (!this.selectedAreasRef.value) return []

@ -26,7 +26,7 @@ class SendTestWebhookRequestJob
Addressable::URI.parse(webhook_url.url).normalize
end
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https'
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' || [443, nil].exclude?(uri.port)
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
end

@ -22,7 +22,7 @@
</div>
<div class="form-control">
<%= ff.label :password, 'Password', class: 'label' %>
<%= ff.password_field :password, value: value['password'], class: 'base-input' %>
<%= ff.password_field :password, class: 'base-input', required: value['password'].present?, placeholder: value['password'].present? ? '*************' : '' %>
</div>
</div>
<div class="grid md:grid-cols-2 gap-4">

@ -22,7 +22,7 @@
<% if params[:modal].present? %>
<% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
<% if url_params[:action] == 'new' %>
<open-modal src="<%= params[:modal] %>"></open-modal>
<open-modal src="<%= url_for(url_params) %>"></open-modal>
<% end %>
<% end %>
<turbo-frame id="modal"></turbo-frame>

@ -4,7 +4,7 @@
"id": "/",
"icons": [
{
"src": "/logo.svg",
"src": "/favicon.svg",
"type": "image/svg+xml",
"sizes": "any"
},

@ -27,4 +27,5 @@
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta name="theme-color" content="#faf7f5">

@ -6,7 +6,7 @@
<% end %>
<% if params[:q].present? %>
<div class="relative">
<a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
<a href="<%= url_for(params: request.query_parameters.except('q')) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
&times;
</a>
</div>

@ -8,7 +8,7 @@
</div>
<div class="form-control">
<%= fff.label :secret_access_key, class: 'label' %>
<%= fff.password_field :secret_access_key, value: configs['secret_access_key'], required: true, class: 'base-input' %>
<%= fff.password_field :secret_access_key, required: true, class: 'base-input', placeholder: configs['secret_access_key'].present? ? '*************' : '' %>
</div>
</div>
<div class="grid md:grid-cols-2 gap-4">

@ -13,7 +13,7 @@
</div>
<div class="form-control">
<%= fff.label :storage_access_key, 'Storage Access Key', class: 'label' %>
<%= fff.password_field :storage_access_key, value: configs['storage_access_key'], required: true, class: 'base-input' %>
<%= fff.password_field :storage_access_key, required: true, class: 'base-input', placeholder: configs['storage_access_key'].present? ? '*************' : '' %>
</div>
<% end %>
<% end %>

@ -13,7 +13,7 @@
</div>
<div class="form-control">
<%= fff.label :credentials, 'Credentials (JSON key content)', class: 'label' %>
<%= fff.text_area :credentials, value: configs['credentials'], required: true, class: 'base-textarea w-full font-mono', rows: 4 %>
<%= fff.text_area :credentials, required: true, class: 'base-textarea w-full font-mono', rows: 4, placeholder: configs['credentials'].present? ? "{\n**REDACTED**\n}" : '' %>
</div>
<% end %>
<% end %>

@ -68,7 +68,7 @@
<% end %>
</div>
<% end %>
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? %>
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
<% end %>
</div>
@ -159,7 +159,7 @@
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
</span>
</div>
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %>
@ -225,15 +225,15 @@
</span>
</div>
<% end %>
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
<div class="mt-2 mb-1">
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
</div>
<% end %>
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
<%= render 'submissions/send_sms_button', submitter: %>
<% end %>
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
<div class="mt-2 mb-1">
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
<%= t('sign_in_person') %>

@ -35,19 +35,19 @@
<% if is_show_tabs %>
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
<a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('list', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('all') %></span>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'pending')) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('clock', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('pending') %></span>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'completed')) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('completed') %></span>

@ -5,7 +5,7 @@
<%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %>
<span class="font-normal truncate"><%= t(params[:status]) %></span>
<% end %>
<%= link_to url_for(params.to_unsafe_h.except(:status)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
<%= link_to url_for(params: request.query_parameters.except('status')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
<%= svg_icon('x', class: 'w-5 h-5') %>
<% end %>
</div>
@ -16,7 +16,7 @@
<%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %>
<span class="font-normal truncate"><%= params[:folder] %></span>
<% end %>
<%= link_to url_for(params.to_unsafe_h.except(:folder)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
<%= link_to url_for(params: request.query_parameters.except('folder')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
<%= svg_icon('x', class: 'w-5 h-5') %>
<% end %>
</div>
@ -27,7 +27,7 @@
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span>
<% end %>
<%= link_to url_for(params.to_unsafe_h.except(:author)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
<%= link_to url_for(params: request.query_parameters.except('author')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
<%= svg_icon('x', class: 'w-5 h-5') %>
<% end %>
</div>
@ -46,7 +46,7 @@
<% end %>
</span>
<% end %>
<%= link_to url_for(params.to_unsafe_h.except(:completed_at_from, :completed_at_to)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
<%= link_to url_for(params: request.query_parameters.except('completed_at_from', 'completed_at_to')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
<%= svg_icon('x', class: 'w-5 h-5') %>
<% end %>
</div>
@ -65,7 +65,7 @@
<% end %>
</span>
<% end %>
<%= link_to url_for(params.to_unsafe_h.except(:created_at_to, :created_at_from)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
<%= link_to url_for(params: request.query_parameters.except('created_at_to', 'created_at_from')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
<%= svg_icon('x', class: 'w-5 h-5') %>
<% end %>
</div>

@ -10,7 +10,7 @@
</div>
<% if params[:with_remove] %>
<div class="text-center w-full mt-4">
<%= link_to t('remove_filter'), "#{params[:path]}?#{params.to_unsafe_h.slice(:q).merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
<%= link_to t('remove_filter'), "#{params[:path]}?#{request.query_parameters.slice('q').merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
</div>
<% end %>
<% end %>

@ -30,7 +30,7 @@
<% if is_show_tabs %>
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
<a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<div class="flex items-center space-x-1">
<%= svg_icon('list', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('all') %></span>
@ -41,7 +41,7 @@
</div>
<% end %>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'pending')) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<div class="flex items-center space-x-1">
<%= svg_icon('clock', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('pending') %></span>
@ -52,7 +52,7 @@
</div>
<% end %>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'completed')) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
<div class="flex items-center space-x-1">
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
<span class="font-normal"><%= t('completed') %></span>

@ -85,9 +85,9 @@
<div class="mt-6">
<h2 id="log" class="text-3xl font-bold"><%= t('events_log') %></h2>
<div class="tabs border-b mt-4">
<%= link_to t('all'), url_for(params.to_unsafe_h.except(:status)), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
<%= link_to t('succeeded'), url_for(params.to_unsafe_h.merge(status: 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
<%= link_to t('failed'), url_for(params.to_unsafe_h.merge(status: 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
<%= link_to t('all'), url_for(params: request.query_parameters.except('status')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
<%= link_to t('succeeded'), url_for(params: request.query_parameters.merge('status' => 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
<%= link_to t('failed'), url_for(params: request.query_parameters.merge('status' => 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
</div>
<% if @webhook_events.present? %>
<div class="divide-y divide-base-300 rounded-lg">

@ -56,7 +56,7 @@ Rails.application.routes.draw do
resources :account_custom_fields, only: %i[create]
resources :user_configs, only: %i[create]
resources :encrypted_user_configs, only: %i[destroy]
resources :timestamp_server, only: %i[create]
resources :timestamp_server, only: %i[create] unless Docuseal.multitenant?
resources :dashboard, only: %i[index]
resources :setup, only: %i[index create]
resource :newsletter, only: %i[show update]

@ -35,16 +35,16 @@ module DownloadUtils
module_function
def call(url)
def call(url, validate: Docuseal.multitenant?)
uri = begin
URI(url)
rescue URI::Error
Addressable::URI.parse(url).normalize
end
validate_uri!(uri) if Docuseal.multitenant?
validate_uri!(uri) if validate
resp = conn.get(uri)
resp = conn(validate:).get(uri)
raise UnableToDownload, "Error loading: #{uri}" if resp.status >= 400
@ -52,14 +52,15 @@ module DownloadUtils
end
def validate_uri!(uri)
raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https'
raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https' ||
[443, nil].exclude?(uri.port)
raise UnableToDownload, "Error loading: #{uri}. Can't download from localhost." if uri.host.in?(LOCALHOSTS)
end
def conn
def conn(validate: Docuseal.multitenant?)
Faraday.new do |faraday|
faraday.response :follow_redirects, callback: lambda { |_, new_env|
validate_uri!(new_env[:url]) if Docuseal.multitenant?
validate_uri!(new_env[:url]) if validate
}
end
end

@ -22,7 +22,7 @@ module SendWebhookRequest
end
if Docuseal.multitenant?
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' &&
raise HttpsError, 'Only HTTPS is allowed.' if (uri.scheme != 'https' || [443, nil].exclude?(uri.port)) &&
!AccountConfig.exists?(key: :allow_http,
account_id: webhook_url.account_id)
raise LocalhostError, "Can't send to localhost." if uri.host.in?(LOCALHOSTS)

@ -18,7 +18,8 @@ module Submissions
def plain_search(submissions, keyword, search_values: false, search_template: false)
return submissions if keyword.blank?
term = "%#{keyword.downcase}%"
sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase)
term = "%#{sanitized}%"
arel_table = Submitter.arel_table
@ -31,7 +32,7 @@ module Submissions
if search_template
submissions = submissions.left_joins(:template)
arel = arel.or(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
arel = arel.or(Template.arel_table[:name].lower.matches("%#{sanitized}%"))
end
submissions.joins(:submitters).where(arel).group(:id)

@ -162,7 +162,7 @@ module Submissions
pdf.trailer.info[:DocumentID] = document_id
pdf.pages.each do |page|
font_size = (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * 9).to_i
font_size = [(([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * 9).to_i, 4].max
cnv = page.canvas(type: :overlay)
text =

@ -14,6 +14,7 @@ module Submitters
UnableToSendCode = Class.new(StandardError)
InvalidOtp = Class.new(StandardError)
MaliciousFileExtension = Class.new(StandardError)
ArgumentError = Class.new(StandardError)
DANGEROUS_EXTENSIONS = Set.new(%w[
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
@ -133,7 +134,7 @@ module Submitters
filename: file.original_filename,
content_type: file.content_type)
else
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
raise ArgumentError, 'file param is missing'
end
ActiveStorage::Attachment.create!(

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Submitters
module AuthorizedForForm
Unauthorized = Class.new(StandardError)
module_function
def call(submitter, current_user, request)
pass_email_2fa?(submitter, request) && pass_link_2fa?(submitter, current_user, request)
end
def pass_email_2fa?(submitter, request)
return false unless submitter
return true if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
submitter.preferences['require_email_2fa'] != true
return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug
token = request.params[:two_factor_token].presence || request.headers['x-two-factor-token'].presence
return true if token.present? &&
Submitter.signed_id_verifier.verified(token, purpose: :email_two_factor) == submitter.slug
false
end
def pass_link_2fa?(submitter, current_user, request)
return false unless submitter
return true if submitter.submission.source != 'link'
return true unless submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug
return true if submitter.email == current_user&.email && current_user&.account_id == submitter.account_id
if (token = request.params[:two_factor_token].presence || request.headers['x-two-factor-token'].presence)
link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':')
return true if Submitter.signed_id_verifier.verified(token, purpose: :email_two_factor) == link_2fa_key
end
false
end
end
end

@ -236,7 +236,7 @@ module Submitters
return blob if blob
data = DownloadUtils.call(url).body
data = DownloadUtils.call(url, validate: true).body
checksum = Digest::MD5.base64digest(data)

@ -20,7 +20,9 @@ module TemplateFolders
def search(folders, keyword)
return folders if keyword.blank?
folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase)
folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{sanitized}%"))
end
def filter_active_folders(template_folders, templates)

@ -52,7 +52,9 @@ module Templates
def plain_search(templates, keyword)
return templates if keyword.blank?
templates.where(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase)
templates.where(Template.arel_table[:name].lower.matches("%#{sanitized}%"))
end
def fulltext_search(current_user, templates, keyword)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 B

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,5 @@
<svg height="180" width="180" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg">
<circle fill="white" cx="90" cy="90" r="88"/>
<path fill="#e0753f" d="M 178.224 72.09 c -0.296 -1.463 -0.627 -2.919 -0.996 -4.364 -0.293 -1.151 -0.616 -2.293 -0.956 -3.433 -0.301 -1.008 -0.612 -2.014 -0.95 -3.012 -0.531 -1.578 -1.113 -3.142 -1.735 -4.694 -0.216 -0.54 -0.433 -1.082 -0.661 -1.618 -0.195 -0.462 -0.399 -0.917 -0.601 -1.375 -0.262 -0.591 -0.53 -1.177 -0.804 -1.762 -0.074 -0.159 -0.151 -0.315 -0.226 -0.474 -0.209 -0.441 -0.422 -0.881 -0.638 -1.318 -0.076 -0.154 -0.153 -0.306 -0.229 -0.459 -0.236 -0.471 -0.477 -0.939 -0.721 -1.406 -0.053 -0.101 -0.105 -0.201 -0.158 -0.302 -1.143 -2.16 -2.367 -4.269 -3.68 -6.322 -0.116 -0.181 -0.237 -0.359 -0.355 -0.539 -0.094 -0.144 -0.189 -0.288 -0.284 -0.432 -0.284 -0.431 -0.57 -0.861 -0.862 -1.287 -0.112 -0.164 -0.225 -0.326 -0.338 -0.489 -0.193 -0.279 -0.382 -0.56 -0.579 -0.836 -0.089 -0.125 -0.182 -0.249 -0.273 -0.374 -0.13 -0.182 -0.264 -0.362 -0.395 -0.542 -0.277 -0.38 -0.556 -0.76 -0.838 -1.135 -0.15 -0.199 -0.303 -0.395 -0.454 -0.593 -0.21 -0.274 -0.417 -0.552 -0.63 -0.823 -0.055 -0.069 -0.111 -0.136 -0.166 -0.205 -0.482 -0.61 -0.971 -1.216 -1.47 -1.814 -0.129 -0.155 -0.262 -0.306 -0.392 -0.461 -0.402 -0.476 -0.808 -0.95 -1.22 -1.417 -0.186 -0.212 -0.375 -0.422 -0.563 -0.631 -0.384 -0.428 -0.773 -0.854 -1.167 -1.276 -0.176 -0.189 -0.351 -0.379 -0.529 -0.567 -0.564 -0.595 -1.134 -1.186 -1.716 -1.768 -1.091 -1.091 -2.207 -2.15 -3.346 -3.178 -1.016 -0.919 -2.05 -1.815 -3.103 -2.684 -0.772 -0.636 -1.557 -1.255 -2.348 -1.864 -3.465 -2.67 -7.112 -5.075 -10.927 -7.209 -2.869 -1.604 -5.83 -3.06 -8.883 -4.351 -2.443 -1.033 -4.922 -1.948 -7.428 -2.756 -8.879 -2.863 -18.13 -4.318 -27.605 -4.318 -3.19 0 -6.354 0.169 -9.488 0.496 -4.036 0.421 -8.019 1.114 -11.94 2.073 -1.732 0.423 -3.452 0.892 -5.157 1.42 -2.856 0.883 -5.673 1.912 -8.447 3.085 -2.645 1.118 -5.222 2.357 -7.729 3.711 -2.574 1.39 -5.073 2.901 -7.494 4.533 -1.195 0.805 -2.37 1.64 -3.527 2.503 -1.156 0.864 -2.292 1.756 -3.408 2.676 -0.553 0.456 -1.1 0.919 -1.643 1.389 -1.649 1.427 -3.252 2.92 -4.806 4.473 -2.582 2.582 -4.991 5.299 -7.222 8.138 -0.892 1.135 -1.756 2.292 -2.59 3.467 -0.417 0.588 -0.827 1.18 -1.23 1.778 -0.403 0.597 -0.798 1.199 -1.186 1.806 -0.388 0.607 -0.769 1.218 -1.143 1.835 -2.241 3.697 -4.216 7.562 -5.916 11.582 -1.095 2.589 -2.059 5.217 -2.901 7.877 -0.153 0.482 -0.3 0.965 -0.444 1.449 -0.339 1.14 -0.663 2.282 -0.956 3.433 -0.369 1.446 -0.7 2.901 -0.996 4.364 -1.034 5.121 -1.618 10.343 -1.749 15.637 -0.018 0.757 -0.028 1.514 -0.028 2.274 0 1.123 0.02 2.244 0.062 3.361 0.285 7.82 1.568 15.475 3.825 22.879 0.044 0.147 0.088 0.295 0.133 0.441 0.877 2.823 1.894 5.608 3.054 8.35 0.85 2.009 1.769 3.98 2.755 5.912 0.539 1.057 1.105 2.099 1.685 3.132 4.013 7.142 8.98 13.698 14.846 19.564 7.713 7.713 16.611 13.878 26.477 18.352 0.705 0.32 1.415 0.632 2.131 0.935 2.081 0.88 4.185 1.679 6.313 2.396 9.217 3.106 18.85 4.677 28.719 4.677 8.031 0 15.902 -1.047 23.522 -3.107 0.633 -0.172 1.266 -0.35 1.895 -0.535 0.757 -0.222 1.509 -0.456 2.26 -0.698 0.717 -0.232 1.431 -0.474 2.145 -0.723 1.752 -0.616 3.49 -1.281 5.211 -2.009 0.755 -0.319 1.503 -0.651 2.247 -0.989 1.237 -0.563 2.459 -1.15 3.664 -1.766 0.644 -0.328 1.283 -0.665 1.917 -1.009 1.654 -0.896 3.274 -1.848 4.865 -2.844 5.736 -3.591 11.06 -7.827 15.912 -12.679 0.775 -0.775 1.534 -1.562 2.278 -2.36 5.204 -5.59 9.636 -11.754 13.246 -18.417 0.343 -0.634 0.68 -1.274 1.009 -1.917 0.482 -0.944 0.943 -1.9 1.392 -2.863 0.471 -1.007 0.928 -2.021 1.364 -3.049 1.22 -2.886 2.281 -5.82 3.187 -8.793 0.559 -1.833 1.056 -3.68 1.494 -5.542 0.108 -0.458 0.211 -0.916 0.312 -1.376 0.194 -0.883 0.373 -1.77 0.539 -2.659 1.02 -5.455 1.542 -11.02 1.542 -16.663 0 -6.074 -0.595 -12.058 -1.776 -17.911 z m -161.733 19.614 c -1.118 -56.662 44.604 -74.877 60.998 -67.647 2.187 0.965 4.732 2.431 7.042 2.96 5.295 1.213 13.432 -3.113 13.521 6.273 0.078 8.156 -3.389 13.108 -10.797 16.177 -7.539 3.124 -14.777 9.181 -19.95 15.493 -21.487 26.216 -31.231 68.556 -7.565 94.296 -13.679 -5.545 -42.418 -25.467 -43.248 -67.552 z m 91.109 72.619 c -0.053 0.008 -4.171 0.775 -4.171 0.775 0 0 -15.862 -22.957 -23.509 -21.719 11.291 16.04 12.649 22.625 12.649 22.625 -0.053 0.001 -0.107 0.001 -0.161 0.003 -51.831 2.131 -42.785 -64.026 -28.246 -86.502 -1.555 13.073 8.878 39.992 39.034 44.1 9.495 1.293 32.302 -3.275 41.015 -11.38 0.098 1.825 0.163 3.85 0.159 6.013 -0.046 23.538 -13.47 42.743 -36.77 46.085 z m 30.575 -15.708 c 9.647 -9.263 12.869 -27.779 9.103 -44.137 -4.608 -20.011 -28.861 -32.383 -40.744 -35.564 5.766 -8.089 27.908 -14.274 39.567 5.363 -5.172 -10.519 -13.556 -23.023 -1.732 -33.128 12.411 13.329 19.411 29.94 20.161 48.7 0.75 18.753 -6.64 41.768 -26.355 58.765 z"/>
<circle fill="#e0753f" cx="71.927" cy="32.004" r="2.829"/>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

@ -16,6 +16,10 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
end
describe '#perform' do
around do |example|
freeze_time { example.run }
end
before do
stub_request(:post, webhook_url.url).to_return(status: 200)
end

@ -16,6 +16,10 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
end
describe '#perform' do
around do |example|
freeze_time { example.run }
end
before do
stub_request(:post, webhook_url.url).to_return(status: 200)
end

@ -16,6 +16,10 @@ RSpec.describe SendFormStartedWebhookRequestJob do
end
describe '#perform' do
around do |example|
freeze_time { example.run }
end
before do
stub_request(:post, webhook_url.url).to_return(status: 200)
end

@ -16,6 +16,10 @@ RSpec.describe SendFormViewedWebhookRequestJob do
end
describe '#perform' do
around do |example|
freeze_time { example.run }
end
before do
stub_request(:post, webhook_url.url).to_return(status: 200)
end

@ -13,6 +13,10 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
end
describe '#perform' do
around do |example|
freeze_time { example.run }
end
before do
stub_request(:post, webhook_url.url).to_return(status: 200)
end

@ -13,6 +13,10 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
end
describe '#perform' do
around do |example|
freeze_time { example.run }
end
before do
stub_request(:post, webhook_url.url).to_return(status: 200)
end

@ -13,6 +13,10 @@ RSpec.describe SendSubmissionExpiredWebhookRequestJob do
end
describe '#perform' do
around do |example|
freeze_time { example.run }
end
before do
stub_request(:post, webhook_url.url).to_return(status: 200)
end

@ -12,6 +12,10 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
end
describe '#perform' do
around do |example|
freeze_time { example.run }
end
before do
stub_request(:post, webhook_url.url).to_return(status: 200)
end

@ -12,6 +12,10 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
end
describe '#perform' do
around do |example|
freeze_time { example.run }
end
before do
stub_request(:post, webhook_url.url).to_return(status: 200)
end

@ -54,6 +54,7 @@ RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
config.include Devise::Test::IntegrationHelpers
config.include SigningFormHelper
config.include ActiveSupport::Testing::TimeHelpers
config.before(:each, type: :system) do
if ENV['HEADLESS'] == 'false'

@ -61,7 +61,6 @@ RSpec.describe 'Email Settings' do
expect(page).to have_field('Host', with: encrypted_config.value['host'])
expect(page).to have_field('Port', with: encrypted_config.value['port'])
expect(page).to have_field('Username', with: encrypted_config.value['username'])
expect(page).to have_field('Password', with: encrypted_config.value['password'])
expect(page).to have_field('Domain', with: encrypted_config.value['domain'])
expect(page).to have_select('Authentication', selected: 'Plain')
expect(page).to have_field('Send from Email', with: encrypted_config.value['from_email'])

Loading…
Cancel
Save