From 97ce32fd5246ccac5b0443305a371eb5076852f7 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 7 Apr 2026 16:34:15 +0300 Subject: [PATCH 01/14] set attachment name --- lib/submitters.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/submitters.rb b/lib/submitters.rb index be0690b1..02f29d4f 100644 --- a/lib/submitters.rb +++ b/lib/submitters.rb @@ -138,7 +138,7 @@ module Submitters ActiveStorage::Attachment.create!( blob:, - name: params[:name], + name: 'attachments', record: submitter ) end From 339ceda18dd1229d173ec7206a2a29ce09da33bb Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 7 Apr 2026 17:59:20 +0300 Subject: [PATCH 02/14] upload attachment if not completed --- app/controllers/api/attachments_controller.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/controllers/api/attachments_controller.rb b/app/controllers/api/attachments_controller.rb index f00552cd..c81aa0f9 100644 --- a/app/controllers/api/attachments_controller.rb +++ b/app/controllers/api/attachments_controller.rb @@ -10,6 +10,12 @@ module Api def create submitter = Submitter.find_by!(slug: params[:submitter_slug]) + unless can_upload?(submitter) + Rollbar.error("Can't upload: #{submitter.id}") if defined?(Rollbar) + + return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content + end + if params[:type].in?(%w[initials signature]) image = Vips::Image.new_from_file(params[:file].path) @@ -40,6 +46,14 @@ module Api render json: { error: e.message }, status: :unprocessable_content end + def can_upload?(submitter) + !submitter.declined_at? && + !submitter.completed_at? && + !submitter.submission.archived_at? && + !submitter.submission.expired? && + !submitter.submission.template&.archived_at? + end + def build_new_cookie_signatures_json(submitter, attachment) values = begin From 20375c3a4224e76ce1e4d0e58cdf501154328f51 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 8 Apr 2026 08:40:41 +0300 Subject: [PATCH 03/14] update gem --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 27ceef50..0d90a305 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -388,7 +388,7 @@ GEM rack (3.2.6) rack-proxy (0.7.7) rack - rack-session (2.1.1) + rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) From 89e797b95f14737923a5b249d88d6055c647fd00 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 8 Apr 2026 10:29:45 +0300 Subject: [PATCH 04/14] update message --- app/controllers/accounts_controller.rb | 4 +++- config/locales/i18n.yml | 21 ++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index b24c6b45..49314f32 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -53,7 +53,9 @@ class AccountsController < ApplicationController # rubocop:disable Layout/LineLength render turbo_stream: turbo_stream.replace( :account_delete_button, - html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) + html: helpers.tag.p(I18n.t('your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) + + helpers.tag.br + + helpers.tag.p(I18n.t('your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end')) ) # rubocop:enable Layout/LineLength end diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 0baaa19b..da006683 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -30,6 +30,7 @@ en: &en thanks: Thanks private: Private _variables: Variables + your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Your email address has been released immediately. If you're joining a team, you can now be invited to your company team without waiting for the deletion period to end. make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Make all newly created templates private to their creator and admins by default. create_templates_with_admin_access_by_default: Create templates with admin access by default require_email_2fa: Require email 2FA @@ -218,7 +219,7 @@ en: &en you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "You are scheduling your account for deletion. After deletion, your data will be permanently removed and cannot be recovered.\n\nClick OK if you would like to continue." account_information_has_been_updated: Account information has been updated. should_be_a_valid_url: should be a valid URL - your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Your account removal request will be processed within 2 months. Please contact us if you want to keep your account. + your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Your account will be permanently deleted within 2 months. Please contact us if you want to keep your account. test_mode: Test mode copy: Copy copied: Copied @@ -1083,6 +1084,7 @@ es: &es re_connect_stripe: Volver a conectar Stripe private: Privado _variables: Variables + your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Su dirección de correo electrónico ha sido liberada inmediatamente. Si se une a un equipo, ahora puede ser invitado al equipo de su empresa sin esperar a que finalice el período de eliminación. make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Hacer que todas las plantillas recién creadas sean privadas para su creador y los administradores por defecto. create_templates_with_admin_access_by_default: Crear plantillas con acceso de administrador por defecto require_email_2fa: Requerir 2FA por correo electrónico @@ -1256,7 +1258,7 @@ es: &es you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Estás programando la eliminación de tu cuenta. Después de la eliminación, tus datos se eliminarán permanentemente y no podrán recuperarse.\n\nHaz clic en OK si deseas continuar." account_information_has_been_updated: La información de la cuenta ha sido actualizada. should_be_a_valid_url: debe ser una URL válida - your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Tu solicitud de eliminación de cuenta se procesará en un plazo de 2 meses. Por favor contáctanos si deseas mantener tu cuenta. + your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Tu solicitud de eliminación de cuenta se procesará en un plazo de 2 meses. Por favor contáctanos si deseas mantener tu cuenta. test_mode: Modo de prueba copy: Copiar copied: Copiado @@ -2118,6 +2120,7 @@ it: &it re_connect_stripe: Ricollega Stripe private: Privato _variables: Variabili + your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Il tuo indirizzo email è stato rilasciato immediatamente. Se stai per unirti a un team, ora puoi essere invitato nel team della tua azienda senza aspettare la fine del periodo di eliminazione. make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Rendere tutte le nuove template private per il creatore e gli amministratori per impostazione predefinita. create_templates_with_admin_access_by_default: Crea modelli con accesso amministratore per impostazione predefinita require_email_2fa: Richiedi 2FA email @@ -2291,7 +2294,7 @@ it: &it you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Stai programmando l'eliminazione del tuo account. Dopo l'eliminazione, i tuoi dati saranno rimossi in modo permanente e non potranno essere recuperati.\n\nFai clic su OK se desideri continuare." account_information_has_been_updated: "Le informazioni dell'account sono state aggiornate." should_be_a_valid_url: deve essere un URL valido - your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: "La tua richiesta di rimozione dell'account sarà elaborata entro 2 mesi. Contattaci se desideri mantenere il tuo account." + your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: "La tua richiesta di rimozione dell'account sarà elaborata entro 2 mesi. Contattaci se desideri mantenere il tuo account." test_mode: Modalità di test copy: Copia copied: Copiato @@ -3154,6 +3157,7 @@ fr: &fr re_connect_stripe: Reconnecter Stripe private: Privé _variables: Variables + your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Votre adresse e-mail a été libérée immédiatement. Si vous rejoignez une équipe, vous pouvez maintenant être invité dans l'équipe de votre entreprise sans attendre la fin de la période de suppression. make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Rendre tous les nouveaux modèles privés pour leur créateur et les administrateurs par défaut. create_templates_with_admin_access_by_default: Créer des modèles avec un accès administrateur par défaut require_email_2fa: Exiger la 2FA par email @@ -3327,7 +3331,7 @@ fr: &fr you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Vous planifiez la suppression de votre compte. Après suppression, vos données seront définitivement supprimées et ne pourront pas être récupérées.\n\nCliquez sur OK pour continuer." account_information_has_been_updated: Les informations du compte ont été mises à jour. should_be_a_valid_url: doit être une URL valide - your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Votre demande de suppression de compte sera traitée sous 2 mois. Veuillez nous contacter si vous souhaitez conserver votre compte. + your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Votre demande de suppression de compte sera traitée sous 2 mois. Veuillez nous contacter si vous souhaitez conserver votre compte. test_mode: Mode test copy: Copier copied: Copié @@ -4186,6 +4190,7 @@ pt: &pt re_connect_stripe: Reconectar Stripe private: Privado _variables: Variáveis + your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Seu endereço de e-mail foi liberado imediatamente. Se você estiver entrando em uma equipe, agora pode ser convidado para a equipe da sua empresa sem esperar o fim do período de exclusão. make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Tornar todos os modelos recém-criados privados para seu criador e administradores por padrão. create_templates_with_admin_access_by_default: Criar modelos com acesso de administrador por padrão require_email_2fa: Exigir 2FA por email @@ -4359,7 +4364,7 @@ pt: &pt you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Você está agendando a exclusão da sua conta. Após a exclusão, seus dados serão permanentemente removidos e não poderão ser recuperados.\n\nClique em OK se desejar continuar." account_information_has_been_updated: As informações da conta foram atualizadas. should_be_a_valid_url: deve ser um URL válido - your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Seu pedido de remoção da conta será processado em até 2 meses. Entre em contato conosco se você quiser manter sua conta. + your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Seu pedido de remoção da conta será processado em até 2 meses. Entre em contato conosco se você quiser manter sua conta. test_mode: Modo de teste copy: Copiar copied: Copiado @@ -5207,6 +5212,7 @@ de: &de thanks: Danke private: Privat _variables: Variablen + your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Ihre E-Mail-Adresse wurde sofort freigegeben. Wenn Sie einem Team beitreten, können Sie jetzt in das Team Ihres Unternehmens eingeladen werden, ohne auf das Ende der Löschfrist warten zu müssen. make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Alle neu erstellten Vorlagen standardmäßig nur für ihren Ersteller und Administratoren sichtbar machen. create_templates_with_admin_access_by_default: Vorlagen standardmäßig mit Administratorzugriff erstellen require_email_2fa: E-Mail 2FA erforderlich @@ -5394,7 +5400,7 @@ de: &de you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "Sie planen die Löschung Ihres Kontos. Nach der Löschung werden Ihre Daten dauerhaft entfernt und können nicht wiederhergestellt werden.\n\nKlicken Sie auf OK, wenn Sie fortfahren möchten." account_information_has_been_updated: Die Kontoinformationen wurden aktualisiert. should_be_a_valid_url: sollte eine gültige URL sein - your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Ihre Anfrage zur Kontolöschung wird innerhalb von 2 Monaten bearbeitet. Bitte kontaktieren Sie uns, wenn Sie Ihr Konto behalten möchten. + your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Ihre Anfrage zur Kontolöschung wird innerhalb von 2 Monaten bearbeitet. Bitte kontaktieren Sie uns, wenn Sie Ihr Konto behalten möchten. test_mode: Testmodus copy: Kopieren copied: Kopiert @@ -6642,6 +6648,7 @@ nl: &nl thanks: Bedankt private: Privé _variables: Variabelen + your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end: Uw e-mailadres is onmiddellijk vrijgegeven. Als u lid wordt van een team, kunt u nu worden uitgenodigd voor het team van uw bedrijf zonder te wachten tot de verwijderingsperiode is afgelopen. make_all_newly_created_templates_private_to_their_creator_and_admins_by_default: Maak alle nieuw aangemaakte sjablonen standaard privé voor hun maker en admins. create_templates_with_admin_access_by_default: Sjablonen standaard met admin-toegang maken require_email_2fa: E-mail 2FA vereist @@ -6830,7 +6837,7 @@ nl: &nl you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue: "U plant uw account voor verwijdering. Na verwijdering worden uw gegevens permanent verwijderd en kunnen ze niet worden hersteld.\n\nKlik op OK als u wilt doorgaan." account_information_has_been_updated: Accountinformatie is bijgewerkt. should_be_a_valid_url: moet een geldige URL zijn - your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Uw verzoek tot verwijdering van uw account wordt binnen 2 maanden verwerkt. Neem contact met ons op als u uw account wilt behouden. + your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account: Uw verzoek tot verwijdering van uw account wordt binnen 2 maanden verwerkt. Neem contact met ons op als u uw account wilt behouden. test_mode: Testmodus copy: Kopiëren copied: Gekopieerd From 2303c21cea273f2cf58ac1bc6ee710f583fd7030 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 8 Apr 2026 11:23:59 +0300 Subject: [PATCH 05/14] download users csv --- app/controllers/users_controller.rb | 14 +++++++++++++- app/views/users/_bottom_links.html.erb | 14 +++++++++++++- lib/users.rb | 15 +++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 lib/users.rb diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 9affdf4c..2d8f818f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -16,7 +16,19 @@ class UsersController < ApplicationController @users.active.where.not(role: 'integration') end - @pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc)) + @users = @users.preload(account: :account_accesses).where(account: current_account).order(id: :desc) + + respond_to do |format| + format.html do + @pagy, @users = pagy(@users) + end + + if current_ability.can?(:manage, current_account) + format.csv do + send_data Users.generate_csv(@users), filename: "users-#{Time.current.iso8601}.csv", type: 'text/csv' + end + end + end end def new; end diff --git a/app/views/users/_bottom_links.html.erb b/app/views/users/_bottom_links.html.erb index 8fbc73e1..7f8c234b 100644 --- a/app/views/users/_bottom_links.html.erb +++ b/app/views/users/_bottom_links.html.erb @@ -1,15 +1,27 @@
+ <% if (is_with_download = can?(:manage, :download_users) && can?(:manage, current_account)) %> + <%= button_to(t(:download), url_for(format: :csv), class: 'link text-sm', form: { class: 'flex' }, method: :get) %> + <% end %> <% if %w[archived integration].include?(params[:status]) %> + <% if is_with_download %> + | + <% end %> <%= link_to t('view_active'), settings_users_path, class: 'link text-sm' %> <% else %> <% archived_exists = current_account.users.archived.where.not(role: 'integration').exists? %> - <% if current_account.users.active.exists?(role: 'integration') %> + <% if (integration_exists = current_account.users.active.exists?(role: 'integration')) %> + <% if is_with_download %> + | + <% end %> <%= link_to t('view_embedding_users'), settings_integration_users_path, class: 'link text-sm' %> <% if archived_exists %> | <% end %> <% end %> <% if archived_exists %> + <% if !integration_exists && is_with_download %> + | + <% end %> <%= link_to t('view_archived'), settings_archived_users_path, class: 'link text-sm' %> <% end %> <% end %> diff --git a/lib/users.rb b/lib/users.rb new file mode 100644 index 00000000..49989a4f --- /dev/null +++ b/lib/users.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Users + module_function + + def generate_csv(users) + headers = %w[email first_name last_name role current_sign_in_at last_sign_in_at updated_at created_at] + + CSVSafe.generate do |csv| + csv << headers + + users.each { |user| csv << user.values_at(*headers) } + end + end +end From cf09a4f733b0564bb653bd0766b95032f4cc1294 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 8 Apr 2026 12:24:31 +0300 Subject: [PATCH 06/14] update gem --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0d90a305..de5ce993 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,7 +75,7 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.9) + addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) annotaterb (4.22.0) activerecord (>= 6.0.0) From ff53436fd49761009856341bcde318b9081759cc Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 8 Apr 2026 17:18:38 +0300 Subject: [PATCH 07/14] fix dynamic editor layout --- app/javascript/template_builder/dynamic_editor.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/javascript/template_builder/dynamic_editor.js b/app/javascript/template_builder/dynamic_editor.js index bb4c7743..745f95d0 100644 --- a/app/javascript/template_builder/dynamic_editor.js +++ b/app/javascript/template_builder/dynamic_editor.js @@ -19,10 +19,6 @@ export const tiptapStylesheet = new CSSStyleSheet() tiptapStylesheet.replaceSync( `.ProseMirror { - position: relative; -} - -.ProseMirror { word-wrap: break-word; white-space: pre-wrap; white-space: break-spaces; From 8f8b36617a85562db34aac3e042de260a6a378f6 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 10 Apr 2026 11:07:19 +0300 Subject: [PATCH 08/14] fix first party download from preview --- .../submissions_download_controller.rb | 60 +++-------------- .../submissions_preview_controller.rb | 8 ++- .../submitters_download_controller.rb | 66 +++++++++++++++++++ app/views/submissions/show.html.erb | 7 +- config/routes.rb | 3 +- lib/submitters.rb | 32 +++++++++ 6 files changed, 118 insertions(+), 58 deletions(-) create mode 100644 app/controllers/submitters_download_controller.rb diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index 39dee165..98e369be 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -5,30 +5,20 @@ class SubmissionsDownloadController < ApplicationController skip_authorization_check TTL = 40.minutes - FILES_TTL = 5.minutes def index - @submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present? + @submission = Submission.find_by!(slug: params[:submission_slug] || params[:submissions_preview_slug]) - signature_valid = - if @submitter&.slug == params[:submitter_slug] - true - else - @submitter = nil - end - - @submitter ||= Submitter.find_by!(slug: params[:submitter_slug]) - - Submissions::EnsureResultGenerated.call(@submitter) - - last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last - - return head :not_found unless last_submitter + last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last Submissions::EnsureResultGenerated.call(last_submitter) - if !signature_valid && !current_user_submitter?(last_submitter) - return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request) + unless current_user_submitter?(last_submitter) + unless Submitters::AuthorizedForForm.call(last_submitter, current_user, request) + Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar) + + return head :not_found + end if last_submitter.completed_at < TTL.ago Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) @@ -40,14 +30,14 @@ class SubmissionsDownloadController < ApplicationController if params[:combined] == 'true' respond_with_combined(last_submitter) else - render json: build_urls(last_submitter) + render json: Submitters.build_document_urls(last_submitter) end end private def respond_with_combined(submitter) - url = build_combined_url(submitter) + url = Submitters.build_combined_url(submitter) if url render json: [url] @@ -59,34 +49,4 @@ class SubmissionsDownloadController < ApplicationController def current_user_submitter?(submitter) current_user && current_ability.can?(:read, submitter) end - - def build_urls(submitter) - filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, - key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value - - Submitters.select_attachments_for_download(submitter).map do |attachment| - ActiveStorage::Blob.proxy_path( - attachment.blob, - expires_at: FILES_TTL.from_now.to_i, - filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) - ) - end - end - - def build_combined_url(submitter) - return if submitter.submission.submitters.exists?(completed_at: nil) - return if submitter.submission.submitters.order(:completed_at).last != submitter - - attachment = submitter.submission.combined_document_attachment - attachment ||= Submissions::EnsureCombinedGenerated.call(submitter) - - filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, - key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value - - ActiveStorage::Blob.proxy_path( - attachment.blob, - expires_at: FILES_TTL.from_now.to_i, - filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) - ) - end end diff --git a/app/controllers/submissions_preview_controller.rb b/app/controllers/submissions_preview_controller.rb index 6163c36b..10e45358 100644 --- a/app/controllers/submissions_preview_controller.rb +++ b/app/controllers/submissions_preview_controller.rb @@ -10,13 +10,15 @@ class SubmissionsPreviewController < ApplicationController TTL = 40.minutes def show - submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present? + @sig_submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present? signature_valid = - if submitter && submitter.submission.slug == params[:slug] - @submission = submitter.submission + if @sig_submitter && @sig_submitter.submission.slug == params[:slug] + @submission = @sig_submitter.submission true + else + @sig_submitter = nil end @submission ||= Submission.find_by!(slug: params[:slug]) diff --git a/app/controllers/submitters_download_controller.rb b/app/controllers/submitters_download_controller.rb new file mode 100644 index 00000000..28a354bc --- /dev/null +++ b/app/controllers/submitters_download_controller.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +class SubmittersDownloadController < ApplicationController + skip_before_action :authenticate_user! + skip_authorization_check + + TTL = 40.minutes + FILES_TTL = 5.minutes + + def index + @submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present? + + signature_valid = + if @submitter&.slug == params[:submitter_slug] + true + else + @submitter = nil + end + + @submitter ||= Submitter.find_by!(slug: params[:submitter_slug]) + + Submissions::EnsureResultGenerated.call(@submitter) + + last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last + + return head :not_found unless last_submitter + + Submissions::EnsureResultGenerated.call(last_submitter) + + if !signature_valid && !current_user_submitter?(last_submitter) + unless Submitters::AuthorizedForForm.call(@submitter, current_user, request) + Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar) + + return head :not_found + end + + 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' + respond_with_combined(last_submitter) + else + render json: Submitters.build_document_urls(last_submitter) + end + end + + private + + def respond_with_combined(submitter) + url = Submitters.build_combined_url(submitter) + + if url + render json: [url] + else + head :not_found + end + end + + def current_user_submitter?(submitter) + current_user && current_ability.can?(:read, submitter) + end +end diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index f925f6ce..bf8e6eba 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -15,7 +15,6 @@ <% (@submission.name || @submission.template.name).split(/(_)/).each do |item| %><%= item %><% end %>
- <% last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %> <% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %> <% if signed_in? && can?(:create, @submission) && @submission.archived_at? && !is_all_completed %> <%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %> @@ -31,10 +30,10 @@ <% end %> <% end %> - <% if last_submitter %> + <% if is_all_completed || @submission.submitters.to_a.any?(&:completed_at?) %> <% if is_all_completed || !is_combined_enabled %>
- + <%= svg_icon('download', class: 'w-6 h-6') %> @@ -53,7 +52,7 @@