reactive-actions 0.1.0.pre.alpha.1 → 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.
@@ -2,31 +2,107 @@
2
2
 
3
3
  ReactiveActions has been installed successfully!
4
4
 
5
- Routes have been added to your config/routes.rb file:
5
+ SETUP COMPLETED:
6
+ ================
7
+
8
+ ✓ Routes added to config/routes.rb:
6
9
  mount ReactiveActions::Engine, at: '/reactive_actions'
7
10
 
8
- You can now access the reactive actions functionality at:
9
- http://localhost:3000/reactive_actions/execute
11
+ JavaScript client configured with automatic initialization
12
+ - Added to config/importmap.rb
13
+ - Initialized in app/javascript/application.js
14
+ - Available globally as window.ReactiveActions
10
15
 
11
- A sample action has been created at:
16
+ Sample action created:
12
17
  app/reactive_actions/example_action.rb
13
18
 
14
- You can try it out with:
15
-
16
- # Using HTTP
17
- curl -X POST http://localhost:3000/reactive_actions/execute \
18
- -H "Content-Type: application/json" \
19
- -d '{"action_name": "example", "action_params": {"name": "YourName"}}'
20
-
21
- # Using JavaScript client
22
- ReactiveActions.execute('example', { name: 'YourName' })
23
- .then(response => console.log(response))
24
-
25
- To create your own actions, add more files to the app/reactive_actions directory.
26
- Each action should inherit from ReactiveActions::ReactiveAction and define
27
- at least the `action` and `response` methods.
28
-
29
- For more information, see the documentation at:
19
+ QUICK START:
20
+ ============
21
+
22
+ 1. Test the API endpoint:
23
+ curl -X POST http://localhost:3000/reactive_actions/execute \
24
+ -H "Content-Type: application/json" \
25
+ -d '{"action_name": "example", "action_params": {"name": "YourName"}}'
26
+
27
+ 2. Use JavaScript client:
28
+ ReactiveActions.execute('example', { name: 'YourName' })
29
+ .then(response => console.log(response))
30
+
31
+ 3. Use DOM binding (no JavaScript required):
32
+ <button reactive-action="click->example"
33
+ reactive-action-name="YourName">
34
+ Test Action
35
+ </button>
36
+
37
+ JAVASCRIPT CLIENT:
38
+ ==================
39
+
40
+ The ReactiveActions client is automatically initialized and available as:
41
+ - window.ReactiveActions (global)
42
+ - Supports all HTTP methods: get, post, put, patch, delete
43
+ - Automatic CSRF token handling
44
+ - DOM binding with mutation observer
45
+
46
+ Examples:
47
+ // Execute actions
48
+ ReactiveActions.execute('action_name', { param: 'value' })
49
+ ReactiveActions.post('create_user', { name: 'John' })
50
+ ReactiveActions.get('fetch_data', { id: 123 })
51
+
52
+ // Configuration (if needed)
53
+ ReactiveActions.configure({
54
+ baseUrl: '/custom/path/execute',
55
+ defaultHttpMethod: 'PUT'
56
+ }).reinitialize()
57
+
58
+ DOM BINDING:
59
+ ============
60
+
61
+ Add reactive-action attributes to any element:
62
+
63
+ <!-- Basic actions -->
64
+ <button reactive-action="click->update_user">Update</button>
65
+ <input reactive-action="change->search" type="text">
66
+ <form reactive-action="submit->create_post">...</form>
67
+
68
+ <!-- With HTTP methods -->
69
+ <button reactive-action="click->put#update_user">Update (PUT)</button>
70
+ <button reactive-action="click->delete#remove_user">Delete</button>
71
+
72
+ <!-- Pass data via attributes -->
73
+ <button reactive-action="click->update_user"
74
+ reactive-action-user-id="123"
75
+ reactive-action-name="John">
76
+ Update User
77
+ </button>
78
+
79
+ CREATING ACTIONS:
80
+ =================
81
+
82
+ Add files to app/reactive_actions/:
83
+
84
+ # app/reactive_actions/update_user_action.rb
85
+ class UpdateUserAction < ReactiveActions::ReactiveAction
86
+ def action
87
+ user = User.find(action_params[:user_id])
88
+ user.update(name: action_params[:name])
89
+ @result = { success: true, user: user.as_json }
90
+ end
91
+
92
+ def response
93
+ render json: @result
94
+ end
95
+ end
96
+
97
+ DOCUMENTATION & SUPPORT:
98
+ ========================
99
+
100
+ For complete documentation and examples:
30
101
  https://github.com/IstvanMs/reactive-actions
31
102
 
103
+ For troubleshooting and configuration options:
104
+ Check config/initializers/reactive_actions.rb
105
+
106
+ Happy coding with ReactiveActions! 🚀
107
+
32
108
  ===============================================================================
@@ -7,7 +7,66 @@ 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
13
58
  ReactiveActions.logger = Rails.logger
59
+
60
+ # JavaScript Client Configuration
61
+ # ================================
62
+ # The JavaScript client is automatically initialized in application.js
63
+ # You can reconfigure it at runtime if needed:
64
+ #
65
+ # ReactiveActions.configure({
66
+ # baseUrl: '/custom/path/execute',
67
+ # enableAutoBinding: true,
68
+ # enableMutationObserver: true,
69
+ # defaultHttpMethod: 'POST'
70
+ # }).reinitialize();
71
+ #
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