otto 1.6.0 → 2.0.0.pre2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -2
  3. data/.github/workflows/claude-code-review.yml +53 -0
  4. data/.github/workflows/claude.yml +49 -0
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +26 -344
  7. data/CHANGELOG.rst +131 -0
  8. data/CLAUDE.md +56 -0
  9. data/Gemfile +11 -4
  10. data/Gemfile.lock +38 -42
  11. data/README.md +2 -0
  12. data/bin/rspec +4 -4
  13. data/changelog.d/README.md +120 -0
  14. data/changelog.d/scriv.ini +5 -0
  15. data/docs/.gitignore +2 -0
  16. data/docs/migrating/v2.0.0-pre1.md +276 -0
  17. data/docs/migrating/v2.0.0-pre2.md +345 -0
  18. data/examples/.gitignore +1 -0
  19. data/examples/advanced_routes/README.md +33 -0
  20. data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
  21. data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
  22. data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
  23. data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
  24. data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
  25. data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
  26. data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
  27. data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
  28. data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
  29. data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
  30. data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
  31. data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
  32. data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
  33. data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
  34. data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
  35. data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
  36. data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
  37. data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
  38. data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
  39. data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
  40. data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
  41. data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
  42. data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
  43. data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
  44. data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
  45. data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
  46. data/examples/advanced_routes/app.rb +33 -0
  47. data/examples/advanced_routes/config.rb +23 -0
  48. data/examples/advanced_routes/config.ru +7 -0
  49. data/examples/advanced_routes/puma.rb +20 -0
  50. data/examples/advanced_routes/routes +167 -0
  51. data/examples/advanced_routes/run.rb +39 -0
  52. data/examples/advanced_routes/test.rb +58 -0
  53. data/examples/authentication_strategies/README.md +32 -0
  54. data/examples/authentication_strategies/app/auth.rb +68 -0
  55. data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
  56. data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
  57. data/examples/authentication_strategies/config.ru +24 -0
  58. data/examples/authentication_strategies/routes +37 -0
  59. data/examples/basic/README.md +29 -0
  60. data/examples/basic/app.rb +7 -35
  61. data/examples/basic/routes +0 -9
  62. data/examples/mcp_demo/README.md +87 -0
  63. data/examples/mcp_demo/app.rb +29 -34
  64. data/examples/mcp_demo/config.ru +9 -60
  65. data/examples/security_features/README.md +46 -0
  66. data/examples/security_features/app.rb +23 -24
  67. data/examples/security_features/config.ru +8 -10
  68. data/lib/otto/core/configuration.rb +167 -0
  69. data/lib/otto/core/error_handler.rb +86 -0
  70. data/lib/otto/core/file_safety.rb +61 -0
  71. data/lib/otto/core/middleware_stack.rb +237 -0
  72. data/lib/otto/core/router.rb +184 -0
  73. data/lib/otto/core/uri_generator.rb +44 -0
  74. data/lib/otto/design_system.rb +7 -5
  75. data/lib/otto/env_keys.rb +114 -0
  76. data/lib/otto/helpers/base.rb +5 -21
  77. data/lib/otto/helpers/request.rb +10 -8
  78. data/lib/otto/helpers/response.rb +27 -4
  79. data/lib/otto/helpers/validation.rb +9 -7
  80. data/lib/otto/mcp/auth/token.rb +10 -9
  81. data/lib/otto/mcp/protocol.rb +24 -27
  82. data/lib/otto/mcp/rate_limiting.rb +8 -3
  83. data/lib/otto/mcp/registry.rb +7 -2
  84. data/lib/otto/mcp/route_parser.rb +10 -15
  85. data/lib/otto/mcp/{validation.rb → schema_validation.rb} +16 -11
  86. data/lib/otto/mcp/server.rb +45 -22
  87. data/lib/otto/response_handlers/auto.rb +39 -0
  88. data/lib/otto/response_handlers/base.rb +16 -0
  89. data/lib/otto/response_handlers/default.rb +16 -0
  90. data/lib/otto/response_handlers/factory.rb +39 -0
  91. data/lib/otto/response_handlers/json.rb +34 -0
  92. data/lib/otto/response_handlers/redirect.rb +25 -0
  93. data/lib/otto/response_handlers/view.rb +24 -0
  94. data/lib/otto/response_handlers.rb +9 -135
  95. data/lib/otto/route.rb +51 -55
  96. data/lib/otto/route_definition.rb +15 -18
  97. data/lib/otto/route_handlers/base.rb +121 -0
  98. data/lib/otto/route_handlers/class_method.rb +89 -0
  99. data/lib/otto/route_handlers/factory.rb +42 -0
  100. data/lib/otto/route_handlers/instance_method.rb +69 -0
  101. data/lib/otto/route_handlers/lambda.rb +59 -0
  102. data/lib/otto/route_handlers/logic_class.rb +93 -0
  103. data/lib/otto/route_handlers.rb +10 -405
  104. data/lib/otto/security/authentication/auth_strategy.rb +44 -0
  105. data/lib/otto/security/authentication/authentication_middleware.rb +140 -0
  106. data/lib/otto/security/authentication/failure_result.rb +44 -0
  107. data/lib/otto/security/authentication/route_auth_wrapper.rb +149 -0
  108. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
  109. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +19 -0
  110. data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
  111. data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
  112. data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
  113. data/lib/otto/security/authentication/strategy_result.rb +337 -0
  114. data/lib/otto/security/authentication.rb +28 -282
  115. data/lib/otto/security/config.rb +14 -23
  116. data/lib/otto/security/configurator.rb +219 -0
  117. data/lib/otto/security/csrf.rb +8 -143
  118. data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
  119. data/lib/otto/security/middleware/rate_limit_middleware.rb +54 -0
  120. data/lib/otto/security/middleware/validation_middleware.rb +252 -0
  121. data/lib/otto/security/rate_limiter.rb +86 -0
  122. data/lib/otto/security/rate_limiting.rb +10 -105
  123. data/lib/otto/security/validator.rb +8 -253
  124. data/lib/otto/static.rb +3 -0
  125. data/lib/otto/utils.rb +14 -0
  126. data/lib/otto/version.rb +3 -1
  127. data/lib/otto.rb +141 -498
  128. data/otto.gemspec +4 -2
  129. metadata +99 -18
  130. data/examples/dynamic_pages/app.rb +0 -115
  131. data/examples/dynamic_pages/config.ru +0 -30
  132. data/examples/dynamic_pages/routes +0 -21
  133. data/examples/helpers_demo/app.rb +0 -244
  134. data/examples/helpers_demo/config.ru +0 -26
  135. data/examples/helpers_demo/routes +0 -7
  136. data/lib/concurrent_cache_store.rb +0 -68
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module V2
4
+ module Logic
5
+ class Processor
6
+ attr_reader :context, :params, :locale
7
+
8
+ def initialize(context, params, locale)
9
+ @context = context
10
+ @params = params
11
+ @locale = locale
12
+ end
13
+
14
+ def process
15
+ {
16
+ v2_processor: 'Complete',
17
+ csrf_exempt: true,
18
+ processing_time: 0.05,
19
+ }
20
+ end
21
+
22
+ def response_data
23
+ { v2_processor: process }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Application Loader
4
+
5
+ # Controllers
6
+ require_relative 'app/controllers/routes_app'
7
+ require_relative 'app/controllers/handlers/async'
8
+ require_relative 'app/controllers/handlers/dynamic'
9
+ require_relative 'app/controllers/handlers/static'
10
+ require_relative 'app/controllers/modules/auth'
11
+ require_relative 'app/controllers/modules/transformer'
12
+ require_relative 'app/controllers/modules/validator'
13
+ require_relative 'app/controllers/v2/admin'
14
+ require_relative 'app/controllers/v2/config'
15
+ require_relative 'app/controllers/v2/settings'
16
+
17
+ # Logic Classes
18
+ require_relative 'app/logic/admin/logic/manager'
19
+ require_relative 'app/logic/admin/panel'
20
+ require_relative 'app/logic/analytics_processor'
21
+ require_relative 'app/logic/complex/business/handler'
22
+ require_relative 'app/logic/data_logic'
23
+ require_relative 'app/logic/data_processor'
24
+ require_relative 'app/logic/input_validator'
25
+ require_relative 'app/logic/nested/feature/logic'
26
+ require_relative 'app/logic/reports_generator'
27
+ require_relative 'app/logic/simple_logic'
28
+ require_relative 'app/logic/system/config/manager'
29
+ require_relative 'app/logic/test_logic'
30
+ require_relative 'app/logic/transform_logic'
31
+ require_relative 'app/logic/upload_logic'
32
+ require_relative 'app/logic/v2/logic/dashboard'
33
+ require_relative 'app/logic/v2/logic/processor'
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require_relative '../../lib/otto'
5
+ require_relative 'app'
6
+
7
+ # Simple Otto configuration demonstrating advanced routes syntax
8
+ otto = Otto.new('routes')
9
+
10
+ # Enable basic security features to demonstrate CSRF functionality
11
+ otto.enable_csrf_protection!
12
+
13
+ # Set error handlers
14
+ otto.not_found = lambda do |_env|
15
+ RoutesApp.not_found
16
+ end
17
+
18
+ otto.server_error = lambda do |_env, _error|
19
+ RoutesApp.server_error
20
+ end
21
+
22
+ # Return the configured app. This allows runner scripts to use the same instance.
23
+ otto
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load the app from our shared config file.
4
+ # This returns a configured Otto instance.
5
+ app = require_relative 'config'
6
+
7
+ run app
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'puma'
5
+
6
+ # The shared config file returns the configured Otto app
7
+ otto_app = require_relative 'config'
8
+
9
+ # Configure Puma server
10
+ Puma::Server.new(otto_app).tap do |server|
11
+ server.add_tcp_listener '127.0.0.1', 9292
12
+
13
+ puts "Otto Advanced Routes Example running on http://localhost:9292"
14
+ puts "Press Ctrl+C to stop"
15
+
16
+ # Handle Ctrl+C gracefully
17
+ trap('INT') { server.stop }
18
+
19
+ server.run
20
+ end
@@ -0,0 +1,167 @@
1
+ # Advanced Routes Syntax - Otto v1.5.0+ Features
2
+ # This file demonstrates the advanced routing syntax without complex authentication
3
+
4
+ # ========================================
5
+ # BASIC ROUTES (Original Otto Syntax)
6
+ # ========================================
7
+
8
+ GET / RoutesApp#index
9
+ POST /feedback RoutesApp#receive_feedback
10
+
11
+ # ========================================
12
+ # RESPONSE TYPE ROUTES
13
+ # ========================================
14
+
15
+ # JSON response routes
16
+ GET /api/users RoutesApp#list_users response=json
17
+ POST /api/users RoutesApp#create_user response=json
18
+ GET /api/health RoutesApp#health_check response=json
19
+ PUT /api/users/:id RoutesApp#update_user response=json
20
+ DELETE /api/users/:id RoutesApp#delete_user response=json
21
+
22
+ # View/HTML response routes
23
+ GET /dashboard RoutesApp#dashboard response=view
24
+ GET /reports RoutesApp#reports response=view
25
+ GET /admin RoutesApp#admin_panel response=view
26
+
27
+ # Redirect response routes
28
+ GET /login RoutesApp#login_redirect response=redirect
29
+ GET /logout RoutesApp#logout_redirect response=redirect
30
+ GET /home RoutesApp#home_redirect response=redirect
31
+
32
+ # Auto response type (content negotiation)
33
+ GET /data RoutesApp#flexible_data response=auto
34
+ GET /content RoutesApp#flexible_content response=auto
35
+
36
+ # ========================================
37
+ # CSRF PROTECTION ROUTES
38
+ # ========================================
39
+
40
+ # CSRF exempt routes (useful for APIs and webhooks)
41
+ POST /api/webhook RoutesApp#webhook_handler csrf=exempt
42
+ PUT /api/external RoutesApp#external_update csrf=exempt
43
+ DELETE /api/cleanup RoutesApp#cleanup_data csrf=exempt
44
+ PATCH /api/sync RoutesApp#sync_data csrf=exempt
45
+
46
+ # Standard CSRF protected routes (default behavior)
47
+ POST /settings RoutesApp#update_settings
48
+ PUT /password RoutesApp#change_password
49
+ DELETE /profile RoutesApp#delete_profile
50
+
51
+ # ========================================
52
+ # MULTIPLE PARAMETER COMBINATIONS
53
+ # ========================================
54
+
55
+ # API routes with response type and CSRF exemption
56
+ GET /api/v1/data RoutesApp#api_data response=json
57
+ POST /api/v1/submit RoutesApp#api_submit response=json csrf=exempt
58
+ PUT /api/v1/update RoutesApp#api_update response=json csrf=exempt
59
+
60
+ # View routes with multiple parameters
61
+ GET /admin/dashboard RoutesApp#admin_dashboard response=view
62
+ POST /admin/settings RoutesApp#admin_settings response=view
63
+
64
+ # Mixed content routes
65
+ GET /mixed/endpoint RoutesApp#mixed_content response=auto csrf=exempt
66
+
67
+ # ========================================
68
+ # LOGIC CLASS ROUTES (New in v1.5.0+)
69
+ # ========================================
70
+
71
+ # Simple Logic class (no . or # in target)
72
+ GET /logic/simple SimpleLogic
73
+ POST /logic/process DataProcessor
74
+ PUT /logic/validate InputValidator
75
+
76
+ # Namespaced Logic classes
77
+ GET /logic/admin Admin::Panel
78
+ GET /logic/reports Reports::Generator
79
+ POST /logic/analytics Analytics::Processor
80
+
81
+ # Logic classes with parameters
82
+ GET /logic/data DataLogic response=json
83
+ POST /logic/upload UploadLogic response=json csrf=exempt
84
+ PUT /logic/transform TransformLogic response=json
85
+
86
+ # Complex namespaced Logic routes
87
+ GET /logic/v2/dashboard V2::Logic::Dashboard response=view
88
+ POST /logic/v2/process V2::Logic::Processor response=json csrf=exempt
89
+ GET /logic/admin/manager Admin::Logic::Manager response=json
90
+
91
+ # Deeply nested Logic classes
92
+ GET /logic/nested/feature Nested::Feature::Logic
93
+ POST /logic/complex/handler Complex::Business::Handler response=json
94
+ PUT /logic/system/config System::Config::Manager response=json csrf=exempt
95
+
96
+ # ========================================
97
+ # NAMESPACED CLASS ROUTES
98
+ # ========================================
99
+
100
+ # Class method routes with namespaces
101
+ GET /v2/admin V2::Admin.show response=view
102
+ POST /v2/config V2::Config.update response=json
103
+ PUT /v2/settings V2::Settings.modify response=json csrf=exempt
104
+
105
+ # Instance method routes with namespaces
106
+ GET /modules/auth Modules::Auth#process
107
+ POST /modules/validator Modules::Validator#validate response=json
108
+ PUT /modules/transformer Modules::Transformer#transform response=json csrf=exempt
109
+
110
+ # Mixed class and instance methods
111
+ GET /handlers/static Handlers::Static.serve
112
+ POST /handlers/dynamic Handlers::Dynamic#process response=json
113
+ PUT /handlers/async Handlers::Async#execute response=json csrf=exempt
114
+
115
+ # ========================================
116
+ # CUSTOM PARAMETERS
117
+ # ========================================
118
+
119
+ # Routes with custom configuration parameters
120
+ GET /config/env RoutesApp#show_config env=production
121
+ GET /config/debug RoutesApp#debug_info env=development debug=true
122
+ POST /config/update RoutesApp#update_config env=production response=json
123
+
124
+ # Routes with multiple custom parameters
125
+ GET /feature/flags RoutesApp#feature_flags feature=advanced mode=enabled
126
+ POST /feature/toggle RoutesApp#toggle_feature feature=beta mode=test response=json csrf=exempt
127
+
128
+ # ========================================
129
+ # PARAMETER VALUE VARIATIONS
130
+ # ========================================
131
+
132
+ # Parameters with special values
133
+ GET /api/v1 RoutesApp#api_v1 version=1.0 response=json
134
+ GET /api/v2 RoutesApp#api_v2 version=2.0 response=json
135
+ POST /api/legacy RoutesApp#api_legacy version=legacy response=json csrf=exempt
136
+
137
+ # Parameters with equals in values (edge case)
138
+ GET /query/complex RoutesApp#complex_query filter=key=value response=json
139
+ POST /config/connection RoutesApp#config_db connection=host=localhost csrf=exempt
140
+
141
+ # ========================================
142
+ # ERROR HANDLERS
143
+ # ========================================
144
+
145
+ GET /404 RoutesApp#not_found response=view
146
+ GET /500 RoutesApp#server_error response=view
147
+
148
+ # ========================================
149
+ # TESTING ROUTES
150
+ # ========================================
151
+
152
+ # Routes for testing different parameter combinations
153
+ GET /test/json RoutesApp#test_json response=json
154
+ GET /test/view RoutesApp#test_view response=view
155
+ GET /test/redirect RoutesApp#test_redirect response=redirect
156
+ GET /test/auto RoutesApp#test_auto response=auto
157
+
158
+ POST /test/csrf RoutesApp#test_csrf
159
+ POST /test/no-csrf RoutesApp#test_no_csrf csrf=exempt
160
+
161
+ GET /test/logic TestLogic
162
+ POST /test/logic-json TestLogic response=json
163
+ PUT /test/logic-exempt TestLogic response=json csrf=exempt
164
+
165
+ # Complex test routes
166
+ GET /test/complex TestLogic response=auto
167
+ POST /test/everything RoutesApp#test_everything response=json csrf=exempt custom=value
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'webrick'
5
+
6
+ # The shared config file returns the configured Otto app
7
+ otto_app = require_relative 'config'
8
+
9
+ # Create a simple WEBrick server
10
+ server = WEBrick::HTTPServer.new(
11
+ Port: 9292,
12
+ Logger: WEBrick::Log.new($stdout, WEBrick::Log::INFO)
13
+ )
14
+
15
+ # Mount the Otto app
16
+ server.mount '/', WEBrick::HTTPServlet::ProcHandler.new(proc { |req, res|
17
+ env = req.meta_vars.merge({
18
+ 'REQUEST_METHOD' => req.request_method,
19
+ 'PATH_INFO' => req.path_info,
20
+ 'QUERY_STRING' => req.query_string || '',
21
+ 'rack.input' => StringIO.new(req.body || ''),
22
+ 'CONTENT_TYPE' => req.content_type,
23
+ 'CONTENT_LENGTH' => req.content_length&.to_s
24
+ })
25
+
26
+ status, headers, body = otto_app.call(env)
27
+
28
+ res.status = status
29
+ headers.each { |k, v| res[k] = v }
30
+ body.each { |chunk| res.body << chunk }
31
+ })
32
+
33
+ # Handle Ctrl+C gracefully
34
+ trap('INT') { server.shutdown }
35
+
36
+ puts "Otto Advanced Routes Example running on http://localhost:9292"
37
+ puts "Press Ctrl+C to stop"
38
+
39
+ server.start
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # The shared config file returns the configured Otto app
5
+ otto_app = require_relative 'config'
6
+
7
+ # Simple test runner to demonstrate routes without a server
8
+ def test_route(method, path, params = {})
9
+ query_string = params.map { |k, v| "#{k}=#{v}" }.join('&')
10
+
11
+ env = {
12
+ 'REQUEST_METHOD' => method.to_s.upcase,
13
+ 'PATH_INFO' => path,
14
+ 'QUERY_STRING' => query_string,
15
+ 'rack.input' => StringIO.new(''),
16
+ 'HTTP_ACCEPT' => 'application/json',
17
+ }
18
+
19
+ status, headers, body = otto_app.call(env)
20
+
21
+ puts "#{method.to_s.upcase} #{path}#{query_string.empty? ? '' : '?' + query_string}"
22
+ puts "Status: #{status}"
23
+ puts "Content-Type: #{headers['content-type'] || headers['Content-Type']}"
24
+ puts "Body: #{body.join}"
25
+ puts "---"
26
+ end
27
+
28
+ puts "Otto Advanced Routes Syntax Test"
29
+ puts "================================"
30
+
31
+ # Test basic routes
32
+ test_route(:get, '/')
33
+ test_route(:post, '/feedback')
34
+
35
+ # Test JSON routes
36
+ test_route(:get, '/api/users')
37
+ test_route(:get, '/api/health')
38
+
39
+ # Test Logic classes
40
+ test_route(:get, '/logic/simple')
41
+ test_route(:post, '/logic/process')
42
+
43
+ # Test namespaced Logic classes
44
+ test_route(:get, '/logic/admin')
45
+ test_route(:get, '/logic/v2/dashboard')
46
+
47
+ # Test CSRF exempt routes
48
+ test_route(:post, '/api/webhook')
49
+ test_route(:put, '/api/external')
50
+
51
+ # Test custom parameters
52
+ test_route(:get, '/config/env')
53
+ test_route(:get, '/api/v1')
54
+
55
+ # Test complex routes
56
+ test_route(:post, '/test/everything')
57
+
58
+ puts "All tests completed!"
@@ -0,0 +1,32 @@
1
+ # Otto - Authentication Strategies Example
2
+
3
+ This example demonstrates how to use Otto's powerful authentication features.
4
+
5
+ ## Structure
6
+
7
+ * `config.ru`: The Rackup file. It initializes Otto and loads the authentication strategies.
8
+ * `routes`: Defines the application's routes and the authentication required for each.
9
+ * `app/auth.rb`: Contains the definitions for all authentication strategies. This is where you would add your own.
10
+ * `app/controllers/`: Contains the controller classes that handle requests.
11
+
12
+ ## Running the Demo
13
+
14
+ 1. Make sure you have the necessary gems installed (`bundle install`).
15
+ 2. Run the application from the root of the `otto` project:
16
+
17
+ ```sh
18
+ rackup examples/authentication_strategies/config.ru
19
+ ```
20
+
21
+ 3. Open your browser and navigate to `http://localhost:9292`.
22
+
23
+ ## Trying the Authentication Strategies
24
+
25
+ You can test the different authentication strategies by providing a `token` or `api_key` parameter in the URL.
26
+
27
+ * **Authenticated User:** [http://localhost:9292/profile?token=demo_token](http://localhost:9292/profile?token=demo_token)
28
+ * **Admin User:** [http://localhost:9292/admin?token=admin_token](http://localhost:9292/admin?token=admin_token)
29
+ * **User with 'write' permission:** [http://localhost:9292/edit?token=demo_token](http://localhost:9292/edit?token=demo_token)
30
+ * **API Key:** [http://localhost:9292/api/data?api_key=demo_api_key_123](http://localhost:9292/api/data?api_key=demo_api_key_123)
31
+
32
+ If you try to access a protected route without the correct token, you'll get an authentication error.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Authentication result data class to contain the session, user and anything
4
+ # else we want to make available to the route handlers/controllers/logic classes.
5
+ AuthResultData = Data.define(:session, :user) do
6
+ def initialize(session: {}, user: {})
7
+ super(session: session, user: user)
8
+ end
9
+
10
+ # Provide user_context method for compatibility with existing AuthResult
11
+ def user_context
12
+ { session: session, user: user }
13
+ end
14
+ end
15
+
16
+ module AuthenticationSetup
17
+ def self.configure(otto)
18
+ # Simple auth strategy that checks for a token parameter or header
19
+ otto.add_auth_strategy('authenticated', lambda do |req|
20
+ token = req.params['token'] || req.get_header('HTTP_AUTHORIZATION')
21
+ if token == 'demo_token'
22
+ AuthResultData.new(
23
+ session: { session_id: 'demo_session_123', user_id: 1 },
24
+ user: { name: 'Demo User', role: 'user', permissions: %w[read write] }
25
+ )
26
+ end
27
+ end)
28
+
29
+ # Role-based auth strategy
30
+ otto.add_auth_strategy('role:admin', lambda do |req|
31
+ token = req.params['token'] || req.get_header('HTTP_AUTHORIZATION')
32
+ if token == 'admin_token'
33
+ AuthResultData.new(
34
+ session: { session_id: 'admin_session_456', user_id: 2 },
35
+ user: { name: 'Admin User', role: 'admin', permissions: %w[read write admin delete] }
36
+ )
37
+ end
38
+ end)
39
+
40
+ # Permission-based auth strategy
41
+ otto.add_auth_strategy('permission:write', lambda do |req|
42
+ token = req.params['token'] || req.get_header('HTTP_AUTHORIZATION')
43
+ case token
44
+ when 'demo_token'
45
+ AuthResultData.new(
46
+ session: { session_id: 'demo_session_123', user_id: 1 },
47
+ user: { name: 'Demo User', role: 'user', permissions: %w[read write] }
48
+ )
49
+ when 'admin_token'
50
+ AuthResultData.new(
51
+ session: { session_id: 'admin_session_456', user_id: 2 },
52
+ user: { name: 'Admin User', role: 'admin', permissions: %w[read write admin delete] }
53
+ )
54
+ end
55
+ end)
56
+
57
+ # API key auth strategy
58
+ otto.add_auth_strategy('api_key', lambda do |req|
59
+ api_key = req.params['api_key'] || req.get_header('HTTP_X_API_KEY')
60
+ if api_key == 'demo_api_key_123'
61
+ AuthResultData.new(
62
+ session: { api_session: 'api_session_abc' },
63
+ user: { name: 'API Client', type: 'api', permissions: ['api_access'] }
64
+ )
65
+ end
66
+ end)
67
+ end
68
+ end
@@ -0,0 +1,29 @@
1
+ require 'json'
2
+
3
+ class AuthController
4
+ def self.login_form
5
+ [200, { 'content-type' => 'text/html' }, ['<h1>Login</h1><p>Use ?token=demo_token, ?token=admin_token, or ?api_key=demo_api_key_123</p>']]
6
+ end
7
+
8
+ def self.login
9
+ # In a real app, you'd handle login logic here.
10
+ # For this demo, we redirect to the profile.
11
+ [302, { 'location' => '/profile' }, ['']]
12
+ end
13
+
14
+ def self.show_profile
15
+ [200, { 'content-type' => 'text/html' }, ['<h1>User Profile</h1><p>You are authenticated.</p>']]
16
+ end
17
+
18
+ def self.admin_panel
19
+ [200, { 'content-type' => 'text/html' }, ['<h1>Admin Panel</h1><p>Role: admin required</p>']]
20
+ end
21
+
22
+ def self.edit_content
23
+ [200, { 'content-type' => 'text/html' }, ['<h1>Edit Content</h1><p>Permission: write required</p>']]
24
+ end
25
+
26
+ def self.api_data
27
+ [200, { 'content-type' => 'application/json' }, ['{"data": "This is some secret API data."}']]
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+ require 'time'
3
+
4
+ class MainController
5
+ def self.index
6
+ [200, { 'content-type' => 'text/html' }, ['<h1>Authentication Strategies Example</h1>']]
7
+ end
8
+
9
+ def self.receive_feedback
10
+ [200, { 'content-type' => 'text/plain' }, ['Feedback received']]
11
+ end
12
+
13
+ def self.dashboard
14
+ [200, { 'content-type' => 'text/html' }, ['<h1>Dashboard</h1><p>Welcome to your dashboard!</p>']]
15
+ end
16
+
17
+ def self.reports
18
+ [200, { 'content-type' => 'text/html' }, ['<h1>Reports</h1><p>Admin-only reports section</p>']]
19
+ end
20
+
21
+ def self.not_found
22
+ [404, { 'content-type' => 'text/html' }, ['<h1>404 - Page Not Found</h1><p>Advanced routes example</p>']]
23
+ end
24
+
25
+ def self.server_error
26
+ [500, { 'content-type' => 'text/html' }, ['<h1>500 - Server Error</h1><p>Something went wrong</p>']]
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require_relative '../../lib/otto'
5
+ require_relative 'app/auth'
6
+ require_relative 'app/controllers/main_controller'
7
+ require_relative 'app/controllers/auth_controller'
8
+
9
+ # Configure Otto with advanced features
10
+ otto = Otto.new('routes')
11
+
12
+ # Enable security features to demonstrate advanced route parameters
13
+ otto.enable_csrf_protection!
14
+ otto.enable_request_validation!
15
+ otto.enable_authentication!
16
+
17
+ # Load and configure authentication strategies
18
+ AuthenticationSetup.configure(otto)
19
+
20
+ # Set error handlers
21
+ otto.not_found = ->(_env) { MainController.not_found }
22
+ otto.server_error = ->(_env, _error) { MainController.server_error }
23
+
24
+ run otto
@@ -0,0 +1,37 @@
1
+ # Authentication Strategies Example
2
+
3
+ # ========================================
4
+ # PUBLIC ROUTES
5
+ # ========================================
6
+
7
+ GET / MainController#index
8
+ GET /login AuthController#login_form
9
+ POST /login AuthController#login
10
+
11
+ # ========================================
12
+ # AUTHENTICATED ROUTES
13
+ # ========================================
14
+
15
+ # Requires a user to be logged in.
16
+ # Try: /profile?token=demo_token
17
+ GET /profile AuthController#show_profile auth=authenticated
18
+
19
+ # Requires the 'admin' role.
20
+ # Try: /admin?token=admin_token
21
+ GET /admin AuthController#admin_panel auth=role:admin
22
+
23
+ # Requires the 'write' permission.
24
+ # A user with the 'user' role has this.
25
+ # Try: /edit?token=demo_token
26
+ GET /edit AuthController#edit_content auth=permission:write
27
+
28
+ # A route protected by an API key
29
+ # Try: /api/data?api_key=demo_api_key_123
30
+ GET /api/data AuthController#api_data auth=api_key response=json
31
+
32
+ # ========================================
33
+ # ERROR HANDLERS
34
+ # ========================================
35
+
36
+ GET /404 MainController#not_found response=view
37
+ GET /500 MainController#server_error response=view
@@ -0,0 +1,29 @@
1
+ # Otto - Basic Example
2
+
3
+ This example demonstrates a basic Otto application with a single route that accepts both GET and POST requests.
4
+
5
+ ## How to Run
6
+
7
+ 1. Make sure you have `bundler` and `thin` installed:
8
+ ```sh
9
+ gem install bundler thin
10
+ ```
11
+
12
+ 2. Install the dependencies from the root of the project:
13
+ ```sh
14
+ bundle install
15
+ ```
16
+
17
+ 3. Start the server from this directory (`examples/basic`):
18
+ ```sh
19
+ thin -e dev -R config.ru -p 10770 start
20
+ ```
21
+
22
+ 4. Open your browser and navigate to `http://localhost:10770`.
23
+
24
+ ## File Structure
25
+
26
+ * `README.md`: This file.
27
+ * `app.rb`: Contains the application logic. It has two methods: `index` to display the main page and `receive_feedback` to handle form submissions.
28
+ * `config.ru`: The Rack configuration file that loads the Otto framework and the application.
29
+ * `routes`: Defines the URL routes and maps them to methods in the `App` class.