rails_nexus 2.1.0 → 2.1.2

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/CHANGELOG.md +110 -0
  3. data/README.md +33 -6
  4. data/SECURITY.md +11 -0
  5. data/THIRD_PARTY_NOTICES.md +14 -0
  6. data/app/assets/config/rails_nexus_manifest.js +1 -1
  7. data/app/assets/javascripts/rails_nexus/application.js +55 -0
  8. data/app/controllers/rails_nexus/analytics_controller.rb +13 -26
  9. data/app/controllers/rails_nexus/application_controller.rb +6 -2
  10. data/app/controllers/rails_nexus/backup_controller.rb +1 -2
  11. data/app/controllers/rails_nexus/cron_jobs_controller.rb +1 -9
  12. data/app/controllers/rails_nexus/database_health_controller.rb +0 -10
  13. data/app/controllers/rails_nexus/logged_exceptions_controller.rb +0 -2
  14. data/app/controllers/rails_nexus/n1_patterns_controller.rb +0 -2
  15. data/app/controllers/rails_nexus/settings_controller.rb +1 -39
  16. data/app/controllers/rails_nexus/source_code_controller.rb +9 -13
  17. data/app/controllers/rails_nexus/stats_controller.rb +43 -43
  18. data/app/helpers/rails_nexus/logged_exceptions_helper.rb +4 -49
  19. data/app/helpers/rails_nexus/source_code_helper.rb +49 -10
  20. data/app/javascript/rails_nexus/application.js +15 -3
  21. data/app/models/rails_nexus/backup_config.rb +31 -0
  22. data/app/models/rails_nexus/logged_exception.rb +27 -13
  23. data/app/services/rails_nexus/backup_service.rb +233 -98
  24. data/app/views/layouts/rails_nexus/application.html.erb +1 -1
  25. data/app/views/rails_nexus/stats/index.html.erb +1 -1
  26. data/lib/generators/rails_nexus/install_generator.rb +1 -1
  27. data/lib/generators/rails_nexus/templates/INSTALL.md +1 -0
  28. data/lib/generators/rails_nexus/templates/rails_nexus.rb +3 -0
  29. data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +1 -1
  30. data/lib/rails_nexus/configuration.rb +10 -0
  31. data/lib/rails_nexus/database_adapter.rb +53 -0
  32. data/lib/rails_nexus/engine.rb +0 -13
  33. data/lib/rails_nexus/logger.rb +1 -12
  34. data/lib/rails_nexus/notifications.rb +27 -20
  35. data/lib/rails_nexus/version.rb +1 -1
  36. data/lib/rails_nexus/webhook_client.rb +151 -0
  37. data/lib/rails_nexus.rb +37 -23
  38. data/rails_nexus.gemspec +3 -2
  39. metadata +8 -16
  40. data/config/importmap.rb +0 -15
@@ -9,7 +9,7 @@
9
9
 
10
10
  <%= stylesheet_link_tag "rails_nexus/application", "data-turbo-track": "reload" %>
11
11
 
12
- <%= javascript_importmap_tags "rails_nexus/application" %>
12
+ <%= javascript_include_tag "rails_nexus/application", defer: true, "data-turbo-track": "reload" %>
13
13
 
14
14
  <% if respond_to?(:turbo_refreshes_with) %>
15
15
  <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
@@ -339,7 +339,7 @@
339
339
  </td>
340
340
  <td>
341
341
  <span class="text-sm" style="color: var(--rn-text-muted)">
342
- <%= table[:size_mb] > 0 ? "#{table[:size_mb]} MB" : "—" %>
342
+ <%= table[:size_mb].to_f > 0 ? "#{table[:size_mb]} MB" : "—" %>
343
343
  </span>
344
344
  </td>
345
345
  <td>
@@ -35,7 +35,7 @@ module RailsNexus
35
35
  content = File.read(destination_controller)
36
36
  unless content.include?("log_exception_handler")
37
37
  inject_into_class app_controller, "ApplicationController" do
38
- "\n rescue_from Exception, with: :log_exception_handler\n"
38
+ "\n rescue_from StandardError, with: :log_exception_handler\n"
39
39
  end
40
40
  end
41
41
  end
@@ -7,3 +7,4 @@ Next steps:
7
7
  3. Visit `/rails_nexus` in the host application.
8
8
 
9
9
  The dashboard denies access until authentication is configured.
10
+ The generator captures `StandardError` application failures and deliberately leaves fatal process and VM exceptions to Rails and Ruby.
@@ -34,6 +34,9 @@ Rails.application.config.to_prepare do
34
34
  # config.webhooks = ["https://hooks.slack.com/services/xxx"]
35
35
  # config.webhook_timeout = 5
36
36
  # config.webhook_headers = { "Authorization" => "Bearer token" }
37
+ # config.webhook_allowed_hosts = ["hooks.example.com", "*.example.org"]
38
+ # config.webhook_denied_hosts = ["blocked.example.org"]
39
+ # config.webhook_allow_http_in_development = false
37
40
 
38
41
  # ─── Cleanup ─────────────────────────────────────────────
39
42
  # Auto-delete exceptions older than N days
@@ -7,7 +7,7 @@
7
7
  <title><%= content_for?(:title) ? yield(:title) : RailsNexus.configuration.application_name || "RailsNexus" %></title>
8
8
 
9
9
  <%= stylesheet_link_tag "rails_nexus/application", "data-turbo-track": "reload" %>
10
- <%= javascript_importmap_tags "rails_nexus/application" %>
10
+ <%= javascript_include_tag "rails_nexus/application", defer: true, "data-turbo-track": "reload" %>
11
11
 
12
12
  <% if respond_to?(:turbo_refreshes_with) %>
13
13
  <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
@@ -49,6 +49,13 @@ module RailsNexus
49
49
  # config.webhook_headers = { "Authorization" => "Bearer token123" }
50
50
  attr_accessor :webhook_headers
51
51
 
52
+ # Host allow/deny policies. Wildcards such as "*.example.com" are accepted.
53
+ attr_accessor :webhook_allowed_hosts
54
+ attr_accessor :webhook_denied_hosts
55
+
56
+ # HTTP remains disabled except when this is true and Rails.env is development.
57
+ attr_accessor :webhook_allow_http_in_development
58
+
52
59
  # ─── Logging ───────────────────────────────────────────────
53
60
 
54
61
  # Enable structured logging (default: true).
@@ -209,6 +216,9 @@ module RailsNexus
209
216
  @webhooks = []
210
217
  @webhook_timeout = 5
211
218
  @webhook_headers = {}
219
+ @webhook_allowed_hosts = []
220
+ @webhook_denied_hosts = []
221
+ @webhook_allow_http_in_development = false
212
222
 
213
223
  # Logging
214
224
  @logging_enabled = true
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ module DatabaseAdapter
5
+ module_function
6
+
7
+ def family(connection = ActiveRecord::Base.connection)
8
+ name = connection.adapter_name.to_s.downcase
9
+ return :postgresql if name.include?("postgres")
10
+ return :mysql if name.include?("mysql") || name.include?("trilogy")
11
+ return :sqlite if name.include?("sqlite")
12
+
13
+ :unsupported
14
+ end
15
+
16
+ def time_bucket_expression(column: "created_at", period: :hour, connection: ActiveRecord::Base.connection)
17
+ column = connection.quote_column_name(column)
18
+
19
+ case [family(connection), period.to_sym]
20
+ when [:postgresql, :hour] then "TO_CHAR(DATE_TRUNC('hour', #{column}), 'YYYY-MM-DD HH24:00')"
21
+ when [:mysql, :hour] then "DATE_FORMAT(#{column}, '%Y-%m-%d %H:00')"
22
+ when [:sqlite, :hour] then "strftime('%Y-%m-%d %H:00', #{column})"
23
+ else
24
+ raise NotImplementedError, "Time grouping is not supported by this database adapter"
25
+ end
26
+ end
27
+
28
+ def time_component_expression(column: "created_at", component:, connection: ActiveRecord::Base.connection)
29
+ column = connection.quote_column_name(column)
30
+
31
+ case [family(connection), component.to_sym]
32
+ when [:postgresql, :hour] then "CAST(EXTRACT(HOUR FROM #{column}) AS INTEGER)"
33
+ when [:mysql, :hour] then "HOUR(#{column})"
34
+ when [:sqlite, :hour] then "CAST(strftime('%H', #{column}) AS INTEGER)"
35
+ when [:postgresql, :weekday] then "CAST(EXTRACT(DOW FROM #{column}) AS INTEGER)"
36
+ when [:mysql, :weekday] then "DAYOFWEEK(#{column}) - 1"
37
+ when [:sqlite, :weekday] then "CAST(strftime('%w', #{column}) AS INTEGER)"
38
+ else
39
+ raise NotImplementedError, "Time component grouping is not supported by this database adapter"
40
+ end
41
+ end
42
+
43
+ def database_version(connection = ActiveRecord::Base.connection)
44
+ case family(connection)
45
+ when :sqlite then connection.select_value("SELECT sqlite_version()")
46
+ when :postgresql, :mysql then connection.select_value("SELECT version()")
47
+ else "N/A"
48
+ end
49
+ rescue ActiveRecord::StatementInvalid
50
+ "N/A"
51
+ end
52
+ end
53
+ end
@@ -11,8 +11,6 @@ module RailsNexus
11
11
  initializer "rails_nexus.assets", before: "propshaft.assets_middleware", group: :all do |app|
12
12
  next unless app.config.respond_to?(:assets)
13
13
 
14
- app.config.assets.paths << root.join("app/javascript")
15
-
16
14
  # Sprockets 4 only compiles assets listed in a manifest or in the
17
15
  # precompile list. Register these here so mounting applications do not
18
16
  # need to modify their own asset manifest.
@@ -20,21 +18,10 @@ module RailsNexus
20
18
  app.config.assets.precompile += %w[
21
19
  rails_nexus/application.css
22
20
  rails_nexus/application.js
23
- controllers/rails_nexus_controller.js
24
- controllers/rails_nexus_detail_controller.js
25
- controllers/rails_nexus_sidebar_controller.js
26
- controllers/rails_nexus_theme_controller.js
27
21
  ]
28
22
  end
29
23
  end
30
24
 
31
- # ─── Importmap support (for importmap-rails users) ────────────
32
-
33
- initializer "rails_nexus.importmap", before: "importmap" do |app|
34
- app.config.importmap.paths << root.join("config/importmap.rb")
35
- app.config.importmap.cache_sweepers << root.join("app/javascript")
36
- end
37
-
38
25
  # ─── Database connection (separate DB support) ─────────────────
39
26
 
40
27
  initializer "rails_nexus.database" do
@@ -170,18 +170,7 @@ module RailsNexus
170
170
  end
171
171
 
172
172
  def filter_params(params)
173
- return {} unless params.is_a?(Hash)
174
-
175
- rails_filters = Rails.application.config.filter_parameters
176
- params.each_with_object({}) do |(key, value), result|
177
- if rails_filters.any? { |f| key.to_s.include?(f.to_s) }
178
- result[key] = "[FILTERED]"
179
- elsif value.is_a?(Hash)
180
- result[key] = filter_params(value)
181
- else
182
- result[key] = value
183
- end
184
- end
173
+ RailsNexus.filter_sensitive_data(params)
185
174
  end
186
175
  end
187
176
  end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "net/http"
4
- require "uri"
5
3
  require "json"
4
+ require "rails_nexus/webhook_client"
6
5
 
7
6
  module RailsNexus
8
7
  module Notifications
@@ -28,28 +27,36 @@ module RailsNexus
28
27
  config.webhooks.each do |url|
29
28
  deliver_webhook(url, payload, config)
30
29
  rescue StandardError => e
31
- Rails.logger.error("[RailsNexus] Webhook failed for #{url}: #{e.message}")
32
- log_webhook_delivery(url: url, payload: payload, error: e.message, event_type: "exception.logged")
30
+ Rails.logger.error("[RailsNexus] Webhook delivery failed: #{e.class}")
31
+ log_webhook_delivery(
32
+ url: RailsNexus::WebhookClient.redacted_url(url),
33
+ payload: payload,
34
+ error: "Webhook delivery failed",
35
+ event_type: "exception.logged"
36
+ )
33
37
  end
34
38
  end
35
39
 
36
40
  def deliver_webhook(url, payload, config)
37
- uri = URI(url)
38
-
39
- http = Net::HTTP.new(uri.host, uri.port)
40
- http.use_ssl = uri.scheme == "https"
41
- http.open_timeout = config.webhook_timeout
42
- http.read_timeout = config.webhook_timeout
43
-
44
- request = Net::HTTP::Post.new(uri)
45
- request["Content-Type"] = "application/json"
46
- config.webhook_headers.each { |k, v| request[k] = v }
47
- request.body = payload.to_json
48
-
49
- response = http.request(request)
50
-
51
- unless response.is_a?(Net::HTTPSuccess)
52
- Rails.logger.error("[RailsNexus] Webhook #{url} returned #{response.code}")
41
+ logged_url = RailsNexus::WebhookClient.redacted_url(url)
42
+ result = RailsNexus::WebhookClient.deliver(
43
+ url: url,
44
+ payload: payload,
45
+ headers: config.webhook_headers,
46
+ timeout: config.webhook_timeout
47
+ )
48
+
49
+ if result[:success]
50
+ log_webhook_delivery(
51
+ url: logged_url,
52
+ payload: payload,
53
+ status_code: result[:status_code],
54
+ response_body: result[:body],
55
+ event_type: "exception.logged"
56
+ )
57
+ else
58
+ Rails.logger.error("[RailsNexus] Webhook delivery failed: #{result[:error]}")
59
+ log_webhook_delivery(url: logged_url, payload: payload, error: result[:error], event_type: "exception.logged")
53
60
  end
54
61
  end
55
62
 
@@ -1,3 +1,3 @@
1
1
  module RailsNexus
2
- VERSION = "2.1.0"
2
+ VERSION = "2.1.2"
3
3
  end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require "json"
5
+ require "net/http"
6
+ require "openssl"
7
+ require "socket"
8
+ require "uri"
9
+
10
+ module RailsNexus
11
+ class WebhookClient
12
+ class ValidationError < StandardError; end
13
+
14
+ BLOCKED_NETWORKS = %w[
15
+ 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16
16
+ 172.16.0.0/12 192.0.0.0/24 192.0.2.0/24 192.168.0.0/16
17
+ 198.18.0.0/15 198.51.100.0/24 203.0.113.0/24 224.0.0.0/4 240.0.0.0/4
18
+ ::/128 ::1/128 ::ffff:0:0/96 100::/64 2001:db8::/32 fc00::/7 fe80::/10 ff00::/8
19
+ ].map { |network| IPAddr.new(network) }.freeze
20
+
21
+ def self.deliver(url:, payload:, headers: {}, timeout: nil)
22
+ new(url: url, payload: payload, headers: headers, timeout: timeout).deliver
23
+ end
24
+
25
+ def self.redacted_url(url)
26
+ uri = URI.parse(url.to_s)
27
+ return "invalid-webhook-url" if uri.host.blank?
28
+
29
+ port = uri.port && uri.port != uri.default_port ? ":#{uri.port}" : ""
30
+ "#{uri.scheme}://#{uri.host}#{port}/[REDACTED]"
31
+ rescue URI::InvalidURIError
32
+ "invalid-webhook-url"
33
+ end
34
+
35
+ def initialize(url:, payload:, headers: {}, timeout: nil, resolver: nil, http_factory: nil)
36
+ @url = url.to_s
37
+ @payload = payload
38
+ @headers = headers || {}
39
+ @timeout = Integer(timeout || RailsNexus.configuration.webhook_timeout || 5)
40
+ @resolver = resolver || method(:resolve_addresses)
41
+ @http_factory = http_factory || ->(host, port) { Net::HTTP.new(host, port) }
42
+ end
43
+
44
+ def deliver
45
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
46
+ uri, address = validated_destination
47
+ http = build_http(uri, address)
48
+ response = http.request(build_request(uri))
49
+ status = response.code.to_i
50
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
51
+
52
+ if status.between?(300, 399)
53
+ return failure("Webhook redirects are not allowed", status_code: response.code, duration: duration)
54
+ end
55
+
56
+ {
57
+ success: status.between?(200, 299),
58
+ status_code: response.code,
59
+ body: response.body.to_s.truncate(500),
60
+ duration: duration.round(3),
61
+ error: status.between?(200, 299) ? nil : "Webhook returned HTTP #{status}"
62
+ }
63
+ rescue ValidationError => error
64
+ failure(error.message)
65
+ rescue StandardError
66
+ failure("Webhook delivery failed")
67
+ end
68
+
69
+ private
70
+
71
+ def validated_destination
72
+ uri = URI.parse(@url)
73
+ allowed_schemes = ["https"]
74
+ if Rails.env.development? && RailsNexus.configuration.webhook_allow_http_in_development
75
+ allowed_schemes << "http"
76
+ end
77
+
78
+ raise ValidationError, "Webhook URL must use HTTPS" unless allowed_schemes.include?(uri.scheme)
79
+ raise ValidationError, "Webhook URL must include a hostname" if uri.host.blank?
80
+ raise ValidationError, "Webhook URL credentials are not allowed" if uri.userinfo.present?
81
+ validate_host_policy!(uri.host)
82
+
83
+ addresses = Array(@resolver.call(uri.host)).map { |address| IPAddr.new(address.to_s) }.uniq
84
+ raise ValidationError, "Webhook host could not be resolved" if addresses.empty?
85
+ raise ValidationError, "Webhook destination is not publicly routable" if addresses.any? { |address| blocked_address?(address) }
86
+
87
+ [uri, addresses.first.to_s]
88
+ rescue URI::InvalidURIError, IPAddr::InvalidAddressError
89
+ raise ValidationError, "Webhook URL is invalid"
90
+ rescue SocketError
91
+ raise ValidationError, "Webhook host could not be resolved"
92
+ end
93
+
94
+ def resolve_addresses(host)
95
+ Addrinfo.getaddrinfo(host, nil, :UNSPEC, :STREAM).map(&:ip_address)
96
+ end
97
+
98
+ def blocked_address?(address)
99
+ BLOCKED_NETWORKS.any? { |network| network.include?(address) }
100
+ end
101
+
102
+ def validate_host_policy!(host)
103
+ denied = Array(RailsNexus.configuration.webhook_denied_hosts)
104
+ allowed = Array(RailsNexus.configuration.webhook_allowed_hosts)
105
+
106
+ raise ValidationError, "Webhook hostname is denied" if denied.any? { |pattern| host_matches?(host, pattern) }
107
+ if allowed.any? && allowed.none? { |pattern| host_matches?(host, pattern) }
108
+ raise ValidationError, "Webhook hostname is not allowed"
109
+ end
110
+ end
111
+
112
+ def host_matches?(host, pattern)
113
+ candidate = pattern.to_s.downcase
114
+ actual = host.to_s.downcase
115
+ return actual.end_with?(candidate.delete_prefix("*")) if candidate.start_with?("*.")
116
+ actual == candidate
117
+ end
118
+
119
+ def build_http(uri, address)
120
+ http = @http_factory.call(uri.host, uri.port)
121
+ http.ipaddr = address
122
+ http.use_ssl = uri.scheme == "https"
123
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER if http.use_ssl?
124
+ http.open_timeout = @timeout
125
+ http.read_timeout = @timeout
126
+ http
127
+ end
128
+
129
+ def build_request(uri)
130
+ request = Net::HTTP::Post.new(uri.request_uri)
131
+ request["Host"] = uri.port == uri.default_port ? uri.host : "#{uri.host}:#{uri.port}"
132
+ request["Content-Type"] = "application/json"
133
+ @headers.each do |name, value|
134
+ validate_header!(name, value)
135
+ request[name.to_s] = value.to_s
136
+ end
137
+ request.body = @payload.to_json
138
+ request
139
+ end
140
+
141
+ def validate_header!(name, value)
142
+ if name.to_s.match?(/[\r\n]/) || value.to_s.match?(/[\r\n]/)
143
+ raise ValidationError, "Webhook header is invalid"
144
+ end
145
+ end
146
+
147
+ def failure(message, status_code: nil, duration: nil)
148
+ { success: false, error: message, status_code: status_code, duration: duration&.round(3) }
149
+ end
150
+ end
151
+ end
data/lib/rails_nexus.rb CHANGED
@@ -1,16 +1,22 @@
1
1
  require "rails"
2
- require "importmap-rails"
2
+ require "active_support/parameter_filter"
3
3
  require "turbo-rails"
4
4
  require "stimulus-rails"
5
5
  require "pagy"
6
6
  require "ipaddr"
7
7
  require "socket"
8
+ require "open3"
8
9
  require "rails_nexus/version"
9
10
  require "rails_nexus/configuration"
10
11
  require "rails_nexus/logger"
12
+ require "rails_nexus/database_adapter"
11
13
  require "rails_nexus/engine"
12
14
 
13
15
  module RailsNexus
16
+ DEFAULT_FILTER_PARAMETERS = %i[
17
+ password passw passwd secret token api_key apikey authorization cookie session
18
+ ].freeze
19
+
14
20
  class << self
15
21
  attr_writer :configuration
16
22
 
@@ -35,6 +41,24 @@ module RailsNexus
35
41
  def logger
36
42
  RailsNexus::Logger
37
43
  end
44
+
45
+ def parameter_filter
46
+ configured = defined?(::Rails) ? ::Rails.application.config.filter_parameters : []
47
+ ActiveSupport::ParameterFilter.new(DEFAULT_FILTER_PARAMETERS + Array(configured))
48
+ end
49
+
50
+ def filter_sensitive_data(value)
51
+ value = value.to_unsafe_h if value.respond_to?(:to_unsafe_h)
52
+
53
+ case value
54
+ when Hash
55
+ parameter_filter.filter(value)
56
+ when Array
57
+ value.map { |entry| filter_sensitive_data(entry) }
58
+ else
59
+ value
60
+ end
61
+ end
38
62
  end
39
63
 
40
64
  # Copyright (c) 2005 Jamis Buck
@@ -78,28 +102,19 @@ module RailsNexus
78
102
 
79
103
  # we log the exception and raise it again, for the normal handling.
80
104
  def log_exception_handler(exception)
81
- log_exception(exception)
105
+ log_exception(exception) if exception.is_a?(StandardError)
82
106
  raise exception
83
107
  end
84
108
 
85
109
  def rescue_action(exception)
86
110
  status = response_code_for_rescue(exception)
87
- log_exception(exception) if status != :not_found
111
+ log_exception(exception) if exception.is_a?(StandardError) && status != :not_found
88
112
  super
89
113
  end
90
114
 
91
- def filter_sub_parameters(params, rails_filter_parameters)
92
- params.each do |key, value|
93
- if(value.class != Hash and value.class != ActiveSupport::HashWithIndifferentAccess)
94
- params[key] = '[FILTERED]' if rails_filter_parameters.include?(key.to_sym)
95
- else
96
- params[key] = filter_sub_parameters(value, rails_filter_parameters)
97
- end
98
- end
99
- params
100
- end
101
-
102
115
  def log_exception(exception)
116
+ return unless exception.is_a?(StandardError)
117
+
103
118
  # Support both the legacy class_attribute and the new configuration DSL
104
119
  deliverer = if self.class.exception_data
105
120
  self.class.exception_data
@@ -113,15 +128,7 @@ module RailsNexus
113
128
  when Proc then deliverer.call(self)
114
129
  end
115
130
 
116
- rails_filter_parameters = defined?(::Rails) ? ::Rails.application.config.filter_parameters : []
117
-
118
- self.request.parameters.each do |key, value|
119
- if(value.class != Hash and value.class != ActiveSupport::HashWithIndifferentAccess)
120
- self.request.parameters[key] = '[FILTERED]' if rails_filter_parameters.include?(key.to_sym)
121
- else
122
- self.request.parameters[key] = filter_sub_parameters(value, rails_filter_parameters)
123
- end
124
- end if rails_filter_parameters.any?
131
+ data = RailsNexus.filter_sensitive_data(data || {})
125
132
 
126
133
  # Log with structured logger
127
134
  RailsNexus::Logger.error(exception, controller: self, extra: data)
@@ -131,3 +138,10 @@ module RailsNexus
131
138
  end
132
139
  end
133
140
  end
141
+
142
+ # Compatibility for applications that integrated the exception concern under
143
+ # the pre-RailsNexus namespace. New integrations should use
144
+ # RailsNexus::ExceptionLoggable.
145
+ module RailsOps
146
+ ExceptionLoggable = RailsNexus::ExceptionLoggable unless const_defined?(:ExceptionLoggable, false)
147
+ end
data/rails_nexus.gemspec CHANGED
@@ -15,10 +15,12 @@ Gem::Specification.new do |spec|
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
16
  spec.metadata["source_code_uri"] = "https://github.com/tamiru/rails_nexus/tree/main"
17
17
  spec.metadata["changelog_uri"] = "https://github.com/tamiru/rails_nexus/blob/main/CHANGELOG.md"
18
+ spec.metadata["bug_tracker_uri"] = "https://github.com/tamiru/rails_nexus/issues"
18
19
  spec.metadata["funding_uri"] = "https://github.com/sponsors/tamiru"
19
20
 
20
21
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
- Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md", "rails_nexus.gemspec"]
22
+ Dir["{app,config,db,lib}/**/*", "CHANGELOG.md", "SECURITY.md", "THIRD_PARTY_NOTICES.md", "MIT-LICENSE", "Rakefile", "README.md", "rails_nexus.gemspec"]
23
+ .reject { |file| file.match?(%r{(?:^|/)(?:\.idea|log|tmp|storage|pkg)(?:/|$)|\.(?:gem|sqlite3|log)\z|Zone\.Identifier\z}) }
22
24
  end
23
25
 
24
26
  spec.required_ruby_version = ">= 3.2.0"
@@ -26,7 +28,6 @@ Gem::Specification.new do |spec|
26
28
  spec.add_dependency "rails", "~> 8.0", ">= 8.0.0"
27
29
  spec.add_dependency "stimulus-rails", "~> 1.3"
28
30
  spec.add_dependency "turbo-rails", "~> 2.0"
29
- spec.add_dependency "importmap-rails", "~> 2.0"
30
31
  spec.add_dependency "pagy", "~> 6.5"
31
32
 
32
33
  spec.add_dependency "ransack", "~> 4.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_nexus
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tamiru Hailu
@@ -57,20 +57,6 @@ dependencies:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
59
  version: '2.0'
60
- - !ruby/object:Gem::Dependency
61
- name: importmap-rails
62
- requirement: !ruby/object:Gem::Requirement
63
- requirements:
64
- - - "~>"
65
- - !ruby/object:Gem::Version
66
- version: '2.0'
67
- type: :runtime
68
- prerelease: false
69
- version_requirements: !ruby/object:Gem::Requirement
70
- requirements:
71
- - - "~>"
72
- - !ruby/object:Gem::Version
73
- version: '2.0'
74
60
  - !ruby/object:Gem::Dependency
75
61
  name: pagy
76
62
  requirement: !ruby/object:Gem::Requirement
@@ -110,10 +96,14 @@ executables: []
110
96
  extensions: []
111
97
  extra_rdoc_files: []
112
98
  files:
99
+ - CHANGELOG.md
113
100
  - MIT-LICENSE
114
101
  - README.md
115
102
  - Rakefile
103
+ - SECURITY.md
104
+ - THIRD_PARTY_NOTICES.md
116
105
  - app/assets/config/rails_nexus_manifest.js
106
+ - app/assets/javascripts/rails_nexus/application.js
117
107
  - app/assets/javascripts/rails_nexus/controllers.js
118
108
  - app/assets/stylesheets/rails_nexus/application.css
119
109
  - app/controllers/rails_nexus/analytics_controller.rb
@@ -178,7 +168,6 @@ files:
178
168
  - app/views/rails_nexus/n1_patterns/index.html.erb
179
169
  - app/views/rails_nexus/settings/index.html.erb
180
170
  - app/views/rails_nexus/stats/index.html.erb
181
- - config/importmap.rb
182
171
  - config/initializers/date_formats.rb
183
172
  - config/locales/en.yml
184
173
  - config/routes.rb
@@ -208,12 +197,14 @@ files:
208
197
  - lib/rails_nexus/breadcrumbs.rb
209
198
  - lib/rails_nexus/cleanup.rb
210
199
  - lib/rails_nexus/configuration.rb
200
+ - lib/rails_nexus/database_adapter.rb
211
201
  - lib/rails_nexus/engine.rb
212
202
  - lib/rails_nexus/logger.rb
213
203
  - lib/rails_nexus/notifications.rb
214
204
  - lib/rails_nexus/ransack_config.rb
215
205
  - lib/rails_nexus/storm_protection.rb
216
206
  - lib/rails_nexus/version.rb
207
+ - lib/rails_nexus/webhook_client.rb
217
208
  - lib/tasks/rails_nexus.rake
218
209
  - lib/tasks/rails_nexus_tasks.rake
219
210
  - rails_nexus.gemspec
@@ -226,6 +217,7 @@ metadata:
226
217
  homepage_uri: https://github.com/tamiru/rails_nexus
227
218
  source_code_uri: https://github.com/tamiru/rails_nexus/tree/main
228
219
  changelog_uri: https://github.com/tamiru/rails_nexus/blob/main/CHANGELOG.md
220
+ bug_tracker_uri: https://github.com/tamiru/rails_nexus/issues
229
221
  funding_uri: https://github.com/sponsors/tamiru
230
222
  rdoc_options: []
231
223
  require_paths:
data/config/importmap.rb DELETED
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # RailsNexus has its own entry point, so it never replaces the host app's
4
- # "application" pin. Turbo and Stimulus are served from their Rails gems.
5
- rails_nexus_entrypoint = "rails_nexus/application"
6
-
7
- pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: rails_nexus_entrypoint
8
- pin "@hotwired/stimulus", to: "stimulus.min.js", preload: rails_nexus_entrypoint
9
- pin rails_nexus_entrypoint, to: "rails_nexus/application.js", preload: rails_nexus_entrypoint
10
-
11
- # Gem controllers
12
- pin "rails_nexus/controllers/rails_nexus", to: "controllers/rails_nexus_controller.js", preload: rails_nexus_entrypoint
13
- pin "rails_nexus/controllers/rails_nexus_detail", to: "controllers/rails_nexus_detail_controller.js", preload: rails_nexus_entrypoint
14
- pin "rails_nexus/controllers/rails_nexus_sidebar", to: "controllers/rails_nexus_sidebar_controller.js", preload: rails_nexus_entrypoint
15
- pin "rails_nexus/controllers/rails_nexus_theme", to: "controllers/rails_nexus_theme_controller.js", preload: rails_nexus_entrypoint