rodauth 2.14.0 → 2.15.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 +8 -0
- data/README.rdoc +29 -6
- data/doc/internal_request.rdoc +463 -0
- data/doc/path_class_methods.rdoc +10 -0
- data/doc/release_notes/2.15.0.txt +48 -0
- data/lib/rodauth.rb +9 -2
- data/lib/rodauth/features/base.rb +11 -1
- data/lib/rodauth/features/change_login.rb +2 -0
- data/lib/rodauth/features/change_password.rb +2 -0
- data/lib/rodauth/features/close_account.rb +2 -0
- data/lib/rodauth/features/create_account.rb +2 -0
- data/lib/rodauth/features/email_auth.rb +4 -0
- data/lib/rodauth/features/internal_request.rb +367 -0
- data/lib/rodauth/features/lockout.rb +11 -2
- data/lib/rodauth/features/login.rb +3 -0
- data/lib/rodauth/features/otp.rb +6 -0
- data/lib/rodauth/features/path_class_methods.rb +22 -0
- data/lib/rodauth/features/recovery_codes.rb +4 -0
- data/lib/rodauth/features/remember.rb +10 -2
- data/lib/rodauth/features/reset_password.rb +3 -0
- data/lib/rodauth/features/sms_codes.rb +7 -0
- data/lib/rodauth/features/two_factor_base.rb +2 -0
- data/lib/rodauth/features/verify_account.rb +3 -0
- data/lib/rodauth/features/verify_login_change.rb +2 -0
- data/lib/rodauth/version.rb +1 -1
- metadata +11 -3
@@ -0,0 +1,10 @@
|
|
1
|
+
= Documentation for Path Class Methods Feature
|
2
|
+
|
3
|
+
The path class methods feature allows for calling the *_path and *_url
|
4
|
+
methods directly on the class, as opposed to an instance of the class.
|
5
|
+
|
6
|
+
In order for the *_url methods to be used, you must use the base_url
|
7
|
+
configuration so that determining the base URL doesn't depend on the
|
8
|
+
submitted request, as the request will not be set when using the
|
9
|
+
class method. Failure to do this will probably result in a NoMethodError
|
10
|
+
being raised.
|
@@ -0,0 +1,48 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* An internal_request feature has been added. This feature allows
|
4
|
+
for interacting with Rodauth by calling methods, instead of having
|
5
|
+
to use a website or JSON API. This feature is designed primarily
|
6
|
+
for administrative use, so that administrators can create accounts,
|
7
|
+
change passwords or logins for accounts, and handle similar actions
|
8
|
+
without the user of the account being involved.
|
9
|
+
|
10
|
+
For example, assuming you've loaded the change_password and
|
11
|
+
internal_request features, and that your Roda class that
|
12
|
+
is loading Rodauth is named App, you can change the password
|
13
|
+
for the account with id 1 using:
|
14
|
+
|
15
|
+
App.rodauth.change_password(account_id: 1, password: 'foobar')
|
16
|
+
|
17
|
+
The internal request methods are implemented as class methods
|
18
|
+
on the Rodauth::Auth subclass (the object returned by App.rodauth).
|
19
|
+
These methods call methods on a subclass of that class specific
|
20
|
+
to internal requests.
|
21
|
+
|
22
|
+
The reason the feature is named internal_request is that these
|
23
|
+
methods are implemented by submitting a request internally, that is
|
24
|
+
processed almost exactly the same way as Rodauth would process a
|
25
|
+
web request.
|
26
|
+
|
27
|
+
See the internal_request feature documentation for details on which
|
28
|
+
internal request methods are available and the options they take.
|
29
|
+
|
30
|
+
* A path_class_methods feature has been added, that allows for calling
|
31
|
+
*_path and *_url as class methods. If you would like to call the
|
32
|
+
*_url methods as class methods, make sure to use the base_url
|
33
|
+
configuration method to set the base URL so that it does not require
|
34
|
+
request-specific information.
|
35
|
+
|
36
|
+
* Rodauth::Auth classes now have a configuration_name method that
|
37
|
+
returns the configuration name associated with the class. They also
|
38
|
+
have a configuration method that returns the configuration
|
39
|
+
associated with the class.
|
40
|
+
|
41
|
+
* Rodauth::Feature now supports an internal_request_method method for
|
42
|
+
specifying which methods are supported as internal request methods.
|
43
|
+
|
44
|
+
= Other Improvements
|
45
|
+
|
46
|
+
* The default base_url configuration method will now use the domain
|
47
|
+
method to get the domain to use, instead of getting the domain
|
48
|
+
information directly from the request environment.
|
data/lib/rodauth.rb
CHANGED
@@ -39,11 +39,11 @@ module Rodauth
|
|
39
39
|
else
|
40
40
|
json_opt != :only
|
41
41
|
end
|
42
|
-
auth_class = (app.opts[:rodauths] ||= {})[opts[:name]] ||= opts[:auth_class] || Class.new(Auth)
|
42
|
+
auth_class = (app.opts[:rodauths] ||= {})[opts[:name]] ||= opts[:auth_class] || Class.new(Auth){@configuration_name = opts[:name]}
|
43
43
|
if !auth_class.roda_class
|
44
44
|
auth_class.roda_class = app
|
45
45
|
elsif auth_class.roda_class != app
|
46
|
-
auth_class = app.opts[:rodauths][opts[:name]] = Class.new(auth_class)
|
46
|
+
auth_class = app.opts[:rodauths][opts[:name]] = Class.new(auth_class){@configuration_name = opts[:name]}
|
47
47
|
auth_class.roda_class = app
|
48
48
|
end
|
49
49
|
auth_class.configure(&block) if block
|
@@ -107,6 +107,7 @@ module Rodauth
|
|
107
107
|
attr_accessor :dependencies
|
108
108
|
attr_accessor :routes
|
109
109
|
attr_accessor :configuration
|
110
|
+
attr_reader :internal_request_methods
|
110
111
|
|
111
112
|
def route(name=feature_name, default=name.to_s.tr('_', '-'), &block)
|
112
113
|
route_meth = :"#{name}_route"
|
@@ -152,6 +153,10 @@ module Rodauth
|
|
152
153
|
FEATURES[name] = feature
|
153
154
|
end
|
154
155
|
|
156
|
+
def internal_request_method(name=feature_name)
|
157
|
+
(@internal_request_methods ||= []) << name
|
158
|
+
end
|
159
|
+
|
155
160
|
def configuration_module_eval(&block)
|
156
161
|
configuration.module_eval(&block)
|
157
162
|
end
|
@@ -260,6 +265,8 @@ module Rodauth
|
|
260
265
|
attr_reader :features
|
261
266
|
attr_reader :routes
|
262
267
|
attr_accessor :route_hash
|
268
|
+
attr_reader :configuration_name
|
269
|
+
attr_reader :configuration
|
263
270
|
end
|
264
271
|
|
265
272
|
def self.inherited(subclass)
|
@@ -115,6 +115,10 @@ module Rodauth
|
|
115
115
|
:around_rodauth
|
116
116
|
)
|
117
117
|
|
118
|
+
internal_request_method :account_exists?
|
119
|
+
internal_request_method :account_id_for_login
|
120
|
+
internal_request_method :internal_request_eval
|
121
|
+
|
118
122
|
configuration_module_eval do
|
119
123
|
def auth_class_eval(&block)
|
120
124
|
auth.class_eval(&block)
|
@@ -445,7 +449,9 @@ module Rodauth
|
|
445
449
|
end
|
446
450
|
|
447
451
|
def base_url
|
448
|
-
request.
|
452
|
+
url = String.new("#{request.scheme}://#{domain}")
|
453
|
+
url << ":#{request.port}" if request.port != Rack::Request::DEFAULT_PORTS[request.scheme]
|
454
|
+
url
|
449
455
|
end
|
450
456
|
|
451
457
|
def domain
|
@@ -734,6 +740,10 @@ module Rodauth
|
|
734
740
|
end
|
735
741
|
end
|
736
742
|
|
743
|
+
def internal_request?
|
744
|
+
false
|
745
|
+
end
|
746
|
+
|
737
747
|
def set_session_value(key, value)
|
738
748
|
session[key] = value
|
739
749
|
end
|
@@ -49,6 +49,10 @@ module Rodauth
|
|
49
49
|
|
50
50
|
auth_private_methods :account_from_email_auth_key
|
51
51
|
|
52
|
+
internal_request_method
|
53
|
+
internal_request_method :email_auth_request
|
54
|
+
internal_request_method :valid_email_auth?
|
55
|
+
|
52
56
|
route(:email_auth_request) do |r|
|
53
57
|
check_already_logged_in
|
54
58
|
before_email_auth_request_route
|
@@ -0,0 +1,367 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
INVALID_DOMAIN = "invalidurl @@.com"
|
7
|
+
|
8
|
+
class InternalRequestError < StandardError
|
9
|
+
attr_accessor :flash
|
10
|
+
attr_accessor :reason
|
11
|
+
attr_accessor :field_errors
|
12
|
+
|
13
|
+
def initialize(attrs)
|
14
|
+
return super if attrs.is_a?(String)
|
15
|
+
|
16
|
+
@flash = attrs[:flash]
|
17
|
+
@reason = attrs[:reason]
|
18
|
+
@field_errors = attrs[:field_errors] || {}
|
19
|
+
|
20
|
+
super(build_message)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def build_message
|
26
|
+
extras = []
|
27
|
+
extras << reason if reason
|
28
|
+
extras << field_errors unless field_errors.empty?
|
29
|
+
extras = (" (#{extras.join(", ")})" unless extras.empty?)
|
30
|
+
|
31
|
+
"#{flash}#{extras}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module InternalRequestMethods
|
36
|
+
attr_accessor :session
|
37
|
+
attr_accessor :params
|
38
|
+
attr_reader :flash
|
39
|
+
attr_accessor :internal_request_block
|
40
|
+
|
41
|
+
def domain
|
42
|
+
d = super
|
43
|
+
if d == INVALID_DOMAIN
|
44
|
+
raise InternalRequestError, "must set domain in configuration, as it cannot be determined from internal request"
|
45
|
+
end
|
46
|
+
d
|
47
|
+
end
|
48
|
+
|
49
|
+
def raw_param(k)
|
50
|
+
@params[k]
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_error_flash(message)
|
54
|
+
@flash = message
|
55
|
+
_handle_internal_request_error
|
56
|
+
end
|
57
|
+
alias set_redirect_error_flash set_error_flash
|
58
|
+
|
59
|
+
def set_notice_flash(message)
|
60
|
+
@flash = message
|
61
|
+
end
|
62
|
+
alias set_notice_now_flash set_notice_flash
|
63
|
+
|
64
|
+
def modifications_require_password?
|
65
|
+
false
|
66
|
+
end
|
67
|
+
alias require_login_confirmation? modifications_require_password?
|
68
|
+
alias require_password_confirmation? modifications_require_password?
|
69
|
+
alias change_login_requires_password? modifications_require_password?
|
70
|
+
alias change_password_requires_password? modifications_require_password?
|
71
|
+
alias close_account_requires_password? modifications_require_password?
|
72
|
+
alias two_factor_modifications_require_password? modifications_require_password?
|
73
|
+
|
74
|
+
def otp_setup_view
|
75
|
+
hash = {:otp_setup=>otp_user_key}
|
76
|
+
hash[:otp_setup_raw] = otp_key if hmac_secret
|
77
|
+
_return_from_internal_request(hash)
|
78
|
+
end
|
79
|
+
|
80
|
+
def add_recovery_codes_view
|
81
|
+
_return_from_internal_request(recovery_codes)
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_internal_request(meth)
|
85
|
+
catch(:halt) do
|
86
|
+
_around_rodauth do
|
87
|
+
before_rodauth
|
88
|
+
send(meth, request)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
@internal_request_return_value
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def internal_request?
|
98
|
+
true
|
99
|
+
end
|
100
|
+
|
101
|
+
def set_error_reason(reason)
|
102
|
+
@error_reason = reason
|
103
|
+
end
|
104
|
+
|
105
|
+
def after_login
|
106
|
+
super
|
107
|
+
_set_internal_request_return_value(account_id) unless @return_false_on_error
|
108
|
+
end
|
109
|
+
|
110
|
+
def after_remember
|
111
|
+
super
|
112
|
+
if params[remember_param] == remember_remember_param_value
|
113
|
+
_set_internal_request_return_value("#{account_id}_#{convert_token_key(remember_key_value)}")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def after_load_memory
|
118
|
+
super
|
119
|
+
_return_from_internal_request(session_value)
|
120
|
+
end
|
121
|
+
|
122
|
+
def before_change_password_route
|
123
|
+
super
|
124
|
+
params[new_password_param] ||= params[password_param]
|
125
|
+
end
|
126
|
+
|
127
|
+
def before_email_auth_request_route
|
128
|
+
super
|
129
|
+
_set_login_param_from_account
|
130
|
+
end
|
131
|
+
|
132
|
+
def before_login_route
|
133
|
+
super
|
134
|
+
_set_login_param_from_account
|
135
|
+
end
|
136
|
+
|
137
|
+
def before_unlock_account_request_route
|
138
|
+
super
|
139
|
+
_set_login_param_from_account
|
140
|
+
end
|
141
|
+
|
142
|
+
def before_reset_password_request_route
|
143
|
+
super
|
144
|
+
_set_login_param_from_account
|
145
|
+
end
|
146
|
+
|
147
|
+
def before_verify_account_resend_route
|
148
|
+
super
|
149
|
+
_set_login_param_from_account
|
150
|
+
end
|
151
|
+
|
152
|
+
def account_from_key(token, status_id=nil)
|
153
|
+
return super unless session_value
|
154
|
+
return unless yield session_value
|
155
|
+
ds = account_ds(session_value)
|
156
|
+
ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
|
157
|
+
ds.first
|
158
|
+
end
|
159
|
+
|
160
|
+
def _set_internal_request_return_value(value)
|
161
|
+
@internal_request_return_value = value
|
162
|
+
end
|
163
|
+
|
164
|
+
def _return_from_internal_request(value)
|
165
|
+
_set_internal_request_return_value(value)
|
166
|
+
throw(:halt)
|
167
|
+
end
|
168
|
+
|
169
|
+
def _handle_internal_request_error
|
170
|
+
if @return_false_on_error
|
171
|
+
_return_from_internal_request(false)
|
172
|
+
else
|
173
|
+
raise InternalRequestError.new(flash: @flash, reason: @error_reason, field_errors: @field_errors)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def _return_false_on_error!
|
178
|
+
@return_false_on_error = true
|
179
|
+
end
|
180
|
+
|
181
|
+
def _set_login_param_from_account
|
182
|
+
if session_value && !params[login_param] && (account = account_ds(session_value).first)
|
183
|
+
params[login_param] = account[login_column]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def _get_remember_cookie
|
188
|
+
params[remember_param]
|
189
|
+
end
|
190
|
+
|
191
|
+
def _handle_internal_request_eval(_)
|
192
|
+
v = instance_eval(&internal_request_block)
|
193
|
+
_set_internal_request_return_value(v) unless defined?(@internal_request_return_value)
|
194
|
+
end
|
195
|
+
|
196
|
+
def _handle_account_id_for_login(_)
|
197
|
+
raise InternalRequestError, "no login provided" unless login = param_or_nil(login_param)
|
198
|
+
raise InternalRequestError, "no account for login" unless account = account_from_login(login)
|
199
|
+
_return_from_internal_request(account[account_id_column])
|
200
|
+
end
|
201
|
+
|
202
|
+
def _handle_account_exists?(_)
|
203
|
+
raise InternalRequestError, "no login provided" unless login = param_or_nil(login_param)
|
204
|
+
_return_from_internal_request(!!account_from_login(login))
|
205
|
+
end
|
206
|
+
|
207
|
+
def _handle_lock_account(_)
|
208
|
+
raised_uniqueness_violation{account_lockouts_ds(session_value).insert(_setup_account_lockouts_hash(session_value, generate_unlock_account_key))}
|
209
|
+
end
|
210
|
+
|
211
|
+
def _handle_remember_setup(request)
|
212
|
+
params[remember_param] = remember_remember_param_value
|
213
|
+
_handle_remember(request)
|
214
|
+
end
|
215
|
+
|
216
|
+
def _handle_remember_disable(request)
|
217
|
+
params[remember_param] = remember_disable_param_value
|
218
|
+
_handle_remember(request)
|
219
|
+
end
|
220
|
+
|
221
|
+
def _handle_account_id_for_remember_key(request)
|
222
|
+
load_memory
|
223
|
+
raise InternalRequestError, "invalid remember key"
|
224
|
+
end
|
225
|
+
|
226
|
+
def _handle_otp_setup_params(request)
|
227
|
+
request.env['REQUEST_METHOD'] = 'GET'
|
228
|
+
_handle_otp_setup(request)
|
229
|
+
end
|
230
|
+
|
231
|
+
def _predicate_internal_request(meth, request)
|
232
|
+
_return_false_on_error!
|
233
|
+
_set_internal_request_return_value(true)
|
234
|
+
send(meth, request)
|
235
|
+
end
|
236
|
+
|
237
|
+
def _handle_valid_login_and_password?(request)
|
238
|
+
_predicate_internal_request(:_handle_login, request)
|
239
|
+
end
|
240
|
+
|
241
|
+
def _handle_valid_email_auth?(request)
|
242
|
+
_predicate_internal_request(:_handle_email_auth, request)
|
243
|
+
end
|
244
|
+
|
245
|
+
def _handle_valid_otp_auth?(request)
|
246
|
+
_predicate_internal_request(:_handle_otp_auth, request)
|
247
|
+
end
|
248
|
+
|
249
|
+
def _handle_valid_recovery_auth?(request)
|
250
|
+
_predicate_internal_request(:_handle_recovery_auth, request)
|
251
|
+
end
|
252
|
+
|
253
|
+
def _handle_valid_sms_auth?(request)
|
254
|
+
_predicate_internal_request(:_handle_sms_auth, request)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
module InternalRequestClassMethods
|
259
|
+
def internal_request(route, opts={}, &block)
|
260
|
+
opts = opts.dup
|
261
|
+
|
262
|
+
env = {
|
263
|
+
'REQUEST_METHOD'=>'POST',
|
264
|
+
'PATH_INFO'=>'/',
|
265
|
+
"SCRIPT_NAME" => "",
|
266
|
+
"HTTP_HOST" => INVALID_DOMAIN,
|
267
|
+
"SERVER_NAME" => INVALID_DOMAIN,
|
268
|
+
"SERVER_PORT" => 443,
|
269
|
+
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
|
270
|
+
"rack.input"=>StringIO.new(''),
|
271
|
+
"rack.url_scheme"=>"https"
|
272
|
+
}
|
273
|
+
env.merge!(opts.delete(:env)) if opts[:env]
|
274
|
+
|
275
|
+
session = {}
|
276
|
+
session.merge!(opts.delete(:session)) if opts[:session]
|
277
|
+
|
278
|
+
params = {}
|
279
|
+
params.merge!(opts.delete(:params)) if opts[:params]
|
280
|
+
|
281
|
+
scope = roda_class.new(env)
|
282
|
+
rodauth = new(scope)
|
283
|
+
rodauth.session = session
|
284
|
+
rodauth.params = params
|
285
|
+
rodauth.internal_request_block = block
|
286
|
+
|
287
|
+
unless account_id = opts.delete(:account_id)
|
288
|
+
if (account_login = opts.delete(:account_login))
|
289
|
+
if (account = rodauth.send(:_account_from_login, account_login))
|
290
|
+
account_id = account[rodauth.account_id_column]
|
291
|
+
else
|
292
|
+
raise InternalRequestError, "no account for login: #{account_login.inspect}"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
if account_id
|
298
|
+
session[rodauth.session_key] = account_id
|
299
|
+
unless authenticated_by = opts.delete(:authenticated_by)
|
300
|
+
authenticated_by = case route
|
301
|
+
when :otp_auth, :sms_request, :sms_auth, :recovery_auth, :valid_otp_auth?, :valid_sms_auth?, :valid_recovery_auth?
|
302
|
+
['internal1']
|
303
|
+
else
|
304
|
+
['internal1', 'internal2']
|
305
|
+
end
|
306
|
+
end
|
307
|
+
session[rodauth.authenticated_by_session_key] = authenticated_by
|
308
|
+
end
|
309
|
+
|
310
|
+
opts.keys.each do |k|
|
311
|
+
meth = :"#{k}_param"
|
312
|
+
params[rodauth.public_send(meth).to_s] = opts.delete(k) if rodauth.respond_to?(meth)
|
313
|
+
end
|
314
|
+
|
315
|
+
unless opts.empty?
|
316
|
+
warn "unhandled options passed to #{route}: #{opts.inspect}"
|
317
|
+
end
|
318
|
+
|
319
|
+
rodauth.handle_internal_request(:"_handle_#{route}")
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
Feature.define(:internal_request, :InternalRequest) do
|
324
|
+
configuration_module_eval do
|
325
|
+
def internal_request_configuration(&block)
|
326
|
+
@auth.instance_exec do
|
327
|
+
(@internal_request_configuration_blocks ||= []) << block
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
def post_configure
|
333
|
+
super
|
334
|
+
|
335
|
+
return if is_a?(InternalRequestMethods)
|
336
|
+
|
337
|
+
klass = self.class
|
338
|
+
internal_class = Class.new(klass) do
|
339
|
+
@roda_class = klass.roda_class
|
340
|
+
@features = klass.features.clone
|
341
|
+
@routes = klass.routes.clone
|
342
|
+
@route_hash = klass.route_hash.clone
|
343
|
+
@configuration = klass.configuration.clone
|
344
|
+
@configuration.instance_variable_set(:@auth, self)
|
345
|
+
end
|
346
|
+
|
347
|
+
if blocks = klass.instance_variable_get(:@internal_request_configuration_blocks)
|
348
|
+
configuration = internal_class.configuration
|
349
|
+
blocks.each do |block|
|
350
|
+
configuration.instance_exec(&block)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
internal_class.send(:extend, InternalRequestClassMethods)
|
354
|
+
internal_class.send(:include, InternalRequestMethods)
|
355
|
+
internal_class.allocate.post_configure
|
356
|
+
|
357
|
+
([:base] + internal_class.features).each do |feature_name|
|
358
|
+
feature = FEATURES[feature_name]
|
359
|
+
if meths = feature.internal_request_methods
|
360
|
+
meths.each do |name|
|
361
|
+
klass.define_singleton_method(name){|opts={}, &block| internal_class.internal_request(name, opts, &block)}
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|