reactive-actions 0.1.0.pre.alpha.2 → 0.1.0.pre.alpha.3

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.
@@ -4,6 +4,9 @@ module ReactiveActions
4
4
  # Main controller for handling reactive action requests
5
5
  # Processes action execution and manages error handling for the ReactiveActions engine
6
6
  class ReactiveActionsController < ApplicationController
7
+ # Include rate limiting functionality
8
+ include ReactiveActions::Controller::RateLimiter
9
+
7
10
  # Allow this action to handle any HTTP method
8
11
  def execute
9
12
  ReactiveActions.logger.info "ReactiveActionsController#execute[#{request.method}]: #{params.inspect}"
@@ -118,7 +121,8 @@ module ReactiveActions
118
121
  'MissingParameterError' => [:bad_request, 'MISSING_PARAMETER'],
119
122
  'InvalidParametersError' => [:bad_request, 'INVALID_PARAMETERS'],
120
123
  'UnauthorizedError' => [:forbidden, 'UNAUTHORIZED'],
121
- 'ActionExecutionError' => [:unprocessable_entity, 'EXECUTION_ERROR']
124
+ 'ActionExecutionError' => [:unprocessable_entity, 'EXECUTION_ERROR'],
125
+ 'RateLimitExceededError' => [:too_many_requests, 'RATE_LIMIT_EXCEEDED']
122
126
  }
123
127
 
124
128
  error_type = exception.class.name.demodulize
@@ -31,6 +31,16 @@ module ReactiveActions
31
31
  class_option :quiet, type: :boolean, default: false,
32
32
  desc: 'Run with minimal output'
33
33
 
34
+ # Rate limiting options
35
+ class_option :enable_rate_limiting, type: :boolean, default: false,
36
+ desc: 'Enable rate limiting features'
37
+ class_option :enable_global_rate_limiting, type: :boolean, default: false,
38
+ desc: 'Enable global controller-level rate limiting'
39
+ class_option :global_rate_limit, type: :numeric, default: 600,
40
+ desc: 'Global rate limit (requests per window)'
41
+ class_option :global_rate_limit_window, type: :string, default: '1.minute',
42
+ desc: 'Global rate limit window (e.g., "1.minute", "5.minutes")'
43
+
34
44
  def welcome_message
35
45
  return if options[:quiet]
36
46
 
@@ -39,24 +49,25 @@ module ReactiveActions
39
49
  say ''
40
50
  end
41
51
 
52
+ def gather_configuration
53
+ # Gather all configuration before creating files
54
+ setup_rate_limiting_configuration_for_initializer
55
+ end
56
+
42
57
  def create_initializer
43
- if options[:quiet] || yes?('Create ReactiveActions initializer? (recommended)', :green)
44
- template 'initializer.rb', 'config/initializers/reactive_actions.rb'
45
- say '✓ Created initializer', :green unless options[:quiet]
46
- else
47
- say '✗ Skipped initializer creation', :yellow
48
- end
58
+ return unless options[:quiet] || yes?('Create ReactiveActions initializer? (recommended)', :green)
59
+
60
+ template 'initializer.rb', 'config/initializers/reactive_actions.rb'
61
+ say '✓ Created initializer', :green unless options[:quiet]
49
62
  end
50
63
 
51
64
  def create_actions_directory
52
- if options[:quiet] || yes?('Create app/reactive_actions directory?', :green)
53
- empty_directory 'app/reactive_actions'
54
- say '✓ Created actions directory', :green unless options[:quiet]
65
+ return unless options[:quiet] || yes?('Create app/reactive_actions directory?', :green)
55
66
 
56
- ask_for_example_action unless options[:skip_example]
57
- else
58
- say '✗ Skipped actions directory creation', :yellow
59
- end
67
+ empty_directory 'app/reactive_actions'
68
+ say '✓ Created actions directory', :green unless options[:quiet]
69
+
70
+ ask_for_example_action unless options[:skip_example]
60
71
  end
61
72
 
62
73
  def configure_routes
@@ -72,14 +83,11 @@ module ReactiveActions
72
83
  def configure_javascript
73
84
  return if options[:skip_javascript]
74
85
  return unless javascript_needed?
86
+ return unless options[:quiet] || yes?('Add ReactiveActions JavaScript client?', :green)
75
87
 
76
- if options[:quiet] || yes?('Add ReactiveActions JavaScript client?', :green)
77
- add_javascript_to_importmap
78
- add_javascript_initialization
79
- say '✓ Added JavaScript client with initialization', :green unless options[:quiet]
80
- else
81
- say '✗ Skipped JavaScript configuration', :yellow
82
- end
88
+ add_javascript_to_importmap
89
+ add_javascript_initialization
90
+ say '✓ Added JavaScript client with initialization', :green unless options[:quiet]
83
91
  end
84
92
 
85
93
  def javascript_configuration_options
@@ -89,6 +97,19 @@ module ReactiveActions
89
97
  configure_javascript_options
90
98
  end
91
99
 
100
+ def rate_limiting_configuration
101
+ # Show rate limiting configuration summary if it was configured
102
+ return unless @rate_limiting_config && @rate_limiting_config[:rate_limiting_enabled] && !options[:quiet]
103
+
104
+ say '', :green
105
+ say '✓ Rate limiting configured:', :green
106
+ say " - Rate limiting: #{@rate_limiting_config[:rate_limiting_enabled] ? 'ENABLED' : 'DISABLED'}", :blue
107
+ say " - Global rate limiting: #{@rate_limiting_config[:global_rate_limiting_enabled] ? 'ENABLED' : 'DISABLED'}", :blue
108
+ return unless @rate_limiting_config[:global_rate_limiting_enabled]
109
+
110
+ say " - Global limit: #{@rate_limiting_config[:global_rate_limit]} requests per #{@rate_limiting_config[:global_rate_limit_window]}", :blue
111
+ end
112
+
92
113
  def configuration_options
93
114
  return if options[:quiet]
94
115
  return unless yes?('Configure advanced options?', :green)
@@ -110,6 +131,62 @@ module ReactiveActions
110
131
 
111
132
  private
112
133
 
134
+ def setup_rate_limiting_configuration_for_initializer
135
+ if rate_limiting_options_provided?
136
+ configure_rate_limiting_from_options_quietly
137
+ elsif !options[:quiet] && ask_about_rate_limiting?
138
+ configure_rate_limiting_interactively
139
+ else
140
+ @rate_limiting_config = {}
141
+ end
142
+ end
143
+
144
+ def rate_limiting_options_provided?
145
+ options[:enable_rate_limiting] || options[:enable_global_rate_limiting]
146
+ end
147
+
148
+ def ask_about_rate_limiting?
149
+ yes?('Configure rate limiting? (optional but recommended for production)', :green)
150
+ end
151
+
152
+ def configure_rate_limiting_from_options_quietly
153
+ @rate_limiting_config = {
154
+ rate_limiting_enabled: options[:enable_rate_limiting] || options[:enable_global_rate_limiting],
155
+ global_rate_limiting_enabled: options[:enable_global_rate_limiting]
156
+ }
157
+
158
+ return unless options[:enable_global_rate_limiting]
159
+
160
+ @rate_limiting_config[:global_rate_limit] = options[:global_rate_limit]
161
+ @rate_limiting_config[:global_rate_limit_window] = options[:global_rate_limit_window]
162
+ end
163
+
164
+ def configure_rate_limiting_interactively
165
+ @rate_limiting_config = {}
166
+
167
+ enable_rate_limiting = yes?('Enable rate limiting features?', :green)
168
+ @rate_limiting_config[:rate_limiting_enabled] = enable_rate_limiting
169
+ return unless enable_rate_limiting
170
+
171
+ enable_global = yes?('Enable global controller-level rate limiting? (recommended)', :green)
172
+ @rate_limiting_config[:global_rate_limiting_enabled] = enable_global
173
+
174
+ return unless enable_global
175
+
176
+ limit = ask('Global rate limit (requests per window):', default: 600)
177
+ @rate_limiting_config[:global_rate_limit] = limit.to_i
178
+
179
+ window = ask('Global rate limit window:',
180
+ limited_to: %w[30.seconds 1.minute 5.minutes 15.minutes 1.hour],
181
+ default: '1.minute')
182
+ @rate_limiting_config[:global_rate_limit_window] = window
183
+
184
+ return unless yes?('Configure custom rate limit key generator? (advanced)', :green)
185
+
186
+ say 'You can customize how rate limit keys are generated in the initializer.'
187
+ @rate_limiting_config[:custom_key_generator] = true
188
+ end
189
+
113
190
  def should_skip_example_action?
114
191
  return true if options[:skip_example]
115
192
  return true if !options[:quiet] && !yes?('Generate example action file?', :green)
@@ -192,26 +269,11 @@ module ReactiveActions
192
269
  app_js_path = app_js_paths.find { |path| File.exist?(path) }
193
270
 
194
271
  if app_js_path
195
- # Check if ReactiveActions is already configured
196
272
  app_js_content = File.read(app_js_path)
197
273
  return if app_js_content.include?('ReactiveActions') || app_js_content.include?('reactive_actions')
198
274
 
199
- # Generate JavaScript initialization code
200
275
  js_config = generate_javascript_config
201
-
202
- js_code = <<~JAVASCRIPT
203
-
204
- // Import and initialize ReactiveActions
205
- import ReactiveActionsClient from "reactive_actions"
206
-
207
- // Create and configure ReactiveActions instance
208
- const reactiveActions = new ReactiveActionsClient(#{js_config});
209
-
210
- #{generate_initialization_code}
211
-
212
- // Make ReactiveActions globally available
213
- window.ReactiveActions = reactiveActions;
214
- JAVASCRIPT
276
+ js_code = build_javascript_code(js_config)
215
277
 
216
278
  append_to_file app_js_path, js_code
217
279
  say "✓ Added ReactiveActions initialization to #{app_js_path}", :green unless options[:quiet]
@@ -222,12 +284,9 @@ module ReactiveActions
222
284
 
223
285
  def generate_javascript_config
224
286
  config = {}
225
-
226
- # Add mount path if different from default
227
287
  mount_path = determine_mount_path || options[:mount_path]
228
288
  config[:baseUrl] = "#{mount_path}/execute" if mount_path != '/reactive_actions'
229
289
 
230
- # Add configuration options
231
290
  config[:enableAutoBinding] = javascript_option_value(:enable_dom_binding)
232
291
  config[:enableMutationObserver] = javascript_option_value(:enable_mutation_observer)
233
292
  config[:defaultHttpMethod] = javascript_option_value(:default_http_method, 'POST')
@@ -240,6 +299,22 @@ module ReactiveActions
240
299
  config.empty? ? '{}' : JSON.pretty_generate(config)
241
300
  end
242
301
 
302
+ def build_javascript_code(js_config)
303
+ <<~JAVASCRIPT
304
+
305
+ // Import and initialize ReactiveActions
306
+ import ReactiveActionsClient from "reactive_actions"
307
+
308
+ // Create and configure ReactiveActions instance
309
+ const reactiveActions = new ReactiveActionsClient(#{js_config});
310
+
311
+ #{generate_initialization_code}
312
+
313
+ // Make ReactiveActions globally available
314
+ window.ReactiveActions = reactiveActions;
315
+ JAVASCRIPT
316
+ end
317
+
243
318
  def generate_initialization_code
244
319
  if javascript_option_value(:auto_initialize)
245
320
  <<~JAVASCRIPT.strip
@@ -249,10 +324,7 @@ module ReactiveActions
249
324
  document.addEventListener('turbo:frame-load', () => reactiveActions.initialize());
250
325
  JAVASCRIPT
251
326
  else
252
- <<~JAVASCRIPT.strip
253
- // Manual initialization - call reactiveActions.initialize() when ready
254
- // Example: reactiveActions.initialize();
255
- JAVASCRIPT
327
+ '// Manual initialization - call reactiveActions.initialize() when ready'
256
328
  end
257
329
  end
258
330
 
@@ -271,22 +343,17 @@ module ReactiveActions
271
343
  def configure_javascript_options
272
344
  return unless yes?('Configure JavaScript client options?', :green)
273
345
 
274
- # Auto-initialize option
275
- auto_init = yes?('Auto-initialize ReactiveActions on page load? (recommended)', :green)
276
346
  @javascript_options ||= {}
277
- @javascript_options[:auto_initialize] = auto_init
347
+ @javascript_options[:auto_initialize] = yes?('Auto-initialize ReactiveActions on page load? (recommended)', :green)
278
348
 
279
- # DOM binding option
280
349
  enable_dom = yes?('Enable automatic DOM binding? (recommended)', :green)
281
350
  @javascript_options[:enable_dom_binding] = enable_dom
282
351
 
283
- # Mutation observer option
284
352
  if enable_dom
285
353
  enable_observer = yes?('Enable mutation observer for dynamic content? (recommended)', :green)
286
354
  @javascript_options[:enable_mutation_observer] = enable_observer
287
355
  end
288
356
 
289
- # Default HTTP method
290
357
  http_methods = %w[POST GET PUT PATCH DELETE]
291
358
  default_method = ask('Default HTTP method:', limited_to: http_methods, default: 'POST')
292
359
  @javascript_options[:default_http_method] = default_method unless default_method == 'POST'
@@ -295,9 +362,7 @@ module ReactiveActions
295
362
  def add_to_sprockets_manifest
296
363
  return unless File.exist?('app/assets/config/manifest.js')
297
364
 
298
- append_to_file 'app/assets/config/manifest.js' do
299
- "\n//= link reactive_actions.js\n"
300
- end
365
+ append_to_file 'app/assets/config/manifest.js', "\n//= link reactive_actions.js\n"
301
366
  end
302
367
 
303
368
  def configure_delegated_methods
@@ -352,6 +417,11 @@ module ReactiveActions
352
417
  template 'example_action.rb', "app/reactive_actions/#{sanitized_name}.rb"
353
418
  say "✓ Created #{sanitized_name}.rb", :green unless options[:quiet]
354
419
  end
420
+
421
+ # Template method to access rate limiting config in templates
422
+ def rate_limiting_config
423
+ @rate_limiting_config || {}
424
+ end
355
425
  end
356
426
  end
357
427
  end
@@ -7,6 +7,51 @@ ReactiveActions.configure do |config|
7
7
 
8
8
  # Configure instance variables to delegate from the controller to action classes
9
9
  # config.delegated_instance_variables += [:custom_variable]
10
+
11
+ # Rate Limiting Configuration
12
+ # ============================
13
+
14
+ <% if rate_limiting_config[:rate_limiting_enabled] -%>
15
+ # Rate limiting is enabled
16
+ config.rate_limiting_enabled = true
17
+
18
+ <% if rate_limiting_config[:global_rate_limiting_enabled] -%>
19
+ # Global controller-level rate limiting is enabled
20
+ config.global_rate_limiting_enabled = true
21
+ config.global_rate_limit = <%= rate_limiting_config[:global_rate_limit] || 600 %>
22
+ config.global_rate_limit_window = <%= rate_limiting_config[:global_rate_limit_window] || '1.minute' %>
23
+
24
+ <% else -%>
25
+ # Global rate limiting is disabled
26
+ config.global_rate_limiting_enabled = false
27
+
28
+ <% end -%>
29
+ <% if rate_limiting_config[:custom_key_generator] -%>
30
+ # Custom rate limit key generator
31
+ # Uncomment and customize as needed:
32
+ # config.rate_limit_key_generator = ->(request, action_name) do
33
+ # # Example: user-based key with action scope
34
+ # user_id = request.headers['X-User-ID'] || 'anonymous'
35
+ # "#{action_name}:user:#{user_id}"
36
+ # end
37
+
38
+ <% end -%>
39
+ <% else -%>
40
+ # Rate limiting is disabled by default
41
+ # To enable rate limiting, uncomment and configure:
42
+ # config.rate_limiting_enabled = true
43
+ # config.global_rate_limiting_enabled = true
44
+ # config.global_rate_limit = 600
45
+ # config.global_rate_limit_window = 1.minute
46
+
47
+ # Custom rate limit key generator (optional)
48
+ # config.rate_limit_key_generator = ->(request, action_name) do
49
+ # # Example: user-based key with action scope
50
+ # user_id = request.headers['X-User-ID'] || 'anonymous'
51
+ # "#{action_name}:user:#{user_id}"
52
+ # end
53
+
54
+ <% end -%>
10
55
  end
11
56
 
12
57
  # Set the logger for ReactiveActions
@@ -24,4 +69,4 @@ ReactiveActions.logger = Rails.logger
24
69
  # defaultHttpMethod: 'POST'
25
70
  # }).reinitialize();
26
71
  #
27
- # Available globally as window.ReactiveActions
72
+ # Available globally as window.ReactiveActions
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactiveActions
4
+ module Concerns
5
+ # Rate limiting module for ReactiveActions that can be included in actions or other classes
6
+ # Provides convenient methods for checking and managing rate limits
7
+ module RateLimiter
8
+ # Helper method for rate limiting in security checks or actions
9
+ # Automatically respects the global rate_limiting_enabled setting
10
+ # @param key [String, nil] Custom cache key, uses default if nil
11
+ # @param limit [Integer] Maximum number of requests allowed
12
+ # @param window [ActiveSupport::Duration] Time window for the limit
13
+ # @param cost [Integer] Cost of this request (default: 1)
14
+ # @raise [RateLimitExceededError] When rate limit is exceeded
15
+ def rate_limit!(key: nil, limit: 100, window: 1.hour, cost: 1)
16
+ # Early return if rate limiting is disabled
17
+ return unless ReactiveActions.configuration.rate_limiting_enabled
18
+
19
+ effective_key = key || default_rate_limit_key
20
+
21
+ ReactiveActions::RateLimiter.check!(
22
+ key: effective_key,
23
+ limit: limit,
24
+ window: window,
25
+ cost: cost
26
+ )
27
+ end
28
+
29
+ # Check rate limit without raising an error (also respects configuration)
30
+ # @param key [String, nil] Custom cache key, uses default if nil
31
+ # @param limit [Integer] Maximum number of requests allowed
32
+ # @param window [ActiveSupport::Duration] Time window for the limit
33
+ # @return [Hash] Rate limit status information
34
+ def rate_limit_status(key: nil, limit: 100, window: 1.hour)
35
+ # Return "unlimited" status if rate limiting is disabled
36
+ unless ReactiveActions.configuration.rate_limiting_enabled
37
+ return {
38
+ limit: Float::INFINITY,
39
+ remaining: Float::INFINITY,
40
+ current: 0,
41
+ window: window,
42
+ exceeded: false,
43
+ enabled: false
44
+ }
45
+ end
46
+
47
+ effective_key = key || default_rate_limit_key
48
+
49
+ ReactiveActions::RateLimiter.check(
50
+ key: effective_key,
51
+ limit: limit,
52
+ window: window
53
+ ).merge(enabled: true)
54
+ end
55
+
56
+ # Reset rate limit for a specific key
57
+ # @param key [String, nil] Custom cache key, uses default if nil
58
+ # @param window [ActiveSupport::Duration] Time window for the limit
59
+ def reset_rate_limit!(key: nil, window: 1.hour)
60
+ return unless ReactiveActions.configuration.rate_limiting_enabled
61
+
62
+ effective_key = key || default_rate_limit_key
63
+
64
+ ReactiveActions::RateLimiter.reset!(
65
+ key: effective_key,
66
+ window: window
67
+ )
68
+ end
69
+
70
+ # Check if rate limiting is currently enabled
71
+ # @return [Boolean] True if rate limiting is enabled
72
+ def rate_limiting_enabled?
73
+ ReactiveActions.configuration.rate_limiting_enabled
74
+ end
75
+
76
+ # Get remaining requests for a specific key without consuming any
77
+ # @param key [String, nil] Custom cache key, uses default if nil
78
+ # @param limit [Integer] Maximum number of requests allowed
79
+ # @param window [ActiveSupport::Duration] Time window for the limit
80
+ # @return [Integer, Float] Number of remaining requests, or Float::INFINITY if disabled
81
+ def rate_limit_remaining(key: nil, limit: 100, window: 1.hour)
82
+ status = rate_limit_status(key: key, limit: limit, window: window)
83
+ status[:remaining]
84
+ end
85
+
86
+ # Check if a key would exceed rate limit for a given cost
87
+ # @param key [String, nil] Custom cache key, uses default if nil
88
+ # @param limit [Integer] Maximum number of requests allowed
89
+ # @param window [ActiveSupport::Duration] Time window for the limit
90
+ # @param cost [Integer] Cost to check (default: 1)
91
+ # @return [Boolean] True if the cost would exceed the limit
92
+ def rate_limit_would_exceed?(key: nil, limit: 100, window: 1.hour, cost: 1)
93
+ return false unless ReactiveActions.configuration.rate_limiting_enabled
94
+
95
+ status = rate_limit_status(key: key, limit: limit, window: window)
96
+ status[:remaining] < cost
97
+ end
98
+
99
+ # Create a rate limit key for a specific scope
100
+ # @param scope [String, Symbol] The scope (e.g., 'api', 'search', 'upload')
101
+ # @param identifier [String, nil] Custom identifier, uses default if nil
102
+ # @return [String] Formatted rate limit key
103
+ def rate_limit_key_for(scope, identifier: nil)
104
+ base_key = identifier || extract_identifier_from_context
105
+ "#{scope}:#{base_key}"
106
+ end
107
+
108
+ # Log rate limiting events for debugging/monitoring
109
+ # @param event [String] Event type (e.g., 'exceeded', 'checked', 'reset')
110
+ # @param details [Hash] Additional details to log
111
+ def log_rate_limit_event(event, details = {})
112
+ return unless ReactiveActions.logger
113
+
114
+ ReactiveActions.logger.info(
115
+ "Rate Limit #{event.capitalize}: #{details.merge(key: default_rate_limit_key)}"
116
+ )
117
+ end
118
+
119
+ private
120
+
121
+ # Default key generation for rate limiting
122
+ # This method tries different approaches to generate a meaningful key
123
+ # @return [String] A rate limit key
124
+ def default_rate_limit_key
125
+ # Try different methods to get a meaningful identifier
126
+ if respond_to?(:current_user) && current_user
127
+ "user:#{current_user.id}"
128
+ elsif respond_to?(:controller) && controller.respond_to?(:request)
129
+ extract_key_from_request(controller.request)
130
+ elsif respond_to?(:request) && request
131
+ extract_key_from_request(request)
132
+ else
133
+ "anonymous:#{SecureRandom.hex(8)}"
134
+ end
135
+ end
136
+
137
+ # Extract identifier from current context (user, request, etc.)
138
+ # @return [String] An identifier for the current context
139
+ def extract_identifier_from_context
140
+ if respond_to?(:current_user) && current_user
141
+ current_user.id.to_s
142
+ elsif respond_to?(:controller) && controller.respond_to?(:request)
143
+ controller.request.remote_ip
144
+ elsif respond_to?(:request) && request
145
+ request.remote_ip
146
+ else
147
+ 'unknown'
148
+ end
149
+ end
150
+
151
+ # Extract a rate limiting key from a request object
152
+ # @param request [ActionDispatch::Request] The request object
153
+ # @return [String] A rate limit key based on the request
154
+ def extract_key_from_request(request)
155
+ # Try to get user info from headers first
156
+ if request.headers['X-User-ID'].present?
157
+ "user:#{request.headers['X-User-ID']}"
158
+ elsif request.headers['Authorization'].present?
159
+ # Extract from API token/key
160
+ auth_header = request.headers['Authorization']
161
+ if auth_header.start_with?('Bearer ')
162
+ token = auth_header.gsub('Bearer ', '')
163
+ "token:#{Digest::SHA256.hexdigest(token)[0..8]}" # Hash for privacy
164
+ else
165
+ "auth:#{request.remote_ip}"
166
+ end
167
+ else
168
+ # Fallback to IP address
169
+ "ip:#{request.remote_ip}"
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactiveActions
4
+ module Concerns
5
+ # Security checks module that provides simple security filtering with function names or lambdas
6
+ module SecurityChecks
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ class_attribute :security_filters, default: []
11
+ end
12
+
13
+ class_methods do
14
+ # Add a security check that runs before the action
15
+ #
16
+ # @param check [Symbol, Proc] The method name or lambda to execute
17
+ # @param options [Hash] Conditions for when to run the check
18
+ # @option options [Symbol, Array<Symbol>] :only Run only for these actions
19
+ # @option options [Symbol, Array<Symbol>] :except Skip for these actions
20
+ # @option options [Symbol, Proc] :if Run only if condition is true
21
+ # @option options [Symbol, Proc] :unless Skip if condition is true
22
+ def security_check(check, **options)
23
+ self.security_filters = security_filters + [{ check: check, **options }]
24
+ end
25
+
26
+ # Skip all security checks for this action
27
+ def skip_security_checks
28
+ self.security_filters = []
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # Run all configured security checks
35
+ def run_security_checks
36
+ self.class.security_filters.each do |filter_config|
37
+ next unless should_run_security_check?(filter_config)
38
+
39
+ execute_security_check(filter_config[:check])
40
+ end
41
+ end
42
+
43
+ # Determine if a security check should run based on conditions
44
+ def should_run_security_check?(filter_config)
45
+ return false unless check_only_condition(filter_config[:only])
46
+ return false if check_except_condition(filter_config[:except])
47
+ return false unless check_if_condition(filter_config[:if])
48
+ return false if check_unless_condition(filter_config[:unless])
49
+
50
+ true
51
+ end
52
+
53
+ def check_only_condition(only_condition)
54
+ return true unless only_condition
55
+
56
+ only_actions = Array(only_condition).map(&:to_s)
57
+ only_actions.include?('action')
58
+ end
59
+
60
+ def check_except_condition(except_condition)
61
+ return false unless except_condition
62
+
63
+ except_actions = Array(except_condition).map(&:to_s)
64
+ except_actions.include?('action')
65
+ end
66
+
67
+ def check_if_condition(if_condition)
68
+ return true unless if_condition
69
+
70
+ evaluate_condition(if_condition)
71
+ end
72
+
73
+ def check_unless_condition(unless_condition)
74
+ return false unless unless_condition
75
+
76
+ evaluate_condition(unless_condition)
77
+ end
78
+
79
+ def evaluate_condition(condition)
80
+ if condition.is_a?(Proc)
81
+ instance_exec(&condition)
82
+ elsif condition.is_a?(Symbol)
83
+ send(condition)
84
+ else
85
+ false
86
+ end
87
+ end
88
+
89
+ # Execute a single security check
90
+ def execute_security_check(check)
91
+ if check.is_a?(Proc)
92
+ instance_exec(&check)
93
+ elsif check.is_a?(Symbol)
94
+ send(check)
95
+ else
96
+ raise ReactiveActions::SecurityCheckError, "Invalid security check: #{check.inspect}"
97
+ end
98
+ rescue ReactiveActions::Error
99
+ # Re-raise ReactiveActions errors as-is
100
+ raise
101
+ rescue StandardError => e
102
+ # Convert other errors to security errors
103
+ ReactiveActions.logger.error "Security check failed in #{self.class.name}: #{e.message}"
104
+ raise ReactiveActions::SecurityCheckError, "Security check failed: #{e.message}"
105
+ end
106
+ end
107
+ end
108
+ end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # ReactiveActions module provides functionality for creating and executing reactive actions in Rails applications
3
+ # ReactiveActions provides functionality for creating and executing reactive actions in Rails applications
4
4
  module ReactiveActions
5
5
  # Configuration class for ReactiveActions
6
- # Manages settings for controller method delegation and instance variable delegation
6
+ # Manages settings for controller method delegation, instance variable delegation, and rate limiting
7
7
  class Configuration
8
- attr_accessor :delegated_controller_methods, :delegated_instance_variables
8
+ attr_accessor :delegated_controller_methods, :delegated_instance_variables, :global_rate_limit, :global_rate_limit_window, :rate_limit_key_generator, :rate_limit_cost_calculator
9
+
10
+ # Rate limiting configuration
11
+ attr_accessor :rate_limiting_enabled, :global_rate_limiting_enabled
9
12
 
10
13
  def initialize
11
14
  # Default methods to delegate
@@ -16,6 +19,23 @@ module ReactiveActions
16
19
 
17
20
  # Default instance variables to delegate
18
21
  @delegated_instance_variables = []
22
+
23
+ # Rate limiting defaults - DISABLED by default
24
+ @rate_limiting_enabled = false # Master switch for all rate limiting
25
+ @global_rate_limiting_enabled = false # Global controller-level rate limiting
26
+ @global_rate_limit = 600 # 600 requests per minute (10/second)
27
+ @global_rate_limit_window = 1.minute # per minute for responsive rate limiting
28
+ @rate_limit_key_generator = nil # Use default logic if nil
29
+ @rate_limit_cost_calculator = nil # Use default cost of 1 if nil
30
+ end
31
+
32
+ # Helper methods for checking rate limiting status
33
+ def rate_limiting_available?
34
+ @rate_limiting_enabled
35
+ end
36
+
37
+ def global_rate_limiting_active?
38
+ @rate_limiting_enabled && @global_rate_limiting_enabled
19
39
  end
20
40
  end
21
41