devise_invitable 2.0.13 → 3.0.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +108 -97
  4. data/app/controllers/devise/invitations_controller.rb +1 -1
  5. data/app/controllers/devise_invitable/registrations_controller.rb +15 -10
  6. data/lib/devise_invitable/invitation.rb +89 -0
  7. data/lib/devise_invitable/inviter.rb +23 -1
  8. data/lib/devise_invitable/migrator.rb +113 -0
  9. data/lib/devise_invitable/models.rb +328 -81
  10. data/lib/devise_invitable/rails.rb +15 -16
  11. data/lib/devise_invitable/version.rb +1 -1
  12. data/lib/devise_invitable.rb +39 -4
  13. data/lib/generators/active_record/devise_invitable_install_generator.rb +22 -0
  14. data/lib/generators/active_record/devise_invitable_upgrade_generator.rb +28 -0
  15. data/lib/generators/active_record/templates/create_devise_invitations.rb +23 -0
  16. data/lib/generators/active_record/templates/upgrade_devise_invitations.rb +72 -0
  17. data/lib/generators/devise_invitable/devise_invitable_generator.rb +2 -4
  18. data/lib/generators/devise_invitable/install_generator.rb +8 -10
  19. data/lib/generators/devise_invitable/upgrade_generator.rb +16 -0
  20. data/lib/generators/mongoid/devise_invitable_install_generator.rb +7 -0
  21. data/lib/generators/mongoid/devise_invitable_upgrade_generator.rb +11 -0
  22. data/lib/tasks/devise_invitable_tasks.rake +10 -0
  23. data/test/functional/registrations_controller_test.rb +24 -3
  24. data/test/generators_test.rb +16 -9
  25. data/test/integration/invitation_test.rb +3 -3
  26. data/test/mailers/invitation_mail_test.rb +1 -0
  27. data/test/mailers/parent_mailer_test.rb +31 -0
  28. data/test/models/separate_invitations_test.rb +181 -0
  29. data/test/models_test.rb +34 -0
  30. data/test/rails_app/app/mailers/application_mailer.rb +2 -0
  31. data/test/rails_app/app/models/user.rb +2 -1
  32. data/test/rails_app/config/initializers/devise.rb +0 -9
  33. data/test/rails_app/db/migrate/20100401102949_create_tables.rb +22 -0
  34. metadata +26 -13
  35. data/lib/generators/active_record/devise_invitable_generator.rb +0 -19
  36. data/lib/generators/active_record/templates/migration.rb +0 -22
  37. 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
- # When an invitation is sent to an email address, an account is created for it.
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
- belongs_to_options = if invited_by_class_name
34
- { class_name: invited_by_class_name }
35
- else
36
- { polymorphic: true }
37
- end
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
- scope :no_active_invitation, lambda { where(invitation_token: nil) }
55
- if defined?(Mongoid) && defined?(Mongoid::Document) && self < Mongoid::Document
56
- scope :created_by_invite, lambda { where(:invitation_created_at.ne => nil) }
57
- scope :invitation_not_accepted, lambda { where(invitation_accepted_at: nil, :invitation_token.ne => nil) }
58
- scope :invitation_accepted, lambda { where(:invitation_accepted_at.ne => nil) }
59
- else
60
- scope :created_by_invite, lambda { where(arel_table[:invitation_created_at].not_eq(nil)) }
61
- scope :invitation_not_accepted, lambda { where(arel_table[:invitation_token].not_eq(nil)).where(invitation_accepted_at: nil) }
62
- scope :invitation_accepted, lambda { where(arel_table[:invitation_accepted_at].not_eq(nil)) }
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,50 +98,135 @@ module Devise
74
98
  end
75
99
  end
76
100
 
77
- def self.required_fields(klass)
78
- fields = [:invitation_token, :invitation_created_at, :invitation_sent_at, :invitation_accepted_at,
79
- :invitation_limit, klass.invited_by_foreign_key || :invited_by_id, :invited_by_type]
80
- fields << :invitations_count if defined?(ActiveRecord) && self < ActiveRecord::Base
81
- fields -= [:invited_by_type] if klass.invited_by_class_name
82
- fields
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
- # Accept an invitation by clearing invitation token and and setting invitation_accepted_at
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
- self.invitation_accepted_at = Time.now.utc
88
- self.invitation_token = nil
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
- # Accept an invitation by clearing invitation token and and setting invitation_accepted_at
92
- # Saves the model and confirms it if model is confirmable, running invitation_accepted callbacks
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 self.invited_to_sign_up?
185
+ if current_invitation&.pending?
95
186
  @accepting_invitation = true
96
187
  run_callbacks :invitation_accepted do
97
- self.accept_invitation
98
- self.confirmed_at ||= self.invitation_accepted_at if self.respond_to?(:confirmed_at=)
99
- self.save
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
- self.rollback_accepted_invitation if !saved
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
- self.invitation_token = self.invitation_token_was
109
- self.invitation_accepted_at = nil
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
- invitation_created_at.present?
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? || (persisted? && invitation_token.present?)
229
+ accepting_invitation? || invitation_pending?
121
230
  end
122
231
 
123
232
  # Returns true if accept_invitation! was called
@@ -127,7 +236,7 @@ module Devise
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? && invitation_accepted_at.present?
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
- # This is an order-dependant assignment, this can't be moved
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
- generate_invitation_token if no_token_present_or_skip_invitation?
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
- self.invitation_created_at = Time.now.utc
154
- self.invitation_sent_at = self.invitation_created_at unless skip_invitation
155
- self.invited_by = invited_by if invited_by
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
- if save(validate: validate)
163
- self.invited_by.decrement_invitation_limit! if !was_invited and self.invited_by.present?
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
- invited_to_sign_up? && invitation_period_valid?
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
- self.update_attribute :invitation_sent_at, Time.now.utc unless self.invitation_sent_at
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
- !invited_to_sign_up?
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
- invited_to_sign_up?
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
- time = invitation_created_at || invitation_sent_at
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
- self.invitation_token = enc
416
+ current_invitation.invitation_token = enc
275
417
  end
276
418
 
277
419
  def generate_invitation_token!
278
- generate_invitation_token && save(validate: false)
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 no_token_present_or_skip_invitation?
286
- self.invitation_token.nil? || (!skip_invitation || @raw_invitation_token.nil?)
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,22 +486,27 @@ 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,v|
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
 
311
497
  invitable = find_or_initialize_with_errors(invite_key_array, attributes_hash)
498
+ invitable.assign_attributes(attributes_hash)
312
499
  invitable.assign_attributes(attributes)
313
- invitable.invited_by = invited_by
314
- unless invitable.password || invitable.encrypted_password.present?
500
+ invitable.skip_invitation = skip_invitation unless skip_invitation.nil?
501
+ invitable.invited_by_for_next_invitation = invited_by
502
+ if create_resource_on_invitation && !(invitable.password || invitable.encrypted_password.present?)
315
503
  invitable.password = random_password
316
504
  end
317
505
 
318
- invitable.valid? if self.validate_on_invite
319
- if invitable.new_record?
506
+ invitable.valid? if self.validate_on_invite && create_resource_on_invitation
507
+ if !create_resource_on_invitation && !resend_invitation && pending_invitations_for(attributes_hash).any?
508
+ invite_key_array.each { |key| invitable.add_taken_error(key) }
509
+ elsif invitable.new_record?
320
510
  invitable.clear_errors_on_valid_keys if !self.validate_on_invite
321
511
  elsif invitable.invitation_taken? || !self.resend_invitation
322
512
  invite_key_array.each do |key|
@@ -324,8 +514,22 @@ module Devise
324
514
  end
325
515
  end
326
516
 
517
+ resource_attribute_names = attributes.keys.map(&:to_sym)
518
+ changes_before_block = invitable.changes.dup
327
519
  yield invitable if block_given?
328
- mail = invitable.invite!(nil, options.merge(validate: false)) if invitable.errors.empty?
520
+ unless create_resource_on_invitation
521
+ block_attribute_names = invitable.changes.each_with_object([]) do |(attribute, change), names|
522
+ names << attribute.to_sym unless changes_before_block[attribute] == change
523
+ end
524
+ changed_resource_attributes =
525
+ (resource_attribute_names + block_attribute_names).uniq - invite_key_array.map(&:to_sym)
526
+ if changed_resource_attributes.any?
527
+ raise DeviseInvitable::ResourceAttributesRequirePlaceholderError,
528
+ "Cannot assign resource attributes (#{changed_resource_attributes.join(', ')}) when " \
529
+ 'create_resource_on_invitation is disabled'
530
+ end
531
+ end
532
+ mail = invitable.invite!(invited_by, options.merge(validate: false)) if invitable.errors.empty?
329
533
  [invitable, mail]
330
534
  end
331
535
 
@@ -354,13 +558,45 @@ module Devise
354
558
  end
355
559
 
356
560
  def find_by_invitation_token(original_token, only_valid)
357
- invitation_token = Devise.token_generator.digest(self, :invitation_token, original_token)
561
+ if original_token.blank?
562
+ invitable = new
563
+ invitable.errors.add(:invitation_token, :blank)
564
+ return invitable unless only_valid
565
+ return
566
+ end
358
567
 
359
- invitable = find_or_initialize_with_error_by(:invitation_token, invitation_token)
360
- invitable.errors.add(:invitation_token, :invalid) if invitable.invitation_token && invitable.persisted? && !invitable.valid_invitation?
568
+ invitation_token = Devise.token_generator.digest(self, :invitation_token, original_token)
569
+ invitation = DeviseInvitable::Invitation.where(
570
+ invitable_type: devise_invitable_base_class.name,
571
+ invitation_token: invitation_token
572
+ ).first
573
+ invitable = invitation&.invitable || new(invitation ? invitation.invite_key : {})
574
+ invitable.instance_variable_set(:@current_invitation, invitation)
575
+ if invitation.nil?
576
+ invitable.errors.add(:invitation_token, :invalid)
577
+ elsif !invitable.valid_invitation?
578
+ invitable.errors.add(:invitation_token, :invalid)
579
+ end
361
580
  invitable unless only_valid && invitable.errors.present?
362
581
  end
363
582
 
583
+ def pending_invitations_for(attributes)
584
+ keys = normalized_invite_keys(attributes)
585
+ digest = DeviseInvitable::Invitation.digest_for(devise_invitable_base_class.name, keys)
586
+ DeviseInvitable::Invitation.pending.where(
587
+ invitable_type: devise_invitable_base_class.name,
588
+ invite_key_digest: digest
589
+ ).to_a
590
+ end
591
+
592
+ def normalized_invite_keys(attributes)
593
+ attributes.to_h.with_indifferent_access.slice(*invite_key_fields).each_with_object({}) do |(key, value), keys|
594
+ value = value.to_s.strip if strip_whitespace_keys.include?(key.to_sym)
595
+ value = value.downcase if value.respond_to?(:downcase) && case_insensitive_keys.include?(key.to_sym)
596
+ keys[key] = value
597
+ end
598
+ end
599
+
364
600
  # Callback convenience methods
365
601
  def before_invitation_created(*args, &blk)
366
602
  set_callback(:invitation_created, :before, *args, &blk)
@@ -389,6 +625,17 @@ module Devise
389
625
  Devise::Models.config(self, :invited_by_counter_cache)
390
626
  Devise::Models.config(self, :allow_insecure_sign_in_after_accept)
391
627
  Devise::Models.config(self, :require_password_on_accepting)
628
+ Devise::Models.config(self, :create_resource_on_invitation)
629
+
630
+ def invited_by_class_name=(value)
631
+ DeviseInvitable.warn_deprecated_invited_by_setting(:invited_by_class_name)
632
+ @invited_by_class_name = value
633
+ end
634
+
635
+ def invited_by_foreign_key=(value)
636
+ DeviseInvitable.warn_deprecated_invited_by_setting(:invited_by_foreign_key)
637
+ @invited_by_foreign_key = value
638
+ end
392
639
 
393
640
  private
394
641
 
@@ -1,26 +1,25 @@
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
7
17
 
8
- # We use to_prepare instead of after_initialize here because Devise is a Rails engine; its
9
- # mailer is reloaded like the rest of the user's app. Got to make sure that our mailer methods
10
- # are included each time Devise.mailer is (re)loaded.
11
- #
12
- # The work is wrapped in ActiveSupport.on_load(:action_mailer) so that resolving
13
- # Devise.mailer (a String#constantize) doesn't force ActionMailer::Base to load
14
- # before app initialization completes. Without this wrapper, Rails edge's
15
- # guard_load_hooks support (rails/rails#56201) logs an early-load-hook warning
16
- # for :action_mailer (and :active_job, via MailDeliveryJob) on every boot.
17
- config.to_prepare do
18
- ActiveSupport.on_load(:action_mailer) do
19
- Devise.mailer.send :include, DeviseInvitable::Mailer
20
- unless Devise.mailer.ancestors.include?(Devise::Mailers::Helpers)
21
- Devise.mailer.send :include, Devise::Mailers::Helpers
22
- end
23
- end
18
+ # Devise::Mailer includes Devise::Mailers::Helpers, so adding our mailer methods there reaches every
19
+ # (re)loaded Devise.mailer without resolving it at boot. Resolving it from an on_load(:action_mailer)
20
+ # hook re-enters the app's parent_mailer while it is still loading (#929).
21
+ initializer "devise_invitable.mailer" do
22
+ Devise::Mailers::Helpers.include DeviseInvitable::Mailer
24
23
  end
25
24
  # extend mapping with after_initialize because it's not reloaded
26
25
  config.after_initialize do
@@ -1,3 +1,3 @@
1
1
  module DeviseInvitable
2
- VERSION = '2.0.13'.freeze
2
+ VERSION = '3.0.0'.freeze
3
3
  end