has_editable_password 0.0.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 ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MWNlODUyZGIyZDllODFkYzI3ZGZjMDdmNWVlNjQ1MTFlNzIwYTA3NA==
5
+ data.tar.gz: !binary |-
6
+ ZjgxY2E2YjkyMjQ2N2EwMzc5Yzg3MTlhMGEyMDgzZTZmMDVmYjcyYg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NWM4MTQwNTdmYjIyMzdlMzQyZDU4ODg3NDg1NDM1OGFjNTljYzI4NGEzMWJi
10
+ ZWE4NjU5NzYyZGY0YzQ3ZTk4NTc5Y2FjM2RkMGE3ZjhlYjlkNzRjODhmM2U3
11
+ OGRlNWEwYmI3OGEzOGNkNDliZjY1NDQwMjI5MTA3ZGFhYzIyNTA=
12
+ data.tar.gz: !binary |-
13
+ YWIxOGNjNDBlN2NkNTI5NDExMmQwZjQzYTIzYjQyMGM4ZDE1YTU0ZjAxMWRi
14
+ OWYzZTNiYzk3NTJmNDc3NDBkOTBlOWI5OGI3YjU2ZDI5NzE5YTk0MGE4MmU4
15
+ ZmIyNDAyNGEyNzE2YmYxNGM1MjZlMTFmOTQzNzU4NjFlNjZmOTU=
@@ -0,0 +1,67 @@
1
+ require 'active_support/concern'
2
+ require 'active_model/secure_password'
3
+ require 'securerandom'
4
+
5
+ module HasEditablePassword
6
+ extend ActiveSupport::Concern
7
+ include ActiveModel::SecurePassword
8
+
9
+ included do
10
+ has_secure_password
11
+
12
+ attr_writer :current_password
13
+ attr_writer :recovery_token
14
+
15
+ validate :password_change, on: :update, if: :password_digest_changed?
16
+
17
+ def password=(value)
18
+ @old_password_digest = self.password_digest unless @old_password_digest or self.password_digest.blank?
19
+ super(value)
20
+ end
21
+ end
22
+
23
+ def generate_recovery_token(options = {})
24
+ token = SecureRandom.urlsafe_base64(options.delete(:length) || 32)
25
+ self.password_recovery_token = BCrypt::Password.create(token)
26
+ self.password_recovery_token_creation = Time.now
27
+ save unless options.delete(:save) == false
28
+ token
29
+ end
30
+
31
+ def valid_recovery_token?
32
+ recovery_token_match? and !recovery_token_expired?
33
+ end
34
+
35
+ def current_password_match?
36
+ if @current_password
37
+ if @old_password_digest
38
+ BCrypt::Password.new(@old_password_digest) == @current_password
39
+ else
40
+ # almost same as #authenticate (returns true instead of the object)
41
+ BCrypt::Password.new(self.password_digest) == @current_password
42
+ end
43
+ else
44
+ false
45
+ end
46
+ end
47
+
48
+ private
49
+ def recovery_token_expired?
50
+ # 86400 = seconds in a day
51
+ (Time.now - self.password_recovery_token_creation).round >= 86400
52
+ end
53
+
54
+ def recovery_token_match?
55
+ BCrypt::Password.new(self.password_recovery_token) == @recovery_token
56
+ rescue
57
+ false
58
+ end
59
+
60
+ def allow_password_change?
61
+ valid_recovery_token? or current_password_match?
62
+ end
63
+
64
+ def password_change
65
+ errors[:password] << 'Unauthorized to change the password' unless allow_password_change?
66
+ end
67
+ end
data/lib/version.rb ADDED
@@ -0,0 +1 @@
1
+ VERSION = '0.0.1'
@@ -0,0 +1,225 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe HasEditablePassword do
4
+ let(:user) { User.new(password: 'secret', password_confirmation: 'secret') }
5
+
6
+ # Since this is an extension of has_secure_password ensure that all the
7
+ # methods defined by has_secure_password are still there
8
+
9
+ it 'has an #authenticate method' do
10
+ expect(user).to respond_to :authenticate
11
+ end
12
+
13
+ it 'has a #password= method' do
14
+ expect(user).to respond_to :password=
15
+ end
16
+
17
+ it 'has a password_confirmation= method' do
18
+ expect(user).to respond_to :password=
19
+ end
20
+
21
+ # Additional accessors for current password or password recovery token
22
+
23
+ it 'has a #current_password= method' do
24
+ expect(user).to respond_to :current_password=
25
+ end
26
+
27
+ it 'has a #recovery_token= method' do
28
+ expect(user).to respond_to :recovery_token=
29
+ end
30
+
31
+ describe '#password=' do
32
+ it 'sets the password_digest field to the hash of the password' do
33
+ user.password = 'secret'
34
+ expect(BCrypt::Password.new(user.password_digest)).to eq 'secret'
35
+ end
36
+ end
37
+
38
+ describe '#generate_recovery_token' do
39
+ it 'returns a url-safe base64 string' do
40
+ token = user.generate_recovery_token
41
+ expect(token).to match(/^[a-z0-9\-_]+=*$/i)
42
+ end
43
+
44
+ it 'returns a 43 bytes token unless specified' do
45
+ token = user.generate_recovery_token
46
+ expect(token.size).to eq 43
47
+ end
48
+
49
+ it 'returns token larger than specified size' do
50
+ token = user.generate_recovery_token length: 100
51
+ token.size.should be >= 100
52
+ end
53
+
54
+ it 'sets the password_recovery_token attribute with the hash of the token' do
55
+ token = user.generate_recovery_token
56
+ expect(BCrypt::Password.new(user.password_recovery_token)).to eq token
57
+ end
58
+
59
+ it 'sets the password_recovery_token_creation to Time.now' do
60
+ user.generate_recovery_token
61
+ expect(user.password_recovery_token_creation.round).to eq Time.now.round
62
+ end
63
+
64
+ it 'calls #save unless specified' do
65
+ expect(user).to receive(:save)
66
+ user.generate_recovery_token
67
+ end
68
+
69
+ it 'does not call #save if specified' do
70
+ expect(user).to_not receive(:save)
71
+ user.generate_recovery_token(save: false)
72
+ end
73
+ end
74
+
75
+ describe '#valid_recovery_token?' do
76
+ context 'a token was never generated' do
77
+ it 'returns false' do
78
+ user.recovery_token = "deadbeef"
79
+ expect(user.valid_recovery_token?).to be_false
80
+ end
81
+ end
82
+
83
+ context 'the creation is more than 24 hours ago' do
84
+ it 'returns false' do
85
+ token = user.generate_recovery_token
86
+ user.password_recovery_token_creation = Time.now - 86401
87
+ user.recovery_token = token # Even if the token is correct
88
+ expect(user.valid_recovery_token?).to be_false
89
+ end
90
+ end
91
+
92
+ context 'the creation is less than 24 hours ago' do
93
+ it 'returns false if the token is a random string' do
94
+ user.generate_recovery_token
95
+ user.recovery_token = "deadbeef"
96
+ expect(user.valid_recovery_token?).to be_false
97
+ end
98
+
99
+ context 'the token is a base64 string' do
100
+ it 'returns false if the token does not match' do
101
+ user.generate_recovery_token
102
+ user.recovery_token = SecureRandom.urlsafe_base64
103
+ expect(user.valid_recovery_token?).to be_false
104
+ end
105
+
106
+ it 'returns true if the token matches' do
107
+ token = user.generate_recovery_token
108
+ user.recovery_token = token
109
+ expect(user.valid_recovery_token?).to be_true
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ describe '#current_password_match?' do
116
+ context 'current_password is not been set' do
117
+ it 'returns false' do
118
+ expect(user.current_password_match?).to be_false
119
+ end
120
+ end
121
+
122
+ context 'current_password is set' do
123
+ context 'current_password does not match' do
124
+ before { user.current_password = 's3cret' }
125
+
126
+ it 'returns false' do
127
+ expect(user.current_password_match?).to be_false
128
+ end
129
+ end
130
+
131
+ context 'current_password does match' do
132
+ before { user.current_password = 'secret' }
133
+
134
+ it 'returns true' do
135
+ expect(user.current_password_match?).to be_true
136
+ end
137
+ end
138
+
139
+ context 'password_digest has been modified' do
140
+ before { user.password = 'new_secret' }
141
+
142
+ context 'current_password is set to the previous password' do
143
+ before { user.current_password = 'secret' }
144
+
145
+ it 'returns true' do
146
+ expect(user.current_password_match?).to be_true
147
+ end
148
+ end
149
+
150
+ context 'current_password is set to the new password' do
151
+ before { user.current_password = 'new_secret' }
152
+
153
+ it 'returns true' do
154
+ expect(user.current_password_match?).to be_false
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ describe 'password editing validation' do
162
+ let(:user) { User.new(password: 'banana', password_confirmation: 'banana') }
163
+
164
+ context 'on create' do
165
+ it 'is valid' do
166
+ expect(user.valid?).to be_true
167
+ end
168
+ end
169
+
170
+ context 'on update' do
171
+ context 'password_digest was not touched' do
172
+ before { user.stub(:password_digest_changed?).and_return false }
173
+
174
+ it 'is valid' do
175
+ expect(user.valid?(:update)).to be_true
176
+ end
177
+ end
178
+
179
+ context 'password_digest was modified' do
180
+ before { user.stub(:password_digest_changed?).and_return true }
181
+
182
+ context 'a valid token is set' do
183
+ let(:token) { user.generate_recovery_token }
184
+
185
+ it 'is valid' do
186
+ user.recovery_token = token
187
+ expect(user.valid?(:update)).to be_true
188
+ end
189
+ end
190
+
191
+ context 'an invalid valid token is set' do
192
+ let(:token) do
193
+ user.generate_recovery_token
194
+ "deadbeef"
195
+ end
196
+
197
+ it 'is not valid' do
198
+ user.recovery_token = token
199
+ expect(user.valid?(:update)).to be_false
200
+ end
201
+ end
202
+
203
+ context 'the current_password is valid' do
204
+ it 'is valid' do
205
+ user.current_password = 'banana'
206
+ expect(user.valid?(:update)).to be_true
207
+ end
208
+ end
209
+
210
+ context 'the current_password is invalid' do
211
+ it 'is valid' do
212
+ user.current_password = 'b4n4n4'
213
+ expect(user.valid?(:update)).to be_false
214
+ end
215
+ end
216
+
217
+ context 'neither is set' do
218
+ it 'is not valid' do
219
+ expect(user.valid?(:update)).to be_false
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,41 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
3
+ require 'has_editable_password.rb'
4
+ require 'active_model'
5
+ require 'bcrypt'
6
+
7
+ # Mock an ActiveRecord-like class.
8
+ # Not using ActiveRecord because we want this to be ORM-agnostic
9
+ class User
10
+ extend ActiveModel::Callbacks
11
+ define_model_callbacks :initialize, :find, :touch, :only => :after
12
+ define_model_callbacks :save, :create, :update, :destroy
13
+
14
+ include ActiveModel::Validations
15
+ include ActiveModel::Validations::HelperMethods
16
+ include HasEditablePassword
17
+
18
+ # we expect the user to define the password_digest field
19
+ attr_accessor :password_digest
20
+ attr_accessor :password_recovery_token
21
+ attr_accessor :password_recovery_token_creation
22
+
23
+ def initialize(hash = {})
24
+ hash.each do |k, v|
25
+ send("#{k}=", v)
26
+ end
27
+ end
28
+
29
+ def attributes
30
+ keys = [ 'password', 'password_confirmation' ]
31
+ values = keys.map do |k|
32
+ send(k)
33
+ end
34
+
35
+ Hash[keys.zip(values)]
36
+ end
37
+
38
+ def save
39
+ valid?
40
+ end
41
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_editable_password
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Francesco Boffa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-12-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ type: :runtime
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ! '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ type: :runtime
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ! '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bcrypt-ruby
43
+ type: :runtime
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ~>
47
+ - !ruby/object:Gem::Version
48
+ version: 3.0.0
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 3.0.0
55
+ description: HasEditablePassword extends has_secure_password with updating capabilities.
56
+ On password update, the old password or a recovery token is asked.
57
+ email: fra.boffa@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - lib/has_editable_password.rb
63
+ - lib/version.rb
64
+ - spec/has_editable_password_spec.rb
65
+ - spec/spec_helper.rb
66
+ homepage: http://rubygems.org/gems/has_editable_password
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.1.10
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: has_secure_password with safe updating capabilities.
90
+ test_files:
91
+ - spec/has_editable_password_spec.rb
92
+ - spec/spec_helper.rb
93
+ has_rdoc: