rodauth 2.12.0 → 2.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +30 -0
  3. data/README.rdoc +50 -7
  4. data/doc/base.rdoc +1 -0
  5. data/doc/error_reasons.rdoc +73 -0
  6. data/doc/internal_request.rdoc +463 -0
  7. data/doc/path_class_methods.rdoc +10 -0
  8. data/doc/release_notes/2.13.0.txt +19 -0
  9. data/doc/release_notes/2.14.0.txt +17 -0
  10. data/doc/release_notes/2.15.0.txt +48 -0
  11. data/doc/release_notes/2.16.0.txt +20 -0
  12. data/doc/remember.rdoc +1 -0
  13. data/lib/rodauth/features/active_sessions.rb +1 -1
  14. data/lib/rodauth/features/base.rb +26 -1
  15. data/lib/rodauth/features/change_login.rb +6 -4
  16. data/lib/rodauth/features/change_password.rb +5 -3
  17. data/lib/rodauth/features/close_account.rb +3 -1
  18. data/lib/rodauth/features/confirm_password.rb +2 -2
  19. data/lib/rodauth/features/create_account.rb +6 -4
  20. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  21. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  22. data/lib/rodauth/features/email_auth.rb +6 -0
  23. data/lib/rodauth/features/internal_request.rb +371 -0
  24. data/lib/rodauth/features/jwt_refresh.rb +1 -1
  25. data/lib/rodauth/features/lockout.rb +15 -4
  26. data/lib/rodauth/features/login.rb +6 -3
  27. data/lib/rodauth/features/login_password_requirements_base.rb +15 -6
  28. data/lib/rodauth/features/otp.rb +13 -6
  29. data/lib/rodauth/features/password_complexity.rb +4 -4
  30. data/lib/rodauth/features/path_class_methods.rb +22 -0
  31. data/lib/rodauth/features/recovery_codes.rb +6 -2
  32. data/lib/rodauth/features/remember.rb +25 -10
  33. data/lib/rodauth/features/reset_password.rb +8 -4
  34. data/lib/rodauth/features/session_expiration.rb +1 -0
  35. data/lib/rodauth/features/single_session.rb +1 -0
  36. data/lib/rodauth/features/sms_codes.rb +17 -5
  37. data/lib/rodauth/features/two_factor_base.rb +6 -1
  38. data/lib/rodauth/features/verify_account.rb +8 -1
  39. data/lib/rodauth/features/verify_account_grace_period.rb +1 -1
  40. data/lib/rodauth/features/verify_login_change.rb +5 -2
  41. data/lib/rodauth/features/webauthn.rb +15 -14
  42. data/lib/rodauth/features/webauthn_login.rb +1 -1
  43. data/lib/rodauth/version.rb +1 -1
  44. data/lib/rodauth.rb +20 -2
  45. data/templates/button.str +1 -1
  46. data/templates/change-password.str +2 -2
  47. data/templates/global-logout-field.str +1 -1
  48. data/templates/login-confirm-field.str +2 -2
  49. data/templates/login-display.str +2 -2
  50. data/templates/login-field.str +2 -2
  51. data/templates/otp-auth-code-field.str +2 -2
  52. data/templates/otp-setup.str +2 -2
  53. data/templates/password-confirm-field.str +2 -2
  54. data/templates/password-field.str +2 -2
  55. data/templates/recovery-auth.str +2 -2
  56. data/templates/remember.str +1 -1
  57. data/templates/sms-code-field.str +2 -2
  58. data/templates/sms-setup.str +2 -2
  59. data/templates/webauthn-remove.str +1 -1
  60. metadata +19 -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,19 @@
1
+ = New Features
2
+
3
+ * A set_error_reason configuration method has been added. This method
4
+ is called whenever a error occurs in Rodauth, with a symbol
5
+ describing the error. The default implementation of this method does
6
+ nothing, it has been added to make it easier for Rodauth users to
7
+ implement custom handling for specific error types. See the Rodauth
8
+ documentation for this method to see the list of symbols this method
9
+ can be called with.
10
+
11
+ = Other Improvements
12
+
13
+ * When using active_sessions and jwt_refresh together, and allowing for
14
+ expired JWTs when refreshing, you can now call
15
+ rodauth.check_active_session before r.rodauth. Previously, this
16
+ did not work, and you had to call rodauth.check_active_session
17
+ after r.rodauth.
18
+
19
+ * The default templates now also support Bootstrap 5.
@@ -0,0 +1,17 @@
1
+ = New Features
2
+
3
+ * A remembered_session_id method has been added for getting the
4
+ account id from a valid remember token, without modifying the
5
+ session to log the account in.
6
+
7
+ = Other Improvements
8
+
9
+ * The jwt_refresh feature's support for allowing refresh with
10
+ an expired access token now works even if the Rodauth
11
+ configuration uses an incorrect prefix.
12
+
13
+ * The internal account_in_unverified_grace_period? method now
14
+ returns false if an account has not been loaded and the
15
+ session has not been logged in. Previously, calling this
16
+ method in such cases would result in an exception being
17
+ 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.
@@ -0,0 +1,20 @@
1
+ = New Features
2
+
3
+ * Rodauth.lib has been added for using Rodauth purely as a library,
4
+ useful in non-web applications:
5
+
6
+ require 'rodauth'
7
+ rodauth = Rodauth.lib do
8
+ enable :create_account, :change_password
9
+ end
10
+ rodauth.create_account(login: 'foo@example.com', password: '...')
11
+ rodauth.change_password(account_id: 24601, password: '...')
12
+
13
+ This is built on top of the internal_request feature, and works by
14
+ creating a Roda application with the rodauth plugin, and returning
15
+ the related Rodauth::Auth class.
16
+
17
+ = Other Improvements
18
+
19
+ * The internal_request feature now works correctly for configurations
20
+ where only_json? is set to true.
data/doc/remember.rdoc CHANGED
@@ -69,6 +69,7 @@ generate_remember_key_value :: A random string to use as the remember key.
69
69
  get_remember_key :: Retrieve the remember key from the database.
70
70
  load_memory :: If the remember key cookie is included in the request, and the user is not currently logged in, check the remember keys table and autologin the user if the remember key cookie matches the current remember key for the account. This method needs to be called manually inside the Roda route block to autologin users.
71
71
  logged_in_via_remember_key? :: Whether the current session was logged in via a remember key.
72
+ remembered_session_id :: The session_id which is validly remembered, if any.
72
73
  remember_key_value :: The current value of the remember key/token.
73
74
  remember_login :: Set the cookie containing the remember token, so that future sessions will be autologged in.
74
75
  remember_view :: The HTML to use for the change remember settings form.
@@ -57,6 +57,7 @@ module Rodauth
57
57
  def no_longer_active_session
58
58
  clear_session
59
59
  set_redirect_error_status inactive_session_error_status
60
+ set_error_reason :inactive_session
60
61
  set_redirect_error_flash active_sessions_error_flash
61
62
  redirect active_sessions_redirect
62
63
  end
@@ -176,4 +177,3 @@ module Rodauth
176
177
  end
177
178
  end
178
179
  end
179
-
@@ -100,6 +100,7 @@ module Rodauth
100
100
  :set_notice_flash,
101
101
  :set_notice_now_flash,
102
102
  :set_redirect_error_flash,
103
+ :set_error_reason,
103
104
  :set_title,
104
105
  :translate,
105
106
  :update_session
@@ -114,6 +115,10 @@ module Rodauth
114
115
  :around_rodauth
115
116
  )
116
117
 
118
+ internal_request_method :account_exists?
119
+ internal_request_method :account_id_for_login
120
+ internal_request_method :internal_request_eval
121
+
117
122
  configuration_module_eval do
118
123
  def auth_class_eval(&block)
119
124
  auth.class_eval(&block)
@@ -294,6 +299,7 @@ module Rodauth
294
299
 
295
300
  def login_required
296
301
  set_redirect_error_status(login_required_error_status)
302
+ set_error_reason :login_required
297
303
  set_redirect_error_flash require_login_error_flash
298
304
  redirect require_login_redirect
299
305
  end
@@ -443,7 +449,9 @@ module Rodauth
443
449
  end
444
450
 
445
451
  def base_url
446
- 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
447
455
  end
448
456
 
449
457
  def domain
@@ -539,6 +547,11 @@ module Rodauth
539
547
  def set_response_error_status(status)
540
548
  response.status = status
541
549
  end
550
+
551
+ def set_response_error_reason_status(reason, status)
552
+ set_error_reason(reason)
553
+ set_response_error_status(status)
554
+ end
542
555
 
543
556
  def throw_error(field, error)
544
557
  set_field_error(field, error)
@@ -550,6 +563,14 @@ module Rodauth
550
563
  throw_error(field, error)
551
564
  end
552
565
 
566
+ def set_error_reason(reason)
567
+ end
568
+
569
+ def throw_error_reason(reason, status, field, message)
570
+ set_error_reason(reason)
571
+ throw_error_status(status, field, message)
572
+ end
573
+
553
574
  def use_date_arithmetic?
554
575
  set_deadline_values?
555
576
  end
@@ -719,6 +740,10 @@ module Rodauth
719
740
  end
720
741
  end
721
742
 
743
+ def internal_request?
744
+ false
745
+ end
746
+
722
747
  def set_session_value(key, value)
723
748
  session[key] = value
724
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
@@ -30,7 +32,7 @@ module Rodauth
30
32
  r.post do
31
33
  catch_error do
32
34
  if change_login_requires_password? && !password_match?(param(password_param))
33
- throw_error_status(invalid_password_error_status, password_param, invalid_password_message)
35
+ throw_error_reason(:invalid_password, invalid_password_error_status, password_param, invalid_password_message)
34
36
  end
35
37
 
36
38
  login = param(login_param)
@@ -39,7 +41,7 @@ module Rodauth
39
41
  end
40
42
 
41
43
  if require_login_confirmation? && login != param(login_confirm_param)
42
- throw_error_status(unmatched_field_error_status, login_param, logins_do_not_match_message)
44
+ throw_error_reason(:logins_do_not_match, unmatched_field_error_status, login_param, logins_do_not_match_message)
43
45
  end
44
46
 
45
47
  transaction do
@@ -65,7 +67,7 @@ module Rodauth
65
67
 
66
68
  def change_login(login)
67
69
  if account_ds.get(login_column).downcase == login.downcase
68
- @login_requirement_message = same_as_current_login_message
70
+ set_login_requirement_error_message(:same_as_current_login, same_as_current_login_message)
69
71
  return false
70
72
  end
71
73
 
@@ -82,7 +84,7 @@ module Rodauth
82
84
  updated = nil
83
85
  raised = raises_uniqueness_violation?{updated = update_account({login_column=>login}, account_ds.exclude(login_column=>login)) == 1}
84
86
  if raised
85
- @login_requirement_message = already_an_account_with_this_login_message
87
+ set_login_requirement_error_message(:already_an_account_with_this_login, already_an_account_with_this_login_message)
86
88
  end
87
89
  updated && !raised
88
90
  end
@@ -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
@@ -33,16 +35,16 @@ module Rodauth
33
35
  r.post do
34
36
  catch_error do
35
37
  if change_password_requires_password? && !password_match?(param(password_param))
36
- throw_error_status(invalid_password_error_status, password_param, invalid_previous_password_message)
38
+ throw_error_reason(:invalid_previous_password, invalid_password_error_status, password_param, invalid_previous_password_message)
37
39
  end
38
40
 
39
41
  password = param(new_password_param)
40
42
  if require_password_confirmation? && password != param(password_confirm_param)
41
- throw_error_status(unmatched_field_error_status, new_password_param, passwords_do_not_match_message)
43
+ throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, new_password_param, passwords_do_not_match_message)
42
44
  end
43
45
 
44
46
  if password_match?(password)
45
- throw_error_status(invalid_field_error_status, new_password_param, same_as_existing_password_message)
47
+ throw_error_reason(:same_as_existing_password, invalid_field_error_status, new_password_param, same_as_existing_password_message)
46
48
  end
47
49
 
48
50
  unless password_meets_requirements?(password)
@@ -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
@@ -35,7 +37,7 @@ module Rodauth
35
37
  r.post do
36
38
  catch_error do
37
39
  if close_account_requires_password? && !password_match?(param(password_param))
38
- throw_error_status(invalid_password_error_status, password_param, invalid_password_message)
40
+ throw_error_reason(:invalid_password, invalid_password_error_status, password_param, invalid_password_message)
39
41
  end
40
42
 
41
43
  transaction do
@@ -40,7 +40,7 @@ module Rodauth
40
40
  set_notice_flash confirm_password_notice_flash
41
41
  redirect confirm_password_redirect
42
42
  else
43
- set_response_error_status(invalid_password_error_status)
43
+ set_response_error_reason_status(:invalid_password, invalid_password_error_status)
44
44
  set_field_error(password_param, invalid_password_message)
45
45
  set_error_flash confirm_password_error_flash
46
46
  confirm_password_view
@@ -53,6 +53,7 @@ module Rodauth
53
53
 
54
54
  if require_password_authentication? && has_password?
55
55
  set_redirect_error_status(password_authentication_required_error_status)
56
+ set_error_reason :password_authentication_required
56
57
  set_redirect_error_flash password_authentication_required_error_flash
57
58
  set_session_value(confirm_password_redirect_session_key, request.fullpath)
58
59
  redirect password_authentication_required_redirect
@@ -89,4 +90,3 @@ module Rodauth
89
90
  end
90
91
  end
91
92
  end
92
-
@@ -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
@@ -43,7 +45,7 @@ module Rodauth
43
45
 
44
46
  catch_error do
45
47
  if require_login_confirmation? && login != param(login_confirm_param)
46
- throw_error_status(unmatched_field_error_status, login_param, logins_do_not_match_message)
48
+ throw_error_reason(:logins_do_not_match, unmatched_field_error_status, login_param, logins_do_not_match_message)
47
49
  end
48
50
 
49
51
  unless login_meets_requirements?(login)
@@ -52,11 +54,11 @@ module Rodauth
52
54
 
53
55
  if create_account_set_password?
54
56
  if require_password_confirmation? && password != param(password_confirm_param)
55
- throw_error_status(unmatched_field_error_status, password_param, passwords_do_not_match_message)
57
+ throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, password_param, passwords_do_not_match_message)
56
58
  end
57
59
 
58
60
  unless password_meets_requirements?(password)
59
- throw_error_status(invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
61
+ throw_error_reason(:password_does_not_meet_requirements, invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
60
62
  end
61
63
 
62
64
  if account_password_hash_column
@@ -100,7 +102,7 @@ module Rodauth
100
102
  raised = raises_uniqueness_violation?{id = db[accounts_table].insert(account)}
101
103
 
102
104
  if raised
103
- @login_requirement_message = already_an_account_with_this_login_message
105
+ set_login_requirement_error_message(:already_an_account_with_this_login, already_an_account_with_this_login_message)
104
106
  end
105
107
 
106
108
  if id
@@ -32,7 +32,7 @@ module Rodauth
32
32
 
33
33
  def password_not_one_of_the_most_common?(password)
34
34
  return true unless password_one_of_most_common?(password)
35
- @password_requirement_message = password_is_one_of_the_most_common_message
35
+ set_password_requirement_error_message(:password_is_one_of_the_most_common, password_is_one_of_the_most_common_message)
36
36
  false
37
37
  end
38
38
  end
@@ -65,7 +65,7 @@ module Rodauth
65
65
  end
66
66
 
67
67
  return true unless match
68
- @password_requirement_message = password_same_as_previous_password_message
68
+ set_password_requirement_error_message(:password_same_as_previous_password, password_same_as_previous_password_message)
69
69
  false
70
70
  end
71
71
 
@@ -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
@@ -58,6 +62,7 @@ module Rodauth
58
62
  _email_auth_request
59
63
  else
60
64
  set_redirect_error_status(no_matching_login_error_status)
65
+ set_error_reason :no_matching_login
61
66
  set_redirect_error_flash email_auth_request_error_flash
62
67
  end
63
68
 
@@ -90,6 +95,7 @@ module Rodauth
90
95
  key = session[email_auth_session_key] || param(email_auth_key_param)
91
96
  unless account_from_email_auth_key(key)
92
97
  set_redirect_error_status(invalid_key_error_status)
98
+ set_error_reason :invalid_email_auth_key
93
99
  set_redirect_error_flash email_auth_error_flash
94
100
  redirect email_auth_email_sent_redirect
95
101
  end
@@ -0,0 +1,371 @@
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
+ def only_json?
96
+ false
97
+ end
98
+
99
+ private
100
+
101
+ def internal_request?
102
+ true
103
+ end
104
+
105
+ def set_error_reason(reason)
106
+ @error_reason = reason
107
+ end
108
+
109
+ def after_login
110
+ super
111
+ _set_internal_request_return_value(account_id) unless @return_false_on_error
112
+ end
113
+
114
+ def after_remember
115
+ super
116
+ if params[remember_param] == remember_remember_param_value
117
+ _set_internal_request_return_value("#{account_id}_#{convert_token_key(remember_key_value)}")
118
+ end
119
+ end
120
+
121
+ def after_load_memory
122
+ super
123
+ _return_from_internal_request(session_value)
124
+ end
125
+
126
+ def before_change_password_route
127
+ super
128
+ params[new_password_param] ||= params[password_param]
129
+ end
130
+
131
+ def before_email_auth_request_route
132
+ super
133
+ _set_login_param_from_account
134
+ end
135
+
136
+ def before_login_route
137
+ super
138
+ _set_login_param_from_account
139
+ end
140
+
141
+ def before_unlock_account_request_route
142
+ super
143
+ _set_login_param_from_account
144
+ end
145
+
146
+ def before_reset_password_request_route
147
+ super
148
+ _set_login_param_from_account
149
+ end
150
+
151
+ def before_verify_account_resend_route
152
+ super
153
+ _set_login_param_from_account
154
+ end
155
+
156
+ def account_from_key(token, status_id=nil)
157
+ return super unless session_value
158
+ return unless yield session_value
159
+ ds = account_ds(session_value)
160
+ ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
161
+ ds.first
162
+ end
163
+
164
+ def _set_internal_request_return_value(value)
165
+ @internal_request_return_value = value
166
+ end
167
+
168
+ def _return_from_internal_request(value)
169
+ _set_internal_request_return_value(value)
170
+ throw(:halt)
171
+ end
172
+
173
+ def _handle_internal_request_error
174
+ if @return_false_on_error
175
+ _return_from_internal_request(false)
176
+ else
177
+ raise InternalRequestError.new(flash: @flash, reason: @error_reason, field_errors: @field_errors)
178
+ end
179
+ end
180
+
181
+ def _return_false_on_error!
182
+ @return_false_on_error = true
183
+ end
184
+
185
+ def _set_login_param_from_account
186
+ if session_value && !params[login_param] && (account = account_ds(session_value).first)
187
+ params[login_param] = account[login_column]
188
+ end
189
+ end
190
+
191
+ def _get_remember_cookie
192
+ params[remember_param]
193
+ end
194
+
195
+ def _handle_internal_request_eval(_)
196
+ v = instance_eval(&internal_request_block)
197
+ _set_internal_request_return_value(v) unless defined?(@internal_request_return_value)
198
+ end
199
+
200
+ def _handle_account_id_for_login(_)
201
+ raise InternalRequestError, "no login provided" unless login = param_or_nil(login_param)
202
+ raise InternalRequestError, "no account for login" unless account = account_from_login(login)
203
+ _return_from_internal_request(account[account_id_column])
204
+ end
205
+
206
+ def _handle_account_exists?(_)
207
+ raise InternalRequestError, "no login provided" unless login = param_or_nil(login_param)
208
+ _return_from_internal_request(!!account_from_login(login))
209
+ end
210
+
211
+ def _handle_lock_account(_)
212
+ raised_uniqueness_violation{account_lockouts_ds(session_value).insert(_setup_account_lockouts_hash(session_value, generate_unlock_account_key))}
213
+ end
214
+
215
+ def _handle_remember_setup(request)
216
+ params[remember_param] = remember_remember_param_value
217
+ _handle_remember(request)
218
+ end
219
+
220
+ def _handle_remember_disable(request)
221
+ params[remember_param] = remember_disable_param_value
222
+ _handle_remember(request)
223
+ end
224
+
225
+ def _handle_account_id_for_remember_key(request)
226
+ load_memory
227
+ raise InternalRequestError, "invalid remember key"
228
+ end
229
+
230
+ def _handle_otp_setup_params(request)
231
+ request.env['REQUEST_METHOD'] = 'GET'
232
+ _handle_otp_setup(request)
233
+ end
234
+
235
+ def _predicate_internal_request(meth, request)
236
+ _return_false_on_error!
237
+ _set_internal_request_return_value(true)
238
+ send(meth, request)
239
+ end
240
+
241
+ def _handle_valid_login_and_password?(request)
242
+ _predicate_internal_request(:_handle_login, request)
243
+ end
244
+
245
+ def _handle_valid_email_auth?(request)
246
+ _predicate_internal_request(:_handle_email_auth, request)
247
+ end
248
+
249
+ def _handle_valid_otp_auth?(request)
250
+ _predicate_internal_request(:_handle_otp_auth, request)
251
+ end
252
+
253
+ def _handle_valid_recovery_auth?(request)
254
+ _predicate_internal_request(:_handle_recovery_auth, request)
255
+ end
256
+
257
+ def _handle_valid_sms_auth?(request)
258
+ _predicate_internal_request(:_handle_sms_auth, request)
259
+ end
260
+ end
261
+
262
+ module InternalRequestClassMethods
263
+ def internal_request(route, opts={}, &block)
264
+ opts = opts.dup
265
+
266
+ env = {
267
+ 'REQUEST_METHOD'=>'POST',
268
+ 'PATH_INFO'=>'/',
269
+ "SCRIPT_NAME" => "",
270
+ "HTTP_HOST" => INVALID_DOMAIN,
271
+ "SERVER_NAME" => INVALID_DOMAIN,
272
+ "SERVER_PORT" => 443,
273
+ "CONTENT_TYPE" => "application/x-www-form-urlencoded",
274
+ "rack.input"=>StringIO.new(''),
275
+ "rack.url_scheme"=>"https"
276
+ }
277
+ env.merge!(opts.delete(:env)) if opts[:env]
278
+
279
+ session = {}
280
+ session.merge!(opts.delete(:session)) if opts[:session]
281
+
282
+ params = {}
283
+ params.merge!(opts.delete(:params)) if opts[:params]
284
+
285
+ scope = roda_class.new(env)
286
+ rodauth = new(scope)
287
+ rodauth.session = session
288
+ rodauth.params = params
289
+ rodauth.internal_request_block = block
290
+
291
+ unless account_id = opts.delete(:account_id)
292
+ if (account_login = opts.delete(:account_login))
293
+ if (account = rodauth.send(:_account_from_login, account_login))
294
+ account_id = account[rodauth.account_id_column]
295
+ else
296
+ raise InternalRequestError, "no account for login: #{account_login.inspect}"
297
+ end
298
+ end
299
+ end
300
+
301
+ if account_id
302
+ session[rodauth.session_key] = account_id
303
+ unless authenticated_by = opts.delete(:authenticated_by)
304
+ authenticated_by = case route
305
+ when :otp_auth, :sms_request, :sms_auth, :recovery_auth, :valid_otp_auth?, :valid_sms_auth?, :valid_recovery_auth?
306
+ ['internal1']
307
+ else
308
+ ['internal1', 'internal2']
309
+ end
310
+ end
311
+ session[rodauth.authenticated_by_session_key] = authenticated_by
312
+ end
313
+
314
+ opts.keys.each do |k|
315
+ meth = :"#{k}_param"
316
+ params[rodauth.public_send(meth).to_s] = opts.delete(k) if rodauth.respond_to?(meth)
317
+ end
318
+
319
+ unless opts.empty?
320
+ warn "unhandled options passed to #{route}: #{opts.inspect}"
321
+ end
322
+
323
+ rodauth.handle_internal_request(:"_handle_#{route}")
324
+ end
325
+ end
326
+
327
+ Feature.define(:internal_request, :InternalRequest) do
328
+ configuration_module_eval do
329
+ def internal_request_configuration(&block)
330
+ @auth.instance_exec do
331
+ (@internal_request_configuration_blocks ||= []) << block
332
+ end
333
+ end
334
+ end
335
+
336
+ def post_configure
337
+ super
338
+
339
+ return if is_a?(InternalRequestMethods)
340
+
341
+ klass = self.class
342
+ internal_class = Class.new(klass) do
343
+ @roda_class = klass.roda_class
344
+ @features = klass.features.clone
345
+ @routes = klass.routes.clone
346
+ @route_hash = klass.route_hash.clone
347
+ @configuration = klass.configuration.clone
348
+ @configuration.instance_variable_set(:@auth, self)
349
+ end
350
+
351
+ if blocks = klass.instance_variable_get(:@internal_request_configuration_blocks)
352
+ configuration = internal_class.configuration
353
+ blocks.each do |block|
354
+ configuration.instance_exec(&block)
355
+ end
356
+ end
357
+ internal_class.send(:extend, InternalRequestClassMethods)
358
+ internal_class.send(:include, InternalRequestMethods)
359
+ internal_class.allocate.post_configure
360
+
361
+ ([:base] + internal_class.features).each do |feature_name|
362
+ feature = FEATURES[feature_name]
363
+ if meths = feature.internal_request_methods
364
+ meths.each do |name|
365
+ klass.define_singleton_method(name){|opts={}, &block| internal_class.internal_request(name, opts, &block)}
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end
371
+ end