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.
@@ -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 :raw_invitation_token
34
+ attr_reader :acceptance_token
34
35
 
35
36
  included do
36
37
  include ::DeviseInvitable::Inviter
37
38
  has_many :devise_invitations,
38
- as: :invitable,
39
- class_name: 'DeviseInvitable::Invitation',
40
- dependent: :nullify,
41
- autosave: true
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
- joins(:devise_invitations).merge(DeviseInvitable::Invitation.provisioning).distinct
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
- joins(:devise_invitations).merge(DeviseInvitable::Invitation.pending)
75
- .merge(DeviseInvitable::Invitation.provisioning).distinct
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
- joins(:devise_invitations).merge(DeviseInvitable::Invitation.accepted).distinct
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 supersede the
183
- # other outstanding invitations for the same resource.
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
- supersede_other_invitations!(accepted_at)
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 = nil if self.respond_to?(: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
- devise_invitations.pending.provisioning.exists?
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
- deliver_invitation(options) unless skip_invitation
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? && self.class.require_password_on_accepting) || super
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
- generate_invitation_token! unless @raw_invitation_token
334
- marked_as_sent = false
335
- unless current_invitation.invitation_sent_at
336
- current_invitation.update_attribute :invitation_sent_at, Time.now.utc
337
- marked_as_sent = true
338
- end
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? && invitation_pending?
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
- current_invitation.save! if current_invitation.persisted?
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.invite_key_fields.each_with_object({}) do |key, values|
435
- values[key] = public_send(key)
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: invite_key_values,
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
- invitation_token = Devise.token_generator.digest(self, :invitation_token, original_token)
581
- invitation = DeviseInvitable::Invitation.where(
582
- invitable_type: devise_invitable_base_class.name,
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.pending.where(
599
- invitable_type: devise_invitable_base_class.name,
600
- invite_key_digest: digest
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 = where(filtered_attributes).limit(2).to_a
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,
@@ -1,3 +1,3 @@
1
1
  module DeviseInvitable
2
- VERSION = '3.0.3'.freeze
2
+ VERSION = '3.1.0'.freeze
3
3
  end
@@ -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 and its invite keys are persisted and the
105
- # resource is created when the invitation is accepted.
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