diff --git a/app/controllers/api/api_base_controller.rb b/app/controllers/api/api_base_controller.rb index 60963b8d..154db075 100644 --- a/app/controllers/api/api_base_controller.rb +++ b/app/controllers/api/api_base_controller.rb @@ -15,6 +15,10 @@ module Api before_action :authenticate_user! check_authorization + rescue_from Params::BaseValidator::InvalidParameterError do |e| + render json: { error: e.message }, status: :unprocessable_entity + end + if Rails.env.production? rescue_from CanCan::AccessDenied do |e| Rollbar.error(e) if defined?(Rollbar) diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 8799b40a..a88cf8d9 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -63,6 +63,8 @@ module Api end def create + Params::SubmissionCreateValidator.call(params, dry_run: true) + return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil? if @template.fields.blank? diff --git a/lib/params/base_validator.rb b/lib/params/base_validator.rb new file mode 100644 index 00000000..bd46510a --- /dev/null +++ b/lib/params/base_validator.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Params + class BaseValidator + InvalidParameterError = Class.new(StandardError) + + def self.call(...) + validator = new(...) + + validator.call + rescue InvalidParameterError => e + Rollbar.error(e) if defined?(Rollbar) + + raise e unless validator.dry_run? + rescue StandardError => e + Rollbar.error(e) if defined?(Rollbar) + end + + attr_reader :params, :dry_run + + alias dry_run? dry_run + + def initialize(params, dry_run: false) + @params = params + @dry_run = dry_run + @current_path = '' + end + + def call + raise NotImplementedError + end + + def raise_error(message) + message += " in `#{@current_path}`." if @current_path.present? + + raise InvalidParameterError, message unless dry_run? + + Rollbar.error(message) if defined?(Rollbar) + end + + def required(params, keys, message: nil) + keys = Array.wrap(keys) + + return if keys.any? { |key| params&.dig(key).present? } + + raise_error(message || "#{keys.join(' or ')} is required") + end + + def type(params, key, type, message: nil) + return if params.blank? + return unless params.key?(key) + + return if params[key].is_a?(type) + + type = 'Object' if type == 'Hash' + + raise_error(message || "#{key} must be a #{type}") + end + + def in_path(params, path = []) + old_path = @current_path + + @current_path = [old_path, *path].compact_blank.map(&:to_s).join('.') + + param = params.dig(*path) + + yield params.dig(*path) if param + + @current_path = old_path + end + + def in_path_each(params, path = []) + old_path = @current_path + + params.dig(*path)&.each_with_index do |item, index| + @current_path = old_path + [*path].map(&:to_s).join('.') + "[#{index}]" + + yield item if item + end + + @current_path = old_path + end + + def boolean(params, key, message: nil) + return if params.blank? + return unless params.key?(key) + + value = ActiveModel::Type::Boolean.new.cast(params[key]) + + return if value.is_a?(TrueClass) || value.is_a?(FalseClass) + + raise_error(message || "#{key} must be true or false") + end + + def value_in(params, key, values, allow_nil: false, message: nil) + return if params.blank? + return if allow_nil && params[key].nil? + return if values.include?(params[key]) + + raise_error(message || "#{key} must be one of #{values.join(', ')}") + end + end +end diff --git a/lib/params/submission_create_validator.rb b/lib/params/submission_create_validator.rb new file mode 100644 index 00000000..e4d4a8a3 --- /dev/null +++ b/lib/params/submission_create_validator.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Params + class SubmissionCreateValidator < BaseValidator + # rubocop:disable Metrics + def call + if params[:submission].blank? && (params[:emails].present? || params[:email].present?) + validate_creation_from_emails(params) + else + validate_creation_from_submission(params) + end + end + + def validate_creation_from_emails(params) + required(params, :template_id) + required(params, %i[emails email]) + + type(params, :emails, String) + boolean(params, :send_email) + type(params, :message, Hash) + + in_path(params, :message) do |message_params| + type(message_params, :subject, String) + type(message_params, :body, String) + + required(message_params, :body) + end + + true + end + + def validate_creation_from_submission(params) + required(params, :template_id) + required(params, %i[submission submissions]) + + in_path(params, :submission) do |submission_params| + required(submission_params, :submitters) if params[:submission] + end + + boolean(params, :send_email) + boolean(params, :send_sms) + type(params, :order, String) + type(params, :completed_redirect_url, String) + type(params, :bcc_completed, String) + type(params, :message, Hash) + + in_path(params, :message) do |message_params| + type(message_params, :subject, String) + type(message_params, :body, String) + + required(message_params, :body) + end + + value_in(params, :order, %w[preserved random], allow_nil: true) + + in_path_each(params, %i[submission submitters]) do |submitter_params| + required(submitter_params, :email) + + type(submitter_params, :name, String) + type(submitter_params, :email, String) + type(submitter_params, :phone, String) + type(submitter_params, :values, Hash) + boolean(submitter_params, :send_email) + boolean(submitter_params, :send_sms) + type(submitter_params, :completed_redirect_url, String) + type(submitter_params, :fields, Array) + + in_path_each(submitter_params, %i[fields]) do |field_params| + required(field_params, :name) + + type(field_params, :name, String) + type(field_params, :default_value, String) + type(field_params, :validation_pattern, String) + type(field_params, :invalid_message, String) + boolean(field_params, :readonly) + end + end + + true + end + # rubocop:enable Metrics + end +end