diff --git a/Dockerfile b/Dockerfile index e0c2b4b8..dc4ee0dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ COPY ./app/views ./app/views RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker -FROM ruby:3.2.2-alpine as app +FROM ruby:3.2.2-alpine3.18 as app ENV RAILS_ENV=production ENV BUNDLE_WITHOUT="development:test" diff --git a/Gemfile b/Gemfile index c967bd8f..da61a3a7 100644 --- a/Gemfile +++ b/Gemfile @@ -21,8 +21,6 @@ gem 'jwt' gem 'lograge' gem 'mysql2', require: false gem 'oj' -gem 'omniauth-google-oauth2' -gem 'omniauth-rails_csrf_protection' gem 'pagy' gem 'pdf-reader' gem 'pg', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 92fb8dcf..4654547c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -245,8 +245,7 @@ GEM signet (>= 0.16, < 2.a) hashdiff (1.0.1) hashery (2.1.2) - hashie (5.0.0) - hexapdf (0.33.0) + hexapdf (0.34.1) cmdparse (~> 3.0, >= 3.0.3) geom2d (~> 0.4, >= 0.4.1) openssl (>= 2.2.1) @@ -296,7 +295,6 @@ GEM minitest (5.20.0) msgpack (1.7.2) multi_json (1.15.0) - multi_xml (0.6.0) multipart-post (2.3.0) mysql2 (0.5.5) net-http-persistent (4.0.2) @@ -316,30 +314,8 @@ GEM racc (~> 1.4) nokogiri (1.15.4-arm64-darwin) racc (~> 1.4) - oauth2 (2.0.9) - faraday (>= 0.17.3, < 3.0) - jwt (>= 1.0, < 3.0) - multi_xml (~> 0.5) - rack (>= 1.2, < 4) - snaky_hash (~> 2.0) - version_gem (~> 1.1) oj (3.16.0) - omniauth (2.1.1) - hashie (>= 3.4.6) - rack (>= 2.2.3) - rack-protection - omniauth-google-oauth2 (1.1.1) - jwt (>= 2.0) - oauth2 (~> 2.0.6) - omniauth (~> 2.0) - omniauth-oauth2 (~> 1.8.0) - omniauth-oauth2 (1.8.0) - oauth2 (>= 1.4, < 3) - omniauth (~> 2.0) - omniauth-rails_csrf_protection (1.0.1) - actionpack (>= 4.2) - omniauth (~> 2.0) - openssl (3.1.0) + openssl (3.2.0) orm_adapter (0.5.0) os (1.1.4) pagy (6.0.4) @@ -372,8 +348,6 @@ GEM nio4r (~> 2.0) racc (1.7.1) rack (2.2.8) - rack-protection (3.1.0) - rack (~> 2.2, >= 2.2.4) rack-proxy (0.7.6) rack rack-test (2.1.0) @@ -514,9 +488,6 @@ GEM simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) smart_properties (1.17.0) - snaky_hash (2.0.1) - hashie - version_gem (~> 1.1, >= 1.1.1) sqlite3 (1.6.3) mini_portile2 (~> 2.8.0) sqlite3 (1.6.3-arm64-darwin) @@ -537,7 +508,6 @@ GEM uber (0.1.0) unicode-display_width (2.5.0) uniform_notifier (1.16.0) - version_gem (1.1.3) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.0) @@ -589,8 +559,6 @@ DEPENDENCIES lograge mysql2 oj - omniauth-google-oauth2 - omniauth-rails_csrf_protection pagy pdf-reader pg diff --git a/README.md b/README.md index 6ec713e5..8b021c4e 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ DocuSeal is an open source platform that provides secure and efficient digital d ## Features - [x] PDF form fields builder (WYSIWYG) -- [x] 10 field types available (Signature, Date, File, Checkbox etc.) +- [x] 11 field types available (Signature, Date, File, Checkbox etc.) - [x] Multiple submitters per document - [x] Automated emails via SMTP -- [x] Files storage on AWS S3, Google Storage, or Azure +- [x] Files storage on disk or AWS S3, Google Storage, Azure Cloud - [x] Automatic PDF eSignature - [x] PDF signature verification - [x] Users management @@ -76,7 +76,7 @@ Run the app under a custom domain over https using docker compose (make sure you HOST=your-domain-name.com docker-compose up ``` -## For Companies +## For Businesses ### Integrate seamless document signing into your web or mobile apps with DocuSeal! At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws. diff --git a/app/controllers/account_configs_controller.rb b/app/controllers/account_configs_controller.rb new file mode 100644 index 00000000..cfa46a14 --- /dev/null +++ b/app/controllers/account_configs_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class AccountConfigsController < ApplicationController + before_action :load_account_config + authorize_resource :account_config + + ALLOWED_KEYS = [ + AccountConfig::ALLOW_TYPED_SIGNATURE, + AccountConfig::FORCE_MFA + ].freeze + + def create + @account_config.update!(account_config_params) + + head :ok + end + + private + + def load_account_config + return head :not_found unless ALLOWED_KEYS.include?(account_config_params[:key]) + + @account_config = + AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key]) + end + + def account_config_params + params.required(:account_config).permit!.tap do |attrs| + attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0]) + end + end +end diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index f2778ab4..3bb3ab88 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -13,6 +13,10 @@ module Api submissions = Submissions.search(@submissions, params[:q]) submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present? + if params[:template_folder].present? + submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] }) + end + submissions = paginate(submissions.preload(:created_by_user, :template, :submitters, audit_trail_attachment: :blob)) @@ -50,35 +54,14 @@ module Api end def create - is_send_email = !params[:send_email].in?(['false', false]) - - submissions = - if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank? - Submissions.create_from_emails(template: @template, - user: current_user, - source: :api, - mark_as_sent: is_send_email, - emails:) - else - submissions_attrs, attachments = normalize_submissions_params!(submissions_params[:submission], @template) - - Submissions.create_from_submitters( - template: @template, - user: current_user, - source: :api, - mark_as_sent: is_send_email, - submitters_order: params[:submitters_order] || 'preserved', - submissions_attrs: - ) - end + params[:send_email] = true unless params.key?(:send_email) + params[:send_sms] = false unless params.key?(:send_sms) - Submissions.send_signature_requests(submissions, send_email: is_send_email) + submissions = create_submissions(@template, params) - submitters = submissions.flat_map(&:submitters) + Submissions.send_signature_requests(submissions) - save_default_value_attachments!(attachments, submitters) - - render json: submitters + render json: submissions.flat_map(&:submitters) rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e render json: { error: e.message }, status: :unprocessable_entity end @@ -91,6 +74,37 @@ module Api private + def create_submissions(template, params) + is_send_email = !params[:send_email].in?(['false', false]) + + if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank? + Submissions.create_from_emails(template:, + user: current_user, + source: :api, + mark_as_sent: is_send_email, + emails:, + params:) + else + submissions_attrs, attachments = + Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template) + + submissions = Submissions.create_from_submitters( + template:, + user: current_user, + source: :api, + mark_as_sent: is_send_email, + submitters_order: params[:submitters_order] || params[:order] || 'preserved', + submissions_attrs:, + params: + ) + + Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, + submissions.flat_map(&:submitters)) + + submissions + end + end + def serialize_params { only: %i[id source submitters_order created_at updated_at], @@ -107,54 +121,19 @@ module Api end def submissions_params - params.permit(submission: [{ - submitters: [[:uuid, :name, :email, :role, :completed, :phone, :application_key, - { values: {}, readonly_fields: [], - fields: [%i[name default_value readonly validation_pattern invalid_message]] }]] - }]) - end - - def normalize_submissions_params!(submissions_params, template) - attachments = [] - - Array.wrap(submissions_params).each do |submission| - submission[:submitters].each_with_index do |submitter, index| - default_values = submitter[:values] || {} - - submitter[:fields]&.each { |f| default_values[f[:name]] = f[:default_value] if f[:default_value].present? } - - next if default_values.blank? - - values, new_attachments = - Submitters::NormalizeValues.call(template, - default_values, - submitter[:role] || template.submitters[index]['name']) - - attachments.push(*new_attachments) - - submitter[:values] = values - end - end - - [submissions_params, attachments] - end - - def save_default_value_attachments!(attachments, submitters) - return if attachments.blank? - - attachments_index = attachments.index_by(&:uuid) - - submitters.each do |submitter| - submitter.values.to_a.each do |_, value| - attachment = attachments_index[value] - - next unless attachment - - attachment.record = submitter - - attachment.save! - end - end + key = params.key?(:submission) ? :submission : :submissions + + params.permit( + key => [ + [:send_email, :send_sms, { + message: %i[subject body], + submitters: [[:send_email, :send_sms, :uuid, :name, :email, :role, + :completed, :phone, :application_key, + { values: {}, readonly_fields: [], message: %i[subject body], + fields: [%i[name default_value readonly validation_pattern invalid_message]] }]] + }] + ] + ).fetch(key, []) end end end diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index a0abb9c8..dec241ba 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -30,5 +30,79 @@ module Api render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true) end + + def update + if @submitter.completed_at? + return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity + end + + role = @submitter.submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name'] + + normalized_params, new_attachments = + Submissions::NormalizeParamUtils.normalize_submitter_params!(submitter_params.merge(role:), @submitter.template, + for_submitter: @submitter) + + Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission, + [normalized_params], + submitter_uuid: @submitter.uuid) + + assign_submitter_attrs(@submitter, normalized_params) + + ApplicationRecord.transaction do + Submissions::NormalizeParamUtils.save_default_value_attachments!(new_attachments, [@submitter]) + + @submitter.save! + + @submitter.submission.save! + end + + if @submitter.completed_at? + ProcessSubmitterCompletionJob.perform_later(@submitter) + elsif normalized_params[:send_email] || normalized_params[:send_sms] + Submitters.send_signature_requests([@submitter]) + end + + render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_events: false) + end + + def submitter_params + submitter_params = params.key?(:submitter) ? params.require(:submitter) : params + + submitter_params.permit( + :send_email, :send_sms, :uuid, :name, :email, :role, :completed, :phone, :application_key, + { values: {}, readonly_fields: [], message: %i[subject body], + fields: [%i[name default_value readonly validation_pattern invalid_message]] } + ) + end + + private + + def assign_submitter_attrs(submitter, attrs) + submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email) + submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone) + submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values] + submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at + submitter.application_key = attrs[:application_key] if attrs.key?(:application_key) + + assign_preferences(submitter, attrs) + + submitter + end + + def assign_preferences(submitter, attrs) + submitter_preferences = Submitters.normalize_preferences(submitter.account, current_user, attrs) + + if submitter_preferences.key?('send_email') + submitter.preferences['send_email'] = submitter_preferences['send_email'] + end + + submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms') + + return unless submitter_preferences.key?('email_message_uuid') + + submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid'] + + submitter + end end end diff --git a/app/controllers/api/templates_clone_controller.rb b/app/controllers/api/templates_clone_controller.rb new file mode 100644 index 00000000..45c76076 --- /dev/null +++ b/app/controllers/api/templates_clone_controller.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Api + class TemplatesCloneController < ApiBaseController + load_and_authorize_resource :template + + def create + authorize!(:manage, @template) + + template = current_account.templates.new(source: :api) + + template.application_key = params[:application_key] + template.name = params[:name] || "#{@template.name} (Clone)" + template.account = @template.account + template.author = current_user + template.assign_attributes(@template.slice(:folder_id, :fields, :schema, :submitters)) + + if params[:folder_name].present? + template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) + end + + template.save! + + Templates::CloneAttachments.call(template:, original_template: @template) + + render json: template.as_json(serialize_params) + end + + private + + def serialize_params + { + include: { author: { only: %i[id email first_name last_name] }, + documents: { only: %i[id uuid], methods: %i[url filename] } } + } + end + end +end diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index 7772afba..fa79b605 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -9,6 +9,7 @@ module Api templates = params[:archived] ? templates.archived : templates.active templates = templates.where(application_key: params[:application_key]) if params[:application_key].present? + templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present? templates = paginate(templates.preload(:author, documents_attachments: :blob)) @@ -57,7 +58,9 @@ module Api schema: [%i[attachment_uuid name]], submitters: [%i[name uuid]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, - { options: [%i[value uuid]], areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] + { preferences: {}, + options: [%i[value uuid]], + areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] ) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 38b541aa..16aa80fa 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -44,8 +44,9 @@ class ApplicationController < ActionController::Base redirect_to setup_index_path unless User.exists? end - def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil) - render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: }) + def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil) + render_to_string(partial: 'shared/button_title', + locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: }) end def svg_icon(icon_name, class: '') diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index aa08ae4d..312f71cd 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -5,6 +5,7 @@ class DashboardController < ApplicationController before_action :maybe_redirect_product_url before_action :maybe_render_landing + before_action :maybe_redirect_mfa_setup load_and_authorize_resource :template_folder, parent: false load_and_authorize_resource :template, parent: false @@ -62,6 +63,17 @@ class DashboardController < ApplicationController redirect_to Docuseal::PRODUCT_URL, allow_other_host: true end + def maybe_redirect_mfa_setup + return unless signed_in? + return if current_user.otp_required_for_login + + return if !current_user.otp_required_for_login && !AccountConfig.exists?(value: true, + account_id: current_user.account_id, + key: AccountConfig::FORCE_MFA) + + redirect_to mfa_setup_path, notice: 'Setup 2FA to continue' + end + def maybe_render_landing return if signed_in? diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb new file mode 100644 index 00000000..19c47507 --- /dev/null +++ b/app/controllers/errors_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class ErrorsController < ActionController::Base + ENTERPRISE_FEATURE_MESSAGE = + 'This feature is available in Enterprise Edition: https://www.docuseal.co/pricing' + + ENTERPRISE_PATHS = [ + '/templates/html', + '/api/templates/html', + '/templates/pdf', + '/api/templates/pdf' + ].freeze + + def show + if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404 + return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found + end + + respond_to do |f| + f.json do + render json: { status: error_status_code }, status: error_status_code + end + + f.html { render error_status_code.to_s, status: error_status_code } + end + end + + private + + def error_status_code + @error_status_code ||= + ActionDispatch::ExceptionWrapper.new(request.env, + request.env['action_dispatch.exception']).status_code + end +end diff --git a/app/controllers/mfa_setup_controller.rb b/app/controllers/mfa_setup_controller.rb index ce690643..c65b2006 100644 --- a/app/controllers/mfa_setup_controller.rb +++ b/app/controllers/mfa_setup_controller.rb @@ -5,13 +5,11 @@ class MfaSetupController < ApplicationController authorize!(:update, current_user) end - def new - current_user.otp_secret ||= User.generate_otp_secret + before_action :set_provision_url, only: %i[show new] - current_user.save! + def show; end - @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name) - end + def new; end def edit; end @@ -26,7 +24,7 @@ class MfaSetupController < ApplicationController @error_message = 'Code is invalid' - render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/new'), status: :unprocessable_entity + render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity end end @@ -41,4 +39,16 @@ class MfaSetupController < ApplicationController render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity end end + + private + + def set_provision_url + return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login + + current_user.otp_secret ||= User.generate_otp_secret + + current_user.save! + + @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name) + end end diff --git a/app/controllers/notifications_settings_controller.rb b/app/controllers/notifications_settings_controller.rb index 2189ccda..14139c55 100644 --- a/app/controllers/notifications_settings_controller.rb +++ b/app/controllers/notifications_settings_controller.rb @@ -12,7 +12,7 @@ class NotificationsSettingsController < ApplicationController def index; end def create - if @account_config.save + if @account_config.value.present? ? @account_config.save : @account_config.delete redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved' else redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save' diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb deleted file mode 100644 index de6d1910..00000000 --- a/app/controllers/omniauth_callbacks_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class OmniauthCallbacksController < Devise::OmniauthCallbacksController - def google_oauth2 - @user = Users.from_omniauth(request.env['omniauth.auth']) - - if @user.persisted? - flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: 'Google') - - sign_in_and_redirect @user, event: :authentication - else - redirect_to new_registration_path(oauth_callback: true, user: @user.slice(:email, :first_name, :last_name)), - notice: 'Please complete registration with Google auth' - end - end -end diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index 75e7c386..e1732ab1 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -14,21 +14,20 @@ class StartFormController < ApplicationController def update @submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil)) + .order(id: :desc) .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } .find_or_initialize_by(**submitter_params.compact_blank) if @submitter.completed_at? redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) else - @submitter.assign_attributes( - uuid: @template.submitters.second.nil? ? @template.submitters.first['uuid'] : @template.submitters.second['uuid'], - ip: request.remote_ip, - ua: request.user_agent - ) + if @template.submitters.to_a.size > 1 && @submitter.new_record? + @error_message = 'Not found' - @submitter.submission ||= Submission.new(template: @template, - template_submitters: @template.submitters, - source: :link) + return render :show + end + + assign_submission_attributes(@submitter, @template) if @submitter.new_record? if @submitter.save redirect_to submit_form_path(@submitter.slug) @@ -46,6 +45,21 @@ class StartFormController < ApplicationController private + def assign_submission_attributes(submitter, template) + submitter.assign_attributes( + uuid: template.submitters.first['uuid'], + ip: request.remote_ip, + ua: request.user_agent, + preferences: { 'send_email' => true } + ) + + submitter.submission ||= Submission.new(template:, + template_submitters: template.submitters, + source: :link) + + submitter + end + def submitter_params params.require(:submitter).permit(:email, :phone, :name).tap do |attrs| attrs[:email] = Submissions.normalize_email(attrs[:email]) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index a327d56e..dda8a829 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -34,23 +34,30 @@ class SubmissionsController < ApplicationController def create authorize!(:create, Submission) + if params[:is_custom_message] != '1' + params.delete(:subject) + params.delete(:body) + end + submissions = if params[:emails].present? Submissions.create_from_emails(template: @template, user: current_user, source: :invite, mark_as_sent: params[:send_email] == '1', - emails: params[:emails]) + emails: params[:emails], + params: params.merge('send_completed_email' => true)) else Submissions.create_from_submitters(template: @template, user: current_user, source: :invite, submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random', mark_as_sent: params[:send_email] == '1', - submissions_attrs: submissions_params[:submission].to_h.values) + submissions_attrs: submissions_params[:submission].to_h.values, + params: params.merge('send_completed_email' => true)) end - Submissions.send_signature_requests(submissions, params) + Submissions.send_signature_requests(submissions) redirect_to template_path(@template), notice: 'New recipients have been added' end diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index 01fa5adc..63134478 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -38,6 +38,16 @@ class SubmitFormController < ApplicationController def update submitter = Submitter.find_by!(slug: params[:slug]) + if submitter.completed_at? + return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity + end + + if submitter.template.deleted_at? || submitter.submission.deleted_at? + Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar) + + return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity + end + Submitters::SubmitValues.call(submitter, params, request) head :ok diff --git a/app/controllers/submitters_send_email_controller.rb b/app/controllers/submitters_send_email_controller.rb index daeb777e..0e6c5d3b 100644 --- a/app/controllers/submitters_send_email_controller.rb +++ b/app/controllers/submitters_send_email_controller.rb @@ -4,6 +4,13 @@ class SubmittersSendEmailController < ApplicationController load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug def create + if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter, + event_type: 'send_email', + created_at: 24.hours.ago..Time.current) + return redirect_back(fallback_location: submission_path(@submitter.submission), + alert: 'Email has been sent already.') + end + SubmitterMailer.invitation_email(@submitter).deliver_later! SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email') diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 971a6ba2..e8861725 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -58,9 +58,11 @@ class TemplatesController < ApplicationController def destroy notice = - if !Docuseal.multitenant? && params[:permanently].present? + if params[:permanently].present? @template.destroy! + Rollbar.info("Remove template: #{@template.id}") if defined?(Rollbar) + 'Template has been removed.' else @template.update!(deleted_at: Time.current) diff --git a/app/controllers/timestamp_server_controller.rb b/app/controllers/timestamp_server_controller.rb new file mode 100644 index 00000000..619a13b7 --- /dev/null +++ b/app/controllers/timestamp_server_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class TimestampServerController < ApplicationController + before_action :build_encrypted_config + authorize_resource :encrypted_config + + def create + return head :not_found if Docuseal.multitenant? + + test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present? + + if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete + redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved' + else + redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save' + end + rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError + redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver' + end + + private + + def test_timeserver_url(url) + pdf = HexaPDF::Document.new + pdf.pages.add + + pkcs = Accounts.load_signing_pkcs(current_account) + + pdf.sign(StringIO.new, + reason: 'Test', + certificate: pkcs.certificate, + key: pkcs.key, + certificate_chain: pkcs.ca_certs || [], + timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url)) + end + + def load_encrypted_config + @encrypted_config + end + + def build_encrypted_config + @encrypted_config = + EncryptedConfig.find_or_initialize_by(account: current_account, + key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY) + + @encrypted_config.assign_attributes(encrypted_config_params) + end + + def encrypted_config_params + params.require(:encrypted_config).permit(:value) + end +end diff --git a/app/controllers/webhook_settings_controller.rb b/app/controllers/webhook_settings_controller.rb index 2382dad3..1748ef4d 100644 --- a/app/controllers/webhook_settings_controller.rb +++ b/app/controllers/webhook_settings_controller.rb @@ -7,7 +7,9 @@ class WebhookSettingsController < ApplicationController def show; end def create - @encrypted_config.update!(encrypted_config_params) + @encrypted_config.assign_attributes(encrypted_config_params) + + @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.') end diff --git a/app/javascript/application.js b/app/javascript/application.js index a0998864..a65a679d 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -79,12 +79,15 @@ window.customElements.define('template-builder', class extends HTMLElement { connectedCallback () { this.appElem = document.createElement('div') + this.appElem.classList.add('md:h-screen') + this.app = createApp(TemplateBuilder, { template: reactive(JSON.parse(this.dataset.template)), templateAttachments: reactive(JSON.parse(this.dataset.templateAttachmentsIndex)), backgroundColor: '#faf7f5', withPhone: this.dataset.withPhone === 'true', withLogo: this.dataset.withLogo !== 'false', + withPayment: this.dataset.withPayment !== 'false', acceptFileTypes: this.dataset.acceptFileTypes, isDirectUpload: this.dataset.isDirectUpload === 'true' }) diff --git a/app/javascript/form.js b/app/javascript/form.js index 6c7eec85..29038984 100644 --- a/app/javascript/form.js +++ b/app/javascript/form.js @@ -19,6 +19,7 @@ window.customElements.define('submission-form', class extends HTMLElement { isDemo: this.dataset.isDemo === 'true', attribution: this.dataset.attribution !== 'false', withConfetti: true, + withTypedSignature: this.dataset.withTypedSignature !== 'false', values: reactive(JSON.parse(this.dataset.values)), completedButton: JSON.parse(this.dataset.completedButton), attachments: reactive(JSON.parse(this.dataset.attachments)), diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 8162a28b..bfe46326 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -98,7 +98,7 @@ v-else class="flex absolute lg:text-base" :style="computedStyle" - :class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }" + :class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" >
diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue index 4157133d..a4c90958 100644 --- a/app/javascript/submission_form/signature_step.vue +++ b/app/javascript/submission_form/signature_step.vue @@ -23,7 +23,7 @@ @@ -149,6 +149,11 @@ export default { required: true, default: false }, + withTypedSignature: { + type: Boolean, + required: false, + default: true + }, attachmentsIndex: { type: Object, required: false, diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue index 3f5ff0e9..35a8f901 100644 --- a/app/javascript/template_builder/area.vue +++ b/app/javascript/template_builder/area.vue @@ -45,7 +45,7 @@ :me-fields="['my_text', 'my_signature', 'my_initials', 'my_date', 'my_check'].includes(field.type)" :hide-select-me="true" :compact="true" - :editable="editable" + :editable="editable && !defaultField" :menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'" :submitters="template.submitters" @update:model-value="save" @@ -55,7 +55,7 @@ v-if="!['my_text', 'my_signature', 'my_initials', 'my_date', 'my_check'].includes(field.type)" v-model="field.type" :button-width="27" - :editable="editable" + :editable="editable && !defaultField" :button-classes="'px-1'" :menu-classes="'bg-white rounded-t-none'" @update:model-value="[maybeUpdateOptions(), save()]" @@ -64,7 +64,7 @@
-
+
@@ -28,10 +30,25 @@ name="buttons" />