From f5caed502f2835230944f137f65cb7639b0623f7 Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Fri, 30 Jun 2023 20:31:17 +0300 Subject: [PATCH] adjust for multitenant --- app/controllers/esign_settings_controller.rb | 6 ++-- app/controllers/setup_controller.rb | 2 +- app/javascript/elements/download_button.js | 2 +- app/javascript/form.js | 1 + app/javascript/submission_form/completed.vue | 8 +++++- app/javascript/submission_form/form.vue | 6 ++++ .../submission_form/signature_step.vue | 6 ++-- app/views/accounts/show.html.erb | 12 ++++---- app/views/devise/registrations/new.html.erb | 2 ++ app/views/shared/_settings_nav.html.erb | 14 ++++++---- app/views/start_form/completed.html.erb | 8 ++++-- app/views/submissions/new.html.erb | 2 +- app/views/submissions/show.html.erb | 2 +- app/views/submit_form/completed.html.erb | 6 ++-- app/views/submit_form/show.html.erb | 2 +- config/environments/development.rb | 2 +- config/environments/production.rb | 28 +++++++++++++++++-- config/routes.rb | 8 ++++-- lib/accounts.rb | 24 ++++++++++++++++ lib/action_mailer_configs_interceptor.rb | 6 +++- lib/docuseal.rb | 11 +++++++- lib/generate_certificate.rb | 12 ++++---- lib/load_active_storage_configs.rb | 5 ++++ .../generate_result_attachments.rb | 16 +++++------ 24 files changed, 139 insertions(+), 52 deletions(-) diff --git a/app/controllers/esign_settings_controller.rb b/app/controllers/esign_settings_controller.rb index 4260b3a3..b9138383 100644 --- a/app/controllers/esign_settings_controller.rb +++ b/app/controllers/esign_settings_controller.rb @@ -12,11 +12,9 @@ class EsignSettingsController < ApplicationController HexaPDF::Document.new(io: StringIO.new(blob.download)) end - cert = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY).value + certs = Accounts.load_signing_certs(current_account) - trusted_certs = [OpenSSL::X509::Certificate.new(cert['cert']), - OpenSSL::X509::Certificate.new(cert['sub_ca']), - OpenSSL::X509::Certificate.new(cert['root_ca'])] + trusted_certs = [certs[:cert], certs[:sub_ca], certs[:root_ca]] render turbo_stream: turbo_stream.replace('result', partial: 'result', locals: { pdfs:, blobs:, trusted_certs: }) rescue HexaPDF::MalformedPDFError diff --git a/app/controllers/setup_controller.rb b/app/controllers/setup_controller.rb index e9a8a806..83bb3788 100644 --- a/app/controllers/setup_controller.rb +++ b/app/controllers/setup_controller.rb @@ -22,7 +22,7 @@ class SetupController < ApplicationController if @user.save encrypted_configs = [ { key: EncryptedConfig::APP_URL_KEY, value: encrypted_config_params[:value] }, - { key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call } + { key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) } ] @account.encrypted_configs.create!(encrypted_configs) diff --git a/app/javascript/elements/download_button.js b/app/javascript/elements/download_button.js index 2e81a176..9fb292ba 100644 --- a/app/javascript/elements/download_button.js +++ b/app/javascript/elements/download_button.js @@ -25,7 +25,7 @@ export default targetable(class extends HTMLElement { const link = document.createElement('a') link.href = blobUrl - link.setAttribute('download', decodeURI(resp.headers.get('content-disposition').split('"')[1])) + link.setAttribute('download', decodeURI(url.split('/').pop())) link.click() diff --git a/app/javascript/form.js b/app/javascript/form.js index 6c2e7dd0..2c9453c6 100644 --- a/app/javascript/form.js +++ b/app/javascript/form.js @@ -12,6 +12,7 @@ window.customElements.define('submission-form', class extends HTMLElement { submitterSlug: this.dataset.submitterSlug, submitterUuid: this.dataset.submitterUuid, authenticityToken: this.dataset.authenticityToken, + canSendEmail: this.dataset.canSendEmail === 'true', values: reactive(JSON.parse(this.dataset.values)), attachments: reactive(JSON.parse(this.dataset.attachments)), fields: JSON.parse(this.dataset.fields) diff --git a/app/javascript/submission_form/completed.vue b/app/javascript/submission_form/completed.vue index 37d23dbc..1342de33 100644 --- a/app/javascript/submission_form/completed.vue +++ b/app/javascript/submission_form/completed.vue @@ -12,6 +12,7 @@

<% end %> - <%= f.fields_for @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) do |ff| %> -
- <%= ff.label :value, 'App URL', class: 'label' %> - <%= ff.text_field :value, autocomplete: 'off', class: 'base-input' %> -
+ <% unless Docuseal.multitenant? %> + <%= f.fields_for @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) do |ff| %> +
+ <%= ff.label :value, 'App URL', class: 'label' %> + <%= ff.text_field :value, autocomplete: 'off', class: 'base-input' %> +
+ <% end %> <% end %>
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 25d6959f..3d722106 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -22,6 +22,8 @@
<% end %> <%= f.fields_for resource.account do |ff| %> + + <%= ff.hidden_field :timezone %>
<%= ff.label :name, 'Company name', class: 'label' %> <%= ff.text_field :name, required: true, class: 'base-input' %> diff --git a/app/views/shared/_settings_nav.html.erb b/app/views/shared/_settings_nav.html.erb index 20560969..8ed90f49 100644 --- a/app/views/shared/_settings_nav.html.erb +++ b/app/views/shared/_settings_nav.html.erb @@ -9,12 +9,14 @@
  • <%= link_to 'Account', settings_account_path, class: 'text-base hover:bg-base-300' %>
  • -
  • - <%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %> -
  • -
  • - <%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %> -
  • + <% unless Docuseal.multitenant? %> +
  • + <%= link_to 'Email', settings_email_index_path, class: 'text-base hover:bg-base-300' %> +
  • +
  • + <%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %> +
  • + <% end %>
  • <%= link_to 'Signature', settings_esign_index_path, class: 'text-base hover:bg-base-300' %>
  • diff --git a/app/views/start_form/completed.html.erb b/app/views/start_form/completed.html.erb index fd5da2ba..0436f4e9 100644 --- a/app/views/start_form/completed.html.erb +++ b/app/views/start_form/completed.html.erb @@ -29,9 +29,11 @@
    Form has been submitter already
    -
    - <%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %> -
    + <% if Accounts.can_send_emails?(@submitter.submission.template.account) %> +
    + <%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %> +
    + <% end %>
    <%= render 'shared/attribution' %> diff --git a/app/views/submissions/new.html.erb b/app/views/submissions/new.html.erb index 40cb9ab6..0a8717c1 100644 --- a/app/views/submissions/new.html.erb +++ b/app/views/submissions/new.html.erb @@ -36,7 +36,7 @@ <% end %>
    - <% is_smtp_configured = Docuseal.multitenant? || current_account.encrypted_configs.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY) %> + <% is_smtp_configured = Accounts.can_send_emails?(current_account) %> <%= f.label :send_email, class: 'flex items-center cursor-pointer' do %> <%= f.check_box :send_email, class: 'base-checkbox', disabled: !is_smtp_configured, onchange: "message_field.classList.toggle('hidden', !event.currentTarget.checked)" %> Send Email diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 8d3f28c0..9b613bfe 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -75,7 +75,7 @@
    <%= svg_icon('writing', class: 'w-5 h-5') %> - <%= submitter&.completed_at? ? l(submitter.completed_at, format: :long) : 'Not completed yet' %> + <%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) : 'Not completed yet' %>
    <% if submitter && !submitter.completed_at? %> diff --git a/app/views/submit_form/completed.html.erb b/app/views/submit_form/completed.html.erb index fd213b7a..f6466b84 100644 --- a/app/views/submit_form/completed.html.erb +++ b/app/views/submit_form/completed.html.erb @@ -27,8 +27,10 @@
    - <%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %> -
    OR
    + <% if Accounts.can_send_emails?(@submitter.submission.template.account) %> + <%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %> +
    OR
    + <% end %> <%= svg_icon('download', class: 'w-6 h-6') %> diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index 020c0e32..0b607238 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -29,7 +29,7 @@
    - +
    diff --git a/config/environments/development.rb b/config/environments/development.rb index 5f7a43c4..11b3069c 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -45,7 +45,7 @@ Rails.application.configure do end # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :disk + config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = true diff --git a/config/environments/production.rb b/config/environments/production.rb index e1b52f66..0ad1f2c8 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -38,7 +38,16 @@ Rails.application.configure do # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local + config.active_storage.service = + if ENV['S3_ATTACHMENTS_BUCKET'].present? + :aws_s3 + elsif ENV['GCS_BUCKET'].present? + :google + elsif ENV['AZURE_CONTAINER'].present? + :azure + else + :local + end # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil @@ -46,7 +55,7 @@ Rails.application.configure do # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true + config.force_ssl = ENV['FORCE_SSL'] == 'true' # Include generic and useful information about system operation, but avoid logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). @@ -61,7 +70,20 @@ Rails.application.configure do # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # config.action_mailer.raise_delivery_errors = false + config.action_mailer.raise_delivery_errors = false + + if ENV['SMTP_USERNAME'] + config.action_mailer.delivery_method = :smtp + config.action_mailer.smtp_settings = { + address: ENV.fetch('SMTP_ADDRESS', nil), + port: ENV.fetch('SMTP_PORT', 587), + domain: ENV.fetch('SMTP_DOMAIN', nil), + user_name: ENV.fetch('SMTP_USERNAME', nil), + password: ENV.fetch('SMTP_PASSWORD', nil), + authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'), + enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true' + } + end # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). diff --git a/config/routes.rb b/config/routes.rb index bdf8ea86..b8ed904f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,7 +8,7 @@ Rails.application.routes.draw do devise_for :users, path: '/', only: %i[sessions passwords] devise_scope :user do - if User.devise_modules.include?(:registerable) + if Docuseal.multitenant? resource :registration, only: %i[create], path: 'sign_up' do get '' => :new, as: :new end @@ -52,8 +52,10 @@ Rails.application.routes.draw do end scope '/settings', as: :settings do - resources :storage, only: %i[index create], controller: 'storage_settings' - resources :email, only: %i[index create], controller: 'email_settings' + unless Docuseal.multitenant? + resources :storage, only: %i[index create], controller: 'storage_settings' + resources :email, only: %i[index create], controller: 'email_settings' + end resources :esign, only: %i[index create], controller: 'esign_settings' resources :users, only: %i[index] resource :account, only: %i[show update] diff --git a/lib/accounts.rb b/lib/accounts.rb index a6237484..2d83c4a3 100644 --- a/lib/accounts.rb +++ b/lib/accounts.rb @@ -3,6 +3,30 @@ module Accounts module_function + def load_signing_certs(account) + certs = + if Docuseal.multitenant? + Docuseal::CERTS + else + EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY).value + end + + { + cert: OpenSSL::X509::Certificate.new(certs['cert']), + key: OpenSSL::PKey::RSA.new(certs['key']), + sub_ca: OpenSSL::X509::Certificate.new(certs['sub_ca']), + sub_key: OpenSSL::PKey::RSA.new(certs['sub_key']), + root_ca: OpenSSL::X509::Certificate.new(certs['root_ca']), + root_key: OpenSSL::PKey::RSA.new(certs['root_key']) + } + end + + def can_send_emails?(account) + return true if Docuseal.multitenant? + + EncryptedConfig.exists?(account_id: account.id, key: EncryptedConfig::EMAIL_SMTP_KEY) + end + def normalize_timezone(timezone) tzinfo = TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[timezone] || timezone) diff --git a/lib/action_mailer_configs_interceptor.rb b/lib/action_mailer_configs_interceptor.rb index c7706d48..4cd93932 100644 --- a/lib/action_mailer_configs_interceptor.rb +++ b/lib/action_mailer_configs_interceptor.rb @@ -4,7 +4,11 @@ module ActionMailerConfigsInterceptor module_function def delivering_email(message) - return message unless Rails.env.production? + if Rails.env.production? && Rails.application.config.action_mailer.delivery_method + message.from = ENV.fetch('SMTP_FROM') + + return message + end email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY) diff --git a/lib/docuseal.rb b/lib/docuseal.rb index 2127d30b..72c0cee7 100644 --- a/lib/docuseal.rb +++ b/lib/docuseal.rb @@ -5,13 +5,22 @@ module Docuseal PRODUCT_NAME = 'DocuSeal' DEFAULT_APP_URL = 'http://localhost:3000' + CERTS = JSON.parse(ENV.fetch('CERTS', '{}')) + + DEFAULT_URL_OPTIONS = { + host: ENV.fetch('HOST', 'localhost'), + protocol: ENV['FORCE_SSL'] == 'true' ? 'https' : 'http' + }.freeze + module_function def multitenant? - ENV['MULTITENANT'] == true + ENV['MULTITENANT'] == 'true' end def default_url_options + return DEFAULT_URL_OPTIONS if multitenant? + @default_url_options ||= begin value = EncryptedConfig.find_by(key: EncryptedConfig::APP_URL_KEY)&.value value ||= DEFAULT_APP_URL diff --git a/lib/generate_certificate.rb b/lib/generate_certificate.rb index bc82dec7..3c35df13 100644 --- a/lib/generate_certificate.rb +++ b/lib/generate_certificate.rb @@ -13,12 +13,12 @@ module GenerateCertificate cert, key = generate_certificate(name, sub_cert, sub_key) { - cert: cert.to_pem, - key: key.to_pem, - root_ca: root_cert.to_pem, - root_key: root_key.to_pem, - sub_ca: sub_cert.to_pem, - sub_key: sub_key.to_pem + cert:, + key:, + root_ca: root_cert, + root_key:, + sub_ca: sub_cert, + sub_key: } end diff --git a/lib/load_active_storage_configs.rb b/lib/load_active_storage_configs.rb index 152955b3..263066fc 100644 --- a/lib/load_active_storage_configs.rb +++ b/lib/load_active_storage_configs.rb @@ -2,6 +2,8 @@ module LoadActiveStorageConfigs STORAGE_YML_PATH = Rails.root.join('config/storage.yml') + IS_ENV_CONFIGURED = + ENV['S3_ATTACHMENTS_BUCKET'].present? || ENV['GCS_BUCKET'].present? || ENV['AZURE_CONTAINER'].present? module_function @@ -14,6 +16,9 @@ module LoadActiveStorageConfigs end def reload + return if Docuseal.multitenant? + return if IS_ENV_CONFIGURED + encrypted_config = EncryptedConfig.find_by(key: EncryptedConfig::FILES_STORAGE_KEY) return unless encrypted_config diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 69ccbc15..391d725d 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -23,8 +23,7 @@ module Submissions template = submitter.submission.template - cert = submitter.submission.template.account.encrypted_configs - .find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value + certs = Accounts.load_signing_certs(submitter.submission.template.account) pdfs_index = build_pdfs_index(submitter) @@ -158,7 +157,7 @@ module Submissions template.schema.map do |item| pdf = pdfs_index[item['attachment_uuid']] - attachment = save_signed_pdf(pdf:, submitter:, cert:, uuid: item['attachment_uuid'], name: item['name']) + attachment = save_signed_pdf(pdf:, submitter:, certs:, uuid: item['attachment_uuid'], name: item['name']) image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image? @@ -176,7 +175,7 @@ module Submissions save_signed_pdf( pdf: images_pdf, submitter:, - cert:, + certs:, uuid: images_pdf_uuid(original_documents.select(&:image?)), name: template.name ) @@ -185,16 +184,15 @@ module Submissions end # rubocop:enable Metrics - def save_signed_pdf(pdf:, submitter:, cert:, uuid:, name:) + def save_signed_pdf(pdf:, submitter:, certs:, uuid:, name:) io = StringIO.new pdf.trailer.info[:Creator] = INFO_CREATOR pdf.sign(io, reason: format(SIGN_REASON, email: submitter.email), - certificate: OpenSSL::X509::Certificate.new(cert['cert']), - key: OpenSSL::PKey::RSA.new(cert['key']), - certificate_chain: [OpenSSL::X509::Certificate.new(cert['sub_ca']), - OpenSSL::X509::Certificate.new(cert['root_ca'])]) + certificate: certs[:cert], + key: certs[:key], + certificate_chain: [certs[:sub_ca], certs[:root_ca]]) ActiveStorage::Attachment.create!( uuid:,