devise_invitable 2.0.14 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +108 -97
- data/app/controllers/devise/invitations_controller.rb +1 -1
- 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 +328 -81
- data/lib/devise_invitable/rails.rb +10 -0
- data/lib/devise_invitable/version.rb +1 -1
- data/lib/devise_invitable.rb +39 -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 +3 -3
- data/test/mailers/invitation_mail_test.rb +1 -0
- data/test/models/separate_invitations_test.rb +181 -0
- data/test/models_test.rb +34 -0
- data/test/rails_app/app/models/user.rb +2 -1
- data/test/rails_app/config/initializers/devise.rb +0 -9
- data/test/rails_app/db/migrate/20100401102949_create_tables.rb +22 -0
- metadata +22 -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,50 +98,135 @@ 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
|
|
@@ -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? &&
|
|
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,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
|
|
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.
|
|
314
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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,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
|
data/lib/devise_invitable.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
module DeviseInvitable
|
|
2
|
+
class ResourceAttributesRequirePlaceholderError < ArgumentError; end
|
|
3
|
+
|
|
2
4
|
autoload :Inviter, 'devise_invitable/inviter'
|
|
5
|
+
autoload :Invitation, 'devise_invitable/invitation'
|
|
3
6
|
autoload :Mailer, 'devise_invitable/mailer'
|
|
7
|
+
autoload :Migrator, 'devise_invitable/migrator'
|
|
4
8
|
autoload :Mapping, 'devise_invitable/mapping'
|
|
5
9
|
autoload :ParameterSanitizer, 'devise_invitable/parameter_sanitizer'
|
|
6
10
|
module Controllers
|
|
@@ -10,6 +14,7 @@ module DeviseInvitable
|
|
|
10
14
|
end
|
|
11
15
|
|
|
12
16
|
require 'devise'
|
|
17
|
+
require 'active_support/deprecation'
|
|
13
18
|
require 'devise_invitable/routes'
|
|
14
19
|
require 'devise_invitable/rails'
|
|
15
20
|
|
|
@@ -58,16 +63,26 @@ module Devise
|
|
|
58
63
|
mattr_accessor :resend_invitation
|
|
59
64
|
@@resend_invitation = true
|
|
60
65
|
|
|
61
|
-
#
|
|
62
|
-
#
|
|
66
|
+
# Deprecated compatibility setting. Invitations now always use a polymorphic
|
|
67
|
+
# invited_by association.
|
|
63
68
|
mattr_accessor :invited_by_class_name
|
|
64
69
|
@@invited_by_class_name = nil
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
def self.invited_by_class_name=(value)
|
|
72
|
+
DeviseInvitable.warn_deprecated_invited_by_setting(:invited_by_class_name)
|
|
73
|
+
@@invited_by_class_name = value
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Deprecated compatibility setting. The shared invitations table always uses
|
|
77
|
+
# invited_by_id and invited_by_type.
|
|
68
78
|
mattr_accessor :invited_by_foreign_key
|
|
69
79
|
@@invited_by_foreign_key = nil
|
|
70
80
|
|
|
81
|
+
def self.invited_by_foreign_key=(value)
|
|
82
|
+
DeviseInvitable.warn_deprecated_invited_by_setting(:invited_by_foreign_key)
|
|
83
|
+
@@invited_by_foreign_key = value
|
|
84
|
+
end
|
|
85
|
+
|
|
71
86
|
# Public: The column name used for counter_cache column. If this is nil,
|
|
72
87
|
# the #invited_by association is declared without counter_cache. (default: nil)
|
|
73
88
|
mattr_accessor :invited_by_counter_cache
|
|
@@ -83,6 +98,26 @@ module Devise
|
|
|
83
98
|
# set when user is invited or it will be set later (default: true).
|
|
84
99
|
mattr_accessor :require_password_on_accepting
|
|
85
100
|
@@require_password_on_accepting = true
|
|
101
|
+
|
|
102
|
+
# Public: Create the invited resource as soon as an invitation is sent.
|
|
103
|
+
# When false, only the invitation and its invite keys are persisted and the
|
|
104
|
+
# resource is created when the invitation is accepted.
|
|
105
|
+
mattr_accessor :create_resource_on_invitation
|
|
106
|
+
@@create_resource_on_invitation = false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
module DeviseInvitable
|
|
110
|
+
def self.deprecator
|
|
111
|
+
@deprecator ||= ActiveSupport::Deprecation.new('3.0', 'DeviseInvitable')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.warn_deprecated_invited_by_setting(setting)
|
|
115
|
+
deprecator.warn(
|
|
116
|
+
"The #{setting} setting is deprecated and has no effect. " \
|
|
117
|
+
"Invitations now always use a polymorphic invited_by association " \
|
|
118
|
+
"with invited_by_id and invited_by_type."
|
|
119
|
+
)
|
|
120
|
+
end
|
|
86
121
|
end
|
|
87
122
|
|
|
88
123
|
Devise.add_module :invitable, controller: :invitations, model: 'devise_invitable/models', route: { invitation: [nil, :new, :accept] }
|