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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +110 -0
- data/README.md +33 -6
- data/SECURITY.md +11 -0
- data/THIRD_PARTY_NOTICES.md +14 -0
- data/app/assets/config/rails_nexus_manifest.js +1 -1
- data/app/assets/javascripts/rails_nexus/application.js +55 -0
- data/app/controllers/rails_nexus/analytics_controller.rb +13 -26
- data/app/controllers/rails_nexus/application_controller.rb +6 -2
- data/app/controllers/rails_nexus/backup_controller.rb +1 -2
- data/app/controllers/rails_nexus/cron_jobs_controller.rb +1 -9
- data/app/controllers/rails_nexus/database_health_controller.rb +0 -10
- data/app/controllers/rails_nexus/logged_exceptions_controller.rb +0 -2
- data/app/controllers/rails_nexus/n1_patterns_controller.rb +0 -2
- data/app/controllers/rails_nexus/settings_controller.rb +1 -39
- data/app/controllers/rails_nexus/source_code_controller.rb +9 -13
- data/app/controllers/rails_nexus/stats_controller.rb +43 -43
- data/app/helpers/rails_nexus/logged_exceptions_helper.rb +4 -49
- data/app/helpers/rails_nexus/source_code_helper.rb +49 -10
- data/app/javascript/rails_nexus/application.js +15 -3
- data/app/models/rails_nexus/backup_config.rb +31 -0
- data/app/models/rails_nexus/logged_exception.rb +27 -13
- data/app/services/rails_nexus/backup_service.rb +233 -98
- data/app/views/layouts/rails_nexus/application.html.erb +1 -1
- data/app/views/rails_nexus/stats/index.html.erb +1 -1
- data/lib/generators/rails_nexus/install_generator.rb +1 -1
- data/lib/generators/rails_nexus/templates/INSTALL.md +1 -0
- data/lib/generators/rails_nexus/templates/rails_nexus.rb +3 -0
- data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +1 -1
- data/lib/rails_nexus/configuration.rb +10 -0
- data/lib/rails_nexus/database_adapter.rb +53 -0
- data/lib/rails_nexus/engine.rb +0 -13
- data/lib/rails_nexus/logger.rb +1 -12
- data/lib/rails_nexus/notifications.rb +27 -20
- data/lib/rails_nexus/version.rb +1 -1
- data/lib/rails_nexus/webhook_client.rb +151 -0
- data/lib/rails_nexus.rb +37 -23
- data/rails_nexus.gemspec +3 -2
- metadata +8 -16
- 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
|
-
<%=
|
|
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 %>
|
|
@@ -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
|
|
38
|
+
"\n rescue_from StandardError, with: :log_exception_handler\n"
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -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
|
-
<%=
|
|
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
|
data/lib/rails_nexus/engine.rb
CHANGED
|
@@ -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
|
data/lib/rails_nexus/logger.rb
CHANGED
|
@@ -170,18 +170,7 @@ module RailsNexus
|
|
|
170
170
|
end
|
|
171
171
|
|
|
172
172
|
def filter_params(params)
|
|
173
|
-
|
|
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
|
|
32
|
-
log_webhook_delivery(
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
data/lib/rails_nexus/version.rb
CHANGED
|
@@ -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 "
|
|
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
|
-
|
|
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.
|
|
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
|