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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +3 -2
- data/.github/workflows/claude-code-review.yml +53 -0
- data/.github/workflows/claude.yml +49 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +26 -344
- data/CHANGELOG.rst +131 -0
- data/CLAUDE.md +56 -0
- data/Gemfile +11 -4
- data/Gemfile.lock +38 -42
- data/README.md +2 -0
- data/bin/rspec +4 -4
- data/changelog.d/README.md +120 -0
- data/changelog.d/scriv.ini +5 -0
- data/docs/.gitignore +2 -0
- data/docs/migrating/v2.0.0-pre1.md +276 -0
- data/docs/migrating/v2.0.0-pre2.md +345 -0
- data/examples/.gitignore +1 -0
- data/examples/advanced_routes/README.md +33 -0
- data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
- data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
- data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
- data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
- data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
- data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
- data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
- data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
- data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
- data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
- data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
- data/examples/advanced_routes/app.rb +33 -0
- data/examples/advanced_routes/config.rb +23 -0
- data/examples/advanced_routes/config.ru +7 -0
- data/examples/advanced_routes/puma.rb +20 -0
- data/examples/advanced_routes/routes +167 -0
- data/examples/advanced_routes/run.rb +39 -0
- data/examples/advanced_routes/test.rb +58 -0
- data/examples/authentication_strategies/README.md +32 -0
- data/examples/authentication_strategies/app/auth.rb +68 -0
- data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
- data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
- data/examples/authentication_strategies/config.ru +24 -0
- data/examples/authentication_strategies/routes +37 -0
- data/examples/basic/README.md +29 -0
- data/examples/basic/app.rb +7 -35
- data/examples/basic/routes +0 -9
- data/examples/mcp_demo/README.md +87 -0
- data/examples/mcp_demo/app.rb +29 -34
- data/examples/mcp_demo/config.ru +9 -60
- data/examples/security_features/README.md +46 -0
- data/examples/security_features/app.rb +23 -24
- data/examples/security_features/config.ru +8 -10
- data/lib/otto/core/configuration.rb +167 -0
- data/lib/otto/core/error_handler.rb +86 -0
- data/lib/otto/core/file_safety.rb +61 -0
- data/lib/otto/core/middleware_stack.rb +237 -0
- data/lib/otto/core/router.rb +184 -0
- data/lib/otto/core/uri_generator.rb +44 -0
- data/lib/otto/design_system.rb +7 -5
- data/lib/otto/env_keys.rb +114 -0
- data/lib/otto/helpers/base.rb +5 -21
- data/lib/otto/helpers/request.rb +10 -8
- data/lib/otto/helpers/response.rb +27 -4
- data/lib/otto/helpers/validation.rb +9 -7
- data/lib/otto/mcp/auth/token.rb +10 -9
- data/lib/otto/mcp/protocol.rb +24 -27
- data/lib/otto/mcp/rate_limiting.rb +8 -3
- data/lib/otto/mcp/registry.rb +7 -2
- data/lib/otto/mcp/route_parser.rb +10 -15
- data/lib/otto/mcp/{validation.rb → schema_validation.rb} +16 -11
- data/lib/otto/mcp/server.rb +45 -22
- data/lib/otto/response_handlers/auto.rb +39 -0
- data/lib/otto/response_handlers/base.rb +16 -0
- data/lib/otto/response_handlers/default.rb +16 -0
- data/lib/otto/response_handlers/factory.rb +39 -0
- data/lib/otto/response_handlers/json.rb +34 -0
- data/lib/otto/response_handlers/redirect.rb +25 -0
- data/lib/otto/response_handlers/view.rb +24 -0
- data/lib/otto/response_handlers.rb +9 -135
- data/lib/otto/route.rb +51 -55
- data/lib/otto/route_definition.rb +15 -18
- data/lib/otto/route_handlers/base.rb +121 -0
- data/lib/otto/route_handlers/class_method.rb +89 -0
- data/lib/otto/route_handlers/factory.rb +42 -0
- data/lib/otto/route_handlers/instance_method.rb +69 -0
- data/lib/otto/route_handlers/lambda.rb +59 -0
- data/lib/otto/route_handlers/logic_class.rb +93 -0
- data/lib/otto/route_handlers.rb +10 -405
- data/lib/otto/security/authentication/auth_strategy.rb +44 -0
- data/lib/otto/security/authentication/authentication_middleware.rb +140 -0
- data/lib/otto/security/authentication/failure_result.rb +44 -0
- data/lib/otto/security/authentication/route_auth_wrapper.rb +149 -0
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
- data/lib/otto/security/authentication/strategies/noauth_strategy.rb +19 -0
- data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
- data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
- data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
- data/lib/otto/security/authentication/strategy_result.rb +337 -0
- data/lib/otto/security/authentication.rb +28 -282
- data/lib/otto/security/config.rb +14 -23
- data/lib/otto/security/configurator.rb +219 -0
- data/lib/otto/security/csrf.rb +8 -143
- data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
- data/lib/otto/security/middleware/rate_limit_middleware.rb +54 -0
- data/lib/otto/security/middleware/validation_middleware.rb +252 -0
- data/lib/otto/security/rate_limiter.rb +86 -0
- data/lib/otto/security/rate_limiting.rb +10 -105
- data/lib/otto/security/validator.rb +8 -253
- data/lib/otto/static.rb +3 -0
- data/lib/otto/utils.rb +14 -0
- data/lib/otto/version.rb +3 -1
- data/lib/otto.rb +141 -498
- data/otto.gemspec +4 -2
- metadata +99 -18
- data/examples/dynamic_pages/app.rb +0 -115
- data/examples/dynamic_pages/config.ru +0 -30
- data/examples/dynamic_pages/routes +0 -21
- data/examples/helpers_demo/app.rb +0 -244
- data/examples/helpers_demo/config.ru +0 -26
- data/examples/helpers_demo/routes +0 -7
- 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,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.
|