devise_invitable 2.0.14 → 3.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 +4 -4
- data/CHANGELOG.md +66 -0
- data/README.md +166 -97
- data/app/controllers/devise/invitations_controller.rb +8 -2
- data/app/controllers/devise_invitable/registrations_controller.rb +15 -10
- data/lib/devise_invitable/invitation.rb +89 -0
- data/lib/devise_invitable/inviter.rb +23 -1
- data/lib/devise_invitable/migrator.rb +113 -0
- data/lib/devise_invitable/models.rb +353 -86
- data/lib/devise_invitable/rails.rb +10 -0
- data/lib/devise_invitable/version.rb +1 -1
- data/lib/devise_invitable.rb +40 -4
- data/lib/generators/active_record/devise_invitable_install_generator.rb +22 -0
- data/lib/generators/active_record/devise_invitable_upgrade_generator.rb +28 -0
- data/lib/generators/active_record/templates/create_devise_invitations.rb +23 -0
- data/lib/generators/active_record/templates/upgrade_devise_invitations.rb +72 -0
- data/lib/generators/devise_invitable/devise_invitable_generator.rb +2 -4
- data/lib/generators/devise_invitable/install_generator.rb +8 -10
- data/lib/generators/devise_invitable/upgrade_generator.rb +16 -0
- data/lib/generators/mongoid/devise_invitable_install_generator.rb +7 -0
- data/lib/generators/mongoid/devise_invitable_upgrade_generator.rb +11 -0
- data/lib/tasks/devise_invitable_tasks.rake +10 -0
- data/test/functional/registrations_controller_test.rb +24 -3
- data/test/generators_test.rb +16 -9
- data/test/integration/invitation_test.rb +24 -3
- data/test/mailers/invitation_mail_test.rb +1 -0
- data/test/models/invitable_test.rb +40 -2
- data/test/models/separate_invitations_test.rb +181 -0
- data/test/models_test.rb +34 -0
- data/test/rails_app/app/controllers/boolean_inviter_invitations_controller.rb +11 -0
- data/test/rails_app/app/controllers/restricted_invitations_controller.rb +9 -0
- data/test/rails_app/app/models/user.rb +8 -2
- data/test/rails_app/config/initializers/devise.rb +0 -9
- data/test/rails_app/config/routes.rb +2 -0
- data/test/rails_app/db/migrate/20100401102949_create_tables.rb +22 -0
- data/test/routes_test.rb +6 -0
- metadata +26 -13
- data/lib/generators/active_record/devise_invitable_generator.rb +0 -19
- data/lib/generators/active_record/templates/migration.rb +0 -22
- data/lib/generators/mongoid/devise_invitable_generator.rb +0 -8
|
@@ -4,7 +4,9 @@ require 'devise_invitable/models/authenticatable'
|
|
|
4
4
|
module Devise
|
|
5
5
|
module Models
|
|
6
6
|
# Invitable is responsible for sending invitation emails.
|
|
7
|
-
#
|
|
7
|
+
# Invitations are stored separately. An account is created immediately only
|
|
8
|
+
# when create_resource_on_invitation is enabled; otherwise it is created on
|
|
9
|
+
# acceptance.
|
|
8
10
|
# Invitation email contains a link allowing the user to accept the invitation
|
|
9
11
|
# by setting a password (as reset password from Devise's recoverable module).
|
|
10
12
|
#
|
|
@@ -26,40 +28,62 @@ module Devise
|
|
|
26
28
|
|
|
27
29
|
attr_accessor :skip_invitation
|
|
28
30
|
attr_accessor :completing_invite
|
|
31
|
+
attr_accessor :registering_directly
|
|
32
|
+
attr_accessor :invited_by_for_next_invitation
|
|
29
33
|
attr_reader :raw_invitation_token
|
|
30
34
|
|
|
31
35
|
included do
|
|
32
36
|
include ::DeviseInvitable::Inviter
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if fk = invited_by_foreign_key
|
|
39
|
-
belongs_to_options[:foreign_key] = fk
|
|
40
|
-
end
|
|
41
|
-
if defined?(ActiveRecord) && defined?(ActiveRecord::Base) && self < ActiveRecord::Base
|
|
42
|
-
counter_cache = Devise.invited_by_counter_cache
|
|
43
|
-
belongs_to_options.merge! counter_cache: counter_cache if counter_cache
|
|
44
|
-
belongs_to_options.merge! optional: true if ActiveRecord::VERSION::MAJOR >= 5
|
|
45
|
-
elsif defined?(Mongoid) && defined?(Mongoid::Document) && self < Mongoid::Document && Mongoid::VERSION >= '6.0.0'
|
|
46
|
-
belongs_to_options.merge! optional: true
|
|
47
|
-
end
|
|
48
|
-
belongs_to :invited_by, **belongs_to_options
|
|
37
|
+
has_many :devise_invitations,
|
|
38
|
+
as: :invitable,
|
|
39
|
+
class_name: 'DeviseInvitable::Invitation',
|
|
40
|
+
dependent: :nullify,
|
|
41
|
+
autosave: true
|
|
49
42
|
|
|
50
43
|
extend ActiveModel::Callbacks
|
|
51
44
|
define_model_callbacks :invitation_created
|
|
52
45
|
define_model_callbacks :invitation_accepted
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if defined?(Mongoid
|
|
56
|
-
scope :created_by_invite, lambda {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
scope :invitation_not_accepted, lambda {
|
|
62
|
-
|
|
46
|
+
after_save :resolve_invitations_after_registration!, if: :registering_directly
|
|
47
|
+
|
|
48
|
+
if defined?(Mongoid::Document) && self < Mongoid::Document
|
|
49
|
+
scope :created_by_invite, lambda {
|
|
50
|
+
ids = DeviseInvitable::Invitation.provisioning
|
|
51
|
+
.where(invitable_type: devise_invitable_base_class.name).pluck(:invitable_id)
|
|
52
|
+
where(:id.in => ids)
|
|
53
|
+
}
|
|
54
|
+
scope :invitation_not_accepted, lambda {
|
|
55
|
+
ids = DeviseInvitable::Invitation.pending.provisioning
|
|
56
|
+
.where(invitable_type: devise_invitable_base_class.name).pluck(:invitable_id)
|
|
57
|
+
where(:id.in => ids)
|
|
58
|
+
}
|
|
59
|
+
scope :invitation_accepted, lambda {
|
|
60
|
+
ids = DeviseInvitable::Invitation.accepted
|
|
61
|
+
.where(invitable_type: devise_invitable_base_class.name).pluck(:invitable_id)
|
|
62
|
+
where(:id.in => ids)
|
|
63
|
+
}
|
|
64
|
+
scope :no_active_invitation, lambda {
|
|
65
|
+
ids = DeviseInvitable::Invitation.pending.where(invitable_type: devise_invitable_base_class.name)
|
|
66
|
+
.where(:invitable_id.ne => nil).pluck(:invitable_id)
|
|
67
|
+
where(:id.nin => ids)
|
|
68
|
+
}
|
|
69
|
+
elsif defined?(ActiveRecord) && defined?(ActiveRecord::Base) && self < ActiveRecord::Base
|
|
70
|
+
scope :created_by_invite, lambda {
|
|
71
|
+
joins(:devise_invitations).merge(DeviseInvitable::Invitation.provisioning).distinct
|
|
72
|
+
}
|
|
73
|
+
scope :invitation_not_accepted, lambda {
|
|
74
|
+
joins(:devise_invitations).merge(DeviseInvitable::Invitation.pending)
|
|
75
|
+
.merge(DeviseInvitable::Invitation.provisioning).distinct
|
|
76
|
+
}
|
|
77
|
+
scope :invitation_accepted, lambda {
|
|
78
|
+
joins(:devise_invitations).merge(DeviseInvitable::Invitation.accepted).distinct
|
|
79
|
+
}
|
|
80
|
+
scope :no_active_invitation, lambda {
|
|
81
|
+
invitation_ids = DeviseInvitable::Invitation.pending
|
|
82
|
+
.where(invitable_type: devise_invitable_base_class.name)
|
|
83
|
+
.where.not(invitable_id: nil)
|
|
84
|
+
.select(:invitable_id)
|
|
85
|
+
where.not(id: invitation_ids)
|
|
86
|
+
}
|
|
63
87
|
|
|
64
88
|
callbacks = [
|
|
65
89
|
:before_invitation_created,
|
|
@@ -74,60 +98,145 @@ module Devise
|
|
|
74
98
|
end
|
|
75
99
|
end
|
|
76
100
|
|
|
77
|
-
def self.required_fields(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
def self.required_fields(_klass)
|
|
102
|
+
[]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def current_invitation
|
|
106
|
+
@current_invitation ||=
|
|
107
|
+
if persisted?
|
|
108
|
+
devise_invitations.pending.order(invitation_created_at: :desc).first ||
|
|
109
|
+
devise_invitations.order(invitation_created_at: :desc).first
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def reload(*args)
|
|
114
|
+
@current_invitation = nil
|
|
115
|
+
super
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def invitation_token
|
|
119
|
+
current_invitation&.invitation_token
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def invitation_token=(value)
|
|
123
|
+
current_invitation&.invitation_token = value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def invitation_created_at
|
|
127
|
+
current_invitation&.invitation_created_at
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def invitation_created_at=(value)
|
|
131
|
+
assign_invitation_attribute(:invitation_created_at, value)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def invitation_sent_at
|
|
135
|
+
current_invitation&.invitation_sent_at
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def invitation_sent_at=(value)
|
|
139
|
+
assign_invitation_attribute(:invitation_sent_at, value)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def invitation_accepted_at
|
|
143
|
+
current_invitation&.invitation_accepted_at
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def invitation_accepted_at=(value)
|
|
147
|
+
assign_invitation_attribute(:invitation_accepted_at, value)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def invited_by
|
|
151
|
+
if instance_variable_defined?(:@invited_by_for_next_invitation)
|
|
152
|
+
invited_by_for_next_invitation
|
|
153
|
+
else
|
|
154
|
+
current_invitation&.invited_by
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def invited_by=(value)
|
|
159
|
+
if current_invitation
|
|
160
|
+
current_invitation.invited_by = value
|
|
161
|
+
else
|
|
162
|
+
self.invited_by_for_next_invitation = value
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def invited_by_id
|
|
167
|
+
current_invitation&.invited_by_id
|
|
83
168
|
end
|
|
84
169
|
|
|
85
|
-
|
|
170
|
+
def invited_by_type
|
|
171
|
+
current_invitation&.invited_by_type
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Accept the current invitation without persisting either record.
|
|
86
175
|
def accept_invitation
|
|
87
|
-
|
|
88
|
-
|
|
176
|
+
return unless current_invitation
|
|
177
|
+
|
|
178
|
+
current_invitation.invitation_accepted_at = Time.now.utc
|
|
179
|
+
current_invitation.invitation_token = nil
|
|
89
180
|
end
|
|
90
181
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
182
|
+
# Save the resource, accept the selected invitation, and supersede the
|
|
183
|
+
# other outstanding invitations for the same resource.
|
|
93
184
|
def accept_invitation!
|
|
94
|
-
if
|
|
185
|
+
if current_invitation&.pending?
|
|
95
186
|
@accepting_invitation = true
|
|
96
187
|
run_callbacks :invitation_accepted do
|
|
97
|
-
|
|
98
|
-
self.confirmed_at ||=
|
|
99
|
-
|
|
188
|
+
accepted_at = Time.now.utc
|
|
189
|
+
self.confirmed_at ||= accepted_at if respond_to?(:confirmed_at=)
|
|
190
|
+
|
|
191
|
+
if save
|
|
192
|
+
current_invitation.invitable = self
|
|
193
|
+
current_invitation.resource_created_at ||= accepted_at
|
|
194
|
+
current_invitation.invitation_accepted_at = accepted_at
|
|
195
|
+
current_invitation.invitation_token = nil
|
|
196
|
+
current_invitation.save!
|
|
197
|
+
supersede_other_invitations!(accepted_at)
|
|
198
|
+
true
|
|
199
|
+
else
|
|
200
|
+
false
|
|
201
|
+
end
|
|
100
202
|
end.tap do |saved|
|
|
101
|
-
|
|
203
|
+
rollback_accepted_invitation unless saved
|
|
102
204
|
@accepting_invitation = false
|
|
103
205
|
end
|
|
104
206
|
end
|
|
105
207
|
end
|
|
106
208
|
|
|
107
209
|
def rollback_accepted_invitation
|
|
108
|
-
|
|
109
|
-
|
|
210
|
+
invitation = current_invitation
|
|
211
|
+
invitation.reload if invitation&.persisted?
|
|
110
212
|
self.confirmed_at = nil if self.respond_to?(:confirmed_at=)
|
|
111
213
|
end
|
|
112
214
|
|
|
113
215
|
# Verify wheather a user is created by invitation, irrespective to invitation status
|
|
114
216
|
def created_by_invite?
|
|
115
|
-
|
|
217
|
+
devise_invitations.provisioning.exists?
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# True only while this record is a placeholder created by an unresolved
|
|
221
|
+
# invitation. An active resource may have other pending invitations.
|
|
222
|
+
def invitation_pending?
|
|
223
|
+
return false unless persisted?
|
|
224
|
+
|
|
225
|
+
devise_invitations.pending.provisioning.exists?
|
|
116
226
|
end
|
|
117
227
|
|
|
118
|
-
# Verifies whether a user has been invited or not
|
|
119
228
|
def invited_to_sign_up?
|
|
120
|
-
accepting_invitation? ||
|
|
229
|
+
accepting_invitation? || invitation_pending?
|
|
121
230
|
end
|
|
122
231
|
|
|
123
232
|
# Returns true if accept_invitation! was called
|
|
124
233
|
def accepting_invitation?
|
|
125
|
-
@accepting_invitation
|
|
234
|
+
instance_variable_defined?(:@accepting_invitation) && @accepting_invitation
|
|
126
235
|
end
|
|
127
236
|
|
|
128
237
|
# Verifies whether a user accepted an invitation (false when user is accepting it)
|
|
129
238
|
def invitation_accepted?
|
|
130
|
-
!accepting_invitation? &&
|
|
239
|
+
!accepting_invitation? && devise_invitations.accepted.exists?
|
|
131
240
|
end
|
|
132
241
|
|
|
133
242
|
# Verifies whether a user has accepted an invitation (false when user is accepting it), or was never invited
|
|
@@ -137,9 +246,7 @@ module Devise
|
|
|
137
246
|
|
|
138
247
|
# Reset invitation token and send invitation again
|
|
139
248
|
def invite!(invited_by = nil, options = {})
|
|
140
|
-
|
|
141
|
-
was_invited = invited_to_sign_up?
|
|
142
|
-
|
|
249
|
+
resource_was_new = new_record?
|
|
143
250
|
# Required to workaround confirmable model's confirmation_required? method
|
|
144
251
|
# being implemented to check for non-nil value of confirmed_at
|
|
145
252
|
if new_record_and_responds_to?(:confirmation_required?)
|
|
@@ -147,20 +254,30 @@ module Devise
|
|
|
147
254
|
end
|
|
148
255
|
|
|
149
256
|
yield self if block_given?
|
|
150
|
-
|
|
257
|
+
# Normalize invite keys before they are copied to the invitation.
|
|
258
|
+
self.downcase_keys if new_record_and_responds_to?(:downcase_keys)
|
|
259
|
+
self.strip_whitespace if new_record_and_responds_to?(:strip_whitespace)
|
|
260
|
+
invitation = build_devise_invitation(invited_by)
|
|
261
|
+
if instance_variable_defined?(:@invited_by_for_next_invitation)
|
|
262
|
+
remove_instance_variable(:@invited_by_for_next_invitation)
|
|
263
|
+
end
|
|
264
|
+
@current_invitation = invitation
|
|
265
|
+
generate_invitation_token
|
|
151
266
|
|
|
152
267
|
run_callbacks :invitation_created do
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# Call these before_validate methods since we aren't validating on save
|
|
158
|
-
self.downcase_keys if new_record_and_responds_to?(:downcase_keys)
|
|
159
|
-
self.strip_whitespace if new_record_and_responds_to?(:strip_whitespace)
|
|
268
|
+
now = Time.now.utc
|
|
269
|
+
invitation.invitation_created_at = now
|
|
270
|
+
invitation.invitation_sent_at = now unless skip_invitation
|
|
160
271
|
|
|
161
272
|
validate = options.key?(:validate) ? options[:validate] : self.class.validate_on_invite
|
|
162
|
-
|
|
163
|
-
|
|
273
|
+
save_invitation = !self.class.create_resource_on_invitation || save(validate: validate)
|
|
274
|
+
if save_invitation
|
|
275
|
+
if self.class.create_resource_on_invitation
|
|
276
|
+
invitation.invitable = self
|
|
277
|
+
invitation.resource_created_at = now if resource_was_new
|
|
278
|
+
end
|
|
279
|
+
invitation.save!
|
|
280
|
+
invitation.invited_by.decrement_invitation_limit! if invitation.invited_by.present?
|
|
164
281
|
deliver_invitation(options) unless skip_invitation
|
|
165
282
|
end
|
|
166
283
|
end
|
|
@@ -170,7 +287,7 @@ module Devise
|
|
|
170
287
|
# invited, we need to calculate if the invitation time has not expired
|
|
171
288
|
# for this user, in other words, if the invitation is still valid.
|
|
172
289
|
def valid_invitation?
|
|
173
|
-
|
|
290
|
+
current_invitation&.pending? && invitation_period_valid?
|
|
174
291
|
end
|
|
175
292
|
|
|
176
293
|
# Only verify password when is not invited
|
|
@@ -178,6 +295,14 @@ module Devise
|
|
|
178
295
|
super unless !accepting_invitation? && block_from_invitation?
|
|
179
296
|
end
|
|
180
297
|
|
|
298
|
+
def active_for_authentication?
|
|
299
|
+
super && !block_from_invitation?
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def inactive_message
|
|
303
|
+
block_from_invitation? ? :invited : super
|
|
304
|
+
end
|
|
305
|
+
|
|
181
306
|
# Prevent password changed email when accepting invitation
|
|
182
307
|
def send_password_change_notification?
|
|
183
308
|
super && !accepting_invitation?
|
|
@@ -207,7 +332,9 @@ module Devise
|
|
|
207
332
|
# Deliver the invitation email
|
|
208
333
|
def deliver_invitation(options = {})
|
|
209
334
|
generate_invitation_token! unless @raw_invitation_token
|
|
210
|
-
|
|
335
|
+
unless current_invitation.invitation_sent_at
|
|
336
|
+
current_invitation.update_attribute :invitation_sent_at, Time.now.utc
|
|
337
|
+
end
|
|
211
338
|
send_devise_notification(:invitation_instructions, @raw_invitation_token, options)
|
|
212
339
|
end
|
|
213
340
|
|
|
@@ -233,13 +360,29 @@ module Devise
|
|
|
233
360
|
end
|
|
234
361
|
|
|
235
362
|
def invitation_taken?
|
|
236
|
-
!
|
|
363
|
+
persisted? && !invitation_pending?
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def revoke_invitation!
|
|
367
|
+
invitation = current_invitation
|
|
368
|
+
return unless invitation&.pending?
|
|
369
|
+
|
|
370
|
+
remove_placeholder = invitation.provisioning? && invitation_pending?
|
|
371
|
+
invitation.update!(revoked_at: Time.now.utc, invitation_token: nil)
|
|
372
|
+
destroy if remove_placeholder
|
|
237
373
|
end
|
|
238
374
|
|
|
239
375
|
protected
|
|
240
376
|
|
|
377
|
+
def assign_invitation_attribute(attribute, value)
|
|
378
|
+
return unless current_invitation
|
|
379
|
+
|
|
380
|
+
current_invitation.public_send("#{attribute}=", value)
|
|
381
|
+
current_invitation.save! if current_invitation.persisted?
|
|
382
|
+
end
|
|
383
|
+
|
|
241
384
|
def block_from_invitation?
|
|
242
|
-
|
|
385
|
+
invitation_pending?
|
|
243
386
|
end
|
|
244
387
|
|
|
245
388
|
# Checks if the invitation for the user is within the limit time.
|
|
@@ -262,8 +405,7 @@ module Devise
|
|
|
262
405
|
# invitation_period_valid? # will always return true
|
|
263
406
|
#
|
|
264
407
|
def invitation_period_valid?
|
|
265
|
-
|
|
266
|
-
self.class.invite_for.to_i.zero? || (time && time.utc >= self.class.invite_for.ago)
|
|
408
|
+
current_invitation&.invitation_period_valid?(self.class.invite_for)
|
|
267
409
|
end
|
|
268
410
|
|
|
269
411
|
# Generates a new random token for invitation, and stores the time
|
|
@@ -271,22 +413,65 @@ module Devise
|
|
|
271
413
|
def generate_invitation_token
|
|
272
414
|
raw, enc = Devise.token_generator.generate(self.class, :invitation_token)
|
|
273
415
|
@raw_invitation_token = raw
|
|
274
|
-
|
|
416
|
+
current_invitation.invitation_token = enc
|
|
275
417
|
end
|
|
276
418
|
|
|
277
419
|
def generate_invitation_token!
|
|
278
|
-
generate_invitation_token && save
|
|
420
|
+
generate_invitation_token && current_invitation.save!
|
|
279
421
|
end
|
|
280
422
|
|
|
281
423
|
def new_record_and_responds_to?(method)
|
|
282
424
|
self.new_record? && self.respond_to?(method, true)
|
|
283
425
|
end
|
|
284
426
|
|
|
285
|
-
def
|
|
286
|
-
self.
|
|
427
|
+
def build_devise_invitation(invited_by)
|
|
428
|
+
invite_key_values = self.class.invite_key_fields.each_with_object({}) do |key, values|
|
|
429
|
+
values[key] = public_send(key)
|
|
430
|
+
end
|
|
431
|
+
DeviseInvitable::Invitation.new(
|
|
432
|
+
invitable_type: self.class.devise_invitable_base_class.name,
|
|
433
|
+
invite_key: invite_key_values,
|
|
434
|
+
invited_by: invited_by || invited_by_for_next_invitation
|
|
435
|
+
)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def supersede_other_invitations!(time)
|
|
439
|
+
invitations = self.class.pending_invitations_for(attributes)
|
|
440
|
+
invitations.each do |invitation|
|
|
441
|
+
next if invitation.id == current_invitation.id
|
|
442
|
+
|
|
443
|
+
invitation.update!(
|
|
444
|
+
invitable: self,
|
|
445
|
+
invitation_token: nil,
|
|
446
|
+
superseded_at: time,
|
|
447
|
+
superseded_reason: 'accepted_other_invitation'
|
|
448
|
+
)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def resolve_invitations_after_registration!
|
|
453
|
+
self.class.pending_invitations_for(attributes).each do |invitation|
|
|
454
|
+
invitation.update!(
|
|
455
|
+
invitable: self,
|
|
456
|
+
invitation_token: nil,
|
|
457
|
+
superseded_at: Time.now.utc,
|
|
458
|
+
superseded_reason: 'registered'
|
|
459
|
+
)
|
|
460
|
+
end
|
|
461
|
+
self.registering_directly = false
|
|
287
462
|
end
|
|
288
463
|
|
|
289
464
|
module ClassMethods
|
|
465
|
+
def devise_invitable_base_class
|
|
466
|
+
if respond_to?(:base_class)
|
|
467
|
+
base_class
|
|
468
|
+
elsif respond_to?(:root_class)
|
|
469
|
+
root_class
|
|
470
|
+
else
|
|
471
|
+
self
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
290
475
|
# Return fields to invite
|
|
291
476
|
def invite_key_fields
|
|
292
477
|
invite_key.keys
|
|
@@ -301,31 +486,56 @@ module Devise
|
|
|
301
486
|
# set in the record
|
|
302
487
|
def _invite(attributes = {}, invited_by = nil, options = {}, &block)
|
|
303
488
|
invite_key_array = invite_key_fields
|
|
489
|
+
skip_invitation = attributes.delete(:skip_invitation)
|
|
304
490
|
attributes_hash = {}
|
|
305
|
-
invite_key_array.each do |k
|
|
491
|
+
invite_key_array.each do |k|
|
|
306
492
|
attribute = attributes.delete(k)
|
|
307
493
|
attribute = attribute.to_s.strip if strip_whitespace_keys.include?(k)
|
|
308
494
|
attributes_hash[k] = attribute
|
|
309
495
|
end
|
|
310
496
|
|
|
497
|
+
ensure_unique_invite_key_match!(invite_key_array, attributes_hash)
|
|
311
498
|
invitable = find_or_initialize_with_errors(invite_key_array, attributes_hash)
|
|
499
|
+
invitable.assign_attributes(attributes_hash)
|
|
500
|
+
# Association writers may persist changes immediately. Reject an
|
|
501
|
+
# existing resource before assigning non-key attributes or yielding.
|
|
502
|
+
if invitable.persisted? && (invitable.invitation_taken? || !self.resend_invitation)
|
|
503
|
+
invite_key_array.each do |key|
|
|
504
|
+
invitable.add_taken_error(key)
|
|
505
|
+
end
|
|
506
|
+
return [invitable, nil]
|
|
507
|
+
end
|
|
508
|
+
|
|
312
509
|
invitable.assign_attributes(attributes)
|
|
313
|
-
invitable.
|
|
314
|
-
|
|
510
|
+
invitable.skip_invitation = skip_invitation unless skip_invitation.nil?
|
|
511
|
+
invitable.invited_by_for_next_invitation = invited_by
|
|
512
|
+
if create_resource_on_invitation && !(invitable.password || invitable.encrypted_password.present?)
|
|
315
513
|
invitable.password = random_password
|
|
316
514
|
end
|
|
317
515
|
|
|
318
|
-
invitable.valid? if self.validate_on_invite
|
|
319
|
-
if
|
|
516
|
+
invitable.valid? if self.validate_on_invite && create_resource_on_invitation
|
|
517
|
+
if !create_resource_on_invitation && !resend_invitation && pending_invitations_for(attributes_hash).any?
|
|
518
|
+
invite_key_array.each { |key| invitable.add_taken_error(key) }
|
|
519
|
+
elsif invitable.new_record?
|
|
320
520
|
invitable.clear_errors_on_valid_keys if !self.validate_on_invite
|
|
321
|
-
elsif invitable.invitation_taken? || !self.resend_invitation
|
|
322
|
-
invite_key_array.each do |key|
|
|
323
|
-
invitable.add_taken_error(key)
|
|
324
|
-
end
|
|
325
521
|
end
|
|
326
522
|
|
|
523
|
+
resource_attribute_names = attributes.keys.map(&:to_sym)
|
|
524
|
+
changes_before_block = invitable.changes.dup
|
|
327
525
|
yield invitable if block_given?
|
|
328
|
-
|
|
526
|
+
unless create_resource_on_invitation
|
|
527
|
+
block_attribute_names = invitable.changes.each_with_object([]) do |(attribute, change), names|
|
|
528
|
+
names << attribute.to_sym unless changes_before_block[attribute] == change
|
|
529
|
+
end
|
|
530
|
+
changed_resource_attributes =
|
|
531
|
+
(resource_attribute_names + block_attribute_names).uniq - invite_key_array.map(&:to_sym)
|
|
532
|
+
if changed_resource_attributes.any?
|
|
533
|
+
raise DeviseInvitable::ResourceAttributesRequirePlaceholderError,
|
|
534
|
+
"Cannot assign resource attributes (#{changed_resource_attributes.join(', ')}) when " \
|
|
535
|
+
'create_resource_on_invitation is disabled'
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
mail = invitable.invite!(invited_by, options.merge(validate: false)) if invitable.errors.empty?
|
|
329
539
|
[invitable, mail]
|
|
330
540
|
end
|
|
331
541
|
|
|
@@ -354,13 +564,45 @@ module Devise
|
|
|
354
564
|
end
|
|
355
565
|
|
|
356
566
|
def find_by_invitation_token(original_token, only_valid)
|
|
357
|
-
|
|
567
|
+
if original_token.blank?
|
|
568
|
+
invitable = new
|
|
569
|
+
invitable.errors.add(:invitation_token, :blank)
|
|
570
|
+
return invitable unless only_valid
|
|
571
|
+
return
|
|
572
|
+
end
|
|
358
573
|
|
|
359
|
-
|
|
360
|
-
|
|
574
|
+
invitation_token = Devise.token_generator.digest(self, :invitation_token, original_token)
|
|
575
|
+
invitation = DeviseInvitable::Invitation.where(
|
|
576
|
+
invitable_type: devise_invitable_base_class.name,
|
|
577
|
+
invitation_token: invitation_token
|
|
578
|
+
).first
|
|
579
|
+
invitable = invitation&.invitable || new(invitation ? invitation.invite_key : {})
|
|
580
|
+
invitable.instance_variable_set(:@current_invitation, invitation)
|
|
581
|
+
if invitation.nil?
|
|
582
|
+
invitable.errors.add(:invitation_token, :invalid)
|
|
583
|
+
elsif !invitable.valid_invitation?
|
|
584
|
+
invitable.errors.add(:invitation_token, :invalid)
|
|
585
|
+
end
|
|
361
586
|
invitable unless only_valid && invitable.errors.present?
|
|
362
587
|
end
|
|
363
588
|
|
|
589
|
+
def pending_invitations_for(attributes)
|
|
590
|
+
keys = normalized_invite_keys(attributes)
|
|
591
|
+
digest = DeviseInvitable::Invitation.digest_for(devise_invitable_base_class.name, keys)
|
|
592
|
+
DeviseInvitable::Invitation.pending.where(
|
|
593
|
+
invitable_type: devise_invitable_base_class.name,
|
|
594
|
+
invite_key_digest: digest
|
|
595
|
+
).to_a
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def normalized_invite_keys(attributes)
|
|
599
|
+
attributes.to_h.with_indifferent_access.slice(*invite_key_fields).each_with_object({}) do |(key, value), keys|
|
|
600
|
+
value = value.to_s.strip if strip_whitespace_keys.include?(key.to_sym)
|
|
601
|
+
value = value.downcase if value.respond_to?(:downcase) && case_insensitive_keys.include?(key.to_sym)
|
|
602
|
+
keys[key] = value
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
|
|
364
606
|
# Callback convenience methods
|
|
365
607
|
def before_invitation_created(*args, &blk)
|
|
366
608
|
set_callback(:invitation_created, :before, *args, &blk)
|
|
@@ -389,9 +631,34 @@ module Devise
|
|
|
389
631
|
Devise::Models.config(self, :invited_by_counter_cache)
|
|
390
632
|
Devise::Models.config(self, :allow_insecure_sign_in_after_accept)
|
|
391
633
|
Devise::Models.config(self, :require_password_on_accepting)
|
|
634
|
+
Devise::Models.config(self, :create_resource_on_invitation)
|
|
635
|
+
|
|
636
|
+
def invited_by_class_name=(value)
|
|
637
|
+
DeviseInvitable.warn_deprecated_invited_by_setting(:invited_by_class_name)
|
|
638
|
+
@invited_by_class_name = value
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def invited_by_foreign_key=(value)
|
|
642
|
+
DeviseInvitable.warn_deprecated_invited_by_setting(:invited_by_foreign_key)
|
|
643
|
+
@invited_by_foreign_key = value
|
|
644
|
+
end
|
|
392
645
|
|
|
393
646
|
private
|
|
394
647
|
|
|
648
|
+
def ensure_unique_invite_key_match!(required_attributes, attributes)
|
|
649
|
+
filtered_attributes = attributes.to_h.with_indifferent_access
|
|
650
|
+
.slice(*required_attributes)
|
|
651
|
+
.delete_if { |_key, value| value.blank? }
|
|
652
|
+
return unless filtered_attributes.size == required_attributes.size
|
|
653
|
+
|
|
654
|
+
filtered_attributes = devise_parameter_filter.filter(filtered_attributes)
|
|
655
|
+
matches = where(filtered_attributes).limit(2).to_a
|
|
656
|
+
return unless matches.many?
|
|
657
|
+
|
|
658
|
+
raise DeviseInvitable::AmbiguousInviteKeyError,
|
|
659
|
+
"Invite key matches multiple #{devise_invitable_base_class.name} records"
|
|
660
|
+
end
|
|
661
|
+
|
|
395
662
|
# The random password, as set after an invitation, must conform
|
|
396
663
|
# to any password format validation rules of the application.
|
|
397
664
|
# This default fixes the most common scenarios: Passwords must contain
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
module DeviseInvitable
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
3
|
|
|
4
|
+
initializer 'devise_invitable.deprecator' do |app|
|
|
5
|
+
if app.respond_to?(:deprecators)
|
|
6
|
+
app.deprecators[:devise_invitable] = DeviseInvitable.deprecator
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
rake_tasks do
|
|
11
|
+
load File.expand_path('../tasks/devise_invitable_tasks.rake', __dir__)
|
|
12
|
+
end
|
|
13
|
+
|
|
4
14
|
ActiveSupport.on_load(:action_controller) do
|
|
5
15
|
include DeviseInvitable::Controllers::Helpers
|
|
6
16
|
end
|