has_editable_password 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|