rage-rb 1.3.0 → 1.5.0
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/CHANGELOG.md +26 -0
- data/Gemfile +3 -0
- data/README.md +1 -1
- data/lib/rage/cli.rb +1 -3
- data/lib/rage/configuration.rb +17 -0
- data/lib/rage/controller/api.rb +39 -0
- data/lib/rage/cookies.rb +241 -0
- data/lib/rage/ext/setup.rb +1 -1
- data/lib/rage/fiber.rb +1 -1
- data/lib/rage/router/backend.rb +10 -4
- data/lib/rage/router/dsl.rb +40 -0
- data/lib/rage/router/util.rb +15 -0
- data/lib/rage/session.rb +105 -0
- data/lib/rage/templates/config.ru +0 -1
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +24 -23
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24a5bc4310d226a4072c06ac0a3cb015eb52163e7c37fc85aced61f5705b6ef5
|
4
|
+
data.tar.gz: 8bedd5b522c64c945a44fa4cb97e19f48d5b3a3aa46bb5687183c4cf3eac75f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 10feb1fe42789d8470ecf9d7754be26b58db2f160f24c7e82aac8a831a5c4b828d297e01c2535b8e914d167de2bcf40a843b1964c5d1f3102ef4429016fefabd
|
7
|
+
data.tar.gz: 87c3ee06201e71f691b7158f911c0cfbf7763d2021801d298885c890772082a934d91fdb5df8c0f6bf65110acd480fd839ef19d6e9adc39d1c78d165a0e2701f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,31 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.5.0] - 2024-05-08
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- Allow to have both Rails and Rage controllers in one application (#83).
|
8
|
+
- Add `authenticate_or_request_with_http_token` (#85).
|
9
|
+
- Add the `member` and `controller` route helpers (#86).
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
|
13
|
+
- Deprecate `Rage.load_middlewares` (#83).
|
14
|
+
|
15
|
+
### Fixed
|
16
|
+
|
17
|
+
- Correctly init console in Rails mode (credit to [efibootmgr](https://github.com/efibootmgr)) (#84).
|
18
|
+
|
19
|
+
## [1.4.0] - 2024-05-01
|
20
|
+
|
21
|
+
### Added
|
22
|
+
|
23
|
+
- Support cookies and sessions (#69).
|
24
|
+
|
25
|
+
### Fixed
|
26
|
+
|
27
|
+
- Improve compatibility with ActiveRecord 7.1 (#80).
|
28
|
+
|
3
29
|
## [1.3.0] - 2024-04-17
|
4
30
|
|
5
31
|
### Added
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -155,8 +155,8 @@ Status | Changes
|
|
155
155
|
:white_check_mark: | ~~Add request logging.~~
|
156
156
|
:white_check_mark: | ~~Automatic code reloading in development with Zeitwerk.~~
|
157
157
|
:white_check_mark: | ~~Support conditional get with `etag` and `last_modified`.~~
|
158
|
+
:white_check_mark: | ~~Expose the `cookies` and `session` objects.~~
|
158
159
|
⏳ | Expose the `send_data` and `send_file` methods.
|
159
|
-
⏳ | Expose the `cookies` and `session` objects.
|
160
160
|
⏳ | Implement Iodine-based equivalent of Action Cable.
|
161
161
|
|
162
162
|
## Development
|
data/lib/rage/cli.rb
CHANGED
@@ -116,10 +116,8 @@ module Rage
|
|
116
116
|
def environment
|
117
117
|
require File.expand_path("config/application.rb", Dir.pwd)
|
118
118
|
|
119
|
-
# in Rails mode we delegate code loading to Rails, and thus need
|
120
|
-
# to manually load application code for CLI utilities to work
|
121
119
|
if Rage.config.internal.rails_mode
|
122
|
-
require "
|
120
|
+
require File.expand_path("config/environment.rb", Dir.pwd)
|
123
121
|
end
|
124
122
|
end
|
125
123
|
|
data/lib/rage/configuration.rb
CHANGED
@@ -15,6 +15,14 @@
|
|
15
15
|
#
|
16
16
|
# > Defines the verbosity of the Rage logger. This option defaults to `:debug` for all environments except production, where it defaults to `:info`. The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, and `:unknown`.
|
17
17
|
#
|
18
|
+
# • _config.secret_key_base_
|
19
|
+
#
|
20
|
+
# > The `secret_key_base` is used as the input secret to the application's key generator, which is used to encrypt cookies. Rage will fall back to the `SECRET_KEY_BASE` environment variable if this is not set.
|
21
|
+
#
|
22
|
+
# • _config.fallback_secret_key_base_
|
23
|
+
#
|
24
|
+
# > Defines one or several old secrets that need to be rotated. Can accept a single key or an array of keys. Rage will fall back to the `FALLBACK_SECRET_KEY_BASE` environment variable if this is not set.
|
25
|
+
#
|
18
26
|
# # Middleware Configuration
|
19
27
|
#
|
20
28
|
# • _config.middleware.use_
|
@@ -100,6 +108,7 @@
|
|
100
108
|
class Rage::Configuration
|
101
109
|
attr_accessor :logger
|
102
110
|
attr_reader :log_formatter, :log_level
|
111
|
+
attr_writer :secret_key_base, :fallback_secret_key_base
|
103
112
|
|
104
113
|
# used in DSL
|
105
114
|
def config = self
|
@@ -113,6 +122,14 @@ class Rage::Configuration
|
|
113
122
|
@log_level = level.is_a?(Symbol) ? Logger.const_get(level.to_s.upcase) : level
|
114
123
|
end
|
115
124
|
|
125
|
+
def secret_key_base
|
126
|
+
@secret_key_base || ENV["SECRET_KEY_BASE"]
|
127
|
+
end
|
128
|
+
|
129
|
+
def fallback_secret_key_base
|
130
|
+
Array(@fallback_secret_key_base || ENV["FALLBACK_SECRET_KEY_BASE"])
|
131
|
+
end
|
132
|
+
|
116
133
|
def server
|
117
134
|
@server ||= Server.new
|
118
135
|
end
|
data/lib/rage/controller/api.rb
CHANGED
@@ -324,6 +324,18 @@ class RageController::API
|
|
324
324
|
@response ||= Rage::Response.new(@__headers, @__body)
|
325
325
|
end
|
326
326
|
|
327
|
+
# Get the cookie object. See {Rage::Cookies}.
|
328
|
+
# @return [Rage::Cookies]
|
329
|
+
def cookies
|
330
|
+
@cookies ||= Rage::Cookies.new(@__env, self)
|
331
|
+
end
|
332
|
+
|
333
|
+
# Get the session object. See {Rage::Session}.
|
334
|
+
# @return [Rage::Session]
|
335
|
+
def session
|
336
|
+
@session ||= Rage::Session.new(self)
|
337
|
+
end
|
338
|
+
|
327
339
|
# Send a response to the client.
|
328
340
|
#
|
329
341
|
# @param json [String, Object] send a json response to the client; objects like arrays will be serialized automatically
|
@@ -416,6 +428,28 @@ class RageController::API
|
|
416
428
|
yield token
|
417
429
|
end
|
418
430
|
|
431
|
+
# Authenticate using an HTTP Bearer token, or otherwise render an HTTP header requesting the client to send a
|
432
|
+
# Bearer token. For the authentication to be considered successful, the block should return a non-nil value.
|
433
|
+
#
|
434
|
+
# @yield [token] token value extracted from the `Authorization` header
|
435
|
+
# @example
|
436
|
+
# before_action :authenticate
|
437
|
+
#
|
438
|
+
# def authenticate
|
439
|
+
# authenticate_or_request_with_http_token do |token|
|
440
|
+
# ApiToken.find_by(token: token)
|
441
|
+
# end
|
442
|
+
# end
|
443
|
+
def authenticate_or_request_with_http_token
|
444
|
+
authenticate_with_http_token { |token| yield(token) } || request_http_token_authentication
|
445
|
+
end
|
446
|
+
|
447
|
+
# Render an HTTP header requesting the client to send a Bearer token for authentication.
|
448
|
+
def request_http_token_authentication
|
449
|
+
headers["Www-Authenticate"] = "Token"
|
450
|
+
render plain: "HTTP Token: Access denied.", status: 401
|
451
|
+
end
|
452
|
+
|
419
453
|
if !defined?(::ActionController::Parameters)
|
420
454
|
# Get the request data. The keys inside the hash are symbols, so `params.keys` returns an array of `Symbol`.<br>
|
421
455
|
# You can also load Strong Params to have Rage automatically wrap `params` in an instance of `ActionController::Parameters`.<br>
|
@@ -477,4 +511,9 @@ class RageController::API
|
|
477
511
|
# def append_info_to_payload(payload)
|
478
512
|
# payload[:response] = response.body
|
479
513
|
# end
|
514
|
+
|
515
|
+
# Reset the entire session. See {Rage::Session}.
|
516
|
+
def reset_session
|
517
|
+
session.clear
|
518
|
+
end
|
480
519
|
end
|
data/lib/rage/cookies.rb
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
require "time"
|
5
|
+
|
6
|
+
if !defined?(DomainName)
|
7
|
+
fail <<~ERR
|
8
|
+
|
9
|
+
rage-rb depends on domain_name to specify the domain name for cookies. Add the following line to your Gemfile:
|
10
|
+
gem "domain_name"
|
11
|
+
|
12
|
+
ERR
|
13
|
+
end
|
14
|
+
|
15
|
+
class Rage::Cookies
|
16
|
+
# @private
|
17
|
+
def initialize(env, controller)
|
18
|
+
@env = env
|
19
|
+
@headers = controller.headers
|
20
|
+
@request_cookies = {}
|
21
|
+
@parsed = false
|
22
|
+
|
23
|
+
@jar = SimpleJar
|
24
|
+
end
|
25
|
+
|
26
|
+
# Read a cookie.
|
27
|
+
#
|
28
|
+
# @param key [Symbol]
|
29
|
+
# @return [String]
|
30
|
+
def [](key)
|
31
|
+
value = request_cookies[key]
|
32
|
+
@jar.load(value) if value
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get the number of cookies.
|
36
|
+
#
|
37
|
+
# @return [Integer]
|
38
|
+
def size
|
39
|
+
request_cookies.count { |_, v| !v.nil? }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Delete a cookie.
|
43
|
+
#
|
44
|
+
# @param key [Symbol]
|
45
|
+
# @param path [String]
|
46
|
+
# @param domain [String]
|
47
|
+
def delete(key, path: "/", domain: nil)
|
48
|
+
@headers.compare_by_identity
|
49
|
+
|
50
|
+
@request_cookies[key] = nil
|
51
|
+
@headers[set_cookie_key(key)] = Rack::Utils.add_cookie_to_header(nil, key, {
|
52
|
+
value: "", expires: Time.at(0), path: path, domain: domain
|
53
|
+
})
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them
|
57
|
+
# for read. If the cookie was tampered with by the user (or a 3rd party), `nil` will be returned.
|
58
|
+
#
|
59
|
+
# This jar requires that you set a suitable secret for the verification on your app's `secret_key_base`.
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# cookies.encrypted[:user_id] = current_user.id
|
63
|
+
def encrypted
|
64
|
+
dup.tap { |c| c.jar = EncryptedJar }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now.
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# cookies.permanent[:user_id] = current_user.id
|
71
|
+
def permanent
|
72
|
+
dup.tap { |c| c.expires = Time.now + 20 * 365 * 24 * 60 * 60 }
|
73
|
+
end
|
74
|
+
|
75
|
+
# Set a cookie.
|
76
|
+
#
|
77
|
+
# @param key [Symbol]
|
78
|
+
# @param value [String, Hash]
|
79
|
+
# @option value [String] :path
|
80
|
+
# @option value [Boolean] :secure
|
81
|
+
# @option value [Boolean] :httponly
|
82
|
+
# @option value [nil, :none, :lax, :strict] :same_site
|
83
|
+
# @option value [Time] :expires
|
84
|
+
# @option value [String, Array<String>, :all] :domain
|
85
|
+
# @option value [String] :value
|
86
|
+
# @example
|
87
|
+
# cookie[:user_id] = current_user.id
|
88
|
+
# @example
|
89
|
+
# cookie[:user_id] = { value: current_user.id, httponly: true, secure: true }
|
90
|
+
def []=(key, value)
|
91
|
+
@headers.compare_by_identity
|
92
|
+
|
93
|
+
unless value.is_a?(Hash)
|
94
|
+
serialized_value = @jar.dump(value)
|
95
|
+
@request_cookies[key] = serialized_value
|
96
|
+
@headers[set_cookie_key(key)] = Rack::Utils.add_cookie_to_header(nil, key, { value: serialized_value, expires: @expires })
|
97
|
+
return
|
98
|
+
end
|
99
|
+
|
100
|
+
if domain = value[:domain]
|
101
|
+
host = @env["HTTP_HOST"]
|
102
|
+
|
103
|
+
_domain = if domain.is_a?(String)
|
104
|
+
domain
|
105
|
+
elsif domain == :all
|
106
|
+
DomainName(host).domain
|
107
|
+
elsif domain.is_a?(Array)
|
108
|
+
host if domain.include?(host)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
serialized_value = @jar.dump(value[:value])
|
113
|
+
cookie = Rack::Utils.add_cookie_to_header(nil, key, {
|
114
|
+
path: value[:path],
|
115
|
+
secure: value[:secure],
|
116
|
+
expires: value[:expires] || @expires,
|
117
|
+
httponly: value[:httponly],
|
118
|
+
same_site: value[:same_site],
|
119
|
+
value: serialized_value,
|
120
|
+
domain: _domain
|
121
|
+
})
|
122
|
+
|
123
|
+
@request_cookies[key] = serialized_value
|
124
|
+
@headers[set_cookie_key(key)] = cookie
|
125
|
+
end
|
126
|
+
|
127
|
+
def inspect
|
128
|
+
cookies = request_cookies.transform_values do |v|
|
129
|
+
decoded = Base64.urlsafe_decode64(v) rescue nil
|
130
|
+
is_encrypted = decoded&.start_with?(EncryptedJar::PADDING)
|
131
|
+
|
132
|
+
is_encrypted ? "<encrypted>" : v
|
133
|
+
end
|
134
|
+
|
135
|
+
"#<#{self.class.name} @request_cookies=#{cookies.inspect}"
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def request_cookies
|
141
|
+
return @request_cookies if @parsed
|
142
|
+
|
143
|
+
@parsed = true
|
144
|
+
if cookie_header = @env["HTTP_COOKIE"]
|
145
|
+
cookie_header.split(/; */n).each do |cookie|
|
146
|
+
next if cookie.empty?
|
147
|
+
key, value = cookie.split("=", 2).yield_self { |k, _| [k.to_sym, _] }
|
148
|
+
unless @request_cookies.has_key?(key)
|
149
|
+
@request_cookies[key] = (Rack::Utils.unescape(value, Encoding::UTF_8) rescue value)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
@request_cookies
|
155
|
+
end
|
156
|
+
|
157
|
+
def set_cookie_key(key)
|
158
|
+
@set_cookie_keys ||= Hash.new { |hash, key| hash[key] = "Set-Cookie".dup }
|
159
|
+
@set_cookie_keys[key]
|
160
|
+
end
|
161
|
+
|
162
|
+
protected
|
163
|
+
|
164
|
+
attr_writer :jar, :expires
|
165
|
+
|
166
|
+
####################
|
167
|
+
#
|
168
|
+
# Cookie Jars
|
169
|
+
#
|
170
|
+
####################
|
171
|
+
|
172
|
+
class SimpleJar
|
173
|
+
def self.load(_)
|
174
|
+
_
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.dump(value)
|
178
|
+
value.to_s
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class EncryptedJar
|
183
|
+
SALT = "encrypted cookie"
|
184
|
+
PADDING = "00"
|
185
|
+
|
186
|
+
class << self
|
187
|
+
def load(value)
|
188
|
+
box = primary_box
|
189
|
+
|
190
|
+
begin
|
191
|
+
box.decrypt(Base64.urlsafe_decode64(value).byteslice(2..))
|
192
|
+
rescue ArgumentError
|
193
|
+
nil
|
194
|
+
rescue RbNaCl::CryptoError
|
195
|
+
i ||= 0
|
196
|
+
if box = fallback_boxes[i]
|
197
|
+
i += 1
|
198
|
+
retry
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def dump(value)
|
204
|
+
# add two bytes to hold meta information, e.g. in case we
|
205
|
+
# need to change the encryption algorithm in the future
|
206
|
+
Base64.urlsafe_encode64(PADDING + primary_box.encrypt(value.to_s))
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def primary_box
|
212
|
+
@primary_box ||= begin
|
213
|
+
if !defined?(RbNaCl) || !(Gem::Version.create(RbNaCl::VERSION) >= Gem::Version.create("3.3.0") && Gem::Version.create(RbNaCl::VERSION) < Gem::Version.create("8.0.0"))
|
214
|
+
fail <<~ERR
|
215
|
+
|
216
|
+
rage-rb depends on rbnacl [>= 3.3, < 8.0] to encrypt cookies. Add the following line to your Gemfile:
|
217
|
+
gem "rbnacl"
|
218
|
+
|
219
|
+
ERR
|
220
|
+
end
|
221
|
+
|
222
|
+
unless Rage.config.secret_key_base
|
223
|
+
raise "Rage.config.secret_key_base should be set to use encrypted cookies"
|
224
|
+
end
|
225
|
+
|
226
|
+
RbNaCl::SimpleBox.from_secret_key(
|
227
|
+
RbNaCl::Hash.blake2b(Rage.config.secret_key_base, digest_size: 32, salt: SALT)
|
228
|
+
)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def fallback_boxes
|
233
|
+
@fallback_boxes ||= begin
|
234
|
+
Rage.config.fallback_secret_key_base.map do |key|
|
235
|
+
RbNaCl::SimpleBox.from_secret_key(RbNaCl::Hash.blake2b(key, digest_size: 32, salt: SALT))
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end # class << self
|
240
|
+
end
|
241
|
+
end
|
data/lib/rage/ext/setup.rb
CHANGED
@@ -4,7 +4,7 @@ if defined?(ActiveSupport::IsolatedExecutionState)
|
|
4
4
|
end
|
5
5
|
|
6
6
|
# release ActiveRecord connections on yield
|
7
|
-
if defined?(ActiveRecord)
|
7
|
+
if defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("7.1.0")
|
8
8
|
class Fiber
|
9
9
|
def self.defer
|
10
10
|
res = Fiber.yield
|
data/lib/rage/fiber.rb
CHANGED
@@ -91,7 +91,7 @@ class Fiber
|
|
91
91
|
|
92
92
|
# @private
|
93
93
|
# under normal circumstances, the method is a copy of `yield`, but it can be overriden to perform
|
94
|
-
# additional steps on yielding, e.g. releasing AR connections; see "lib/rage/
|
94
|
+
# additional steps on yielding, e.g. releasing AR connections; see "lib/rage/ext/setup.rb"
|
95
95
|
class << self
|
96
96
|
alias_method :defer, :yield
|
97
97
|
end
|
data/lib/rage/router/backend.rb
CHANGED
@@ -68,12 +68,18 @@ class Rage::Router::Backend
|
|
68
68
|
raise "Invalid route handler format, expected to match the 'controller#action' pattern" unless handler =~ STRING_HANDLER_REGEXP
|
69
69
|
|
70
70
|
controller, action = Rage::Router::Util.path_to_class($1), $2
|
71
|
-
run_action_method_name = controller.__register_action(action.to_sym)
|
72
71
|
|
73
|
-
|
74
|
-
|
72
|
+
if controller.ancestors.include?(RageController::API)
|
73
|
+
run_action_method_name = controller.__register_action(action.to_sym)
|
75
74
|
|
76
|
-
|
75
|
+
meta[:controller] = $1
|
76
|
+
meta[:action] = $2
|
77
|
+
|
78
|
+
handler = eval("->(env, params) { #{controller}.new(env, params).#{run_action_method_name} }")
|
79
|
+
else
|
80
|
+
# this is a Rails controller; notify `Rage::Router::Util::Cascade` to forward the request to Rails
|
81
|
+
handler = ->(_, _) { [404, { "X-Cascade" => "pass" }, []] }
|
82
|
+
end
|
77
83
|
else
|
78
84
|
raise "Non-string route handler should respond to `call`" unless handler.respond_to?(:call)
|
79
85
|
# while regular handlers are expected to be called with the `env` and `params` objects,
|
data/lib/rage/router/dsl.rb
CHANGED
@@ -209,6 +209,7 @@ class Rage::Router::DSL
|
|
209
209
|
# @param [Hash] opts scope options.
|
210
210
|
# @option opts [String] :module module option
|
211
211
|
# @option opts [String] :path path option
|
212
|
+
# @option opts [String] :controller controller option
|
212
213
|
# @example Route `/photos` to `Api::PhotosController`
|
213
214
|
# scope module: "api" do
|
214
215
|
# get "photos", to: "photos#index"
|
@@ -217,6 +218,11 @@ class Rage::Router::DSL
|
|
217
218
|
# scope path: "admin" do
|
218
219
|
# get "photos", to: "photos#index"
|
219
220
|
# end
|
221
|
+
# @example Route `/like` to `photos#like` and `/dislike` to `photos#dislike`
|
222
|
+
# scope controller: "photos" do
|
223
|
+
# post "like"
|
224
|
+
# post "dislike"
|
225
|
+
# end
|
220
226
|
# @example Nested calls
|
221
227
|
# scope module: "admin" do
|
222
228
|
# get "photos", to: "photos#index"
|
@@ -252,6 +258,19 @@ class Rage::Router::DSL
|
|
252
258
|
@defaults.pop
|
253
259
|
end
|
254
260
|
|
261
|
+
# Scopes routes to a specific controller.
|
262
|
+
#
|
263
|
+
# @example
|
264
|
+
# controller "photos" do
|
265
|
+
# post "like"
|
266
|
+
# post "dislike"
|
267
|
+
# end
|
268
|
+
def controller(controller, &block)
|
269
|
+
@controllers << controller
|
270
|
+
instance_eval &block
|
271
|
+
@controllers.pop
|
272
|
+
end
|
273
|
+
|
255
274
|
# Add a route to the collection.
|
256
275
|
#
|
257
276
|
# @example Add a `photos/search` path instead of `photos/:photo_id/search`
|
@@ -267,6 +286,27 @@ class Rage::Router::DSL
|
|
267
286
|
@path_prefixes = orig_path_prefixes
|
268
287
|
end
|
269
288
|
|
289
|
+
# Add a member route.
|
290
|
+
#
|
291
|
+
# @example Add a `photos/:id/preview` path instead of `photos/:photo_id/preview`
|
292
|
+
# resources :photos do
|
293
|
+
# member do
|
294
|
+
# get "preview"
|
295
|
+
# end
|
296
|
+
# end
|
297
|
+
def member(&block)
|
298
|
+
orig_path_prefixes = @path_prefixes
|
299
|
+
|
300
|
+
if (param_prefix = @path_prefixes.last)&.start_with?(":") && @controllers.any?
|
301
|
+
member_prefix = param_prefix.delete_prefix(":#{to_singular(@controllers.last)}_")
|
302
|
+
@path_prefixes = [*@path_prefixes[0...-1], ":#{member_prefix}"]
|
303
|
+
end
|
304
|
+
|
305
|
+
instance_eval &block
|
306
|
+
|
307
|
+
@path_prefixes = orig_path_prefixes
|
308
|
+
end
|
309
|
+
|
270
310
|
# Automatically create REST routes for a resource.
|
271
311
|
#
|
272
312
|
# @example Create five REST routes, all mapping to the `Photos` controller:
|
data/lib/rage/router/util.rb
CHANGED
@@ -30,4 +30,19 @@ class Rage::Router::Util
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
33
|
+
|
34
|
+
# @private
|
35
|
+
class Cascade
|
36
|
+
def initialize(rage_app, rails_app)
|
37
|
+
@rage_app = rage_app
|
38
|
+
@rails_app = rails_app
|
39
|
+
end
|
40
|
+
|
41
|
+
def call(env)
|
42
|
+
result = @rage_app.call(env)
|
43
|
+
return result if result[0] == :__http_defer__ || result[1]["X-Cascade".freeze] != "pass".freeze
|
44
|
+
|
45
|
+
@rails_app.call(env)
|
46
|
+
end
|
47
|
+
end
|
33
48
|
end
|
data/lib/rage/session.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
class Rage::Session
|
6
|
+
# @private
|
7
|
+
KEY = Rack::RACK_SESSION.to_sym
|
8
|
+
|
9
|
+
# @private
|
10
|
+
def initialize(controller)
|
11
|
+
@cookies = controller.cookies.encrypted
|
12
|
+
end
|
13
|
+
|
14
|
+
# Writes the value to the session.
|
15
|
+
#
|
16
|
+
# @param key [Symbol]
|
17
|
+
# @param value [String]
|
18
|
+
def []=(key, value)
|
19
|
+
write_session(add: { key => value })
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the value of the key stored in the session or `nil` if the given key is not found.
|
23
|
+
#
|
24
|
+
# @param key [Symbol]
|
25
|
+
def [](key)
|
26
|
+
read_session[key]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the value of the given key from the session, or raises `KeyError` if the given key is not found
|
30
|
+
# and no default value is set. Returns the default value if specified.
|
31
|
+
#
|
32
|
+
# @param key [Symbol]
|
33
|
+
def fetch(key, default = nil, &block)
|
34
|
+
if default.nil?
|
35
|
+
read_session.fetch(key, &block)
|
36
|
+
else
|
37
|
+
read_session.fetch(key, default, &block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Deletes the given key from the session.
|
42
|
+
#
|
43
|
+
# @param key [Symbol]
|
44
|
+
def delete(key)
|
45
|
+
write_session(remove: key)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Clears the session.
|
49
|
+
def clear
|
50
|
+
write_session(clear: true)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the session as Hash.
|
54
|
+
def to_hash
|
55
|
+
read_session
|
56
|
+
end
|
57
|
+
|
58
|
+
alias_method :to_h, :to_hash
|
59
|
+
|
60
|
+
def empty?
|
61
|
+
read_session.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns `true` if the given key is present in the session.
|
65
|
+
def has_key?(key)
|
66
|
+
read_session.has_key?(key)
|
67
|
+
end
|
68
|
+
|
69
|
+
alias_method :key?, :has_key?
|
70
|
+
alias_method :include?, :has_key?
|
71
|
+
|
72
|
+
def each(&block)
|
73
|
+
read_session.each(&block)
|
74
|
+
end
|
75
|
+
|
76
|
+
def dig(*keys)
|
77
|
+
read_session.dig(*keys)
|
78
|
+
end
|
79
|
+
|
80
|
+
def inspect
|
81
|
+
"#<#{self.class.name} @session=#{to_h.inspect}"
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def write_session(add: nil, remove: nil, clear: nil)
|
87
|
+
if add
|
88
|
+
read_session.merge!(add)
|
89
|
+
elsif remove && read_session.has_key?(remove)
|
90
|
+
read_session.reject! { |k, _| k == remove }
|
91
|
+
elsif clear
|
92
|
+
read_session.clear
|
93
|
+
end
|
94
|
+
|
95
|
+
@cookies[KEY] = { httponly: true, same_site: :lax, value: read_session.to_json }
|
96
|
+
end
|
97
|
+
|
98
|
+
def read_session
|
99
|
+
@session ||= begin
|
100
|
+
JSON.parse(@cookies[KEY] || "{}", symbolize_names: true)
|
101
|
+
rescue JSON::ParserError
|
102
|
+
{}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/rage/version.rb
CHANGED
data/lib/rage-rb.rb
CHANGED
@@ -7,7 +7,25 @@ require "pathname"
|
|
7
7
|
|
8
8
|
module Rage
|
9
9
|
def self.application
|
10
|
-
Application.new(__router)
|
10
|
+
app = Application.new(__router)
|
11
|
+
|
12
|
+
config.middleware.middlewares.reverse.inject(app) do |next_in_chain, (middleware, args, block)|
|
13
|
+
# in Rails compatibility mode we first check if the middleware is a part of the Rails middleware stack;
|
14
|
+
# if it is - it is expected to be built using `ActionDispatch::MiddlewareStack::Middleware#build`
|
15
|
+
if Rage.config.internal.rails_mode
|
16
|
+
rails_middleware = Rails.application.config.middleware.middlewares.find { |m| m.name == middleware.name }
|
17
|
+
end
|
18
|
+
|
19
|
+
if rails_middleware
|
20
|
+
rails_middleware.build(next_in_chain)
|
21
|
+
else
|
22
|
+
middleware.new(next_in_chain, *args, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.multi_application
|
28
|
+
Rage::Router::Util::Cascade.new(application, Rails.application)
|
11
29
|
end
|
12
30
|
|
13
31
|
def self.routes
|
@@ -43,28 +61,8 @@ module Rage
|
|
43
61
|
@logger ||= config.logger
|
44
62
|
end
|
45
63
|
|
46
|
-
def self.load_middlewares(
|
47
|
-
|
48
|
-
# in Rails compatibility mode we first check if the middleware is a part of the Rails middleware stack;
|
49
|
-
# if it is - it is expected to be built using `ActionDispatch::MiddlewareStack::Middleware#build`, but Rack
|
50
|
-
# expects the middleware to respond to `#new`, so we wrap the middleware into a helper module
|
51
|
-
if Rage.config.internal.rails_mode
|
52
|
-
rails_middleware = Rails.application.config.middleware.middlewares.find { |m| m.name == middleware.name }
|
53
|
-
if rails_middleware
|
54
|
-
wrapper = Module.new do
|
55
|
-
extend self
|
56
|
-
attr_accessor :middleware
|
57
|
-
def new(app, *, &)
|
58
|
-
middleware.build(app)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
wrapper.middleware = rails_middleware
|
62
|
-
middleware = wrapper
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
rack_builder.use(middleware, *args, &block)
|
67
|
-
end
|
64
|
+
def self.load_middlewares(_)
|
65
|
+
puts "`Rage.load_middlewares` is deprecated and has been merged into `Rage.application`. Please remove this call."
|
68
66
|
end
|
69
67
|
|
70
68
|
def self.code_loader
|
@@ -102,6 +100,9 @@ module Rage
|
|
102
100
|
autoload :ConnectionPool, "rage/ext/active_record/connection_pool"
|
103
101
|
end
|
104
102
|
end
|
103
|
+
|
104
|
+
autoload :Cookies, "rage/cookies"
|
105
|
+
autoload :Session, "rage/session"
|
105
106
|
end
|
106
107
|
|
107
108
|
module RageController
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rage-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roman Samoilov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-05-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -105,6 +105,7 @@ files:
|
|
105
105
|
- lib/rage/code_loader.rb
|
106
106
|
- lib/rage/configuration.rb
|
107
107
|
- lib/rage/controller/api.rb
|
108
|
+
- lib/rage/cookies.rb
|
108
109
|
- lib/rage/env.rb
|
109
110
|
- lib/rage/errors.rb
|
110
111
|
- lib/rage/ext/active_record/connection_pool.rb
|
@@ -130,6 +131,7 @@ files:
|
|
130
131
|
- lib/rage/router/strategies/host.rb
|
131
132
|
- lib/rage/router/util.rb
|
132
133
|
- lib/rage/rspec.rb
|
134
|
+
- lib/rage/session.rb
|
133
135
|
- lib/rage/setup.rb
|
134
136
|
- lib/rage/sidekiq_session.rb
|
135
137
|
- lib/rage/templates/Gemfile
|