@@ -39,6 +39,10 @@ module ClassMethods
39
39
# <tt>validations: false</tt> as an argument. This allows complete
40
40
# customizability of validation behavior.
41
41
#
42
+ # Finally, a password reset token that's valid for 15 minutes after issue
43
+ # is automatically configured when +reset_token+ is set to true (which it is by default)
44
+ # and the object reponds to +generates_token_for+ (which Active Records do).
45
+ #
42
46
# To use +has_secure_password+, add bcrypt (~> 3.1.7) to your Gemfile:
43
47
#
44
48
# gem "bcrypt", "~> 3.1.7"
@@ -98,7 +102,18 @@ module ClassMethods
98
102
# account.is_guest = true
99
103
# account.valid? # => true
100
104
#
101
- def has_secure_password ( attribute = :password , validations : true )
105
+ # ===== Using the password reset token
106
+ #
107
+ # user = User.create!(name: "david", password: "123", password_confirmation: "123")
108
+ # token = user.password_reset_token
109
+ # User.find_by_password_reset_token(token) # returns user
110
+ #
111
+ # # 16 minutes later...
112
+ # User.find_by_password_reset_token(token) # returns nil
113
+ #
114
+ # # raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
115
+ # User.find_by_password_reset_token!(token)
116
+ def has_secure_password ( attribute = :password , validations : true , reset_token : true )
102
117
# Load bcrypt gem only when has_secure_password is used.
103
118
# This is to avoid ActiveModel (and by extension the entire framework)
104
119
# being dependent on a binary library.
@@ -109,7 +124,7 @@ def has_secure_password(attribute = :password, validations: true)
109
124
raise
110
125
end
111
126
112
- include InstanceMethodsOnActivation . new ( attribute )
127
+ include InstanceMethodsOnActivation . new ( attribute , reset_token : reset_token )
113
128
114
129
if validations
115
130
include ActiveModel ::Validations
@@ -142,11 +157,30 @@ def has_secure_password(attribute = :password, validations: true)
142
157
143
158
validates_confirmation_of attribute , allow_blank : true
144
159
end
160
+
161
+ # Only generate tokens for records that are capable of doing so (Active Records, not vanilla Active Models)
162
+ if reset_token && respond_to? ( :generates_token_for )
163
+ generates_token_for :"#{ attribute } _reset" , expires_in : 15 . minutes do
164
+ public_send ( :"#{ attribute } _salt" ) &.last ( 10 )
165
+ end
166
+
167
+ class_eval <<-RUBY , __FILE__ , __LINE__ + 1
168
+ silence_redefinition_of_method :find_by_#{ attribute } _reset_token
169
+ def self.find_by_#{ attribute } _reset_token(token)
170
+ find_by_token_for("#{ attribute } _reset", token)
171
+ end
172
+
173
+ silence_redefinition_of_method :find_by_#{ attribute } _reset_token!
174
+ def self.find_by_#{ attribute } _reset_token!(token)
175
+ find_by_token_for!("#{ attribute } _reset", token)
176
+ end
177
+ RUBY
178
+ end
145
179
end
146
180
end
147
181
148
182
class InstanceMethodsOnActivation < Module
149
- def initialize ( attribute )
183
+ def initialize ( attribute , reset_token : )
150
184
attr_reader attribute
151
185
152
186
define_method ( "#{ attribute } =" ) do |unencrypted_password |
@@ -184,6 +218,13 @@ def initialize(attribute)
184
218
end
185
219
186
220
alias_method :authenticate , :authenticate_password if attribute == :password
221
+
222
+ if reset_token
223
+ # Returns the class-level configured reset token for the password.
224
+ define_method ( "#{ attribute } _reset_token" ) do
225
+ generate_token_for ( "#{ attribute } _reset" )
226
+ end
227
+ end
187
228
end
188
229
end
189
230
end
0 commit comments