diff --git a/app/controllers/api/active_storage_blobs_proxy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_controller.rb index a542c637..8ade86c6 100644 --- a/app/controllers/api/active_storage_blobs_proxy_controller.rb +++ b/app/controllers/api/active_storage_blobs_proxy_controller.rb @@ -33,9 +33,15 @@ module Api else http_cache_forever public: true do response.headers['Accept-Ranges'] = 'bytes' - response.headers['Content-Length'] = blob.byte_size.to_s - send_blob_stream blob, disposition: params[:disposition] + if request.head? + response.headers['Content-Type'] = blob.content_type_for_serving + head :ok + else + send_blob_stream blob, disposition: params[:disposition] + end + + response.headers['Content-Length'] = blob.byte_size.to_s end end end @@ -57,8 +63,6 @@ module Api return if !require_ttl && !require_auth end - Rollbar.error('Blob unauthorized') if defined?(Rollbar) - raise CanCan::AccessDenied end end diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index 1b20b87d..fbaf3477 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -203,10 +203,15 @@ module Api submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms') submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to') + if submitter_preferences.key?('require_phone_2fa') submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa'] end + if submitter_preferences.key?('require_email_2fa') + submitter.preferences['require_email_2fa'] = submitter_preferences['require_email_2fa'] + end + if submitter_preferences.key?('go_to_last') submitter.preferences['go_to_last'] = submitter_preferences['go_to_last'] end diff --git a/app/controllers/preview_document_page_controller.rb b/app/controllers/preview_document_page_controller.rb index d0b69f8d..2befe3bf 100644 --- a/app/controllers/preview_document_page_controller.rb +++ b/app/controllers/preview_document_page_controller.rb @@ -41,7 +41,7 @@ class PreviewDocumentPageController < ActionController::API end def find_or_create_document_tempfile_path(attachment) - file_path = "#{Dir.tmpdir}/#{attachment.uuid}" + file_path = "#{Dir.tmpdir}/attachment-#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}" File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f| f.flock(File::LOCK_EX) diff --git a/app/controllers/reveal_access_token_controller.rb b/app/controllers/reveal_access_token_controller.rb index c8959afd..eef24b50 100644 --- a/app/controllers/reveal_access_token_controller.rb +++ b/app/controllers/reveal_access_token_controller.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true class RevealAccessTokenController < ApplicationController + rate_limit to: 4, within: 1.minute, only: %i[create], by: -> { current_user.id }, with: lambda { + Rollbar.error('Rate limit api key') if defined?(Rollbar) + + render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show', + locals: { error_message: I18n.t(:too_many_attempts) }), + status: :unprocessable_content + } + def show authorize!(:manage, current_user.access_token) end diff --git a/app/controllers/send_submission_email_controller.rb b/app/controllers/send_submission_email_controller.rb index c3a95158..41f06bf7 100644 --- a/app/controllers/send_submission_email_controller.rb +++ b/app/controllers/send_submission_email_controller.rb @@ -14,7 +14,7 @@ class SendSubmissionEmailController < ApplicationController template = Template.find_by!(slug: params[:template_slug]) @submitter = - Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase) + Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase) elsif params[:submission_slug] submission = Submission.find_by(slug: params[:submission_slug]) @@ -27,9 +27,11 @@ class SendSubmissionEmailController < ApplicationController @submitter = Submitter.completed.find_by!(slug: params[:submitter_slug]) end - RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes) + if @submitter + RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes) - SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter) + SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter) + end respond_to do |f| f.html { render :success } diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 2adb18b3..f32e1e1e 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class TemplatesController < ApplicationController + TEMPLATE_FIELDS = %i[id author_id folder_id external_id name slug + schema fields submitters variables_schema preferences + shared_link source archived_at created_at updated_at].freeze + load_and_authorize_resource :template def show @@ -33,10 +37,11 @@ class TemplatesController < ApplicationController ).call @template_data = - @template.as_json.merge( + @template.as_json(only: TEMPLATE_FIELDS).merge( documents: @template.schema_documents.as_json( + only: %i[id uuid], methods: %i[metadata signed_key], - include: { preview_images: { methods: %i[url metadata filename] } } + include: { preview_images: { only: %i[id], methods: %i[url metadata filename] } } ) ).to_json diff --git a/app/controllers/testing_accounts_controller.rb b/app/controllers/testing_accounts_controller.rb index 6c6cf3d1..44274eef 100644 --- a/app/controllers/testing_accounts_controller.rb +++ b/app/controllers/testing_accounts_controller.rb @@ -3,7 +3,7 @@ class TestingAccountsController < ApplicationController skip_authorization_check only: :destroy - def show + def create authorize!(:manage, current_account) authorize!(:manage, current_user) diff --git a/app/javascript/draw.js b/app/javascript/draw.js index 3b95b5ea..30d493a3 100644 --- a/app/javascript/draw.js +++ b/app/javascript/draw.js @@ -11,6 +11,7 @@ window.customElements.define('draw-signature', class extends HTMLElement { this.resizeObserver = new ResizeObserver(() => { requestAnimationFrame(() => { if (!this.canvas) return + if (!this.canvas.parentNode?.clientWidth) return const { width, height } = this.canvas @@ -89,7 +90,7 @@ window.customElements.define('draw-signature', class extends HTMLElement { } redrawCanvas (oldWidth, oldHeight) { - if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) { + if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) { const sx = this.canvas.width / oldWidth const sy = this.canvas.height / oldHeight diff --git a/app/javascript/elements/signature_form.js b/app/javascript/elements/signature_form.js index 5fc9af48..aa60b6fb 100644 --- a/app/javascript/elements/signature_form.js +++ b/app/javascript/elements/signature_form.js @@ -14,6 +14,7 @@ export default targetable(class extends HTMLElement { this.resizeObserver = new ResizeObserver(() => { requestAnimationFrame(() => { if (!this.canvas) return + if (!this.canvas.parentNode?.clientWidth) return const { width, height } = this.canvas @@ -80,7 +81,7 @@ export default targetable(class extends HTMLElement { } redrawCanvas (oldWidth, oldHeight) { - if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) { + if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) { const sx = this.canvas.width / oldWidth const sy = this.canvas.height / oldHeight diff --git a/app/javascript/submission_form/appears_on.vue b/app/javascript/submission_form/appears_on.vue index 48bdb028..219ef193 100644 --- a/app/javascript/submission_form/appears_on.vue +++ b/app/javascript/submission_form/appears_on.vue @@ -42,10 +42,25 @@ export default { const areas = {} this.field.areas?.forEach((area) => { - areas[area.attachment_uuid + area.page] ||= area + areas[area.attachment_uuid] ||= [] + areas[area.attachment_uuid].push(area) }) - return Object.values(areas).slice(0, 6) + const sortedAreas = Object.values(areas).reduce((acc, group) => { + const seen = {} + const sortedGroup = [...group].sort((a, b) => a.page - b.page) + + sortedGroup.forEach((area) => { + if (!seen[area.page]) { + seen[area.page] = true + acc.push(area) + } + }) + + return acc + }, []) + + return sortedAreas.slice(0, 6) } } } diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue index c1f19be9..e568f59c 100644 --- a/app/javascript/submission_form/signature_step.vue +++ b/app/javascript/submission_form/signature_step.vue @@ -544,6 +544,7 @@ export default { this.resizeObserver = new ResizeObserver(() => { requestAnimationFrame(() => { if (!this.$refs.canvas) return + if (!this.$refs.canvas.parentNode?.clientWidth) return const { width, height } = this.$refs.canvas @@ -586,7 +587,7 @@ export default { redrawCanvas (oldWidth, oldHeight) { const canvas = this.$refs.canvas - if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) { + if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && canvas.width > 0 && canvas.height > 0) { const sx = canvas.width / oldWidth const sy = canvas.height / oldHeight diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 8e8d9c30..b805af92 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -1012,6 +1012,13 @@ export default { fieldsDragFieldRef: () => ref(), customDragFieldRef: () => ref(), selectedAreasRef: () => ref([]), + attachmentUuidsIndex () { + return this.template.schema.reduce((acc, e, index) => { + acc[e.attachment_uuid] = index + + return acc + }, {}) + }, language () { return this.locale.split('-')[0].toLowerCase() }, @@ -1663,32 +1670,25 @@ export default { this.save() } }, - findFieldInsertIndex (field) { - if (!field.areas?.length) return -1 - - const area = field.areas[0] - - const attachmentUuidsIndex = this.template.schema.reduce((acc, e, index) => { - acc[e.attachment_uuid] = index - - return acc - }, {}) + compareAreas (a, b) { + const aAttIdx = this.attachmentUuidsIndex[a.attachment_uuid] + const bAttIdx = this.attachmentUuidsIndex[b.attachment_uuid] - const compareAreas = (a, b) => { - const aAttIdx = attachmentUuidsIndex[a.attachment_uuid] - const bAttIdx = attachmentUuidsIndex[b.attachment_uuid] + if (aAttIdx !== bAttIdx) return aAttIdx - bAttIdx + if (a.page !== b.page) return a.page - b.page - if (aAttIdx !== bAttIdx) return aAttIdx - bAttIdx - if (a.page !== b.page) return a.page - b.page + const aY = a.y + a.h + const bY = b.y + b.h - const aY = a.y + a.h - const bY = b.y + b.h + if (Math.abs(aY - bY) < 0.01) return a.x - b.x + if (a.h < b.h ? a.y >= b.y && aY <= bY : b.y >= a.y && bY <= aY) return a.x - b.x - if (Math.abs(aY - bY) < 0.01) return a.x - b.x - if (a.h < b.h ? a.y >= b.y && aY <= bY : b.y >= a.y && bY <= aY) return a.x - b.x + return aY - bY + }, + findFieldInsertIndex (field) { + if (!field.areas?.length) return -1 - return aY - bY - } + const area = field.areas[0] let closestBeforeIndex = -1 let closestBeforeArea = null @@ -1698,15 +1698,15 @@ export default { this.template.fields.forEach((f, index) => { if (f.submitter_uuid === field.submitter_uuid) { (f.areas || []).forEach((a) => { - const cmp = compareAreas(a, area) + const cmp = this.compareAreas(a, area) if (cmp < 0) { - if (!closestBeforeArea || (compareAreas(a, closestBeforeArea) > 0 && closestBeforeIndex < index)) { + if (!closestBeforeArea || (this.compareAreas(a, closestBeforeArea) > 0 && closestBeforeIndex < index)) { closestBeforeIndex = index closestBeforeArea = a } } else { - if (!closestAfterArea || (compareAreas(a, closestAfterArea) < 0 && closestAfterIndex > index)) { + if (!closestAfterArea || (this.compareAreas(a, closestAfterArea) < 0 && closestAfterIndex > index)) { closestAfterIndex = index closestAfterArea = a } @@ -1729,6 +1729,17 @@ export default { this.template.fields.push(field) } }, + insertArea (field, area) { + field.areas ||= [] + + const insertIndex = field.areas.findIndex((a) => this.compareAreas(a, area) > 0) + + if (insertIndex === -1) { + field.areas.push(area) + } else { + field.areas.splice(insertIndex, 0, area) + } + }, insertDetectedField (field) { if (!this.withDetectExistingFields || !field.name) { this.insertField(field) @@ -1744,7 +1755,7 @@ export default { if (existingField) { existingField.areas = existingField.areas || [] - existingField.areas.push(...(field.areas || [])) + field.areas.forEach((area) => this.insertArea(existingField, area)) } else { const customField = this.detectCustomFieldsIndex[indexKey] || this.detectCustomFieldsIndex[nameKey] @@ -2249,7 +2260,7 @@ export default { fieldUuidIndex[field.uuid] = newField - newField.areas.push(newArea) + this.insertArea(newField, newArea) newAreas.push(newArea) if (['radio', 'multiple'].includes(field.type) && field.options?.length) { @@ -2362,17 +2373,7 @@ export default { area.y -= area.h / 2 } - this.drawField.areas ||= [] - - const insertBeforeAreaIndex = this.drawField.areas.findIndex((a) => { - return a.attachment_uuid === area.attachment_uuid && a.page > area.page - }) - - if (insertBeforeAreaIndex !== -1) { - this.drawField.areas.splice(insertBeforeAreaIndex, 0, area) - } else { - this.drawField.areas.push(area) - } + this.insertArea(this.drawField, area) if (this.template.fields.indexOf(this.drawField) === -1) { this.insertField(this.drawField) @@ -2513,9 +2514,7 @@ export default { delete field.height } - field.areas ||= [] - - field.areas.push(fieldArea) + this.insertArea(field, fieldArea) if (this.selectedAreasRef.value.length < 2) { this.selectedAreasRef.value = [fieldArea] @@ -2585,7 +2584,7 @@ export default { } } - field.areas.push(fieldArea) + this.insertArea(field, fieldArea) }) } else { const fieldArea = { diff --git a/app/javascript/template_builder/field_settings.vue b/app/javascript/template_builder/field_settings.vue index 511ad6b4..4b5a71f0 100644 --- a/app/javascript/template_builder/field_settings.vue +++ b/app/javascript/template_builder/field_settings.vue @@ -786,17 +786,27 @@ export default { }, copyToAllPages (field) { const areaString = JSON.stringify(field.areas[0]) + const newAreas = [] + const existingAreasIndex = field.areas.reduce((acc, area) => { + acc[`${area.attachment_uuid}-${area.page}`] = area + + return acc + }, {}) + + this.template.schema.forEach((item) => { + const attachment = this.template.documents.find((d) => d.uuid === item.attachment_uuid) - this.template.documents.forEach((attachment) => { const numberOfPages = attachment.metadata?.pdf?.number_of_pages || attachment.preview_images.length for (let page = 0; page <= numberOfPages - 1; page++) { - if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === page)) { - field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page }) - } + const existing = existingAreasIndex[`${attachment.uuid}-${page}`] + + newAreas.push(existing || { ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page }) } }) + field.areas = newAreas + this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1]) this.$emit('save') diff --git a/app/javascript/template_builder/payment_settings.vue b/app/javascript/template_builder/payment_settings.vue index 58de6bf7..331481fe 100644 --- a/app/javascript/template_builder/payment_settings.vue +++ b/app/javascript/template_builder/payment_settings.vue @@ -143,8 +143,8 @@ > { pending.where(expire_at: ..Time.current) } + scope :expired, lambda { + where(expire_at: ..Time.current) + .where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]) + .and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists) + } enum :source, { invite: 'invite', bulk: 'bulk', api: 'api', embed: 'embed', + mcp: 'mcp', link: 'link' }, scope: false, prefix: true diff --git a/app/views/devise/sessions/_omniauthable.html.erb b/app/views/devise/sessions/_omniauthable.html.erb new file mode 100644 index 00000000..e69de29b diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index af589843..350e5169 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -18,24 +18,7 @@ <%= f.button button_title(title: t(:sign_in), disabled_with: t(:signing_in)), class: 'base-button' %> <% end %> - <% if devise_mapping.omniauthable? %> -
- <% if User.omniauth_providers.include?(:google_oauth2) %> - <%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %> - - <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %> - <%= f.button button_title(title: t('sign_in_with_google'), icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %> - <% end %> - <% end %> - <% if User.omniauth_providers.include?(:microsoft_office365) %> - <%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %> - - <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %> - <%= f.button button_title(title: t('sign_in_with_microsoft'), icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %> - <% end %> - <% end %> -
- <% end %> + <%= render 'omniauthable' %> <%= render 'extra_links' %> <%= render 'devise/shared/links' %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 5558e869..09cfb218 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -20,9 +20,10 @@ <% if params[:modal].present? %> - <% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %> + <% modal_uri = Addressable::URI.parse(params[:modal]) %> + <% url_params = Rails.application.routes.recognize_path(modal_uri.path, method: :get) %> <% if url_params[:action] == 'new' %> - + <% end %> <% end %> diff --git a/app/views/shared/_github.html.erb b/app/views/shared/_github.html.erb index 5bcc786a..62eca00a 100644 --- a/app/views/shared/_github.html.erb +++ b/app/views/shared/_github.html.erb @@ -1,6 +1,6 @@ <%= svg_icon('start', class: 'h-3 w-3') %> - 11k + 12k diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index 6d66674c..c647dc27 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -59,7 +59,7 @@ <% end %> <% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %> - <%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %> + <%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full py-1' } do |f| %>