rodauth 2.14.0 → 2.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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