revise 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.
- 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
|