has_editable_password 0.1.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/lib/has_editable_password.rb +53 -19
- data/lib/version.rb +1 -1
- data/spec/has_editable_password_spec.rb +23 -9
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZjYyMDNiNzRhNWQ2MjI0NGI3YWU1MGEwZWI3ZjZiZTg2MjkxNjFhNQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
NTIzMmE0ODliNzM4YjBhMTc3MjY5ZjI0NjViY2FiNDNkZjg5YTI5Mw==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
Y2Y4ODU1OTUzY2RhOTU1NjJkNzE0NDYzMTU5NmIwNmI2OTUyMjQ3ZjE3M2Q1
|
10
|
+
M2MzMjIzZWUxOGIyOTU0NmQwNzVhNmNiNmIyMjFlMTJmYzdjOGI2YmUzN2I1
|
11
|
+
ODg2MDM1YmE3OGE5MDc5MTEyYzM5OTBiZTcyOWQ3YmQ0ZmMwMmE=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
YzNhMzQ5YmQ2MTE5YWZiNGE2MzQzODMwZjEyMzc3YTNkNzg5NDNkZTJhM2Iz
|
14
|
+
NjBmMDFmNjI2ZjQ0YjczMmMzZWVjZmIyMDkwZjkzMDU4YmY3MjFhZWMzODZk
|
15
|
+
MzYxZmJmMDI4YzE5NjY4NWQ1ZDdhNjY3MzcxNWYzMTRkMzMwODg=
|
@@ -2,6 +2,9 @@ require 'active_support/concern'
|
|
2
2
|
require 'active_model/secure_password'
|
3
3
|
require 'securerandom'
|
4
4
|
|
5
|
+
##
|
6
|
+
# Just include this module into your model to have all of its nice features
|
7
|
+
# :)
|
5
8
|
module HasEditablePassword
|
6
9
|
extend ActiveSupport::Concern
|
7
10
|
include ActiveModel::SecurePassword
|
@@ -14,17 +17,13 @@ module HasEditablePassword
|
|
14
17
|
|
15
18
|
validate :password_change, on: :update, if: :password_digest_changed?
|
16
19
|
|
20
|
+
##
|
21
|
+
# Overrides the has_secure_password implementation to provide nice features:
|
22
|
+
# * Password backup
|
23
|
+
# * Password update timestamp
|
17
24
|
def password=(value)
|
18
25
|
@old_password_digest = password_digest unless @old_password_digest or password_digest.blank?
|
19
|
-
|
20
|
-
unless password_digest.blank? or password_digest_changed?
|
21
|
-
self.previous_password_digest = password_digest if respond_to? :previous_password_digest=
|
22
|
-
end
|
23
|
-
|
24
|
-
unless password_digest_changed?
|
25
|
-
self.password_digest_updated = Time.now if respond_to? :password_digest_updated=
|
26
|
-
end
|
27
|
-
|
26
|
+
changing_password
|
28
27
|
super(value)
|
29
28
|
end
|
30
29
|
end
|
@@ -36,11 +35,11 @@ module HasEditablePassword
|
|
36
35
|
# +password_recovery_token_creation+ to the current time.
|
37
36
|
# Unless specified it calls +save+ to store the token in the database.
|
38
37
|
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
38
|
+
# * +:length+ - this is the length of the SecureRandom string generated
|
39
|
+
# as the token. Since the token is base64_encoded it will be longer than
|
40
|
+
# that. Default is 32.
|
41
|
+
# * +:save+ - you can use this if you don't want save to be called.
|
42
|
+
# generate_recovery_token(save: false)
|
44
43
|
#
|
45
44
|
def generate_recovery_token(options = {})
|
46
45
|
token = SecureRandom.urlsafe_base64(options.delete(:length) || 32)
|
@@ -51,11 +50,12 @@ module HasEditablePassword
|
|
51
50
|
end
|
52
51
|
|
53
52
|
##
|
54
|
-
# Returns true if the +
|
53
|
+
# Returns true if the +token+ matches with the stored one and the
|
55
54
|
# token creation time is less than 24 hours ago
|
56
55
|
#
|
57
|
-
|
58
|
-
|
56
|
+
# If +token+ is +nil+, the stored token is compared with +@recovery_token+
|
57
|
+
def valid_recovery_token?(token = nil)
|
58
|
+
recovery_token_match?(token) and !recovery_token_expired?
|
59
59
|
end
|
60
60
|
|
61
61
|
##
|
@@ -75,22 +75,56 @@ module HasEditablePassword
|
|
75
75
|
end
|
76
76
|
|
77
77
|
private
|
78
|
+
# True if the token has been updated more than 24 hours ago
|
78
79
|
def recovery_token_expired?
|
79
80
|
# 86400 = seconds in a day
|
80
81
|
(Time.now - self.password_recovery_token_creation).round >= 86400
|
81
82
|
end
|
82
83
|
|
83
|
-
|
84
|
-
|
84
|
+
##
|
85
|
+
# Compares password_recovery_token with:
|
86
|
+
# * @recovery_token if +token+ is nil
|
87
|
+
# * +token+ otherwise
|
88
|
+
#
|
89
|
+
# True if password_recovery_token matches.
|
90
|
+
# False if password_recovery_token is nil
|
91
|
+
# False if @recovery_token (or +token+) do not match the stored token
|
92
|
+
def recovery_token_match?(token = nil)
|
93
|
+
BCrypt::Password.new(self.password_recovery_token) == (token || @recovery_token)
|
85
94
|
rescue
|
86
95
|
false
|
87
96
|
end
|
88
97
|
|
98
|
+
##
|
99
|
+
# True if a valid recovery token or current password have been set
|
100
|
+
#
|
89
101
|
def allow_password_change?
|
90
102
|
valid_recovery_token? or current_password_match?
|
91
103
|
end
|
92
104
|
|
105
|
+
##
|
106
|
+
# Validation called on :update when the password_digest is touched.
|
107
|
+
# Sets an error on password unless the current_password or a valid recovery_token is set
|
93
108
|
def password_change
|
94
109
|
errors[:password] << 'Unauthorized to change the password' unless allow_password_change?
|
95
110
|
end
|
111
|
+
|
112
|
+
def changing_password
|
113
|
+
unless password_digest_changed?
|
114
|
+
update_previous_digest
|
115
|
+
update_digest_timestamp
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def update_previous_digest
|
120
|
+
if respond_to?(:previous_password_digest=) and !password_digest.blank?
|
121
|
+
self.previous_password_digest = password_digest
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def update_digest_timestamp
|
126
|
+
if respond_to? :password_digest_updated=
|
127
|
+
self.password_digest_updated = Time.now
|
128
|
+
end
|
129
|
+
end
|
96
130
|
end
|
data/lib/version.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
VERSION = '0.
|
1
|
+
VERSION = '0.2.1'
|
@@ -136,23 +136,37 @@ describe HasEditablePassword do
|
|
136
136
|
end
|
137
137
|
|
138
138
|
context 'the creation is less than 24 hours ago' do
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
139
|
+
context 'the argument is nil' do
|
140
|
+
it 'returns false if the stored token is a random string' do
|
141
|
+
user.generate_recovery_token
|
142
|
+
user.recovery_token = "deadbeef"
|
143
|
+
expect(user.valid_recovery_token?).to be_false
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'the stored token is a base64 string' do
|
147
|
+
it 'returns false if the token does not match' do
|
148
|
+
user.generate_recovery_token
|
149
|
+
user.recovery_token = SecureRandom.urlsafe_base64
|
150
|
+
expect(user.valid_recovery_token?).to be_false
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'returns true if the token matches' do
|
154
|
+
token = user.generate_recovery_token
|
155
|
+
user.recovery_token = token
|
156
|
+
expect(user.valid_recovery_token?).to be_true
|
157
|
+
end
|
158
|
+
end
|
143
159
|
end
|
144
160
|
|
145
|
-
context 'the
|
161
|
+
context 'the argument is not nil' do
|
146
162
|
it 'returns false if the token does not match' do
|
147
163
|
user.generate_recovery_token
|
148
|
-
user.
|
149
|
-
expect(user.valid_recovery_token?).to be_false
|
164
|
+
expect(user.valid_recovery_token?(SecureRandom.urlsafe_base64)).to be_false
|
150
165
|
end
|
151
166
|
|
152
167
|
it 'returns true if the token matches' do
|
153
168
|
token = user.generate_recovery_token
|
154
|
-
user.
|
155
|
-
expect(user.valid_recovery_token?).to be_true
|
169
|
+
expect(user.valid_recovery_token?(token)).to be_true
|
156
170
|
end
|
157
171
|
end
|
158
172
|
end
|