devise_invitable 3.0.3 → 3.1.0
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 +23 -1
- data/README.md +259 -23
- data/app/controllers/devise/invitations_controller.rb +2 -1
- data/app/controllers/devise_invitable/registrations_controller.rb +1 -1
- data/lib/devise_invitable/acceptance_token.rb +58 -0
- data/lib/devise_invitable/invitation.rb +33 -6
- data/lib/devise_invitable/invitation_delivery.rb +114 -0
- data/lib/devise_invitable/invitation_delivery_job.rb +15 -0
- data/lib/devise_invitable/models.rb +217 -47
- data/lib/devise_invitable/version.rb +1 -1
- data/lib/devise_invitable.rb +42 -2
- data/lib/generators/devise_invitable/install_generator.rb +21 -0
- data/test/integration/invitation_test.rb +26 -0
- data/test/jobs/invitation_delivery_job_test.rb +138 -0
- data/test/lazy_loading_test.rb +17 -0
- data/test/mailers/invitation_mail_test.rb +9 -5
- data/test/models/invitable_test.rb +72 -8
- data/test/models/separate_invitations_test.rb +181 -3
- data/test/models_test.rb +17 -0
- data/test/rails_app/app/models/user.rb +4 -1
- data/test/rails_app/db/migrate/20261103170000_add_options_to_users.rb +5 -0
- metadata +10 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'active_support/deprecation'
|
|
2
|
+
require 'active_support/security_utils'
|
|
2
3
|
require 'devise_invitable/models/authenticatable'
|
|
3
4
|
|
|
4
5
|
module Devise
|
|
@@ -30,16 +31,30 @@ module Devise
|
|
|
30
31
|
attr_accessor :completing_invite
|
|
31
32
|
attr_accessor :registering_directly
|
|
32
33
|
attr_accessor :invited_by_for_next_invitation
|
|
33
|
-
attr_reader :
|
|
34
|
+
attr_reader :acceptance_token
|
|
34
35
|
|
|
35
36
|
included do
|
|
36
37
|
include ::DeviseInvitable::Inviter
|
|
37
38
|
has_many :devise_invitations,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
as: :invitable,
|
|
40
|
+
class_name: 'DeviseInvitable::Invitation',
|
|
41
|
+
dependent: :nullify,
|
|
42
|
+
autosave: true
|
|
43
|
+
if defined?(Mongoid::Document) && self < Mongoid::Document
|
|
44
|
+
has_many :pending_invitations,
|
|
45
|
+
as: :invitable,
|
|
46
|
+
class_name: 'DeviseInvitable::Invitation',
|
|
47
|
+
foreign_key: :invitable_id,
|
|
48
|
+
inverse_of: false,
|
|
49
|
+
scope: -> { pending.provisioning }
|
|
50
|
+
else
|
|
51
|
+
has_many :pending_invitations,
|
|
52
|
+
-> { pending.provisioning },
|
|
53
|
+
as: :invitable,
|
|
54
|
+
class_name: 'DeviseInvitable::Invitation',
|
|
55
|
+
foreign_key: :invitable_id,
|
|
56
|
+
inverse_of: false
|
|
57
|
+
end
|
|
43
58
|
extend ActiveModel::Callbacks
|
|
44
59
|
define_model_callbacks :invitation_created
|
|
45
60
|
define_model_callbacks :invitation_accepted
|
|
@@ -68,14 +83,22 @@ module Devise
|
|
|
68
83
|
}
|
|
69
84
|
elsif defined?(ActiveRecord) && defined?(ActiveRecord::Base) && self < ActiveRecord::Base
|
|
70
85
|
scope :created_by_invite, lambda {
|
|
71
|
-
|
|
86
|
+
invitation_ids = DeviseInvitable::Invitation.provisioning
|
|
87
|
+
.where(invitable_type: devise_invitable_base_class.name)
|
|
88
|
+
.select(:invitable_id)
|
|
89
|
+
where(id: invitation_ids)
|
|
72
90
|
}
|
|
73
91
|
scope :invitation_not_accepted, lambda {
|
|
74
|
-
|
|
75
|
-
.
|
|
92
|
+
invitation_ids = DeviseInvitable::Invitation.pending.provisioning
|
|
93
|
+
.where(invitable_type: devise_invitable_base_class.name)
|
|
94
|
+
.select(:invitable_id)
|
|
95
|
+
where(id: invitation_ids)
|
|
76
96
|
}
|
|
77
97
|
scope :invitation_accepted, lambda {
|
|
78
|
-
|
|
98
|
+
invitation_ids = DeviseInvitable::Invitation.accepted
|
|
99
|
+
.where(invitable_type: devise_invitable_base_class.name)
|
|
100
|
+
.select(:invitable_id)
|
|
101
|
+
where(id: invitation_ids)
|
|
79
102
|
}
|
|
80
103
|
scope :no_active_invitation, lambda {
|
|
81
104
|
invitation_ids = DeviseInvitable::Invitation.pending
|
|
@@ -123,6 +146,13 @@ module Devise
|
|
|
123
146
|
current_invitation&.invitation_token = value
|
|
124
147
|
end
|
|
125
148
|
|
|
149
|
+
# The public raw token is an encrypted acceptance envelope for new
|
|
150
|
+
# invitations. Before persistence it falls back to the underlying Devise
|
|
151
|
+
# token so invitation callbacks retain their historical behavior.
|
|
152
|
+
def raw_invitation_token
|
|
153
|
+
acceptance_token || @raw_invitation_token
|
|
154
|
+
end
|
|
155
|
+
|
|
126
156
|
def invitation_created_at
|
|
127
157
|
current_invitation&.invitation_created_at
|
|
128
158
|
end
|
|
@@ -179,37 +209,42 @@ module Devise
|
|
|
179
209
|
current_invitation.invitation_token = nil
|
|
180
210
|
end
|
|
181
211
|
|
|
182
|
-
# Save the resource, accept the selected invitation, and
|
|
183
|
-
# other outstanding invitations for the same
|
|
212
|
+
# Save the resource, accept the selected invitation, and resolve the
|
|
213
|
+
# other outstanding invitations for the same invite keys.
|
|
184
214
|
def accept_invitation!
|
|
185
215
|
if current_invitation&.pending?
|
|
216
|
+
@accepted_invitation_for_provisioned_resource = resource_previously_provisioned?
|
|
217
|
+
provisioning_acceptance = !accepted_invitation_for_provisioned_resource?
|
|
218
|
+
previous_confirmed_at = confirmed_at if respond_to?(:confirmed_at)
|
|
186
219
|
@accepting_invitation = true
|
|
220
|
+
skip_reconfirmation! if respond_to?(:skip_reconfirmation!)
|
|
187
221
|
run_callbacks :invitation_accepted do
|
|
188
222
|
accepted_at = Time.now.utc
|
|
189
223
|
self.confirmed_at ||= accepted_at if respond_to?(:confirmed_at=)
|
|
190
224
|
|
|
191
225
|
if save
|
|
192
226
|
current_invitation.invitable = self
|
|
193
|
-
current_invitation.resource_created_at ||= accepted_at
|
|
227
|
+
current_invitation.resource_created_at ||= accepted_at if provisioning_acceptance
|
|
194
228
|
current_invitation.invitation_accepted_at = accepted_at
|
|
195
229
|
current_invitation.invitation_token = nil
|
|
196
230
|
current_invitation.save!
|
|
197
|
-
|
|
231
|
+
resolve_other_invitations_after_acceptance!(accepted_at)
|
|
232
|
+
reset_devise_invitation_associations
|
|
198
233
|
true
|
|
199
234
|
else
|
|
200
235
|
false
|
|
201
236
|
end
|
|
202
237
|
end.tap do |saved|
|
|
203
|
-
rollback_accepted_invitation unless saved
|
|
238
|
+
rollback_accepted_invitation(previous_confirmed_at) unless saved
|
|
204
239
|
@accepting_invitation = false
|
|
205
240
|
end
|
|
206
241
|
end
|
|
207
242
|
end
|
|
208
243
|
|
|
209
|
-
def rollback_accepted_invitation
|
|
244
|
+
def rollback_accepted_invitation(previous_confirmed_at = nil)
|
|
210
245
|
invitation = current_invitation
|
|
211
246
|
invitation.reload if invitation&.persisted?
|
|
212
|
-
self.confirmed_at =
|
|
247
|
+
self.confirmed_at = previous_confirmed_at if respond_to?(:confirmed_at=)
|
|
213
248
|
end
|
|
214
249
|
|
|
215
250
|
# Verify wheather a user is created by invitation, irrespective to invitation status
|
|
@@ -217,12 +252,25 @@ module Devise
|
|
|
217
252
|
devise_invitations.provisioning.exists?
|
|
218
253
|
end
|
|
219
254
|
|
|
255
|
+
def resource_previously_provisioned?
|
|
256
|
+
persisted? && !invitation_pending?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def accepted_invitation_for_provisioned_resource?
|
|
260
|
+
!!@accepted_invitation_for_provisioned_resource
|
|
261
|
+
end
|
|
262
|
+
|
|
220
263
|
# True only while this record is a placeholder created by an unresolved
|
|
221
264
|
# invitation. An active resource may have other pending invitations.
|
|
222
265
|
def invitation_pending?
|
|
223
266
|
return false unless persisted?
|
|
224
267
|
|
|
225
|
-
|
|
268
|
+
invitations = pending_invitations
|
|
269
|
+
if devise_invitable_association_loaded?(invitations)
|
|
270
|
+
return invitations.any?
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
invitations.exists?
|
|
226
274
|
end
|
|
227
275
|
|
|
228
276
|
def invited_to_sign_up?
|
|
@@ -246,6 +294,8 @@ module Devise
|
|
|
246
294
|
|
|
247
295
|
# Reset invitation token and send invitation again
|
|
248
296
|
def invite!(invited_by = nil, options = {})
|
|
297
|
+
options = options.dup
|
|
298
|
+
invitation_data = options.delete(:invitation_data)
|
|
249
299
|
resource_was_new = new_record?
|
|
250
300
|
# Required to workaround confirmable model's confirmation_required? method
|
|
251
301
|
# being implemented to check for non-nil value of confirmed_at
|
|
@@ -258,6 +308,7 @@ module Devise
|
|
|
258
308
|
self.downcase_keys if new_record_and_responds_to?(:downcase_keys)
|
|
259
309
|
self.strip_whitespace if new_record_and_responds_to?(:strip_whitespace)
|
|
260
310
|
invitation = build_devise_invitation(invited_by)
|
|
311
|
+
invitation.invitation_data = invitation_data
|
|
261
312
|
if instance_variable_defined?(:@invited_by_for_next_invitation)
|
|
262
313
|
remove_instance_variable(:@invited_by_for_next_invitation)
|
|
263
314
|
end
|
|
@@ -265,6 +316,8 @@ module Devise
|
|
|
265
316
|
generate_invitation_token
|
|
266
317
|
|
|
267
318
|
run_callbacks :invitation_created do
|
|
319
|
+
invitation.invitation_data = DeviseInvitable::AcceptanceToken
|
|
320
|
+
.normalize_invitation_data(invitation.invitation_data)
|
|
268
321
|
now = Time.now.utc
|
|
269
322
|
invitation.invitation_created_at = now
|
|
270
323
|
|
|
@@ -276,8 +329,16 @@ module Devise
|
|
|
276
329
|
invitation.resource_created_at = now if resource_was_new
|
|
277
330
|
end
|
|
278
331
|
invitation.save!
|
|
332
|
+
reset_devise_invitation_associations
|
|
333
|
+
generate_acceptance_token
|
|
279
334
|
invitation.invited_by.decrement_invitation_limit! if invitation.invited_by.present?
|
|
280
|
-
|
|
335
|
+
unless skip_invitation
|
|
336
|
+
if self.class.deliver_invitation_later
|
|
337
|
+
enqueue_invitation_delivery(options)
|
|
338
|
+
else
|
|
339
|
+
deliver_invitation(options)
|
|
340
|
+
end
|
|
341
|
+
end
|
|
281
342
|
end
|
|
282
343
|
end
|
|
283
344
|
end
|
|
@@ -309,7 +370,8 @@ module Devise
|
|
|
309
370
|
|
|
310
371
|
# Enforce password when invitation is being accepted
|
|
311
372
|
def password_required?
|
|
312
|
-
(accepting_invitation? &&
|
|
373
|
+
(accepting_invitation? && !resource_previously_provisioned? &&
|
|
374
|
+
self.class.require_password_on_accepting) || super
|
|
313
375
|
end
|
|
314
376
|
|
|
315
377
|
def unauthenticated_message
|
|
@@ -330,16 +392,12 @@ module Devise
|
|
|
330
392
|
|
|
331
393
|
# Deliver the invitation email
|
|
332
394
|
def deliver_invitation(options = {})
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
send_devise_notification(:invitation_instructions, @raw_invitation_token, options)
|
|
340
|
-
rescue
|
|
341
|
-
current_invitation.update_attribute(:invitation_sent_at, nil) if marked_as_sent
|
|
342
|
-
raise
|
|
395
|
+
DeviseInvitable::InvitationDelivery.call(
|
|
396
|
+
current_invitation,
|
|
397
|
+
resource: self,
|
|
398
|
+
acceptance_token: acceptance_token,
|
|
399
|
+
options: options
|
|
400
|
+
)
|
|
343
401
|
end
|
|
344
402
|
|
|
345
403
|
# provide alias to the encrypted invitation_token stored by devise
|
|
@@ -371,24 +429,48 @@ module Devise
|
|
|
371
429
|
invitation = current_invitation
|
|
372
430
|
return unless invitation&.pending?
|
|
373
431
|
|
|
374
|
-
remove_placeholder = invitation.provisioning?
|
|
432
|
+
remove_placeholder = invitation.provisioning?
|
|
375
433
|
invitation.update!(revoked_at: Time.now.utc, invitation_token: nil)
|
|
434
|
+
reset_devise_invitation_associations
|
|
376
435
|
destroy if remove_placeholder
|
|
377
436
|
end
|
|
378
437
|
|
|
379
438
|
protected
|
|
380
439
|
|
|
440
|
+
def devise_invitable_association_loaded?(association)
|
|
441
|
+
return association.loaded? if association.respond_to?(:loaded?)
|
|
442
|
+
|
|
443
|
+
association._loaded?
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def enqueue_invitation_delivery(options)
|
|
447
|
+
DeviseInvitable::InvitationDelivery.enqueue(
|
|
448
|
+
current_invitation,
|
|
449
|
+
resource: self,
|
|
450
|
+
acceptance_token: acceptance_token,
|
|
451
|
+
options: options
|
|
452
|
+
)
|
|
453
|
+
end
|
|
454
|
+
|
|
381
455
|
def assign_invitation_attribute(attribute, value)
|
|
382
456
|
return unless current_invitation
|
|
383
457
|
|
|
384
458
|
current_invitation.public_send("#{attribute}=", value)
|
|
385
|
-
|
|
459
|
+
if current_invitation.persisted?
|
|
460
|
+
current_invitation.save!
|
|
461
|
+
reset_devise_invitation_associations
|
|
462
|
+
end
|
|
386
463
|
end
|
|
387
464
|
|
|
388
465
|
def block_from_invitation?
|
|
389
466
|
invitation_pending?
|
|
390
467
|
end
|
|
391
468
|
|
|
469
|
+
def reset_devise_invitation_associations
|
|
470
|
+
devise_invitations.reset
|
|
471
|
+
pending_invitations.reset
|
|
472
|
+
end
|
|
473
|
+
|
|
392
474
|
# Checks if the invitation for the user is within the limit time.
|
|
393
475
|
# We do this by calculating if the difference between today and the
|
|
394
476
|
# invitation sent date does not exceed the invite for time configured.
|
|
@@ -426,19 +508,49 @@ module Devise
|
|
|
426
508
|
generate_invitation_token && current_invitation.save!
|
|
427
509
|
end
|
|
428
510
|
|
|
511
|
+
def generate_acceptance_token
|
|
512
|
+
@acceptance_token = DeviseInvitable::AcceptanceToken.generate(
|
|
513
|
+
invitation: current_invitation,
|
|
514
|
+
raw_token: @raw_invitation_token,
|
|
515
|
+
invite_key: current_invitation.invite_key_for_acceptance.presence ||
|
|
516
|
+
self.class.normalized_invite_keys(attributes),
|
|
517
|
+
invitation_data: current_invitation.invitation_data
|
|
518
|
+
)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def prepare_invitation_delivery
|
|
522
|
+
generate_invitation_token! unless @raw_invitation_token
|
|
523
|
+
generate_acceptance_token unless acceptance_token
|
|
524
|
+
acceptance_token
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def send_invitation_notification_now(token, options)
|
|
528
|
+
devise_mailer
|
|
529
|
+
.send(:invitation_instructions, self, token, options)
|
|
530
|
+
.deliver_now
|
|
531
|
+
end
|
|
532
|
+
|
|
429
533
|
def new_record_and_responds_to?(method)
|
|
430
534
|
self.new_record? && self.respond_to?(method, true)
|
|
431
535
|
end
|
|
432
536
|
|
|
433
537
|
def build_devise_invitation(invited_by)
|
|
434
|
-
invite_key_values = self.class.
|
|
435
|
-
|
|
436
|
-
end
|
|
437
|
-
DeviseInvitable::Invitation.new(
|
|
538
|
+
invite_key_values = self.class.normalized_invite_keys(attributes)
|
|
539
|
+
invitation = DeviseInvitable::Invitation.new(
|
|
438
540
|
invitable_type: self.class.devise_invitable_base_class.name,
|
|
439
|
-
invite_key:
|
|
541
|
+
invite_key: {},
|
|
440
542
|
invited_by: invited_by || invited_by_for_next_invitation
|
|
441
543
|
)
|
|
544
|
+
invitation.transient_invite_key = invite_key_values
|
|
545
|
+
invitation
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def resolve_other_invitations_after_acceptance!(time)
|
|
549
|
+
if self.class.supersede_other_invitations_on_accept
|
|
550
|
+
supersede_other_invitations!(time)
|
|
551
|
+
else
|
|
552
|
+
preserve_other_invitations!
|
|
553
|
+
end
|
|
442
554
|
end
|
|
443
555
|
|
|
444
556
|
def supersede_other_invitations!(time)
|
|
@@ -455,6 +567,16 @@ module Devise
|
|
|
455
567
|
end
|
|
456
568
|
end
|
|
457
569
|
|
|
570
|
+
def preserve_other_invitations!
|
|
571
|
+
self.class.pending_invitations_for(attributes).each do |invitation|
|
|
572
|
+
next if invitation.id == current_invitation.id
|
|
573
|
+
|
|
574
|
+
invitation.invitable = self
|
|
575
|
+
invitation.resource_created_at = nil if current_invitation.provisioning?
|
|
576
|
+
invitation.save!
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
458
580
|
def resolve_invitations_after_registration!
|
|
459
581
|
self.class.pending_invitations_for(attributes).each do |invitation|
|
|
460
582
|
invitation.update!(
|
|
@@ -464,6 +586,7 @@ module Devise
|
|
|
464
586
|
superseded_reason: 'registered'
|
|
465
587
|
)
|
|
466
588
|
end
|
|
589
|
+
reset_devise_invitation_associations
|
|
467
590
|
self.registering_directly = false
|
|
468
591
|
end
|
|
469
592
|
|
|
@@ -563,6 +686,10 @@ module Devise
|
|
|
563
686
|
original_token = attributes.delete(:invitation_token)
|
|
564
687
|
invitable = find_by_invitation_token(original_token, false)
|
|
565
688
|
if invitable.errors.empty?
|
|
689
|
+
if invitable.resource_previously_provisioned?
|
|
690
|
+
attributes.delete(:password)
|
|
691
|
+
attributes.delete(:password_confirmation)
|
|
692
|
+
end
|
|
566
693
|
invitable.assign_attributes(attributes)
|
|
567
694
|
invitable.accept_invitation!
|
|
568
695
|
end
|
|
@@ -577,12 +704,9 @@ module Devise
|
|
|
577
704
|
return
|
|
578
705
|
end
|
|
579
706
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
invitation_token: invitation_token
|
|
584
|
-
).first
|
|
585
|
-
invitable = invitation&.invitable || new(invitation ? invitation.invite_key : {})
|
|
707
|
+
invitation, invite_key = invitation_and_key_from_token(original_token)
|
|
708
|
+
invitable = invitation&.invitable || new(invite_key || {})
|
|
709
|
+
invitable.assign_attributes(invite_key) if invitation && invite_key.present?
|
|
586
710
|
invitable.instance_variable_set(:@current_invitation, invitation)
|
|
587
711
|
if invitation.nil?
|
|
588
712
|
invitable.errors.add(:invitation_token, :invalid)
|
|
@@ -595,9 +719,14 @@ module Devise
|
|
|
595
719
|
def pending_invitations_for(attributes)
|
|
596
720
|
keys = normalized_invite_keys(attributes)
|
|
597
721
|
digest = DeviseInvitable::Invitation.digest_for(devise_invitable_base_class.name, keys)
|
|
598
|
-
DeviseInvitable::Invitation.
|
|
599
|
-
|
|
600
|
-
|
|
722
|
+
DeviseInvitable::Invitation.to_adapter.find_all(
|
|
723
|
+
conditions: {
|
|
724
|
+
invitation_accepted_at: nil,
|
|
725
|
+
superseded_at: nil,
|
|
726
|
+
revoked_at: nil,
|
|
727
|
+
invitable_type: devise_invitable_base_class.name,
|
|
728
|
+
invite_key_digest: digest
|
|
729
|
+
}
|
|
601
730
|
).to_a
|
|
602
731
|
end
|
|
603
732
|
|
|
@@ -638,6 +767,9 @@ module Devise
|
|
|
638
767
|
Devise::Models.config(self, :allow_insecure_sign_in_after_accept)
|
|
639
768
|
Devise::Models.config(self, :require_password_on_accepting)
|
|
640
769
|
Devise::Models.config(self, :create_resource_on_invitation)
|
|
770
|
+
Devise::Models.config(self, :store_invite_keys_until_delivery)
|
|
771
|
+
Devise::Models.config(self, :deliver_invitation_later)
|
|
772
|
+
Devise::Models.config(self, :supersede_other_invitations_on_accept)
|
|
641
773
|
|
|
642
774
|
def invited_by_class_name=(value)
|
|
643
775
|
DeviseInvitable.warn_deprecated_invited_by_setting(:invited_by_class_name)
|
|
@@ -651,6 +783,44 @@ module Devise
|
|
|
651
783
|
|
|
652
784
|
private
|
|
653
785
|
|
|
786
|
+
def invitation_and_key_from_token(original_token)
|
|
787
|
+
payload = DeviseInvitable::AcceptanceToken.decode(original_token)
|
|
788
|
+
return legacy_invitation_and_key(original_token) unless payload
|
|
789
|
+
return [nil, nil] unless payload[:invitable_type] == devise_invitable_base_class.name
|
|
790
|
+
|
|
791
|
+
invitation_token = Devise.token_generator.digest(
|
|
792
|
+
self,
|
|
793
|
+
:invitation_token,
|
|
794
|
+
payload[:raw_token]
|
|
795
|
+
)
|
|
796
|
+
invitation = DeviseInvitable::Invitation.to_adapter.find_first(
|
|
797
|
+
id: payload[:invitation_id],
|
|
798
|
+
invitable_type: devise_invitable_base_class.name,
|
|
799
|
+
invitation_token: invitation_token
|
|
800
|
+
)
|
|
801
|
+
invitation.invitation_data = payload[:invitation_data] || {} if invitation
|
|
802
|
+
invite_key = normalized_invite_keys(payload[:invite_key] || {})
|
|
803
|
+
expected_digest = DeviseInvitable::Invitation.digest_for(
|
|
804
|
+
devise_invitable_base_class.name,
|
|
805
|
+
invite_key
|
|
806
|
+
)
|
|
807
|
+
invitation = nil unless invitation &&
|
|
808
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
|
809
|
+
invitation.invite_key_digest,
|
|
810
|
+
expected_digest
|
|
811
|
+
)
|
|
812
|
+
[invitation, invite_key]
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def legacy_invitation_and_key(original_token)
|
|
816
|
+
invitation_token = Devise.token_generator.digest(self, :invitation_token, original_token)
|
|
817
|
+
invitation = DeviseInvitable::Invitation.to_adapter.find_first(
|
|
818
|
+
invitable_type: devise_invitable_base_class.name,
|
|
819
|
+
invitation_token: invitation_token
|
|
820
|
+
)
|
|
821
|
+
[invitation, invitation&.invite_key]
|
|
822
|
+
end
|
|
823
|
+
|
|
654
824
|
def ensure_unique_invite_key_match!(required_attributes, attributes)
|
|
655
825
|
filtered_attributes = attributes.to_h.with_indifferent_access
|
|
656
826
|
.slice(*required_attributes)
|
|
@@ -658,7 +828,7 @@ module Devise
|
|
|
658
828
|
return unless filtered_attributes.size == required_attributes.size
|
|
659
829
|
|
|
660
830
|
filtered_attributes = devise_parameter_filter.filter(filtered_attributes)
|
|
661
|
-
matches =
|
|
831
|
+
matches = to_adapter.find_all(conditions: filtered_attributes, limit: 2).to_a
|
|
662
832
|
return unless matches.many?
|
|
663
833
|
|
|
664
834
|
raise DeviseInvitable::AmbiguousInviteKeyError,
|
data/lib/devise_invitable.rb
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
module DeviseInvitable
|
|
2
2
|
class AmbiguousInviteKeyError < StandardError; end
|
|
3
|
+
class InvitationDataTooLargeError < ArgumentError; end
|
|
4
|
+
class MissingInviteKeyError < StandardError; end
|
|
3
5
|
class ResourceAttributesRequirePlaceholderError < ArgumentError; end
|
|
4
6
|
|
|
7
|
+
autoload :AcceptanceToken, 'devise_invitable/acceptance_token'
|
|
5
8
|
autoload :Inviter, 'devise_invitable/inviter'
|
|
6
9
|
autoload :Invitation, 'devise_invitable/invitation'
|
|
10
|
+
autoload :InvitationDelivery, 'devise_invitable/invitation_delivery'
|
|
11
|
+
autoload :InvitationDeliveryJob, 'devise_invitable/invitation_delivery_job'
|
|
7
12
|
autoload :Mailer, 'devise_invitable/mailer'
|
|
8
13
|
autoload :Migrator, 'devise_invitable/migrator'
|
|
9
14
|
autoload :Mapping, 'devise_invitable/mapping'
|
|
@@ -12,6 +17,15 @@ module DeviseInvitable
|
|
|
12
17
|
autoload :Registrations, 'devise_invitable/controllers/registrations'
|
|
13
18
|
autoload :Helpers, 'devise_invitable/controllers/helpers'
|
|
14
19
|
end
|
|
20
|
+
|
|
21
|
+
def self.invitation_delivery_job
|
|
22
|
+
job = Devise.invitation_delivery_job
|
|
23
|
+
job = job.constantize if job.respond_to?(:constantize)
|
|
24
|
+
return job if job.respond_to?(:perform_later)
|
|
25
|
+
|
|
26
|
+
raise ArgumentError,
|
|
27
|
+
'Devise.invitation_delivery_job must respond to perform_later'
|
|
28
|
+
end
|
|
15
29
|
end
|
|
16
30
|
|
|
17
31
|
require 'devise'
|
|
@@ -101,10 +115,36 @@ module Devise
|
|
|
101
115
|
@@require_password_on_accepting = true
|
|
102
116
|
|
|
103
117
|
# Public: Create the invited resource as soon as an invitation is sent.
|
|
104
|
-
# When false, only the invitation
|
|
105
|
-
#
|
|
118
|
+
# When false, only the invitation record is persisted and the resource is
|
|
119
|
+
# created when the invitation is accepted.
|
|
106
120
|
mattr_accessor :create_resource_on_invitation
|
|
107
121
|
@@create_resource_on_invitation = false
|
|
122
|
+
|
|
123
|
+
# Public: Store invite keys until an invitation is delivered so a skipped,
|
|
124
|
+
# resource-less invitation can be delivered later. Stored keys are cleared
|
|
125
|
+
# after successful delivery.
|
|
126
|
+
mattr_accessor :store_invite_keys_until_delivery
|
|
127
|
+
@@store_invite_keys_until_delivery = false
|
|
128
|
+
|
|
129
|
+
# Public: Deliver invitation emails through the configured invitation job.
|
|
130
|
+
# Other Devise notifications continue to use send_devise_notification.
|
|
131
|
+
mattr_accessor :deliver_invitation_later
|
|
132
|
+
@@deliver_invitation_later = false
|
|
133
|
+
|
|
134
|
+
# Public: Active Job class used by Invitation#deliver_later. It may be a
|
|
135
|
+
# class or a class name and must respond to perform_later.
|
|
136
|
+
mattr_accessor :invitation_delivery_job
|
|
137
|
+
@@invitation_delivery_job = 'DeviseInvitable::InvitationDeliveryJob'
|
|
138
|
+
|
|
139
|
+
# Public: Queue used by the default invitation delivery job.
|
|
140
|
+
mattr_accessor :invitation_delivery_queue_name
|
|
141
|
+
@@invitation_delivery_queue_name = :mailers
|
|
142
|
+
|
|
143
|
+
# Public: Supersede other pending invitations for the same invite keys when
|
|
144
|
+
# one invitation is accepted. Disable this only when each invitation carries
|
|
145
|
+
# an independently acceptable application-specific context.
|
|
146
|
+
mattr_accessor :supersede_other_invitations_on_accept
|
|
147
|
+
@@supersede_other_invitations_on_accept = true
|
|
108
148
|
end
|
|
109
149
|
|
|
110
150
|
module DeviseInvitable
|
|
@@ -51,6 +51,27 @@ module DeviseInvitable
|
|
|
51
51
|
# Default: false
|
|
52
52
|
# config.create_resource_on_invitation = true
|
|
53
53
|
|
|
54
|
+
# Store invite keys until delivery so skipped invitations without placeholder
|
|
55
|
+
# resources can be delivered later. Keys are cleared after successful delivery.
|
|
56
|
+
# Default: false
|
|
57
|
+
# config.store_invite_keys_until_delivery = true
|
|
58
|
+
|
|
59
|
+
# Deliver invitation emails with the configured invitation delivery job.
|
|
60
|
+
# Other Devise notifications still use send_devise_notification.
|
|
61
|
+
# Default: false
|
|
62
|
+
# config.deliver_invitation_later = true
|
|
63
|
+
|
|
64
|
+
# Active Job class and queue used by invitation.deliver_later. Configure a
|
|
65
|
+
# custom job class when invitations should inherit from ApplicationJob or use
|
|
66
|
+
# application-specific retry and callback behavior.
|
|
67
|
+
# config.invitation_delivery_job = "MyInvitationDeliveryJob"
|
|
68
|
+
# config.invitation_delivery_queue_name = :mailers
|
|
69
|
+
|
|
70
|
+
# Supersede other pending invitations for the same invite keys when one is
|
|
71
|
+
# accepted. Disable this only for independently acceptable invitation data.
|
|
72
|
+
# Default: true
|
|
73
|
+
# config.supersede_other_invitations_on_accept = false
|
|
74
|
+
|
|
54
75
|
# The column name used for counter_cache column. If this is nil,
|
|
55
76
|
# the #invited_by association is declared without counter_cache.
|
|
56
77
|
# Default: nil
|
|
@@ -147,6 +147,32 @@ class InvitationTest < ActionDispatch::IntegrationTest
|
|
|
147
147
|
Devise.allow_insecure_sign_in_after_accept = original_option_value
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
test 'does not sign in an already provisioned user when accepting another invitation' do
|
|
151
|
+
original_create_resource = User.create_resource_on_invitation
|
|
152
|
+
original_supersede = User.supersede_other_invitations_on_accept
|
|
153
|
+
User.create_resource_on_invitation = false
|
|
154
|
+
User.supersede_other_invitations_on_accept = false
|
|
155
|
+
|
|
156
|
+
first_invited = User.invite!(email: 'valid@email.com')
|
|
157
|
+
first_token = first_invited.raw_invitation_token
|
|
158
|
+
second_invited = User.invite!(email: 'valid@email.com')
|
|
159
|
+
second_token = second_invited.raw_invitation_token
|
|
160
|
+
user = User.accept_invitation!(
|
|
161
|
+
invitation_token: first_token,
|
|
162
|
+
password: '123456789',
|
|
163
|
+
password_confirmation: '123456789'
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
set_password invitation_token: second_token
|
|
167
|
+
|
|
168
|
+
assert_equal new_user_session_path, current_path
|
|
169
|
+
assert user.reload.valid_password?('123456789')
|
|
170
|
+
refute user.valid_password?('987654321')
|
|
171
|
+
ensure
|
|
172
|
+
User.create_resource_on_invitation = original_create_resource
|
|
173
|
+
User.supersede_other_invitations_on_accept = original_supersede
|
|
174
|
+
end
|
|
175
|
+
|
|
150
176
|
test 'clear token and set invitation_accepted_at after recover password instead of accept_invitation' do
|
|
151
177
|
user = User.invite!(email: 'valid@email.com')
|
|
152
178
|
|