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.
@@ -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.base_url
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
@@ -19,6 +19,8 @@ module Rodauth
19
19
 
20
20
  auth_methods :change_login
21
21
 
22
+ internal_request_method
23
+
22
24
  route do |r|
23
25
  require_account
24
26
  before_change_login_route
@@ -22,6 +22,8 @@ module Rodauth
22
22
  :invalid_previous_password_message
23
23
  )
24
24
 
25
+ internal_request_method
26
+
25
27
  route do |r|
26
28
  require_account
27
29
  before_change_password_route
@@ -24,6 +24,8 @@ module Rodauth
24
24
  :delete_account
25
25
  )
26
26
 
27
+ internal_request_method
28
+
27
29
  route do |r|
28
30
  require_account
29
31
  before_close_account_route
@@ -27,6 +27,8 @@ module Rodauth
27
27
  :new_account
28
28
  )
29
29
 
30
+ internal_request_method
31
+
30
32
  route do |r|
31
33
  check_already_logged_in
32
34
  before_create_account_route
@@ -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