revise 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/padrino/revise.rb +8 -0
- data/lib/revise.rb +110 -0
- data/lib/revise/controllers/accounts.rb +59 -0
- data/lib/revise/controllers/confirmations.rb +26 -0
- data/lib/revise/controllers/invitations.rb +55 -0
- data/lib/revise/controllers/main.rb +13 -0
- data/lib/revise/controllers/recovery.rb +60 -0
- data/lib/revise/controllers/sessions.rb +22 -0
- data/lib/revise/core_ext/module.rb +52 -0
- data/lib/revise/core_ext/string.rb +14 -0
- data/lib/revise/helpers/authentication.rb +21 -0
- data/lib/revise/helpers/core.rb +48 -0
- data/lib/revise/inviter.rb +40 -0
- data/lib/revise/locale/en.yml +11 -0
- data/lib/revise/mailers/confirmable.rb +18 -0
- data/lib/revise/mailers/invitable.rb +18 -0
- data/lib/revise/mailers/recoverable.rb +18 -0
- data/lib/revise/models.rb +99 -0
- data/lib/revise/models/authenticatable.rb +137 -0
- data/lib/revise/models/confirmable.rb +236 -0
- data/lib/revise/models/database_authenticatable.rb +107 -0
- data/lib/revise/models/invitable.rb +237 -0
- data/lib/revise/models/recoverable.rb +99 -0
- data/lib/revise/orm/mongo_mapper.rb +2 -0
- data/lib/revise/param_filter.rb +41 -0
- data/test/controllers/accounts_test.rb +148 -0
- data/test/controllers/invitations_test.rb +105 -0
- data/test/controllers/sessions_test.rb +35 -0
- data/test/factories/account.rb +15 -0
- data/test/models/account_test.rb +45 -0
- data/test/models/invitation_test.rb +423 -0
- data/test/test.rake +13 -0
- data/test/test_config.rb +23 -0
- metadata +181 -0
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'bcrypt'
|
2
|
+
|
3
|
+
module Revise
|
4
|
+
module Models
|
5
|
+
module DatabaseAuthenticatable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
MAILERS = []
|
9
|
+
HELPERS = ['Authentication']
|
10
|
+
CONTROLLERS = ['Sessions', 'Accounts']
|
11
|
+
|
12
|
+
included do
|
13
|
+
attr_reader :password, :current_password
|
14
|
+
attr_accessor :password_confirmation
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid_for_authentication?
|
18
|
+
if super && valid_password?
|
19
|
+
true
|
20
|
+
else
|
21
|
+
false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.required_fields(klass)
|
26
|
+
[:encrypted_password] + klass.authentication_keys
|
27
|
+
end
|
28
|
+
|
29
|
+
def password=(new_password)
|
30
|
+
@password = new_password
|
31
|
+
self.encrypted_password = password_digest(@password) if @password.present?
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid_password?(password=nil)
|
35
|
+
password = @password if password == nil
|
36
|
+
|
37
|
+
return false if encrypted_password.blank?
|
38
|
+
|
39
|
+
bcrypt = ::BCrypt::Password.new(encrypted_password)
|
40
|
+
password = ::BCrypt::Engine.hash_secret("#{password}#{self.class.pepper}", bcrypt.salt)
|
41
|
+
|
42
|
+
String.secure_compare(password, encrypted_password)
|
43
|
+
end
|
44
|
+
|
45
|
+
def clean_up_passwords
|
46
|
+
self.password = self.password_confirmation = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def update_with_password(params, *options)
|
50
|
+
current_password = params.delete(:current_password)
|
51
|
+
|
52
|
+
if params[:password].blank?
|
53
|
+
params.delete(:password)
|
54
|
+
params.delete(:password_confirmation) if params[:password_confirmation].blank?
|
55
|
+
end
|
56
|
+
|
57
|
+
result = if valid_password?(current_password)
|
58
|
+
update_attributes(params, *options)
|
59
|
+
else
|
60
|
+
self.assign_attributes(params, *options)
|
61
|
+
self.valid?
|
62
|
+
self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
clean_up_passwords
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
def update_without_password(params, *options)
|
71
|
+
params.delete(:password)
|
72
|
+
params.delete(:password_confirmation)
|
73
|
+
|
74
|
+
result = update_attributes(params, *options)
|
75
|
+
clean_up_passwords
|
76
|
+
result
|
77
|
+
end
|
78
|
+
|
79
|
+
def after_database_authentication
|
80
|
+
end
|
81
|
+
|
82
|
+
def authenticatable_salt
|
83
|
+
encrypted_password[0,29] if encrypted_password
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def password_digest(password)
|
89
|
+
::BCrypt::Password.create("#{password}#{self.class.pepper}", :cost => self.class.stretches).to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
module ClassMethods
|
93
|
+
Revise::Models.config(self, :pepper, :stretches)
|
94
|
+
|
95
|
+
def authenticate(email, password)
|
96
|
+
account = Account.find_by_email(email)
|
97
|
+
return false unless account
|
98
|
+
if account.valid_password?(password)
|
99
|
+
return account
|
100
|
+
else
|
101
|
+
return false
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
module Revise
|
2
|
+
module Models
|
3
|
+
module Invitable
|
4
|
+
MAILERS = ['Invitable']
|
5
|
+
HELPERS = ['Authentication']
|
6
|
+
CONTROLLERS = ['Main', 'Sessions', 'Accounts', 'Invitations']
|
7
|
+
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
attr_accessor :skip_invitation
|
11
|
+
attr_accessor :completing_invite
|
12
|
+
|
13
|
+
included do
|
14
|
+
include ::Revise::Inviter
|
15
|
+
|
16
|
+
if Revise.invited_by_class_name
|
17
|
+
belongs_to :invited_by, :class_name => Revise.invited_by_class_name
|
18
|
+
else
|
19
|
+
belongs_to :invited_by, :polymorphic => true
|
20
|
+
end
|
21
|
+
|
22
|
+
include ActiveSupport::Callbacks
|
23
|
+
define_callbacks :invitation_accepted
|
24
|
+
|
25
|
+
attr_accessor :skip_password
|
26
|
+
|
27
|
+
scope :invitation_not_accepted, where(:invitation_accepted_at => nil)
|
28
|
+
scope :invitation_accepted, where(:invitation_accepted_at.ne => nil)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.required_fields(klass)
|
32
|
+
fields = [:invitation_token, :invitation_sent_at, :invitation_accepted_at, :invitation_limit, :invited_by_id,
|
33
|
+
:invited_by_type]
|
34
|
+
fields -= [:invited_by_type] if Revise.invited_by_class_name
|
35
|
+
fields
|
36
|
+
end
|
37
|
+
|
38
|
+
def invitation_fields
|
39
|
+
fields = [:invitation_sent_at, :invited_by_id, :invited_by_type]
|
40
|
+
fields -= [:invited_by_type] if Revise.invited_by_class_name
|
41
|
+
fields
|
42
|
+
end
|
43
|
+
|
44
|
+
# Accept an invitation by clearing invitation token and and setting invitation_accepted_at
|
45
|
+
# Confirms it if model is confirmable
|
46
|
+
def accept_invitation!
|
47
|
+
self.invitation_accepted_at = Time.now.utc
|
48
|
+
if self.invited_to_sign_up? && self.valid?
|
49
|
+
run_callbacks :invitation_accepted do
|
50
|
+
self.invitation_token = nil
|
51
|
+
self.confirmed_at = self.invitation_accepted_at if self.respond_to?(:confirmed_at)
|
52
|
+
self.save(:validate => false)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Verifies whether a user has been invited or not
|
58
|
+
def invited_to_sign_up?
|
59
|
+
invitation_token
|
60
|
+
end
|
61
|
+
|
62
|
+
# Verifies whether a user accepted an invitation (or is accepting it)
|
63
|
+
def invitation_accepted?
|
64
|
+
invitation_accepted_at
|
65
|
+
end
|
66
|
+
|
67
|
+
# Verifies whether a user has accepted an invitation (or is accepting it), or was never invited
|
68
|
+
def accepted_or_not_invited?
|
69
|
+
invitation_accepted? || !invited_to_sign_up?
|
70
|
+
end
|
71
|
+
|
72
|
+
# Reset invitation token and send invitation again
|
73
|
+
def invite!(invited_by = nil)
|
74
|
+
was_invited = invited_to_sign_up?
|
75
|
+
|
76
|
+
# Required to workaround confirmable model's confirmation_required? method
|
77
|
+
# being implemented to check for non-nil value of confirmed_at
|
78
|
+
if self.new_record? && self.respond_to?(:confirmation_required?)
|
79
|
+
def self.confirmation_required?; false; end
|
80
|
+
end
|
81
|
+
|
82
|
+
generate_invitation_token if self.invitation_token.nil?
|
83
|
+
self.invitation_sent_at = Time.now.utc
|
84
|
+
self.invited_by = invited_by if invited_by
|
85
|
+
|
86
|
+
# Call these before_validate methods since we aren't validating on save
|
87
|
+
self.downcase_keys if self.new_record? && self.respond_to?(:downcase_keys)
|
88
|
+
self.strip_whitespace if self.new_record? && self.respond_to?(:strip_whitespace)
|
89
|
+
|
90
|
+
if save(:validate => false)
|
91
|
+
self.invited_by.decrement_invitation_limit! if !was_invited and self.invited_by.present?
|
92
|
+
deliver_invitation unless @skip_invitation
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Verify whether a invitation is active or not. If the user has been
|
97
|
+
# invited, we need to calculate if the invitation time has not expired
|
98
|
+
# for this user, in other words, if the invitation is still valid.
|
99
|
+
def valid_invitation?
|
100
|
+
invited_to_sign_up? && invitation_period_valid?
|
101
|
+
end
|
102
|
+
|
103
|
+
# Only verify password when is not invited
|
104
|
+
def valid_password?(password)
|
105
|
+
super unless invited_to_sign_up?
|
106
|
+
end
|
107
|
+
|
108
|
+
def reset_password!(new_password, new_password_confirmation)
|
109
|
+
super
|
110
|
+
accept_invitation!
|
111
|
+
end
|
112
|
+
|
113
|
+
def invite_key_valid?
|
114
|
+
return true unless self.class.invite_key.is_a? Hash # FIXME: remove this line when deprecation is removed
|
115
|
+
self.class.invite_key.all? do |key, regexp|
|
116
|
+
regexp.nil? || self.send(key).try(:match, regexp)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def password_required?
|
121
|
+
!@skip_password && (encrypted_password.blank? || password.present?)
|
122
|
+
end
|
123
|
+
|
124
|
+
protected
|
125
|
+
# Deliver the invitation email
|
126
|
+
def deliver_invitation
|
127
|
+
send_revise_notification(:invitable, :invitation_instructions, self.name, self.email, self.invitation_token)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Checks if the invitation for the user is within the limit time.
|
131
|
+
# We do this by calculating if the difference between today and the
|
132
|
+
# invitation sent date does not exceed the invite for time configured.
|
133
|
+
# Invite_for is a model configuration, must always be an integer value.
|
134
|
+
#
|
135
|
+
# Example:
|
136
|
+
#
|
137
|
+
# # invite_for = 1.day and invitation_sent_at = today
|
138
|
+
# invitation_period_valid? # returns true
|
139
|
+
#
|
140
|
+
# # invite_for = 5.days and invitation_sent_at = 4.days.ago
|
141
|
+
# invitation_period_valid? # returns true
|
142
|
+
#
|
143
|
+
# # invite_for = 5.days and invitation_sent_at = 5.days.ago
|
144
|
+
# invitation_period_valid? # returns false
|
145
|
+
#
|
146
|
+
# # invite_for = nil
|
147
|
+
# invitation_period_valid? # will always return true
|
148
|
+
#
|
149
|
+
def invitation_period_valid?
|
150
|
+
invitation_sent_at && (self.class.invite_for.to_i.zero? || invitation_sent_at.utc >= self.class.invite_for.ago)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Generates a new random token for invitation, and stores the time
|
154
|
+
# this token is being generated
|
155
|
+
def generate_invitation_token
|
156
|
+
self.invitation_token = self.class.invitation_token
|
157
|
+
end
|
158
|
+
|
159
|
+
module ClassMethods
|
160
|
+
# Return fields to invite
|
161
|
+
def invite_key_fields
|
162
|
+
invite_key.keys
|
163
|
+
end
|
164
|
+
|
165
|
+
# Attempt to find a user by it's email. If a record is not found, create a new
|
166
|
+
# user and send invitation to it. If user is found, returns the user with an
|
167
|
+
# email already exists error.
|
168
|
+
# If user is found and still have pending invitation, email is resend unless
|
169
|
+
# resend_invitation is set to false
|
170
|
+
# Attributes must contain the user email, other attributes will be set in the record
|
171
|
+
def _invite(attributes={}, invited_by=nil, &block)
|
172
|
+
attributes.symbolize_keys!
|
173
|
+
invite_key_array = invite_key_fields
|
174
|
+
attributes_hash = {}
|
175
|
+
invite_key_array.each do |k,v|
|
176
|
+
attributes_hash[k] = attributes.delete(k)
|
177
|
+
end
|
178
|
+
|
179
|
+
invitable = find_or_initialize_with_errors(invite_key_array, attributes_hash)
|
180
|
+
invitable.invited_by = invited_by
|
181
|
+
|
182
|
+
invitable.skip_password = true
|
183
|
+
invitable.valid? if self.validate_on_invite
|
184
|
+
if invitable.new_record?
|
185
|
+
invitable.errors.clear if !self.validate_on_invite and invitable.invite_key_valid?
|
186
|
+
elsif !invitable.invited_to_sign_up? || !self.resend_invitation
|
187
|
+
invite_key_array.each do |key|
|
188
|
+
invitable.errors.add(key, :taken)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
if invitable.errors.empty?
|
193
|
+
yield invitable if block_given?
|
194
|
+
mail = invitable.invite!
|
195
|
+
end
|
196
|
+
[invitable, mail]
|
197
|
+
end
|
198
|
+
|
199
|
+
def invite!(attributes={}, invited_by=nil, &block)
|
200
|
+
invitable, mail = _invite(attributes, invited_by, &block)
|
201
|
+
invitable
|
202
|
+
end
|
203
|
+
|
204
|
+
def invite_mail!(attributes={}, invited_by=nil, &block)
|
205
|
+
invitable, mail = _invite(attributes, invited_by, &block)
|
206
|
+
mail
|
207
|
+
end
|
208
|
+
|
209
|
+
# Attempt to find a user by it's invitation_token to set it's password.
|
210
|
+
# If a user is found, reset it's password and automatically try saving
|
211
|
+
# the record. If not user is found, returns a new user containing an
|
212
|
+
# error in invitation_token attribute.
|
213
|
+
# Attributes must contain invitation_token, password and confirmation
|
214
|
+
def accept_invitation!(attributes={})
|
215
|
+
invitable = find_or_initialize_with_error_by(:invitation_token, attributes.delete(:invitation_token))
|
216
|
+
invitable.errors.add(:invitation_token, :invalid) if invitable.invitation_token && invitable.persisted? && !invitable.valid_invitation?
|
217
|
+
if invitable.errors.empty?
|
218
|
+
invitable.attributes = attributes
|
219
|
+
invitable.accept_invitation!
|
220
|
+
end
|
221
|
+
invitable
|
222
|
+
end
|
223
|
+
|
224
|
+
# Generate a token checking if one does not already exist in the database.
|
225
|
+
def invitation_token
|
226
|
+
generate_token(:invitation_token)
|
227
|
+
end
|
228
|
+
|
229
|
+
Revise::Models.config(self, :invite_for)
|
230
|
+
Revise::Models.config(self, :validate_on_invite)
|
231
|
+
Revise::Models.config(self, :invitation_limit)
|
232
|
+
Revise::Models.config(self, :invite_key)
|
233
|
+
Revise::Models.config(self, :resend_invitation)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Revise
|
2
|
+
module Models
|
3
|
+
module Recoverable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
MAILERS = ['Recoverable']
|
7
|
+
HELPERS = []
|
8
|
+
CONTROLLERS = ['Recovery']
|
9
|
+
|
10
|
+
def self.required_fields(klass)
|
11
|
+
[:reset_password_sent_at, :reset_password_token, :email]
|
12
|
+
end
|
13
|
+
|
14
|
+
def reset_password!(new_password, new_password_confirmation)
|
15
|
+
self.password = new_password
|
16
|
+
self.password_confirmation = new_password_confirmation
|
17
|
+
|
18
|
+
if valid?
|
19
|
+
clear_reset_password_token
|
20
|
+
after_password_reset
|
21
|
+
end
|
22
|
+
|
23
|
+
save
|
24
|
+
end
|
25
|
+
|
26
|
+
def send_reset_password_instructions
|
27
|
+
generate_reset_password_token! if should_generate_reset_token?
|
28
|
+
send_revise_notification(:recoverable, :reset_password_instructions, self.name, self.email, self.reset_password_token)
|
29
|
+
end
|
30
|
+
|
31
|
+
def reset_password_period_valid?
|
32
|
+
reset_password_sent_at && reset_password_sent_at.utc >= self.class.reset_password_within.ago
|
33
|
+
end
|
34
|
+
|
35
|
+
# Generates a new random token for reset password
|
36
|
+
def generate_reset_password_token
|
37
|
+
self.reset_password_token = self.class.reset_password_token
|
38
|
+
self.reset_password_sent_at = Time.now.utc
|
39
|
+
self.reset_password_token
|
40
|
+
end
|
41
|
+
|
42
|
+
# Resets the reset password token with and save the record without
|
43
|
+
# validating
|
44
|
+
def generate_reset_password_token!
|
45
|
+
generate_reset_password_token && save(:validate => false)
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
def should_generate_reset_token?
|
50
|
+
reset_password_token.nil? || !reset_password_period_valid?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Removes reset_password token
|
54
|
+
def clear_reset_password_token
|
55
|
+
self.reset_password_token = nil
|
56
|
+
self.reset_password_sent_at = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def after_password_reset
|
60
|
+
end
|
61
|
+
|
62
|
+
module ClassMethods
|
63
|
+
# Attempt to find a user by its email. If a record is found, send new
|
64
|
+
# password instructions to it. If not user is found, returns a new user
|
65
|
+
# with an email not found error.
|
66
|
+
# Attributes must contain the user email
|
67
|
+
def send_reset_password_instructions(attributes={})
|
68
|
+
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
|
69
|
+
recoverable.send_reset_password_instructions if recoverable.persisted?
|
70
|
+
recoverable
|
71
|
+
end
|
72
|
+
|
73
|
+
# Generate a token checking if one does not already exist in the database.
|
74
|
+
def reset_password_token
|
75
|
+
generate_token(:reset_password_token)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Attempt to find a user by its reset_password_token to reset its
|
79
|
+
# password. If a user is found and token is still valid, reset its password and automatically
|
80
|
+
# try saving the record. If not user is found, returns a new user
|
81
|
+
# containing an error in reset_password_token attribute.
|
82
|
+
# Attributes must contain reset_password_token, password and confirmation
|
83
|
+
def reset_password_by_token(attributes={})
|
84
|
+
recoverable = find_or_initialize_with_error_by(:reset_password_token, attributes[:reset_password_token])
|
85
|
+
if recoverable.persisted?
|
86
|
+
if recoverable.reset_password_period_valid?
|
87
|
+
recoverable.reset_password!(attributes[:password], attributes[:password_confirmation])
|
88
|
+
else
|
89
|
+
recoverable.errors.add(:reset_password_token, :expired)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
recoverable
|
93
|
+
end
|
94
|
+
|
95
|
+
Revise::Models.config(self, :reset_password_keys, :reset_password_within)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Revise
|
2
|
+
class ParamFilter
|
3
|
+
def initialize(case_insensitive_keys, strip_whitespace_keys)
|
4
|
+
@case_insensitive_keys = case_insensitive_keys || []
|
5
|
+
@strip_whitespace_keys = strip_whitespace_keys || []
|
6
|
+
end
|
7
|
+
|
8
|
+
def filter(conditions)
|
9
|
+
conditions = stringify_params(conditions.dup)
|
10
|
+
|
11
|
+
@case_insensitive_keys.each do |k|
|
12
|
+
value = conditions[k]
|
13
|
+
next unless value.respond_to?(:downcase)
|
14
|
+
conditions[k] = value.downcase
|
15
|
+
end
|
16
|
+
|
17
|
+
@strip_whitespace_keys.each do |k|
|
18
|
+
value = conditions[k]
|
19
|
+
next unless value.respond_to?(:strip)
|
20
|
+
conditions[k] = value.strip
|
21
|
+
end
|
22
|
+
|
23
|
+
conditions
|
24
|
+
end
|
25
|
+
|
26
|
+
# Force keys to be string to avoid injection on mongoid related database.
|
27
|
+
def stringify_params(conditions)
|
28
|
+
return conditions unless conditions.is_a?(Hash)
|
29
|
+
conditions.each do |k, v|
|
30
|
+
conditions[k] = v.to_s if param_requires_string_conversion?(v)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Determine which values should be transformed to string or passed as-is to the query builder underneath
|
37
|
+
def param_requires_string_conversion?(value)
|
38
|
+
[Fixnum, TrueClass, FalseClass, Regexp].none? {|clz| value.is_a? clz }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|