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.
data/README.md CHANGED
@@ -4,14 +4,14 @@ ReactiveActions is a Rails gem that provides a framework for handling reactive a
4
4
 
5
5
  ## 🚧 Status
6
6
 
7
- This gem is currently in alpha (0.1.0-alpha.2). The API may change between versions.
7
+ This gem is currently in alpha (0.1.0-alpha.3). The API may change between versions.
8
8
 
9
9
  ## πŸ“¦ Installation
10
10
 
11
11
  Add this line to your application's Gemfile:
12
12
 
13
13
  ```ruby
14
- gem 'reactive-actions', '0.1.0-alpha.2'
14
+ gem 'reactive-actions', '0.1.0-alpha.3'
15
15
  ```
16
16
 
17
17
  And then execute:
@@ -64,6 +64,17 @@ Add ReactiveActions JavaScript client? (y/n) y
64
64
  βœ“ Added JavaScript client to importmap
65
65
  βœ“ Added ReactiveActions import to app/javascript/application.js
66
66
 
67
+ Configure rate limiting? (optional but recommended for production) (y/n) y
68
+ Enable rate limiting features? (y/n) y
69
+ Enable global controller-level rate limiting? (recommended) (y/n) y
70
+ Global rate limit (requests per window): [600] 1000
71
+ Global rate limit window: [1.minute] 5.minutes
72
+ Configure custom rate limit key generator? (advanced) (y/n) n
73
+ βœ“ Rate limiting configured:
74
+ - Rate limiting: ENABLED
75
+ - Global rate limiting: ENABLED
76
+ - Global limit: 1000 requests per 5.minutes
77
+
67
78
  Configure advanced options? (y/n) n
68
79
 
69
80
  ================================================================
@@ -85,18 +96,37 @@ $ rails generate reactive_actions:install --mount-path=/api/reactive
85
96
  # Skip example action generation
86
97
  $ rails generate reactive_actions:install --skip-example
87
98
 
99
+ # Enable rate limiting during installation
100
+ $ rails generate reactive_actions:install --enable-rate-limiting --enable-global-rate-limiting
101
+
102
+ # Configure rate limiting with custom limits
103
+ $ rails generate reactive_actions:install --enable-rate-limiting --global-rate-limit=1000 --global-rate-limit-window="5.minutes"
104
+
88
105
  # Quiet installation with defaults
89
106
  $ rails generate reactive_actions:install --quiet
90
107
  ```
91
108
 
92
109
  ### Available Options
93
110
 
111
+ **Basic Options:**
94
112
  - `--skip-routes` - Skip adding routes to your application
95
113
  - `--skip-javascript` - Skip adding JavaScript imports and setup
96
114
  - `--skip-example` - Skip generating the example action file
97
115
  - `--mount-path=PATH` - Specify custom mount path (default: `/reactive_actions`)
98
116
  - `--quiet` - Run installation with minimal output and default settings
99
117
 
118
+ **JavaScript Client Options:**
119
+ - `--auto-initialize` - Auto-initialize ReactiveActions on page load (default: true)
120
+ - `--enable-dom-binding` - Enable automatic DOM binding (default: true)
121
+ - `--enable-mutation-observer` - Enable mutation observer for dynamic content (default: true)
122
+ - `--default-http-method=METHOD` - Default HTTP method for actions (default: 'POST')
123
+
124
+ **Rate Limiting Options:**
125
+ - `--enable-rate-limiting` - Enable rate limiting features
126
+ - `--enable-global-rate-limiting` - Enable global controller-level rate limiting
127
+ - `--global-rate-limit=NUMBER` - Global rate limit (requests per window, default: 600)
128
+ - `--global-rate-limit-window=DURATION` - Global rate limit window (default: '1.minute')
129
+
100
130
  ### What Gets Installed
101
131
 
102
132
  The generator will:
@@ -106,6 +136,7 @@ The generator will:
106
136
  - βœ… Add JavaScript to your `config/importmap.rb` (Rails 8 native)
107
137
  - βœ… Automatically import ReactiveActions in your `application.js`
108
138
  - βœ… Create an initializer file with configuration options
139
+ - βœ… Configure rate limiting settings (if enabled)
109
140
  - βœ… Optionally configure advanced settings like custom delegated methods
110
141
 
111
142
  ## ⚑ Rails 8 Native JavaScript Integration
@@ -544,173 +575,860 @@ class GenerateReportAction < ReactiveActions::ReactiveAction
544
575
  end
545
576
  ```
546
577
 
547
- ## πŸ’» Complete DOM Binding Examples
578
+ ## πŸ” Security Checks
548
579
 
549
- ### User Management Interface
580
+ ReactiveActions provides a comprehensive security system through the `SecurityChecks` module, allowing you to define custom security filters that run before your actions execute.
550
581
 
551
- ```html
552
- <!-- User List with Actions -->
553
- <div class="user-list">
554
- <% @users.each do |user| %>
555
- <div class="user-card" id="user-<%= user.id %>">
556
- <h3><%= user.name %></h3>
557
- <p><%= user.email %></p>
558
-
559
- <!-- Update user -->
560
- <button reactive-action="click->put#update_user"
561
- reactive-action-user-id="<%= user.id %>"
562
- reactive-action-name="<%= user.name %>"
563
- reactive-action-success="handleUserUpdate">
564
- Quick Update
565
- </button>
566
-
567
- <!-- Delete user -->
568
- <button reactive-action="click->delete#delete_user"
569
- reactive-action-user-id="<%= user.id %>"
570
- reactive-action-success="handleUserDelete"
571
- class="danger">
572
- Delete
573
- </button>
574
-
575
- <!-- Show preview on hover -->
576
- <div reactive-action="mouseenter->get#show_user_preview mouseleave->post#hide_preview"
577
- reactive-action-user-id="<%= user.id %>"
578
- reactive-action-target="preview-<%= user.id %>">
579
- <img src="<%= user.avatar %>" alt="Hover for details">
580
- </div>
581
- </div>
582
- <% end %>
583
- </div>
584
-
585
- <!-- Live Search -->
586
- <div class="search-container">
587
- <input type="text"
588
- reactive-action="input->get#search_users"
589
- reactive-action-min-length="2"
590
- reactive-action-success="updateSearchResults"
591
- placeholder="Search users...">
592
-
593
- <div id="search-results"></div>
594
- </div>
595
-
596
- <!-- Create User Form -->
597
- <form reactive-action="submit->post#create_user"
598
- reactive-action-success="handleUserCreate">
599
- <input name="name" type="text" placeholder="Name" required>
600
- <input name="email" type="email" placeholder="Email" required>
601
- <button type="submit">Create User</button>
602
- </form>
582
+ ### Basic Security Checks
603
583
 
604
- <script>
605
- function handleUserUpdate(response, element, event) {
606
- if (response.success) {
607
- // Update the UI without page refresh
608
- const userCard = element.closest('.user-card');
609
- userCard.querySelector('h3').textContent = response.user.name;
584
+ Add security checks to your actions using the `security_check` class method:
585
+
586
+ ```ruby
587
+ # app/reactive_actions/protected_action.rb
588
+ class ProtectedAction < ReactiveActions::ReactiveAction
589
+ # Single security check
590
+ security_check :require_authentication
591
+
592
+ def action
593
+ @result = { message: "This action requires authentication" }
594
+ end
595
+
596
+ def response
597
+ render json: @result
598
+ end
599
+
600
+ private
601
+
602
+ def require_authentication
603
+ raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
604
+ end
605
+ end
606
+ ```
607
+
608
+ ### Multiple Security Checks
609
+
610
+ Chain multiple security checks for layered protection:
611
+
612
+ ```ruby
613
+ # app/reactive_actions/admin_action.rb
614
+ class AdminAction < ReactiveActions::ReactiveAction
615
+ # Multiple security checks run in order
616
+ security_check :require_authentication
617
+ security_check :require_admin_role
618
+
619
+ def action
620
+ @result = { message: "Admin-only action executed successfully" }
621
+ end
622
+
623
+ def response
624
+ render json: @result
625
+ end
626
+
627
+ private
628
+
629
+ def require_authentication
630
+ raise ReactiveActions::SecurityCheckError, "Please log in" unless current_user
631
+ end
632
+
633
+ def require_admin_role
634
+ raise ReactiveActions::SecurityCheckError, "Admin access required" unless current_user.admin?
635
+ end
636
+ end
637
+ ```
638
+
639
+ ### Lambda-Based Security Checks
640
+
641
+ Use inline lambdas for simple or dynamic security checks:
642
+
643
+ ```ruby
644
+ # app/reactive_actions/ownership_action.rb
645
+ class OwnershipAction < ReactiveActions::ReactiveAction
646
+ # Inline lambda security check
647
+ security_check -> {
648
+ raise ReactiveActions::SecurityCheckError, "Must be logged in" unless current_user
610
649
 
611
- // Show success message
612
- showFlash('User updated successfully!', 'success');
650
+ if action_params[:user_id].present?
651
+ unless current_user.id.to_s == action_params[:user_id].to_s
652
+ raise ReactiveActions::SecurityCheckError, "Can only access your own data"
653
+ end
654
+ end
613
655
  }
614
- }
615
656
 
616
- function handleUserDelete(response, element, event) {
617
- if (response.success) {
618
- // Remove the user card from the UI
619
- const userCard = element.closest('.user-card');
620
- userCard.remove();
621
-
622
- showFlash('User deleted successfully!', 'success');
657
+ def action
658
+ @result = { message: "Ownership check passed" }
659
+ end
660
+
661
+ def response
662
+ render json: @result
663
+ end
664
+ end
665
+ ```
666
+
667
+ ### Conditional Security Checks
668
+
669
+ Apply security checks conditionally using `:if`, `:unless`, `:only`, or `:except`:
670
+
671
+ ```ruby
672
+ # app/reactive_actions/conditional_action.rb
673
+ class ConditionalAction < ReactiveActions::ReactiveAction
674
+ # Always require authentication
675
+ security_check :require_authentication
676
+
677
+ # Only require special access if special mode is enabled
678
+ security_check :require_special_access, if: -> { action_params[:special_mode] == "true" }
679
+
680
+ # Skip ownership check for admin users
681
+ security_check :require_ownership, unless: -> { current_user&.admin? }
682
+
683
+ def action
684
+ @result = { message: "Conditional security checks passed" }
685
+ end
686
+
687
+ def response
688
+ render json: @result
689
+ end
690
+
691
+ private
692
+
693
+ def require_authentication
694
+ raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
695
+ end
696
+
697
+ def require_special_access
698
+ unless current_user.special_access?
699
+ raise ReactiveActions::SecurityCheckError, "Special access required"
700
+ end
701
+ end
702
+
703
+ def require_ownership
704
+ resource_id = action_params[:resource_id]
705
+ resource = current_user.resources.find_by(id: resource_id)
706
+ raise ReactiveActions::SecurityCheckError, "Resource not found" unless resource
707
+ end
708
+ end
709
+ ```
710
+
711
+ ### Skipping Security Checks
712
+
713
+ For public actions that don't need any security checks:
714
+
715
+ ```ruby
716
+ # app/reactive_actions/public_action.rb
717
+ class PublicAction < ReactiveActions::ReactiveAction
718
+ # Skip all security checks for this action
719
+ skip_security_checks
720
+
721
+ def action
722
+ @result = { message: "This is a public action" }
723
+ end
724
+
725
+ def response
726
+ render json: @result
727
+ end
728
+ end
729
+ ```
730
+
731
+ ### Security Check Options
732
+
733
+ The `security_check` method supports several options for fine-grained control:
734
+
735
+ ```ruby
736
+ class ExampleAction < ReactiveActions::ReactiveAction
737
+ # Run only for specific actions (if you have multiple action methods)
738
+ security_check :check_method, only: [:create, :update]
739
+
740
+ # Skip for specific actions
741
+ security_check :check_method, except: [:index, :show]
742
+
743
+ # Conditional execution
744
+ security_check :check_method, if: :some_condition?
745
+ security_check :check_method, unless: :some_other_condition?
746
+
747
+ # Combine conditions
748
+ security_check :check_method, if: -> { params[:secure] == "true" }, unless: :development_mode?
749
+
750
+ private
751
+
752
+ def check_method
753
+ # Your security logic here
754
+ end
755
+
756
+ def some_condition?
757
+ # Your condition logic
758
+ end
759
+
760
+ def development_mode?
761
+ Rails.env.development?
762
+ end
763
+ end
764
+ ```
765
+
766
+ ### Security Error Handling
767
+
768
+ Security checks raise `ReactiveActions::SecurityCheckError` when they fail. This error is automatically caught and returned as a proper HTTP response:
769
+
770
+ ```json
771
+ {
772
+ "success": false,
773
+ "error": {
774
+ "type": "SecurityCheckError",
775
+ "message": "Authentication required",
776
+ "code": "SECURITY_CHECK_FAILED"
623
777
  }
624
778
  }
779
+ ```
780
+
781
+ ### Real-World Security Examples
782
+
783
+ #### User Resource Access Control
784
+ ```ruby
785
+ # app/reactive_actions/update_profile_action.rb
786
+ class UpdateProfileAction < ReactiveActions::ReactiveAction
787
+ security_check :require_authentication
788
+ security_check :verify_profile_ownership
789
+
790
+ def action
791
+ profile = current_user.profile
792
+ profile.update!(action_params.slice(:bio, :website, :location))
793
+ @result = { profile: profile.as_json }
794
+ end
795
+
796
+ def response
797
+ render json: @result
798
+ end
799
+
800
+ private
801
+
802
+ def require_authentication
803
+ raise ReactiveActions::SecurityCheckError, "Please log in" unless current_user
804
+ end
625
805
 
626
- function handleUserCreate(response, element, event) {
627
- if (response.success) {
628
- // Reset the form
629
- element.reset();
806
+ def verify_profile_ownership
807
+ profile_id = action_params[:profile_id]
808
+ return unless profile_id.present? # Skip check if no profile_id specified
630
809
 
631
- // Add new user to the list or refresh
632
- location.reload(); // Or dynamically add to the list
810
+ unless current_user.profile.id.to_s == profile_id.to_s
811
+ raise ReactiveActions::SecurityCheckError, "Can only update your own profile"
812
+ end
813
+ end
814
+ end
815
+ ```
816
+
817
+ #### Role-Based Access Control
818
+ ```ruby
819
+ # app/reactive_actions/moderate_content_action.rb
820
+ class ModerateContentAction < ReactiveActions::ReactiveAction
821
+ security_check :require_authentication
822
+ security_check :require_moderator_role
823
+
824
+ def action
825
+ content = Content.find(action_params[:content_id])
826
+ content.update!(status: action_params[:status],
827
+ moderated_by: current_user.id)
828
+ @result = { content: content.as_json }
829
+ end
830
+
831
+ def response
832
+ render json: @result
833
+ end
834
+
835
+ private
836
+
837
+ def require_authentication
838
+ raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
839
+ end
840
+
841
+ def require_moderator_role
842
+ unless current_user.moderator? || current_user.admin?
843
+ raise ReactiveActions::SecurityCheckError, "Moderator access required"
844
+ end
845
+ end
846
+ end
847
+ ```
848
+
849
+ #### API Key Validation
850
+ ```ruby
851
+ # app/reactive_actions/api_action.rb
852
+ class ApiAction < ReactiveActions::ReactiveAction
853
+ security_check :validate_api_key
854
+ security_check :check_rate_limit
855
+
856
+ def action
857
+ @result = { data: "API response data" }
858
+ end
859
+
860
+ def response
861
+ render json: @result
862
+ end
863
+
864
+ private
865
+
866
+ def validate_api_key
867
+ api_key = action_params[:api_key] || controller.request.headers['X-API-Key']
633
868
 
634
- showFlash('User created successfully!', 'success');
635
- }
636
- }
869
+ unless api_key.present? && ApiKey.valid?(api_key)
870
+ raise ReactiveActions::SecurityCheckError, "Invalid or missing API key"
871
+ end
872
+
873
+ @api_key = ApiKey.find_by(key: api_key)
874
+ end
637
875
 
638
- function updateSearchResults(response, element, event) {
639
- const resultsDiv = document.getElementById('search-results');
640
- resultsDiv.innerHTML = response.users.map(user =>
641
- `<div class="search-result">
642
- <strong>${user.name}</strong> - ${user.email}
643
- </div>`
644
- ).join('');
645
- }
876
+ def check_rate_limit
877
+ return unless @api_key
878
+
879
+ if @api_key.rate_limit_exceeded?
880
+ raise ReactiveActions::SecurityCheckError, "Rate limit exceeded"
881
+ end
882
+ end
883
+ end
884
+ ```
646
885
 
647
- function showFlash(message, type) {
648
- // Your flash message implementation
649
- console.log(`${type}: ${message}`);
650
- }
651
- </script>
886
+ ## 🚦 Rate Limiting
887
+
888
+ ReactiveActions provides comprehensive rate limiting functionality to protect your application from abuse and ensure fair resource usage. Rate limiting is **disabled by default** and must be explicitly enabled in your configuration.
889
+
890
+ ### πŸ”§ Configuration
891
+
892
+ Rate limiting is configured in your `config/initializers/reactive_actions.rb` file:
893
+
894
+ ```ruby
895
+ ReactiveActions.configure do |config|
896
+ # Enable rate limiting functionality
897
+ config.rate_limiting_enabled = true
898
+
899
+ # Enable global controller-level rate limiting
900
+ config.global_rate_limiting_enabled = true
901
+ config.global_rate_limit = 600 # 600 requests per window
902
+ config.global_rate_limit_window = 1.minute # per minute
903
+
904
+ # Optional: Custom rate limit key generator
905
+ config.rate_limit_key_generator = ->(request, action_name) do
906
+ user_id = request.headers['X-User-ID'] || 'anonymous'
907
+ "#{action_name}:user:#{user_id}"
908
+ end
909
+ end
652
910
  ```
653
911
 
654
- ### E-commerce Product Interactions
912
+ ### Configuration Options
655
913
 
656
- ```html
657
- <!-- Product Cards -->
658
- <div class="products-grid">
659
- <% @products.each do |product| %>
660
- <div class="product-card">
661
- <h3><%= product.name %></h3>
662
- <p class="price">$<%= product.price %></p>
914
+ | Option | Default | Description |
915
+ |--------|---------|-------------|
916
+ | `rate_limiting_enabled` | `false` | Master switch for all rate limiting features |
917
+ | `global_rate_limiting_enabled` | `false` | Enable controller-level rate limiting |
918
+ | `global_rate_limit` | `600` | Global rate limit (requests per window) |
919
+ | `global_rate_limit_window` | `1.minute` | Time window for global rate limiting |
920
+ | `rate_limit_key_generator` | `nil` | Custom key generator proc |
921
+
922
+ ### 🎯 Action-Level Rate Limiting
923
+
924
+ Include the `RateLimiter` concern in your actions to add rate limiting functionality:
925
+
926
+ ```ruby
927
+ # app/reactive_actions/api_action.rb
928
+ class ApiAction < ReactiveActions::ReactiveAction
929
+ include ReactiveActions::Concerns::RateLimiter
930
+
931
+ def action
932
+ # Basic rate limiting: 10 requests per minute per user
933
+ rate_limit!(key: "user:#{current_user&.id}", limit: 10, window: 1.minute)
934
+
935
+ @result = { data: "API response" }
936
+ end
937
+
938
+ def response
939
+ render json: @result
940
+ end
941
+ end
942
+ ```
943
+
944
+ ### πŸ”‘ Key-Based Rate Limiting
945
+
946
+ Rate limiting works with different key strategies:
947
+
948
+ ```ruby
949
+ class FlexibleRateLimitAction < ReactiveActions::ReactiveAction
950
+ include ReactiveActions::Concerns::RateLimiter
951
+
952
+ def action
953
+ case action_params[:rate_limit_type]
954
+ when 'user'
955
+ # User-specific rate limiting
956
+ rate_limit!(key: "user:#{current_user.id}", limit: 100, window: 1.hour)
957
+
958
+ when 'ip'
959
+ # IP-based rate limiting
960
+ rate_limit!(key: "ip:#{controller.request.remote_ip}", limit: 50, window: 15.minutes)
961
+
962
+ when 'api_key'
963
+ # API key-based rate limiting
964
+ api_key = action_params[:api_key]
965
+ rate_limit!(key: "api:#{api_key}", limit: 1000, window: 1.hour)
663
966
 
664
- <!-- Add to cart -->
665
- <button reactive-action="click->post#add_to_cart"
666
- reactive-action-product-id="<%= product.id %>"
667
- reactive-action-quantity="1"
668
- reactive-action-success="updateCartCount">
669
- Add to Cart
670
- </button>
967
+ when 'global'
968
+ # Global rate limiting for expensive operations
969
+ rate_limit!(key: "global:expensive_operation", limit: 10, window: 1.minute)
970
+ end
971
+
972
+ @result = { message: "Rate limit check passed" }
973
+ end
974
+
975
+ def response
976
+ render json: @result
977
+ end
978
+ end
979
+ ```
980
+
981
+ ### πŸ’° Cost-Based Rate Limiting
982
+
983
+ Assign different costs to different operations:
984
+
985
+ ```ruby
986
+ class CostBasedRateLimitAction < ReactiveActions::ReactiveAction
987
+ include ReactiveActions::Concerns::RateLimiter
988
+
989
+ def action
990
+ operation_type = action_params[:operation]
991
+ user_key = "user:#{current_user.id}"
992
+
993
+ case operation_type
994
+ when 'search'
995
+ # Light operation: cost 1
996
+ rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 1)
997
+
998
+ when 'export'
999
+ # Medium operation: cost 5
1000
+ rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 5)
1001
+
1002
+ when 'bulk_import'
1003
+ # Heavy operation: cost 20
1004
+ rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 20)
1005
+
1006
+ when 'report_generation'
1007
+ # Very heavy operation: cost 50
1008
+ rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 50)
1009
+ end
1010
+
1011
+ perform_operation(operation_type)
1012
+ end
1013
+
1014
+ def response
1015
+ render json: @result
1016
+ end
1017
+
1018
+ private
1019
+
1020
+ def perform_operation(type)
1021
+ @result = { operation: type, status: 'completed' }
1022
+ end
1023
+ end
1024
+ ```
1025
+
1026
+ ### πŸ“Š Rate Limiting Status and Management
1027
+
1028
+ Check and manage rate limiting status:
1029
+
1030
+ ```ruby
1031
+ class RateLimitManagementAction < ReactiveActions::ReactiveAction
1032
+ include ReactiveActions::Concerns::RateLimiter
1033
+
1034
+ def action
1035
+ user_key = "user:#{current_user.id}"
1036
+
1037
+ case action_params[:action_type]
1038
+ when 'status'
1039
+ # Check current rate limit status without consuming a request
1040
+ status = rate_limit_status(key: user_key, limit: 100, window: 1.hour)
1041
+ @result = { rate_limit_status: status }
671
1042
 
672
- <!-- Wishlist toggle -->
673
- <button reactive-action="click->post#toggle_wishlist"
674
- reactive-action-product-id="<%= product.id %>"
675
- reactive-action-success="toggleWishlistUI"
676
- class="<%= 'wishlisted' if current_user.wishlist.include?(product) %>">
677
- β™₯ Wishlist
678
- </button>
1043
+ when 'check_would_exceed'
1044
+ # Check if a specific cost would exceed the limit
1045
+ cost = action_params[:cost] || 1
1046
+ would_exceed = rate_limit_would_exceed?(
1047
+ key: user_key,
1048
+ limit: 100,
1049
+ window: 1.hour,
1050
+ cost: cost
1051
+ )
1052
+ @result = { would_exceed: would_exceed, cost: cost }
679
1053
 
680
- <!-- Quick view on hover -->
681
- <div reactive-action="mouseenter->get#quick_view"
682
- reactive-action-product-id="<%= product.id %>"
683
- reactive-action-success="showQuickView">
684
- <img src="<%= product.image %>" alt="<%= product.name %>">
685
- </div>
1054
+ when 'reset'
1055
+ # Reset rate limit for the user (admin functionality)
1056
+ reset_rate_limit!(key: user_key, window: 1.hour)
1057
+ @result = { message: "Rate limit reset for user", user_id: current_user.id }
686
1058
 
687
- <!-- Quantity selector -->
688
- <select reactive-action="change->put#update_cart_quantity"
689
- reactive-action-product-id="<%= product.id %>"
690
- reactive-action-success="updateCartTotal">
691
- <% (1..10).each do |qty| %>
692
- <option value="<%= qty %>"><%= qty %></option>
693
- <% end %>
694
- </select>
695
- </div>
696
- <% end %>
697
- </div>
698
-
699
- <!-- Product Filter -->
700
- <div class="filters">
701
- <select reactive-action="change->get#filter_products"
702
- reactive-action-success="updateProductGrid">
703
- <option value="">All Categories</option>
704
- <option value="electronics">Electronics</option>
705
- <option value="clothing">Clothing</option>
706
- <option value="books">Books</option>
707
- </select>
1059
+ when 'remaining'
1060
+ # Get remaining requests
1061
+ remaining = rate_limit_remaining(key: user_key, limit: 100, window: 1.hour)
1062
+ @result = { remaining: remaining }
1063
+ end
1064
+ end
1065
+
1066
+ def response
1067
+ render json: @result
1068
+ end
1069
+ end
1070
+ ```
1071
+
1072
+ ### 🌐 Global Controller-Level Rate Limiting
1073
+
1074
+ Enable global rate limiting across all ReactiveActions requests:
1075
+
1076
+ ```ruby
1077
+ # config/initializers/reactive_actions.rb
1078
+ ReactiveActions.configure do |config|
1079
+ config.rate_limiting_enabled = true
1080
+ config.global_rate_limiting_enabled = true
1081
+ config.global_rate_limit = 600 # 10 requests per second
1082
+ config.global_rate_limit_window = 1.minute # per minute window
1083
+ end
1084
+ ```
1085
+
1086
+ This automatically adds rate limiting to all ReactiveActions controller requests with appropriate headers:
1087
+
1088
+ ```
1089
+ X-RateLimit-Limit: 600
1090
+ X-RateLimit-Remaining: 599
1091
+ X-RateLimit-Window: 60
1092
+ X-RateLimit-Reset: 1672531260
1093
+ Retry-After: 30 # (when rate limited)
1094
+ ```
1095
+
1096
+ ### πŸŽ›οΈ Advanced Rate Limiting Features
1097
+
1098
+ #### Scoped Keys
1099
+ ```ruby
1100
+ class ScopedRateLimitAction < ReactiveActions::ReactiveAction
1101
+ include ReactiveActions::Concerns::RateLimiter
1102
+
1103
+ def action
1104
+ # Create scoped keys for different features
1105
+ api_key = rate_limit_key_for('api', identifier: current_user.id)
1106
+ search_key = rate_limit_key_for('search', identifier: current_user.id)
1107
+ upload_key = rate_limit_key_for('upload', identifier: current_user.id)
1108
+
1109
+ case action_params[:feature]
1110
+ when 'api'
1111
+ rate_limit!(key: api_key, limit: 1000, window: 1.hour)
1112
+ when 'search'
1113
+ rate_limit!(key: search_key, limit: 100, window: 1.minute)
1114
+ when 'upload'
1115
+ rate_limit!(key: upload_key, limit: 10, window: 1.minute)
1116
+ end
1117
+
1118
+ @result = { feature: action_params[:feature], status: 'allowed' }
1119
+ end
1120
+
1121
+ def response
1122
+ render json: @result
1123
+ end
1124
+ end
1125
+ ```
1126
+
1127
+ #### Custom Key Generators
1128
+ ```ruby
1129
+ # config/initializers/reactive_actions.rb
1130
+ ReactiveActions.configure do |config|
1131
+ config.rate_limiting_enabled = true
1132
+
1133
+ # Custom key generator for sophisticated rate limiting
1134
+ config.rate_limit_key_generator = ->(request, action_name) do
1135
+ # Multi-factor key generation
1136
+ user_id = request.headers['X-User-ID']
1137
+ api_key = request.headers['X-API-Key']
1138
+ user_tier = request.headers['X-User-Tier'] || 'basic'
1139
+
1140
+ if api_key.present?
1141
+ # API requests get higher limits
1142
+ "api:#{api_key}:#{action_name}"
1143
+ elsif user_id.present?
1144
+ # User-based with tier consideration
1145
+ "user:#{user_tier}:#{user_id}:#{action_name}"
1146
+ else
1147
+ # Anonymous requests get IP-based limiting
1148
+ "ip:#{request.remote_ip}:#{action_name}"
1149
+ end
1150
+ end
1151
+ end
1152
+ ```
1153
+
1154
+ #### Rate Limiting with Security Integration
1155
+ ```ruby
1156
+ class SecureRateLimitedAction < ReactiveActions::ReactiveAction
1157
+ include ReactiveActions::Concerns::RateLimiter
1158
+
1159
+ # Security checks run before rate limiting
1160
+ security_check :require_authentication
1161
+
1162
+ def action
1163
+ # Apply different limits based on user role
1164
+ limit = determine_user_limit
1165
+ window = determine_user_window
1166
+
1167
+ rate_limit!(
1168
+ key: "role:#{current_user.role}:#{current_user.id}",
1169
+ limit: limit,
1170
+ window: window
1171
+ )
1172
+
1173
+ perform_secure_operation
1174
+ end
1175
+
1176
+ def response
1177
+ render json: @result
1178
+ end
1179
+
1180
+ private
1181
+
1182
+ def require_authentication
1183
+ raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
1184
+ end
1185
+
1186
+ def determine_user_limit
1187
+ case current_user.role
1188
+ when 'admin'
1189
+ 1000 # Admins get higher limits
1190
+ when 'premium'
1191
+ 500 # Premium users get medium limits
1192
+ when 'basic'
1193
+ 100 # Basic users get standard limits
1194
+ else
1195
+ 50 # Default for other roles
1196
+ end
1197
+ end
1198
+
1199
+ def determine_user_window
1200
+ current_user.role == 'admin' ? 1.minute : 5.minutes
1201
+ end
1202
+
1203
+ def perform_secure_operation
1204
+ @result = {
1205
+ message: "Secure operation completed",
1206
+ user_role: current_user.role,
1207
+ rate_limit_applied: true
1208
+ }
1209
+ end
1210
+ end
1211
+ ```
1212
+
1213
+ ### πŸ—οΈ Custom Controller Rate Limiting
1214
+
1215
+ Add rate limiting to your own controllers:
1216
+
1217
+ ```ruby
1218
+ class ApiController < ApplicationController
1219
+ include ReactiveActions::Controller::RateLimiter
708
1220
 
709
- <input type="range"
710
- reactive-action="input->get#filter_by_price"
711
- reactive-action-success="updateProductGrid"
712
- min="0" max="1000" step="10">
713
- </div>
1221
+ # Rate limit specific actions
1222
+ rate_limit_action :show, limit: 100, window: 1.minute
1223
+ rate_limit_action :create, limit: 10, window: 1.minute, only: [:create]
1224
+
1225
+ # Skip rate limiting for certain actions
1226
+ skip_rate_limiting :health_check, :status
1227
+
1228
+ def show
1229
+ # This action is automatically rate limited
1230
+ render json: { data: "API response" }
1231
+ end
1232
+
1233
+ def create
1234
+ # This action has stricter rate limiting
1235
+ render json: { created: true }
1236
+ end
1237
+
1238
+ def health_check
1239
+ # This action skips rate limiting
1240
+ render json: { status: "ok" }
1241
+ end
1242
+ end
1243
+ ```
1244
+
1245
+ ### πŸ“ˆ Rate Limiting Monitoring and Logging
1246
+
1247
+ Monitor rate limiting events:
1248
+
1249
+ ```ruby
1250
+ class MonitoredRateLimitAction < ReactiveActions::ReactiveAction
1251
+ include ReactiveActions::Concerns::RateLimiter
1252
+
1253
+ def action
1254
+ user_key = "user:#{current_user.id}"
1255
+
1256
+ begin
1257
+ # Log rate limiting attempt
1258
+ log_rate_limit_event('attempt', {
1259
+ user_id: current_user.id,
1260
+ action: 'api_call'
1261
+ })
1262
+
1263
+ rate_limit!(key: user_key, limit: 100, window: 1.hour)
1264
+
1265
+ # Log successful rate limit check
1266
+ log_rate_limit_event('success', {
1267
+ user_id: current_user.id,
1268
+ remaining: rate_limit_remaining(key: user_key, limit: 100, window: 1.hour)
1269
+ })
1270
+
1271
+ @result = { status: 'success' }
1272
+
1273
+ rescue ReactiveActions::RateLimitExceededError => e
1274
+ # Log rate limit exceeded
1275
+ log_rate_limit_event('exceeded', {
1276
+ user_id: current_user.id,
1277
+ limit: e.limit,
1278
+ current: e.current,
1279
+ retry_after: e.retry_after
1280
+ })
1281
+
1282
+ raise e
1283
+ end
1284
+ end
1285
+
1286
+ def response
1287
+ render json: @result
1288
+ end
1289
+ end
1290
+ ```
1291
+
1292
+ ### πŸŽ›οΈ Rate Limiting Configuration Options
1293
+
1294
+ #### Enable Rate Limiting During Installation
1295
+
1296
+ ```bash
1297
+ # Enable rate limiting during installation
1298
+ $ rails generate reactive_actions:install --enable-rate-limiting --enable-global-rate-limiting --global-rate-limit=1000
1299
+ ```
1300
+
1301
+ #### Runtime Configuration Checks
1302
+
1303
+ ```ruby
1304
+ class ConditionalRateLimitAction < ReactiveActions::ReactiveAction
1305
+ include ReactiveActions::Concerns::RateLimiter
1306
+
1307
+ def action
1308
+ # Check if rate limiting is enabled before applying
1309
+ if rate_limiting_enabled?
1310
+ rate_limit!(key: "feature:#{action_params[:feature]}", limit: 50, window: 1.minute)
1311
+ @result = { rate_limiting: 'enabled', status: 'limited' }
1312
+ else
1313
+ @result = { rate_limiting: 'disabled', status: 'unlimited' }
1314
+ end
1315
+ end
1316
+
1317
+ def response
1318
+ render json: @result
1319
+ end
1320
+ end
1321
+ ```
1322
+
1323
+ ### ⚑ Rate Limiting Error Handling
1324
+
1325
+ Rate limiting errors are automatically handled and return structured responses:
1326
+
1327
+ ```json
1328
+ {
1329
+ "success": false,
1330
+ "error": {
1331
+ "type": "RateLimitExceededError",
1332
+ "message": "Rate limit exceeded: 101/100 requests in 1 minute",
1333
+ "code": "RATE_LIMIT_EXCEEDED",
1334
+ "limit": 100,
1335
+ "window": 60,
1336
+ "retry_after": 45
1337
+ }
1338
+ }
1339
+ ```
1340
+
1341
+ ### πŸš€ Performance Considerations
1342
+
1343
+ Rate limiting uses Rails cache for storage:
1344
+
1345
+ - **Production**: Use Redis or Memcached for distributed caching
1346
+ - **Development**: Uses memory store automatically
1347
+ - **Test**: Uses memory store to avoid cache pollution
1348
+
1349
+ ```ruby
1350
+ # config/environments/production.rb
1351
+ config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
1352
+ ```
1353
+
1354
+ ### πŸ”§ Rate Limiting Best Practices
1355
+
1356
+ 1. **Start Conservative**: Begin with generous limits and tighten based on usage patterns
1357
+ 2. **Use Appropriate Windows**: Shorter windows (1-5 minutes) for responsive limiting
1358
+ 3. **Different Limits for Different Operations**: Heavier operations should cost more
1359
+ 4. **Monitor and Alert**: Set up monitoring for rate limit violations
1360
+ 5. **Graceful Degradation**: Provide meaningful error messages and retry guidance
1361
+ 6. **User Tier Consideration**: Different limits for different user tiers
1362
+ 7. **API Documentation**: Document rate limits in your API documentation
1363
+
1364
+ ## πŸ’» Simple DOM Binding Examples
1365
+
1366
+ ### Basic Button Actions
1367
+
1368
+ ```html
1369
+ <!-- Simple button click -->
1370
+ <button reactive-action="click->test">Test Action</button>
1371
+
1372
+ <!-- Button with data attributes -->
1373
+ <button reactive-action="click->update_status"
1374
+ reactive-action-status="active">
1375
+ Update Status
1376
+ </button>
1377
+
1378
+ <!-- Button with HTTP method -->
1379
+ <button reactive-action="click->delete#remove_item">Delete Item</button>
1380
+ ```
1381
+
1382
+ ### Form Examples
1383
+
1384
+ ```html
1385
+ <!-- Simple form submission -->
1386
+ <form reactive-action="submit->create_item">
1387
+ <input name="title" type="text" required>
1388
+ <button type="submit">Create</button>
1389
+ </form>
1390
+
1391
+ <!-- Form with custom data -->
1392
+ <form reactive-action="submit->post#save_data"
1393
+ reactive-action-category="important">
1394
+ <input name="message" type="text" required>
1395
+ <button type="submit">Save</button>
1396
+ </form>
1397
+ ```
1398
+
1399
+ ### Input Events
1400
+
1401
+ ```html
1402
+ <!-- Live search -->
1403
+ <input type="text"
1404
+ reactive-action="input->search"
1405
+ placeholder="Search...">
1406
+
1407
+ <!-- Select dropdown -->
1408
+ <select reactive-action="change->filter_results">
1409
+ <option value="all">All Items</option>
1410
+ <option value="active">Active Only</option>
1411
+ </select>
1412
+ ```
1413
+
1414
+ ### Success/Error Handling
1415
+
1416
+ ```html
1417
+ <button reactive-action="click->test"
1418
+ reactive-action-success="showSuccess"
1419
+ reactive-action-error="showError">
1420
+ Test with Callbacks
1421
+ </button>
1422
+
1423
+ <script>
1424
+ function showSuccess(response) {
1425
+ alert('Success: ' + response.message);
1426
+ }
1427
+
1428
+ function showError(error) {
1429
+ alert('Error: ' + error.message);
1430
+ }
1431
+ </script>
714
1432
  ```
715
1433
 
716
1434
  ## Security
@@ -729,9 +1447,10 @@ ReactiveActions implements several security measures:
729
1447
  ```ruby
730
1448
  # Always validate user permissions
731
1449
  class SecureAction < ReactiveActions::ReactiveAction
1450
+ security_check :require_authentication
1451
+ security_check :validate_ownership
1452
+
732
1453
  def action
733
- raise ReactiveActions::UnauthorizedError unless current_user&.admin?
734
-
735
1454
  # Validate and sanitize inputs
736
1455
  user_id = action_params[:user_id].to_i
737
1456
  raise ReactiveActions::InvalidParametersError if user_id <= 0
@@ -741,6 +1460,19 @@ class SecureAction < ReactiveActions::ReactiveAction
741
1460
 
742
1461
  @result = User.find(user_id).update(permitted_params)
743
1462
  end
1463
+
1464
+ private
1465
+
1466
+ def require_authentication
1467
+ raise ReactiveActions::SecurityCheckError unless current_user
1468
+ end
1469
+
1470
+ def validate_ownership
1471
+ user_id = action_params[:user_id].to_i
1472
+ unless current_user.id == user_id || current_user.admin?
1473
+ raise ReactiveActions::SecurityCheckError, "Access denied"
1474
+ end
1475
+ end
744
1476
  end
745
1477
  ```
746
1478
 
@@ -765,6 +1497,8 @@ ReactiveActions provides structured error handling:
765
1497
  - `InvalidParametersError` - Invalid parameter format
766
1498
  - `UnauthorizedError` - Permission denied
767
1499
  - `ActionExecutionError` - Runtime execution error
1500
+ - `SecurityCheckError` - Security check failed
1501
+ - `RateLimitExceededError` - Rate limit exceeded
768
1502
 
769
1503
  ## πŸš„ Rails 8 Compatibility
770
1504
 
@@ -778,8 +1512,6 @@ Designed specifically for Rails 8:
778
1512
  ## πŸ—ΊοΈ Roadmap & Future Improvements
779
1513
 
780
1514
  Planned features:
781
- - Security hooks for authentication/authorization
782
- - Rate limiting and throttling
783
1515
  - Enhanced error handling
784
1516
  - Action composition for complex workflows
785
1517
  - Built-in testing utilities