quo_vadis 1.4.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +11 -8
- data/CHANGELOG.md +5 -0
- data/Gemfile +14 -1
- data/Gemfile.lock +178 -0
- data/LICENSE.txt +21 -0
- data/README.md +435 -127
- data/Rakefile +15 -9
- data/app/controllers/quo_vadis/confirmations_controller.rb +56 -0
- data/app/controllers/quo_vadis/logs_controller.rb +20 -0
- data/app/controllers/quo_vadis/password_resets_controller.rb +65 -0
- data/app/controllers/quo_vadis/passwords_controller.rb +26 -0
- data/app/controllers/quo_vadis/recovery_codes_controller.rb +54 -0
- data/app/controllers/quo_vadis/sessions_controller.rb +50 -132
- data/app/controllers/quo_vadis/totps_controller.rb +72 -0
- data/app/controllers/quo_vadis/twofas_controller.rb +26 -0
- data/app/mailers/quo_vadis/mailer.rb +73 -0
- data/app/models/quo_vadis/account.rb +59 -0
- data/app/models/quo_vadis/account_confirmation_token.rb +17 -0
- data/app/models/quo_vadis/log.rb +57 -0
- data/app/models/quo_vadis/password.rb +52 -0
- data/app/models/quo_vadis/password_reset_token.rb +17 -0
- data/app/models/quo_vadis/recovery_code.rb +26 -0
- data/app/models/quo_vadis/session.rb +55 -0
- data/app/models/quo_vadis/token.rb +42 -0
- data/app/models/quo_vadis/totp.rb +56 -0
- data/bin/console +15 -0
- data/bin/rails +21 -0
- data/bin/setup +8 -0
- data/config/locales/quo_vadis.en.yml +50 -23
- data/config/routes.rb +40 -12
- data/db/migrate/202102150904_setup.rb +48 -0
- data/lib/generators/quo_vadis/install_generator.rb +4 -23
- data/lib/quo_vadis.rb +100 -98
- data/lib/quo_vadis/controller.rb +227 -0
- data/lib/quo_vadis/crypt.rb +43 -0
- data/lib/quo_vadis/current_request_details.rb +11 -0
- data/lib/quo_vadis/defaults.rb +18 -0
- data/lib/quo_vadis/encrypted_type.rb +17 -0
- data/lib/quo_vadis/engine.rb +9 -11
- data/lib/quo_vadis/hmacable.rb +26 -0
- data/lib/quo_vadis/ip_masking.rb +31 -0
- data/lib/quo_vadis/model.rb +86 -0
- data/lib/quo_vadis/version.rb +3 -1
- data/quo_vadis.gemspec +18 -25
- metadata +46 -246
- data/app/controllers/controller_mixin.rb +0 -109
- data/app/mailers/quo_vadis/notifier.rb +0 -30
- data/app/models/model_mixin.rb +0 -128
- data/lib/generators/quo_vadis/templates/migration.rb.erb +0 -18
- data/lib/generators/quo_vadis/templates/quo_vadis.rb.erb +0 -96
- data/test/dummy/.gitignore +0 -2
- data/test/dummy/Rakefile +0 -7
- data/test/dummy/app/controllers/application_controller.rb +0 -3
- data/test/dummy/app/controllers/articles_controller.rb +0 -20
- data/test/dummy/app/controllers/users_controller.rb +0 -17
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/helpers/articles_helper.rb +0 -2
- data/test/dummy/app/models/article.rb +0 -2
- data/test/dummy/app/models/person.rb +0 -3
- data/test/dummy/app/models/user.rb +0 -3
- data/test/dummy/app/views/articles/index.html.erb +0 -1
- data/test/dummy/app/views/articles/new.html.erb +0 -11
- data/test/dummy/app/views/layouts/application.html.erb +0 -30
- data/test/dummy/app/views/layouts/sessions.html.erb +0 -3
- data/test/dummy/app/views/quo_vadis/notifier/change_password.text.erb +0 -9
- data/test/dummy/app/views/quo_vadis/notifier/invite.text.erb +0 -8
- data/test/dummy/app/views/sessions/edit.html.erb +0 -11
- data/test/dummy/app/views/sessions/forgotten.html.erb +0 -13
- data/test/dummy/app/views/sessions/invite.html.erb +0 -31
- data/test/dummy/app/views/sessions/new.html.erb +0 -15
- data/test/dummy/app/views/users/new.html.erb +0 -14
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -21
- data/test/dummy/config/boot.rb +0 -10
- data/test/dummy/config/database.yml +0 -22
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -26
- data/test/dummy/config/environments/production.rb +0 -49
- data/test/dummy/config/environments/test.rb +0 -37
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/inflections.rb +0 -10
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/quo_vadis.rb +0 -77
- data/test/dummy/config/initializers/rack_patch.rb +0 -16
- data/test/dummy/config/initializers/secret_token.rb +0 -7
- data/test/dummy/config/initializers/session_store.rb +0 -8
- data/test/dummy/config/locales/en.yml +0 -5
- data/test/dummy/config/locales/quo_vadis.en.yml +0 -21
- data/test/dummy/config/routes.rb +0 -5
- data/test/dummy/db/migrate/20110124125037_create_users.rb +0 -13
- data/test/dummy/db/migrate/20110124131535_create_articles.rb +0 -14
- data/test/dummy/db/migrate/20110127094709_add_authentication_to_users.rb +0 -18
- data/test/dummy/db/migrate/20111004112209_create_people.rb +0 -13
- data/test/dummy/db/migrate/20111004132342_add_authentication_to_people.rb +0 -18
- data/test/dummy/db/schema.rb +0 -33
- data/test/dummy/public/404.html +0 -26
- data/test/dummy/public/422.html +0 -26
- data/test/dummy/public/500.html +0 -26
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/javascripts/application.js +0 -2
- data/test/dummy/public/javascripts/controls.js +0 -965
- data/test/dummy/public/javascripts/dragdrop.js +0 -974
- data/test/dummy/public/javascripts/effects.js +0 -1123
- data/test/dummy/public/javascripts/prototype.js +0 -6001
- data/test/dummy/public/javascripts/rails.js +0 -175
- data/test/dummy/public/stylesheets/.gitkeep +0 -0
- data/test/dummy/script/rails +0 -6
- data/test/integration/activation_test.rb +0 -108
- data/test/integration/authenticate_test.rb +0 -39
- data/test/integration/blocked_test.rb +0 -23
- data/test/integration/config_test.rb +0 -118
- data/test/integration/cookie_test.rb +0 -67
- data/test/integration/csrf_test.rb +0 -41
- data/test/integration/forgotten_test.rb +0 -93
- data/test/integration/helper_test.rb +0 -18
- data/test/integration/locale_test.rb +0 -197
- data/test/integration/navigation_test.rb +0 -7
- data/test/integration/sign_in_person_test.rb +0 -26
- data/test/integration/sign_in_test.rb +0 -24
- data/test/integration/sign_out_test.rb +0 -20
- data/test/integration/sign_up_test.rb +0 -21
- data/test/quo_vadis_test.rb +0 -7
- data/test/support/integration_case.rb +0 -11
- data/test/test_helper.rb +0 -86
- data/test/unit/user_test.rb +0 -75
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module QuoVadis
|
4
|
+
module Controller
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.before_action { CurrentRequestDetails.request = request }
|
8
|
+
|
9
|
+
base.helper_method :authenticated_model, :logged_in?
|
10
|
+
|
11
|
+
# Remember the last activity time so we can timeout idle sessions.
|
12
|
+
# This has to be done after that timestamp is checked (in `#authenticated_model`)
|
13
|
+
# otherwise sessions could never look idle.
|
14
|
+
base.after_action { |controller| controller.qv.touch_session_last_seen_at }
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def require_password_authentication
|
19
|
+
return if logged_in?
|
20
|
+
flash[:notice] = QuoVadis.translate 'flash.require_authentication'
|
21
|
+
session[:qv_bookmark] = request.original_fullpath
|
22
|
+
redirect_to quo_vadis.login_path
|
23
|
+
end
|
24
|
+
alias_method :require_authentication, :require_password_authentication
|
25
|
+
|
26
|
+
|
27
|
+
# implies require_password_authentication
|
28
|
+
def require_two_factor_authentication
|
29
|
+
return require_authentication unless logged_in?
|
30
|
+
return unless qv.second_factor_required?
|
31
|
+
return if qv.second_factor_authenticated?
|
32
|
+
redirect_to quo_vadis.challenge_totps_path and return
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# To be called with a model which has authenticated with a password.
|
37
|
+
#
|
38
|
+
# browser_session - true: login only for duration of browser session
|
39
|
+
# false: login for QuoVadis.session_lifetime (which may be browser session anyway)
|
40
|
+
def login(model, browser_session = true)
|
41
|
+
qv.log model.qv_account, Log::LOGIN_SUCCESS
|
42
|
+
|
43
|
+
qv.prevent_rails_session_fixation
|
44
|
+
|
45
|
+
lifetime_expires_at = qv.lifetime_expires_at browser_session
|
46
|
+
|
47
|
+
qv_session = model.qv_account.sessions.create!(
|
48
|
+
ip: request.remote_ip,
|
49
|
+
user_agent: (request.user_agent || ''),
|
50
|
+
lifetime_expires_at: lifetime_expires_at
|
51
|
+
)
|
52
|
+
|
53
|
+
qv.store_session_id qv_session.id, lifetime_expires_at
|
54
|
+
|
55
|
+
# It is not necessary to set the instance variable here -- the
|
56
|
+
# `authenticated_model` method will figure it out from the qv.session --
|
57
|
+
# but doing so saves that method a couple of database calls.
|
58
|
+
@authenticated_model = model
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def logged_in?
|
63
|
+
!authenticated_model.nil?
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# Returns the model instance which has been authenticated by password,
|
68
|
+
# or nil.
|
69
|
+
def authenticated_model
|
70
|
+
return @authenticated_model if defined? @authenticated_model
|
71
|
+
|
72
|
+
# Was not logged in so no need to log out.
|
73
|
+
return (@authenticated_model = nil) unless qv.session_id
|
74
|
+
|
75
|
+
_qv_session = qv.session
|
76
|
+
|
77
|
+
# If _qv_session is nil: user was logged in (because qv.session_id is not nil)
|
78
|
+
# but now isn't (because there is no corresponding record in the database). This
|
79
|
+
# means the user has remotely logged out this session from another.
|
80
|
+
if _qv_session.nil? || _qv_session.expired?
|
81
|
+
qv.logout
|
82
|
+
return (@authenticated_model = nil)
|
83
|
+
end
|
84
|
+
|
85
|
+
@authenticated_model = _qv_session.account.model
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def request_confirmation(model)
|
90
|
+
token = QuoVadis::AccountConfirmationToken.generate model.qv_account
|
91
|
+
QuoVadis.deliver :account_confirmation, email: model.email, url: quo_vadis.edit_confirmation_url(token)
|
92
|
+
|
93
|
+
flash[:notice] = QuoVadis.translate 'flash.confirmation.create'
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
def qv
|
98
|
+
@qv_wrapper ||= QuoVadisWrapper.new self
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
|
105
|
+
class QuoVadisWrapper
|
106
|
+
def initialize(controller)
|
107
|
+
@controller = controller
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns the current QuoVadis session or nil.
|
111
|
+
def session
|
112
|
+
return nil unless session_id
|
113
|
+
QuoVadis::Session.find_by id: session_id
|
114
|
+
end
|
115
|
+
|
116
|
+
def session_id
|
117
|
+
cookies.encrypted[QuoVadis.cookie_name]
|
118
|
+
end
|
119
|
+
|
120
|
+
# Store the session id in an encrypted cookie.
|
121
|
+
#
|
122
|
+
# Given that the cookie is encrypted, it is safe to store the database primary key of the
|
123
|
+
# session rather than a random-value candidate key.
|
124
|
+
#
|
125
|
+
# expires_at - the end of the QuoVadis session's lifetime (regardless of the idle timeout)
|
126
|
+
def store_session_id(id, expires_at)
|
127
|
+
cookies.encrypted[QuoVadis.cookie_name] = {
|
128
|
+
value: id,
|
129
|
+
httponly: true,
|
130
|
+
secure: Rails.env.production?,
|
131
|
+
same_site: :lax,
|
132
|
+
expires: expires_at # setting expires_at to nil has the same effect as not setting it
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
def clear_session_id
|
137
|
+
cookies.delete QuoVadis.cookie_name
|
138
|
+
end
|
139
|
+
|
140
|
+
def prevent_rails_session_fixation
|
141
|
+
old_session = rails_session.to_hash
|
142
|
+
reset_session
|
143
|
+
old_session.each { |k,v| rails_session[k] = v }
|
144
|
+
end
|
145
|
+
|
146
|
+
# Assumes user is logged in.
|
147
|
+
def second_factor_required?
|
148
|
+
QuoVadis.two_factor_authentication_mandatory || authenticated_model.qv_account.has_two_factors?
|
149
|
+
end
|
150
|
+
|
151
|
+
def second_factor_authenticated?
|
152
|
+
session.second_factor_authenticated?
|
153
|
+
end
|
154
|
+
|
155
|
+
def touch_session_last_seen_at
|
156
|
+
session&.touch :last_seen_at
|
157
|
+
end
|
158
|
+
|
159
|
+
def session_authenticated_with_second_factor
|
160
|
+
session.authenticated_with_second_factor
|
161
|
+
end
|
162
|
+
|
163
|
+
def replace_session
|
164
|
+
prevent_rails_session_fixation
|
165
|
+
|
166
|
+
sess = session.replace
|
167
|
+
store_session_id sess.id, sess.lifetime_expires_at
|
168
|
+
|
169
|
+
controller.instance_variable_set :@authenticated_model, sess.account.model
|
170
|
+
end
|
171
|
+
|
172
|
+
def lifetime_expires_at(browser_session)
|
173
|
+
return nil if browser_session
|
174
|
+
return nil if QuoVadis.session_lifetime == :session
|
175
|
+
|
176
|
+
t = ActiveSupport::Duration.build(QuoVadis.session_lifetime).from_now
|
177
|
+
QuoVadis.session_lifetime_extend_to_end_of_day ? t.end_of_day : t
|
178
|
+
end
|
179
|
+
|
180
|
+
def logout
|
181
|
+
session&.destroy
|
182
|
+
clear_session_id
|
183
|
+
reset_session
|
184
|
+
controller.instance_variable_set :@authenticated_model, nil
|
185
|
+
end
|
186
|
+
|
187
|
+
def logout_other_sessions
|
188
|
+
session.logout_other_sessions
|
189
|
+
end
|
190
|
+
|
191
|
+
def log(account, action, metadata = {})
|
192
|
+
Log.create account: account, action: action, ip: request.remote_ip, metadata: metadata
|
193
|
+
end
|
194
|
+
|
195
|
+
def path_after_authentication
|
196
|
+
if (bookmark = rails_session[:qv_bookmark])
|
197
|
+
rails_session.delete :qv_bookmark
|
198
|
+
return bookmark
|
199
|
+
end
|
200
|
+
return main_app.after_login_path if main_app.respond_to?(:after_login_path)
|
201
|
+
return main_app.root_path if main_app.respond_to?(:root_path)
|
202
|
+
raise RuntimeError, 'Missing routes: after_login_path, root_path; define at least one of them.'
|
203
|
+
end
|
204
|
+
|
205
|
+
def path_after_password_change
|
206
|
+
return main_app.after_password_change_path if main_app.respond_to?(:after_password_change_path)
|
207
|
+
return main_app.root_path if main_app.respond_to?(:root_path)
|
208
|
+
raise RuntimeError, 'Missing routes: after_password_change_path, root_path; define at least one of them.'
|
209
|
+
end
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
attr_reader :controller
|
214
|
+
|
215
|
+
delegate :request, :reset_session, :authenticated_model, :main_app, to: :controller
|
216
|
+
|
217
|
+
def cookies
|
218
|
+
controller.send :cookies # private method
|
219
|
+
end
|
220
|
+
|
221
|
+
def rails_session
|
222
|
+
controller.session
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module QuoVadis
|
4
|
+
class Crypt
|
5
|
+
|
6
|
+
def self.encrypt(value)
|
7
|
+
return nil if value.nil?
|
8
|
+
return '' if value == ''
|
9
|
+
|
10
|
+
salt = SecureRandom.hex KEY_LENGTH
|
11
|
+
crypt = encryptor key(salt)
|
12
|
+
ciphertext = crypt.encrypt_and_sign value
|
13
|
+
[salt, ciphertext].join SEPARATOR
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.decrypt(value)
|
17
|
+
return nil if value.nil?
|
18
|
+
return '' if value == ''
|
19
|
+
|
20
|
+
salt, data = value.split SEPARATOR
|
21
|
+
crypt = encryptor key(salt)
|
22
|
+
crypt.decrypt_and_verify(data)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
KEY_LENGTH = ActiveSupport::MessageEncryptor.key_len
|
28
|
+
SEPARATOR = '$$'
|
29
|
+
|
30
|
+
def self.encryptor(key)
|
31
|
+
ActiveSupport::MessageEncryptor.new(key)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.key(salt)
|
35
|
+
ActiveSupport::KeyGenerator.new(secret).generate_key(salt, KEY_LENGTH)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.secret
|
39
|
+
Rails.application.secret_key_base
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
|
3
|
+
QuoVadis.configure do
|
4
|
+
password_minimum_length 12
|
5
|
+
mask_ips false
|
6
|
+
cookie_name '__Host-qv'
|
7
|
+
session_lifetime :session
|
8
|
+
session_lifetime_extend_to_end_of_day false
|
9
|
+
session_idle_timeout :lifetime
|
10
|
+
password_reset_token_lifetime 10.minutes
|
11
|
+
accounts_require_confirmation false
|
12
|
+
account_confirmation_token_lifetime 10.minutes
|
13
|
+
mail_headers ({ from: 'Example App <support@example.com>' })
|
14
|
+
enqueue_transactional_emails true
|
15
|
+
app_name Rails.app_class.to_s.deconstantize # for the TOTP QR code
|
16
|
+
two_factor_authentication_mandatory true
|
17
|
+
mount_point '/'
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module QuoVadis
|
4
|
+
class EncryptedType < ActiveRecord::Type::Value
|
5
|
+
|
6
|
+
def deserialize(value)
|
7
|
+
Crypt.decrypt(value)
|
8
|
+
end
|
9
|
+
|
10
|
+
def serialize(value)
|
11
|
+
Crypt.encrypt(value)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveRecord::Type.register :qv_encrypted, QuoVadis::EncryptedType
|
data/lib/quo_vadis/engine.rb
CHANGED
@@ -1,15 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module QuoVadis
|
4
|
+
|
5
|
+
def self.table_name_prefix
|
6
|
+
'qv_'
|
7
|
+
end
|
8
|
+
|
9
|
+
|
2
10
|
class Engine < ::Rails::Engine
|
3
|
-
|
4
|
-
ActiveSupport.on_load(:active_record) do
|
5
|
-
include ModelMixin
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
initializer 'quo_vadis.controller' do |app|
|
10
|
-
ActiveSupport.on_load(:action_controller) do
|
11
|
-
include ControllerMixin
|
12
|
-
end
|
13
|
-
end
|
11
|
+
isolate_namespace QuoVadis
|
14
12
|
end
|
15
13
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
|
6
|
+
# Much of this comes from Rodauth.
|
7
|
+
module QuoVadis
|
8
|
+
module Hmacable
|
9
|
+
|
10
|
+
def compute_hmac(data)
|
11
|
+
OpenSSL::HMAC.hexdigest 'SHA256', hmac_secret, data
|
12
|
+
end
|
13
|
+
|
14
|
+
def timing_safe_eql?(provided, actual)
|
15
|
+
provided = provided.to_s
|
16
|
+
Rack::Utils.secure_compare(provided.ljust(actual.length), actual) && provided.length == actual.length
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def hmac_secret
|
22
|
+
Rails.application.secret_key_base
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ipaddr'
|
4
|
+
|
5
|
+
module QuoVadis
|
6
|
+
module IpMasking
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend ClassMethods
|
10
|
+
base.before_validation :mask_ip, if: -> { QuoVadis.mask_ips }
|
11
|
+
end
|
12
|
+
|
13
|
+
def mask_ip
|
14
|
+
self.ip = self.class.mask_ip ip
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
# Based on Google Analytics masking
|
19
|
+
# https://support.google.com/analytics/answer/2763052
|
20
|
+
def mask_ip(ip)
|
21
|
+
addr = IPAddr.new ip
|
22
|
+
if addr.ipv4?
|
23
|
+
addr.mask(24).to_s # set last octet to 0
|
24
|
+
else
|
25
|
+
addr.mask(48).to_s # set last 80 bits to 0
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module QuoVadis
|
4
|
+
module Model
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def authenticates(identifier: :email)
|
13
|
+
include InstanceMethodsOnActivation
|
14
|
+
|
15
|
+
has_one :qv_account, as: :model, class_name: 'QuoVadis::Account', dependent: :destroy, autosave: true
|
16
|
+
|
17
|
+
before_validation :qv_build_account_and_password, on: :create
|
18
|
+
before_validation :qv_copy_identifier_to_account
|
19
|
+
|
20
|
+
# Enable a new-user form to set a password.
|
21
|
+
validate :qv_copy_password_errors, on: :create
|
22
|
+
|
23
|
+
unless validators_on(identifier).any? { |v| ActiveRecord::Validations::UniquenessValidator === v }
|
24
|
+
raise NotImplementedError, <<~END
|
25
|
+
Missing uniqueness validation on #{name}##{identifier}.
|
26
|
+
Try adding: `validates :#{identifier}, uniqueness: {case_sensitive: false}`
|
27
|
+
END
|
28
|
+
end
|
29
|
+
|
30
|
+
define_method :qv_copy_identifier_to_account do
|
31
|
+
qv_account.identifier = send identifier
|
32
|
+
end
|
33
|
+
|
34
|
+
after_update :qv_log_email_change, if: :saved_change_to_email?
|
35
|
+
after_update :qv_notify_email_change, if: :saved_change_to_email?
|
36
|
+
|
37
|
+
QuoVadis.register_model self.name, identifier
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
module InstanceMethodsOnActivation
|
43
|
+
attr_reader :password, :password_confirmation
|
44
|
+
|
45
|
+
def password=(val)
|
46
|
+
@password = val
|
47
|
+
self.qv_account ||= build_qv_account
|
48
|
+
raise PasswordExistsError if qv_account.password&.persisted?
|
49
|
+
(qv_account.password || qv_account.build_password).password = val
|
50
|
+
end
|
51
|
+
|
52
|
+
def password_confirmation=(val)
|
53
|
+
@password_confirmation = val
|
54
|
+
self.qv_account ||= build_qv_account
|
55
|
+
(qv_account.password || qv_account.build_password).password_confirmation = val
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def qv_build_account_and_password
|
61
|
+
self.qv_account ||= build_qv_account
|
62
|
+
qv_account.password || qv_account.build_password
|
63
|
+
end
|
64
|
+
|
65
|
+
def qv_copy_password_errors
|
66
|
+
qv_account.password.valid? # force qv_account.password to validate
|
67
|
+
qv_account.password.errors[:password ].each { |message| errors.add :password, message }
|
68
|
+
qv_account.password.errors[:password_confirmation].each { |message| errors.add :password_confirmation, message }
|
69
|
+
end
|
70
|
+
|
71
|
+
def qv_log_email_change
|
72
|
+
from, to = saved_change_to_email
|
73
|
+
Log.create(
|
74
|
+
account: qv_account,
|
75
|
+
action: Log::EMAIL_CHANGE,
|
76
|
+
ip: (CurrentRequestDetails.ip || ''),
|
77
|
+
metadata: {from: from, to: to}
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def qv_notify_email_change
|
82
|
+
QuoVadis.notify :email_change_notification, email: saved_change_to_email[0]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|