diff --git a/Dockerfile b/Dockerfile index 932d8b13..a5a8f855 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf WORKDIR /app -RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf +RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf RUN echo $'.include = /etc/ssl/openssl.cnf\n\ \n\ diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index 5e8da098..25c0c537 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -100,6 +100,7 @@ module Api permitted_params = [ :name, :external_id, + :shared_link, { submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]], fields: [[:uuid, :submitter_uuid, :name, :type, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a3cb0242..7500acdb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -55,6 +55,14 @@ class ApplicationController < ActionController::Base request.session[:impersonated_user_id] = user.uuid end + def pagy_auto(collection, **keyword_args) + if current_ability.can?(:manage, :countless) + pagy_countless(collection, **keyword_args) + else + pagy(collection, **keyword_args) + end + end + private def with_locale(&) diff --git a/app/controllers/email_smtp_settings_controller.rb b/app/controllers/email_smtp_settings_controller.rb index d41ca570..766a9545 100644 --- a/app/controllers/email_smtp_settings_controller.rb +++ b/app/controllers/email_smtp_settings_controller.rb @@ -9,7 +9,9 @@ class EmailSmtpSettingsController < ApplicationController def create if @encrypted_config.update(email_configs) - SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now! + unless Docuseal.multitenant? + SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now! + end redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved') else diff --git a/app/controllers/send_submission_email_controller.rb b/app/controllers/send_submission_email_controller.rb index 6d1f34c3..ef6ff62b 100644 --- a/app/controllers/send_submission_email_controller.rb +++ b/app/controllers/send_submission_email_controller.rb @@ -11,15 +11,15 @@ class SendSubmissionEmailController < ApplicationController def create if params[:template_slug] - @submitter = Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase, - template: { slug: params[:template_slug] }) + @submitter = Submitter.completed.joins(submission: :template).find_by!(email: params[:email].to_s.downcase, + template: { slug: params[:template_slug] }) elsif params[:submission_slug] - @submitter = Submitter.joins(:submission).find_by(email: params[:email].to_s.downcase, - submission: { slug: params[:submission_slug] }) + @submitter = Submitter.completed.joins(:submission).find_by(email: params[:email].to_s.downcase, + submission: { slug: params[:submission_slug] }) return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter else - @submitter = Submitter.find_by!(slug: params[:submitter_slug]) + @submitter = Submitter.completed.find_by!(slug: params[:submitter_slug]) end RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes) diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index c79535ec..415adab4 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -8,20 +8,28 @@ class StartFormController < ApplicationController around_action :with_browser_locale, only: %i[show completed] before_action :maybe_redirect_com, only: %i[show completed] + before_action :load_resubmit_submitter, only: :update before_action :load_template + before_action :authorize_start!, only: :update def show - raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] == true + raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] - @submitter = @template.submissions.new(account_id: @template.account_id) - .submitters.new(account_id: @template.account_id, - uuid: (filter_undefined_submitters(@template).first || - @template.submitters.first)['uuid']) + if @template.shared_link? + @submitter = @template.submissions.new(account_id: @template.account_id) + .submitters.new(account_id: @template.account_id, + uuid: (filter_undefined_submitters(@template).first || + @template.submitters.first)['uuid']) + else + Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar) + + return render :private if current_user && current_ability.can?(:read, @template) + + raise ActionController::RoutingError, I18n.t('not_found') + end end def update - return redirect_to start_form_path(@template.slug) if @template.archived_at? - @submitter = find_or_initialize_submitter(@template, submitter_params) if @submitter.completed_at? @@ -59,6 +67,8 @@ class StartFormController < ApplicationController end def completed + return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at? + @submitter = Submitter.where(submission: @template.submissions) .where.not(completed_at: nil) .find_by!(email: params[:email]) @@ -66,6 +76,24 @@ class StartFormController < ApplicationController private + def load_resubmit_submitter + @resubmit_submitter = + if params[:resubmit].present? && !params[:resubmit].in?([true, 'true']) + Submitter.find_by(slug: params[:resubmit]) + end + end + + def authorize_start! + return redirect_to start_form_path(@template.slug) if @template.archived_at? + + return if @resubmit_submitter + return if @template.shared_link? || (current_user && current_ability.can?(:read, @template)) + + Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar) + + redirect_to start_form_path(@template.slug) + end + def enqueue_submission_create_webhooks(submitter) WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url| SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id, @@ -74,31 +102,29 @@ class StartFormController < ApplicationController end def find_or_initialize_submitter(template, submitter_params) - Submitter.where(submission: template.submissions.where(expire_at: Time.current..) - .or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) - .order(id: :desc) - .where(declined_at: nil) - .where(external_id: nil) - .where(ip: [nil, request.remote_ip]) - .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } - .find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank) + Submitter + .where(submission: template.submissions.where(expire_at: Time.current..) + .or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) + .order(id: :desc) + .where(declined_at: nil) + .where(external_id: nil) + .where(ip: [nil, request.remote_ip]) + .then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel } + .find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank) end def assign_submission_attributes(submitter, template) - resubmit_submitter = - (Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?) - submitter.assign_attributes( uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'], ip: request.remote_ip, ua: request.user_agent, - values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {}, - preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true }, - metadata: resubmit_submitter&.metadata.presence || {} + values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {}, + preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true }, + metadata: @resubmit_submitter&.metadata.presence || {} ) if submitter.values.present? - resubmit_submitter.attachments.each do |attachment| + @resubmit_submitter.attachments.each do |attachment| submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid) end end @@ -120,15 +146,21 @@ class StartFormController < ApplicationController end def submitter_params + return current_user.slice(:email) if params[:selfsign] + return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present? + params.require(:submitter).permit(:email, :phone, :name).tap do |attrs| attrs[:email] = Submissions.normalize_email(attrs[:email]) end end def load_template - slug = params[:slug] || params[:start_form_slug] - - @template = Template.find_by!(slug:) + @template = + if @resubmit_submitter + @resubmit_submitter.template + else + Template.find_by!(slug: params[:slug] || params[:start_form_slug]) + end end def multiple_submitters_error_message diff --git a/app/controllers/submissions_archived_controller.rb b/app/controllers/submissions_archived_controller.rb index f71638c7..3ad5d936 100644 --- a/app/controllers/submissions_archived_controller.rb +++ b/app/controllers/submissions_archived_controller.rb @@ -18,6 +18,6 @@ class SubmissionsArchivedController < ApplicationController @submissions.order(id: :desc) end - @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events)) + @pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events)) end end diff --git a/app/controllers/submissions_dashboard_controller.rb b/app/controllers/submissions_dashboard_controller.rb index 3386edd8..55fb6ae0 100644 --- a/app/controllers/submissions_dashboard_controller.rb +++ b/app/controllers/submissions_dashboard_controller.rb @@ -19,6 +19,6 @@ class SubmissionsDashboardController < ApplicationController @submissions.order(id: :desc) end - @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events)) + @pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events)) end end diff --git a/app/controllers/template_folders_controller.rb b/app/controllers/template_folders_controller.rb index e5cb2bbf..c22f2fed 100644 --- a/app/controllers/template_folders_controller.rb +++ b/app/controllers/template_folders_controller.rb @@ -9,7 +9,7 @@ class TemplateFoldersController < ApplicationController @templates = Templates.search(@templates, params[:q]) @templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order]) - @pagy, @templates = pagy(@templates, limit: 12) + @pagy, @templates = pagy_auto(@templates, limit: 12) end def edit; end diff --git a/app/controllers/templates_archived_controller.rb b/app/controllers/templates_archived_controller.rb index f75e83a1..f52e5af7 100644 --- a/app/controllers/templates_archived_controller.rb +++ b/app/controllers/templates_archived_controller.rb @@ -7,6 +7,6 @@ class TemplatesArchivedController < ApplicationController @templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc) @templates = Templates.search(@templates, params[:q]) - @pagy, @templates = pagy(@templates, limit: 12) + @pagy, @templates = pagy_auto(@templates, limit: 12) end end diff --git a/app/controllers/templates_archived_submissions_controller.rb b/app/controllers/templates_archived_submissions_controller.rb index bf023677..9c9da083 100644 --- a/app/controllers/templates_archived_submissions_controller.rb +++ b/app/controllers/templates_archived_submissions_controller.rb @@ -15,7 +15,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController @submissions.order(id: :desc) end - @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events)) + @pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events)) rescue ActiveRecord::RecordNotFound redirect_to root_path end diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 6eb91ed8..b3d0ccec 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -21,7 +21,7 @@ class TemplatesController < ApplicationController submissions.order(id: :desc) end - @pagy, @submissions = pagy(submissions.preload(:template_accesses, submitters: :start_form_submission_events)) + @pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events)) rescue ActiveRecord::RecordNotFound redirect_to root_path end diff --git a/app/controllers/templates_dashboard_controller.rb b/app/controllers/templates_dashboard_controller.rb index 319b0297..b4806fbc 100644 --- a/app/controllers/templates_dashboard_controller.rb +++ b/app/controllers/templates_dashboard_controller.rb @@ -17,7 +17,7 @@ class TemplatesDashboardController < ApplicationController @pagy, @template_folders = pagy( @template_folders, - items: FOLDERS_PER_PAGE, + limit: FOLDERS_PER_PAGE, page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1 ) @@ -35,7 +35,7 @@ class TemplatesDashboardController < ApplicationController (@template_folders.size < 7 ? 9 : 6) end - @pagy, @templates = pagy(@templates, limit:) + @pagy, @templates = pagy_auto(@templates, limit:) end end diff --git a/app/controllers/templates_share_link_controller.rb b/app/controllers/templates_share_link_controller.rb new file mode 100644 index 00000000..5b84f6ca --- /dev/null +++ b/app/controllers/templates_share_link_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class TemplatesShareLinkController < ApplicationController + load_and_authorize_resource :template + + def show; end + + def create + authorize!(:update, @template) + + @template.update!(template_params) + + head :ok + end + + private + + def template_params + params.require(:template).permit(:shared_link) + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index c2e12079..f951b8fe 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -24,6 +24,7 @@ import SubmitForm from './elements/submit_form' import PromptPassword from './elements/prompt_password' import EmailsTextarea from './elements/emails_textarea' import ToggleOnSubmit from './elements/toggle_on_submit' +import CheckOnClick from './elements/check_on_click' import PasswordInput from './elements/password_input' import SearchInput from './elements/search_input' import ToggleAttribute from './elements/toggle_attribute' @@ -103,6 +104,7 @@ safeRegisterElement('set-date-button', SetDateButton) safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox) safeRegisterElement('app-tour', AppTour) safeRegisterElement('dashboard-dropzone', DashboardDropzone) +safeRegisterElement('check-on-click', CheckOnClick) safeRegisterElement('template-builder', class extends HTMLElement { connectedCallback () { diff --git a/app/javascript/elements/check_on_click.js b/app/javascript/elements/check_on_click.js new file mode 100644 index 00000000..8b3b9ab8 --- /dev/null +++ b/app/javascript/elements/check_on_click.js @@ -0,0 +1,14 @@ +export default class extends HTMLElement { + connectedCallback () { + this.addEventListener('click', () => { + if (!this.element.checked) { + this.element.checked = true + this.element.dispatchEvent(new Event('change', { bubbles: true })) + } + }) + } + + get element () { + return document.getElementById(this.dataset.elementId) + } +} diff --git a/app/javascript/elements/clipboard_copy.js b/app/javascript/elements/clipboard_copy.js index 777727d1..4118b9e2 100644 --- a/app/javascript/elements/clipboard_copy.js +++ b/app/javascript/elements/clipboard_copy.js @@ -3,8 +3,6 @@ export default class extends HTMLElement { this.clearChecked() this.addEventListener('click', (e) => { - e.stopPropagation() - const text = this.dataset.text || this.innerText.trim() if (navigator.clipboard) { diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 01819533..5aed2a7a 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -23,7 +23,7 @@