rails_spotlight 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -4
  3. data/Dockerfile +1 -2
  4. data/Dockerfile-rails-6.0 +2 -2
  5. data/Dockerfile-rails-6.1 +2 -2
  6. data/Dockerfile-rails-7.1 +44 -0
  7. data/{Dockerfile-rails-7.0 → Dockerfile-rails-8.0} +3 -3
  8. data/{Dockerfile-rails-5.2 → Dockerfile-rails-8.0.2} +10 -5
  9. data/README.md +47 -15
  10. data/docker-compose.yml +14 -9
  11. data/docs/assets/images/sql_execution_toggle.gif +0 -0
  12. data/fake_spec_res/config/rails_spotlight.yml +47 -0
  13. data/fake_spec_res/rails_spotlight_spec.rb +23 -5
  14. data/lib/rails_spotlight/app_notifications.rb +2 -3
  15. data/lib/rails_spotlight/channels/handlers/{live_console_handler.rb → console_handler.rb} +3 -3
  16. data/lib/rails_spotlight/channels/handlers/logs_handler.rb +1 -1
  17. data/lib/rails_spotlight/channels/handlers.rb +3 -3
  18. data/lib/rails_spotlight/channels/spotlight_channel.rb +20 -1
  19. data/lib/rails_spotlight/configuration.rb +40 -58
  20. data/lib/rails_spotlight/event.rb +21 -5
  21. data/lib/rails_spotlight/log_interceptor.rb +34 -57
  22. data/lib/rails_spotlight/middlewares/handlers/base_action_handler.rb +45 -55
  23. data/lib/rails_spotlight/middlewares/handlers/code_analysis_action_handler.rb +8 -13
  24. data/lib/rails_spotlight/middlewares/handlers/console_action_handler.rb +8 -12
  25. data/lib/rails_spotlight/middlewares/handlers/directory_index_action_handler.rb +16 -27
  26. data/lib/rails_spotlight/middlewares/handlers/file_action_handler.rb +21 -55
  27. data/lib/rails_spotlight/middlewares/handlers/meta_action_handler.rb +3 -8
  28. data/lib/rails_spotlight/middlewares/handlers/not_found_action_handler.rb +1 -1
  29. data/lib/rails_spotlight/middlewares/handlers/sql_action_handler.rb +18 -35
  30. data/lib/rails_spotlight/middlewares/handlers/verify_action_handler.rb +1 -3
  31. data/lib/rails_spotlight/middlewares/header_marker.rb +1 -3
  32. data/lib/rails_spotlight/middlewares/request_completed.rb +2 -2
  33. data/lib/rails_spotlight/rails_command_executor.rb +3 -5
  34. data/lib/rails_spotlight/railtie.rb +2 -4
  35. data/lib/rails_spotlight/render_view_reporter.rb +3 -3
  36. data/lib/rails_spotlight/storage.rb +4 -14
  37. data/lib/rails_spotlight/utils.rb +1 -1
  38. data/lib/rails_spotlight/version.rb +1 -1
  39. data/lib/tasks/init.rake +26 -6
  40. metadata +16 -16
@@ -4,12 +4,14 @@ require 'yaml'
4
4
  require 'erb'
5
5
 
6
6
  module RailsSpotlight
7
- class Configuration # rubocop:disable Metrics/ClassLength
7
+ class Configuration
8
8
  DEFAULT_NOT_ENCODABLE_EVENT_VALUES = {
9
9
  'ActiveRecord' => [
10
10
  'ActiveRecord::ConnectionAdapters::AbstractAdapter',
11
11
  'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter',
12
- 'ActiveRecord::ConnectionAdapters::RealTransaction'
12
+ 'ActiveRecord::ConnectionAdapters::RealTransaction',
13
+ 'ActiveRecord::Transaction',
14
+ 'ActiveRecord::SchemaMigration'
13
15
  ],
14
16
  'ActionDispatch' => ['ActionDispatch::Request', 'ActionDispatch::Response']
15
17
  }.freeze
@@ -39,10 +41,10 @@ module RailsSpotlight
39
41
  ].freeze
40
42
 
41
43
  attr_reader :project_name, :source_path, :logger, :storage_path, :storage_pool_size, :middleware_skipped_paths,
42
- :not_encodable_event_values, :action_cable_mount_path,
43
- :block_editing_files, :block_editing_files_outside_of_the_project, :skip_rendered_ivars,
44
- :directory_index_ignore, :rubocop_config_path, :use_action_cable, :default_rs_src,
45
- :form_js_execution_token
44
+ :not_encodable_event_values, :cable_mount_path,
45
+ :file_manager_enabled, :block_editing_files, :block_editing_files_outside_of_the_project, :skip_rendered_ivars,
46
+ :directory_index_ignore, :rubocop_enabled, :rubocop_config_path, :use_cable, :default_rs_src,
47
+ :form_js_execution_token, :sql_console_enabled, :irb_console_enabled, :data_access_token
46
48
 
47
49
  def initialize(opts = {}) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
48
50
  @project_name = opts[:project_name] || detect_project_name
@@ -50,50 +52,43 @@ module RailsSpotlight
50
52
  @logger = opts[:logger] || Logger.new(File.join(self.class.rails_root, 'log', 'rails_spotlight.log'))
51
53
  @storage_path = opts[:storage_path] || File.join(self.class.rails_root, 'tmp', 'data', 'rails_spotlight')
52
54
  @storage_pool_size = opts[:storage_pool_size] || 20
53
- @live_console_enabled = opts[:live_console_enabled].nil? ? false : true?(opts[:live_console_enabled])
54
- @request_completed_broadcast_enabled = opts[:request_completed_broadcast_enabled].nil? ? false : true?(opts[:request_completed_broadcast_enabled])
55
+ @cable_console_enabled = bool_val(:cable_console_enabled, opts)
56
+ @request_completed_broadcast_enabled = bool_val(:request_completed_broadcast_enabled, opts)
55
57
  @middleware_skipped_paths = opts[:middleware_skipped_paths] || []
56
58
  @not_encodable_event_values = DEFAULT_NOT_ENCODABLE_EVENT_VALUES.merge(opts[:not_encodable_event_values] || {})
57
- @use_action_cable = opts[:use_action_cable].nil? ? false : true?(opts[:use_action_cable])
58
- @auto_mount_action_cable = opts[:auto_mount_action_cable].nil? ? false : true?(opts[:auto_mount_action_cable])
59
- @action_cable_mount_path = opts[:action_cable_mount_path] || '/cable'
60
- @block_editing_files = opts[:block_editing_files].nil? ? false : true?(opts[:block_editing_files])
61
- @block_editing_files_outside_of_the_project = opts[:block_editing_files_outside_of_the_project].nil? ? true : true?(opts[:block_editing_files_outside_of_the_project])
59
+ @use_cable = bool_val(:use_cable, opts)
60
+ @auto_mount_cable = bool_val(:auto_mount_cable, opts)
61
+ @cable_mount_path = opts[:cable_mount_path] || '/cable'
62
+ @block_editing_files = bool_val(:block_editing_files, opts)
63
+ @block_editing_files_outside_of_the_project = bool_val(:block_editing_files_outside_of_the_project, opts, default: true)
64
+ @file_manager_enabled = bool_val(:file_manager_enabled, opts, default: true)
62
65
  @skip_rendered_ivars = SKIP_RENDERED_IVARS + (opts[:skip_rendered_ivars] || []).map(&:to_sym)
63
66
  @directory_index_ignore = opts[:directory_index_ignore] || DEFAULT_DIRECTORY_INDEX_IGNORE
67
+ @rubocop_enabled = bool_val(:rubocop_enabled, opts, default: true)
64
68
  @rubocop_config_path = opts[:rubocop_config_path] ? File.join(self.class.rails_root, opts[:rubocop_config_path]) : nil
65
- @live_logs_enabled = opts[:live_logs_enabled].nil? ? false : true?(opts[:live_logs_enabled])
69
+ @cable_logs_enabled = bool_val(:cable_logs_enabled, opts)
66
70
  @default_rs_src = opts[:default_rs_src] || 'default'
67
71
  @form_js_execution_token = opts[:form_js_execution_token] || Digest::MD5.hexdigest(detect_project_name)
72
+ @sql_console_enabled = bool_val(:sql_console_enabled, opts, default: true)
73
+ @irb_console_enabled = bool_val(:irb_console_enabled, opts, default: true)
74
+ @data_access_token = opts[:data_access_token].present? ? opts[:data_access_token] : nil
68
75
  end
69
76
 
70
- def live_console_enabled
71
- @live_console_enabled && use_action_cable && action_cable_present?
72
- end
73
-
74
- def live_logs_enabled
75
- @live_logs_enabled && use_action_cable && action_cable_present?
76
- end
77
-
78
- alias live_console_enabled? live_console_enabled
79
- alias live_logs_enabled? live_logs_enabled
80
- alias use_action_cable? use_action_cable
81
-
82
- def request_completed_broadcast_enabled
83
- @request_completed_broadcast_enabled && use_action_cable && action_cable_present?
84
- end
77
+ def cable_console_enabled = @cable_console_enabled && use_cable && action_cable_present?
78
+ def cable_logs_enabled = @cable_logs_enabled && use_cable && action_cable_present?
79
+ def request_completed_broadcast_enabled = @request_completed_broadcast_enabled && use_cable && action_cable_present?
80
+ def auto_mount_cable = @auto_mount_cable && use_cable && action_cable_present?
81
+ def action_cable_present? = defined?(ActionCable) && true
85
82
 
83
+ alias cable_console_enabled? cable_console_enabled
84
+ alias cable_logs_enabled? cable_logs_enabled
85
+ alias use_cable? use_cable
86
86
  alias request_completed_broadcast_enabled? request_completed_broadcast_enabled
87
-
88
- def auto_mount_action_cable
89
- @auto_mount_action_cable && use_action_cable && action_cable_present?
90
- end
91
-
92
- alias auto_mount_action_cable? auto_mount_action_cable
93
-
94
- def action_cable_present?
95
- defined?(ActionCable) && true
96
- end
87
+ alias auto_mount_cable? auto_mount_cable
88
+ alias file_manager_enabled? file_manager_enabled
89
+ alias rubocop_enabled? rubocop_enabled
90
+ alias sql_console_enabled? sql_console_enabled
91
+ alias irb_console_enabled? irb_console_enabled
97
92
 
98
93
  def self.load_config
99
94
  config_file = File.join(rails_root, 'config', 'rails_spotlight.yml')
@@ -111,32 +106,19 @@ module RailsSpotlight
111
106
  new(opts)
112
107
  end
113
108
 
114
- def self.rails_root
115
- @rails_root ||= (Rails.root.to_s.presence || Dir.pwd).freeze
116
- end
117
-
118
- def rails_root
119
- self.class.rails_root
120
- end
109
+ def self.rails_root = @rails_root ||= (Rails.root.to_s.presence || Dir.pwd).freeze
110
+ def rails_root = self.class.rails_root
121
111
 
122
112
  private
123
113
 
124
- def true?(value)
125
- [true, 'true', 1, '1'].include?(value)
126
- end
127
-
128
114
  def detect_project_name
129
115
  return ENV['RAILS_SPOTLIGHT_PROJECT'] if ENV['RAILS_SPOTLIGHT_PROJECT'].present?
130
116
 
131
- if app_class.respond_to?(:module_parent_name)
132
- app_class.module_parent_name
133
- else
134
- app_class.parent_name
135
- end
117
+ app_class.respond_to?(:module_parent_name) ? app_class.module_parent_name : app_class.parent_name
136
118
  end
137
119
 
138
- def app_class
139
- @app_class ||= Rails.application.class
140
- end
120
+ def app_class = @app_class ||= Rails.application.class
121
+ def true?(value) = [true, 'true', 1, '1'].include?(value)
122
+ def bool_val(key, opts, default: false) = opts[key].nil? ? default : true?(opts[key])
141
123
  end
142
124
  end
@@ -15,6 +15,8 @@ module RailsSpotlight
15
15
  def initialize(name, start, ending, transaction_id, payload)
16
16
  super(name, start, ending, transaction_id, json_encodable(payload))
17
17
  @duration = 1000.0 * (ending - start)
18
+ rescue # rubocop:disable Lint/RedundantCopDisableDirective, Style/RescueStandardError
19
+ @duration = 0
18
20
  end
19
21
 
20
22
  def self.events_for_exception(exception_wrapper)
@@ -28,7 +30,7 @@ module RailsSpotlight
28
30
  end
29
31
  trace.unshift "#{exception.class} (#{exception.message})"
30
32
  trace.map do |call|
31
- Event.new('process_action.action_controller.exception', 0, 0, nil, call: call)
33
+ Event.new('process_action.action_controller.exception', 0, 0, nil, call:)
32
34
  end
33
35
  end
34
36
 
@@ -42,7 +44,7 @@ module RailsSpotlight
42
44
  value = value.to_h.select { |k, _| k.upcase == k }
43
45
  elsif value.is_a?(Array) && defined?(ActiveRecord::Relation::QueryAttribute) && value.first.is_a?(ActiveRecord::Relation::QueryAttribute)
44
46
  value = value.map(&method(:map_relation_query_attribute))
45
- elsif not_encodable?(value)
47
+ elsif !value.respond_to?(:to_json) || not_encodable?(value)
46
48
  value = NOT_JSON_ENCODABLE
47
49
  end
48
50
 
@@ -54,6 +56,8 @@ module RailsSpotlight
54
56
  end
55
57
  hash[key] = new_value # encode_value(value)
56
58
  end.with_indifferent_access
59
+ rescue # rubocop:disable Lint/RedundantCopDisableDirective, Style/RescueStandardError
60
+ {}
57
61
  end
58
62
 
59
63
  # ActiveRecord::Relation::QueryAttribute implementation changed in Rails 7.1 it getting binds need to be manually added
@@ -71,13 +75,25 @@ module RailsSpotlight
71
75
 
72
76
  def not_encodable?(value)
73
77
  ::RailsSpotlight.config.not_encodable_event_values.any? do |module_name, class_names|
74
- next unless defined?(module_name.constantize)
78
+ next unless safe_constantize(module_name)
75
79
 
76
- class_names.any? { |class_name| value.is_a?(class_name.constantize) }
77
- rescue # rubocop:disable Lint/SuppressedException, Style/RescueStandardError
80
+ class_names.any? do |class_name|
81
+ klass = safe_constantize(class_name)
82
+ next false unless klass
83
+
84
+ value.is_a?(klass)
85
+ end
86
+ rescue # rubocop:disable Style/RescueStandardError
87
+ true
78
88
  end
79
89
  end
80
90
 
91
+ def safe_constantize(name)
92
+ name.constantize
93
+ rescue NameError
94
+ nil
95
+ end
96
+
81
97
  def transform_hash(original, options = {}, &block)
82
98
  options[:safe_descent] ||= {}.compare_by_identity
83
99
 
@@ -4,95 +4,72 @@ require_relative 'channels'
4
4
 
5
5
  module RailsSpotlight
6
6
  module LogInterceptor
7
- SEVERITY = %w[debug info warn error fatal unknown].freeze
8
7
  SEVERITY_MAP = { 0 => 'debug', 1 => 'info', 2 => 'warn', 3 => 'error', 4 => 'fatal', 5 => 'unknown' }.freeze
9
8
 
10
- def add(severity, message = nil, progname = nil) # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
11
- severity ||= 5
12
- return true if @logdev.nil? || severity < level
13
-
14
- progname = @progname if progname.nil?
15
-
16
- if message.nil?
17
- if block_given?
18
- message = yield
19
- else
20
- message = progname
21
- progname = @progname
22
- end
23
- end
24
- return true if _skip_logging?(message)
25
-
26
- _rails_spotlight_log(SEVERITY_MAP[severity], message, progname, :broadcast)
27
- super(severity, message, progname) if defined?(super)
28
- true
29
- end
30
-
31
-
32
- def debug(message = nil, *args, &block)
33
- _rails_spotlight_log(:debug, message, nil, :event, &block)
9
+ def debug(message = nil, *args, &)
10
+ _rails_spotlight_log(:debug, message, nil, &)
34
11
  super
35
12
  end
36
13
 
37
- def info(message = nil, *args, &block)
38
- _rails_spotlight_log(:info, message, nil, :event, &block)
14
+ def info(message = nil, *args, &)
15
+ _rails_spotlight_log(:info, message, nil, &)
39
16
  super
40
17
  end
41
18
 
42
- def warn(message = nil, *args, &block)
43
- _rails_spotlight_log(:warn, message, nil, :event, &block)
19
+ def warn(message = nil, *args, &)
20
+ _rails_spotlight_log(:warn, message, nil, &)
44
21
  super
45
22
  end
46
23
 
47
- def error(message = nil, *args, &block)
48
- _rails_spotlight_log(:error, message, nil, :event, &block)
24
+ def error(message = nil, *args, &)
25
+ _rails_spotlight_log(:error, message, nil, &)
49
26
  super
50
27
  end
51
28
 
52
- def fatal(message = nil, *args, &block)
53
- _rails_spotlight_log(:fatal, message, nil, :event, &block)
29
+ def fatal(message = nil, *args, &)
30
+ _rails_spotlight_log(:fatal, message, nil, &)
54
31
  super
55
32
  end
56
33
 
57
- def unknown(message = nil, *args, &block)
58
- _rails_spotlight_log(:unknown, message, nil, :event, &block)
34
+ def unknown(message = nil, *args, &)
35
+ _rails_spotlight_log(:unknown, message, nil, &)
59
36
  super
60
37
  end
61
38
 
62
39
  private
63
40
 
64
41
  def _skip_logging?(message)
65
- return false unless ::RailsSpotlight.config.use_action_cable?
42
+ return false unless ::RailsSpotlight.config.use_cable?
66
43
  return false unless message.is_a?(String)
67
44
 
68
45
  message.include?(::RailsSpotlight::Channels::SPOTLIGHT_CHANNEL)
69
46
  end
70
47
 
71
- def _rails_spotlight_log(level, message, progname = nil, output = :event)
72
- callsite = Utils.dev_callsite(caller.drop(1))
73
- name = progname.is_a?(String) || progname.is_a?(Symbol) ? progname : nil
74
- message = yield if output == :event && message.nil? && block_given?
75
- output == :event ? _push_event(level, message, name, callsite) : _broadcast_log(message, level, callsite, name)
76
- rescue StandardError => e
77
- RailsSpotlight.config.logger.fatal("#{e.message}\n #{e.backtrace.join("\n ")}")
78
- end
48
+ def _rails_spotlight_log(severity, message, progname = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
49
+ return if message.nil? && !block_given?
79
50
 
80
- def _push_event(level, message, progname = nil, callsite = {})
81
- name = progname.is_a?(String) || progname.is_a?(Symbol) ? progname : nil
82
- AppRequest.current.events << Event.new('rsl.notification.log', 0, 0, 0, (callsite || {}).merge(message: message, level: level, progname: name)) if AppRequest.current
83
- rescue StandardError => e
84
- RailsSpotlight.config.logger.fatal("#{e.message}\n #{e.backtrace.join("\n ")}")
85
- end
51
+ severity ||= :unknown
52
+ level = SEVERITY_MAP[severity.to_s]
53
+
54
+ if message.nil?
55
+ if block_given?
56
+ message = yield
57
+ else
58
+ message = progname
59
+ progname = @progname
60
+ end
61
+ end
62
+
63
+ return if _skip_logging?(message)
86
64
 
87
- def _broadcast_log(message, level, callsite = {}, name = nil)
88
- return unless ::RailsSpotlight.config.use_action_cable?
89
- return if message.blank?
65
+ callsite = Utils.dev_callsite(caller.drop(1))
66
+ name = progname.is_a?(String) || progname.is_a?(Symbol) ? progname : nil
67
+ message = yield if message.nil? && block_given?
90
68
 
91
- id = AppRequest.current ? AppRequest.current.id : nil # rubocop:disable Style/SafeNavigation
92
- payload = (callsite || {}).merge(msg: message, src: ENV['RS_SRC'] || ::RailsSpotlight.config.default_rs_src, l: level, dt: Time.now.to_f, id: id, pg: name)
93
- ::RailsSpotlight::Channels::SpotlightChannel.broadcast(type: 'logs', payload: payload)
69
+ AppRequest.current.events << Event.new('rsl.notification.log', 0, 0, 0, (callsite || {}).merge(message:, level: severity, progname: name)) if AppRequest.current
70
+ ::RailsSpotlight::Channels::SpotlightChannel.broadcast_log(message, level, callsite, name)
94
71
  rescue StandardError => e
95
- RailsSpotlight.config.logger.fatal("#{e.message}\n #{e.backtrace.join("\n ")}")
72
+ RailsSpotlight.config.logger.fatal("#{e.message}\n #{e.backtrace&.join("\n ")}")
96
73
  end
97
74
  end
98
75
  end
@@ -4,10 +4,25 @@ module RailsSpotlight
4
4
  module Middlewares
5
5
  module Handlers
6
6
  class BaseActionHandler
7
- IncorrectResponseContentType = Class.new(StandardError)
8
- NotFound = Class.new(StandardError)
9
- UnprocessableEntity = Class.new(StandardError)
10
- Forbidden = Class.new(StandardError)
7
+ BaseError = Class.new(StandardError) do
8
+ attr_reader :code, :status
9
+
10
+ def initialize(message = nil, code: nil)
11
+ super(message)
12
+ @code = code
13
+ @status = case self.class.name.demodulize.underscore
14
+ when 'unsupported_media_type' then 415
15
+ when 'not_found' then 404
16
+ when 'unprocessable_entity' then 422
17
+ when 'forbidden' then 403
18
+ else 500
19
+ end
20
+ end
21
+ end
22
+ UnsupportedMediaType = Class.new(BaseError)
23
+ NotFound = Class.new(BaseError)
24
+ UnprocessableEntity = Class.new(BaseError)
25
+ Forbidden = Class.new(BaseError)
11
26
 
12
27
  def initialize(request_id, request, content_type)
13
28
  @request_id = request_id
@@ -18,45 +33,34 @@ module RailsSpotlight
18
33
  attr_reader :request_id, :request, :content_type
19
34
 
20
35
  def call
36
+ validate_data_access! if data_access_token
21
37
  validate_project! unless skip_project_validation?
22
38
  execute
23
39
  response
24
- rescue NotFound => e
25
- not_found_response(e.message)
26
- rescue UnprocessableEntity => e
27
- unprocessed_response(e.message)
28
- rescue Forbidden => e
29
- forbidden_response(e.message)
40
+ rescue BaseError => e
41
+ error_response(e)
30
42
  rescue => e # rubocop:disable Style/RescueStandardError
31
- internal_server_error_response(e.message)
43
+ error_response(BaseError.new(e.message))
32
44
  end
33
45
 
34
46
  protected
35
47
 
36
48
  attr_writer :status, :headers
37
49
 
38
- def skip_project_validation?
39
- false
40
- end
41
-
42
- def headers
43
- @headers ||= {}
44
- end
45
-
46
- def status
47
- @status ||= 200
48
- end
50
+ def skip_project_validation? = false
51
+ def headers = @headers ||= {}
52
+ def status = @status ||= 200
49
53
 
50
54
  def execute
51
55
  raise 'Not implemented yet'
52
56
  end
53
57
 
54
58
  def json_response_body
55
- raise IncorrectResponseContentType, content_type
59
+ raise UnsupportedMediaType.new(content_type, code: :unsupported_media_type_json)
56
60
  end
57
61
 
58
62
  def text_response_body
59
- raise IncorrectResponseContentType, content_type
63
+ raise UnsupportedMediaType.new(content_type, code: :unsupported_media_type_text)
60
64
  end
61
65
 
62
66
  def json_request_body
@@ -65,29 +69,9 @@ module RailsSpotlight
65
69
  raise 'Invalid JSON'
66
70
  end
67
71
 
68
- def body_fetch(*args)
69
- json_request_body.fetch(*args)
70
- end
71
-
72
- def internal_server_error_response(message)
73
- response(500, message_to_body(message))
74
- end
75
-
76
- def unprocessed_response(message)
77
- response(422, message_to_body(message))
78
- end
79
-
80
- def forbidden_response(message)
81
- response(403, message_to_body(message))
82
- end
83
-
84
- def not_found_response(message)
85
- response(404, message_to_body(message))
86
- end
87
-
88
- def message_to_body(message)
89
- content_type == :json ? { message: message } : message
90
- end
72
+ def body_fetch(*args) = json_request_body.fetch(*args)
73
+ def error_response(error) = response(error.status || 500, message_to_body(error.message, code: error.code, status: error.status || 500))
74
+ def message_to_body(message, code: :none, status: 500) = content_type == :json ? { message:, code:, status: } : message
91
75
 
92
76
  def response_headers(headers = {})
93
77
  {
@@ -107,19 +91,25 @@ module RailsSpotlight
107
91
  [overridden_status.present? ? overridden_status : status, response_headers(headers), [body]]
108
92
  end
109
93
 
110
- def request_spotlight_version
111
- @request_spotlight_version ||= request.get_header('HTTP_X_RAILS_SPOTLIGHT')
112
- end
113
-
114
- def request_for_projects
115
- @request_for_projects ||= (request.get_header('HTTP_X_FOR_PROJECTS') || '').split(',').map(&:strip)
116
- end
94
+ def request_spotlight_version = @request_spotlight_version ||= request.get_header('HTTP_X_RAILS_SPOTLIGHT')
95
+ def request_for_projects = @request_for_projects ||= (request.get_header('HTTP_X_FOR_PROJECTS') || '').split(',').map(&:strip)
117
96
 
118
97
  def validate_project!
119
98
  return if request_for_projects.blank?
120
99
  return if request_for_projects.include?(::RailsSpotlight.config.project_name)
121
100
 
122
- raise Forbidden, "Check your settings the current request is not allowed to be executed on the #{::RailsSpotlight.config.project_name} project"
101
+ raise Forbidden.new(
102
+ "Check your settings the current request is not allowed to be executed on the #{::RailsSpotlight.config.project_name} project",
103
+ code: :project_mismatch
104
+ )
105
+ end
106
+
107
+ def data_access_token = ::RailsSpotlight.config.data_access_token
108
+
109
+ def validate_data_access!
110
+ return if request.get_header('HTTP_X_DATA_ACCESS_TOKEN') == data_access_token
111
+
112
+ raise Forbidden.new('Invalid data access token', code: :invalid_data_access_token)
123
113
  end
124
114
  end
125
115
  end
@@ -7,12 +7,11 @@ module RailsSpotlight
7
7
  module Middlewares
8
8
  module Handlers
9
9
  class CodeAnalysisActionHandler < BaseActionHandler
10
- def skip_project_validation?
11
- true
12
- end
10
+ def skip_project_validation? = true
13
11
 
14
12
  def execute
15
- raise UnprocessableEntity, 'Please add rubocop to your project' unless rubocop_installed?
13
+ raise Forbidden.new('Code analysis is disabled', code: :disabled_rubocop_settings) unless enabled?
14
+ raise UnprocessableEntity.new('Please add rubocop to your project', code: :rubocop_not_installed) unless rubocop_installed?
16
15
  end
17
16
 
18
17
  private
@@ -64,8 +63,8 @@ module RailsSpotlight
64
63
 
65
64
  {
66
65
  source: variant == :autofix ? corrected_source : source,
67
- analysis_result: analysis_result,
68
- variant: variant
66
+ analysis_result:,
67
+ variant:
69
68
  }
70
69
  ensure
71
70
  # Close and unlink the tempfile
@@ -76,13 +75,9 @@ module RailsSpotlight
76
75
  end
77
76
  end
78
77
 
79
- def source
80
- @source ||= body_fetch('source')
81
- end
82
-
83
- def variant
84
- @variant ||= body_fetch('variant', 'check').to_sym
85
- end
78
+ def source = @source ||= body_fetch('source')
79
+ def variant = @variant ||= body_fetch('variant', 'check').to_sym
80
+ def enabled? = ::RailsSpotlight.config.rubocop_enabled?
86
81
  end
87
82
  end
88
83
  end
@@ -6,29 +6,25 @@ module RailsSpotlight
6
6
  module Handlers
7
7
  class ConsoleActionHandler < BaseActionHandler
8
8
  def execute
9
+ raise Forbidden.new('Console is disabled', code: :disabled_irb_console_settings) unless enabled?
10
+
9
11
  RailsSpotlight.config.logger && RailsSpotlight.config.logger.info("Executing command: #{command}") # rubocop:disable Style/SafeNavigation
10
12
  executor.execute(command)
11
13
  end
12
14
 
13
15
  private
14
16
 
15
- def executor
16
- @executor ||= ::RailsSpotlight::RailsCommandExecutor.new
17
- end
18
-
19
- def inspect_types
20
- @inspect_types ||= body_fetch('inspect_types')
21
- end
22
-
23
- def command
24
- @command ||= body_fetch('command')
25
- end
17
+ def executor = @executor ||= ::RailsSpotlight::RailsCommandExecutor.new
18
+ def inspect_types = @inspect_types ||= body_fetch('inspect_types')
19
+ def command = @command ||= body_fetch('command')
26
20
 
27
21
  def json_response_body
28
22
  return executor.result_as_json unless executor.execution_successful?
29
23
 
30
- { result: executor.result_as_json(inspect_types: inspect_types) }
24
+ { result: executor.result_as_json(inspect_types:) }
31
25
  end
26
+
27
+ def enabled? = ::RailsSpotlight.config.irb_console_enabled?
32
28
  end
33
29
  end
34
30
  end
@@ -9,9 +9,13 @@ module RailsSpotlight
9
9
  module Handlers
10
10
  class DirectoryIndexActionHandler < BaseActionHandler
11
11
  def execute
12
- @result = directory_to_json(::RailsSpotlight.config.rails_root)
13
- rescue => e # rubocop:disable Style/RescueStandardError
14
- raise UnprocessableEntity, e.message
12
+ raise Forbidden.new('File manager is disabled', code: :disabled_file_manager_settings) unless enabled?
13
+
14
+ @result = begin
15
+ directory_to_json(::RailsSpotlight.config.rails_root)
16
+ rescue => e # rubocop:disable Style/RescueStandardError
17
+ raise UnprocessableEntity.new(e.message, code: :directory_index_error)
18
+ end
15
19
  end
16
20
 
17
21
  private
@@ -55,29 +59,12 @@ module RailsSpotlight
55
59
  end
56
60
  end
57
61
 
58
- def ignore
59
- @ignore ||= body_fetch('ignore', [])
60
- end
61
-
62
- def sort_folders_first
63
- @sort_folders_first ||= body_fetch('sort_folders_first', true)
64
- end
65
-
66
- def omnit_gitignore
67
- @omnit_gitignore ||= body_fetch('omnit_gitignore', false)
68
- end
69
-
70
- def ignore_patterns
71
- @ignore_patterns ||= ignore + ::RailsSpotlight.config.directory_index_ignore + (omnit_gitignore ? [] : gitignore_patterns)
72
- end
73
-
74
- def gitignore_file
75
- @gitignore_file ||= File.join(::RailsSpotlight.config.rails_root, '.gitignore')
76
- end
77
-
78
- def show_empty_directories
79
- @show_empty_directories ||= body_fetch('show_empty_directories', false)
80
- end
62
+ def ignore = @ignore ||= body_fetch('ignore', [])
63
+ def sort_folders_first = @sort_folders_first ||= body_fetch('sort_folders_first', true)
64
+ def omnit_gitignore = @omnit_gitignore ||= body_fetch('omnit_gitignore', false)
65
+ def ignore_patterns = @ignore_patterns ||= ignore + ::RailsSpotlight.config.directory_index_ignore + (omnit_gitignore ? [] : gitignore_patterns)
66
+ def gitignore_file = @gitignore_file ||= File.join(::RailsSpotlight.config.rails_root, '.gitignore')
67
+ def show_empty_directories = @show_empty_directories ||= body_fetch('show_empty_directories', false)
81
68
 
82
69
  def gitignore_patterns
83
70
  @gitignore_patterns ||= if File.exist?(gitignore_file)
@@ -103,9 +90,11 @@ module RailsSpotlight
103
90
  def json_response_body
104
91
  {
105
92
  root_path: ::RailsSpotlight.config.rails_root,
106
- result: result
93
+ result:
107
94
  }
108
95
  end
96
+
97
+ def enabled? = ::RailsSpotlight.config.file_manager_enabled
109
98
  end
110
99
  end
111
100
  end