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.
- 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 +15 -16
- 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/mailers/parent_mailer_test.rb +31 -0
- data/test/models/separate_invitations_test.rb +181 -0
- data/test/models_test.rb +34 -0
- data/test/rails_app/app/mailers/application_mailer.rb +2 -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 +26 -13
- data/lib/generators/active_record/devise_invitable_generator.rb +0 -19
- data/lib/generators/active_record/templates/migration.rb +0 -22
- data/lib/generators/mongoid/devise_invitable_generator.rb +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52005f95e25dca6edd01fb9193500a319035974cdf3cd643133ae7c2f42e7825
|
|
4
|
+
data.tar.gz: 25b2fa545162bb87340c1133811bacf61861fc2eb914b002618531815f576fe7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f512c5c9be3eb3a5b80abc6adec76b5dd5432f4ed02e3596f9ab9c9bc297589b452556c0a828f0a64db91bd7152ad8b31f43b91a82b013e7bce32e1fd76bf13
|
|
7
|
+
data.tar.gz: 10d177422be705e605c9287785cc0d6ec42d5b42ae40ee646d7bb8f6c08521612217fb3735ae790975bfc7da7b7f6b459eb0beeb2d3c06e9648d340f9538526a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
## 3.0.0
|
|
2
|
+
|
|
3
|
+
- Drop support for Ruby < 2.7, Rails < 7.0, and Devise < 5.0
|
|
4
|
+
- Store invitations in a shared `devise_invitations` table/collection with
|
|
5
|
+
polymorphic `invitable` and `invited_by` associations, allowing multiple
|
|
6
|
+
invitations and inviters for the same resource ([#228](https://github.com/scambra/devise_invitable/issues/228),
|
|
7
|
+
[#388](https://github.com/scambra/devise_invitable/issues/388))
|
|
8
|
+
- Defer resource creation until invitation acceptance by default. Add
|
|
9
|
+
`create_resource_on_invitation` to optionally create placeholder resources
|
|
10
|
+
immediately; additional resource attributes require this option
|
|
11
|
+
- Add `invitation_pending?` and prevent pending placeholders from
|
|
12
|
+
authenticating through both password validation and
|
|
13
|
+
`active_for_authentication?`
|
|
14
|
+
- Accept only the invitation whose token was used and supersede other pending
|
|
15
|
+
invitations. Direct registration supersedes matching invitations without
|
|
16
|
+
marking them accepted
|
|
17
|
+
- Make the install generator create the shared invitations store through ORM
|
|
18
|
+
hooks. The model generator now only adds `:invitable` and no longer creates
|
|
19
|
+
per-model invitation-column migrations
|
|
20
|
+
- Add `devise_invitable:upgrade MODEL` to generate an Active Record data
|
|
21
|
+
migration from the legacy per-model columns, with `--invite-keys` support
|
|
22
|
+
- Add `devise_invitable:migrate_invitations[MODEL]` for Mongoid legacy-data
|
|
23
|
+
upgrades; Mongoid indexes continue to be created with
|
|
24
|
+
`db:mongoid:create_indexes`
|
|
25
|
+
|
|
26
|
+
## 2.0.14
|
|
27
|
+
- Fix `NameError` when `Devise.parent_mailer` is an application mailer ([#930](https://github.com/scambra/devise_invitable/pull/930))
|
|
28
|
+
|
|
29
|
+
## 2.0.13
|
|
30
|
+
- Avoid loading Action Mailer and Active Job before application initialization ([#926](https://github.com/scambra/devise_invitable/pull/926))
|
|
31
|
+
- Test against Devise 5 while retaining support for Devise 4.6 and newer
|
|
32
|
+
- Fix email interpolation in the Hong Kong and Taiwanese Chinese translations ([#927](https://github.com/scambra/devise_invitable/pull/927))
|
|
33
|
+
|
|
34
|
+
## 2.0.12
|
|
35
|
+
- Add Ruby 3.4 and Rails 8.0 compatibility coverage ([#920](https://github.com/scambra/devise_invitable/pull/920))
|
|
36
|
+
- Fix the Italian invitation instructions ([#923](https://github.com/scambra/devise_invitable/issues/923))
|
|
37
|
+
- Correct the Brazilian Portuguese locale identifier ([#924](https://github.com/scambra/devise_invitable/pull/924))
|
|
38
|
+
|
|
39
|
+
## 2.0.11
|
|
40
|
+
- Make `invitation_due_at` return `nil` for users who were not invited ([#919](https://github.com/scambra/devise_invitable/pull/919))
|
|
41
|
+
|
|
1
42
|
## 2.0.10
|
|
2
43
|
- Use invited_by_class_name and invited_by_foreign_key settings from the model
|
|
3
44
|
|
data/README.md
CHANGED
|
@@ -12,10 +12,11 @@ invitation setting the password.
|
|
|
12
12
|
|
|
13
13
|
## Requirements
|
|
14
14
|
|
|
15
|
-
The latest version of DeviseInvitable
|
|
15
|
+
The latest version of DeviseInvitable requires Ruby >= 2.7, Rails >= 7.0,
|
|
16
|
+
and Devise >= 5.0.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
Use DeviseInvitable 2.x with Devise 4.6 through 4.9. For earlier Devise 4
|
|
19
|
+
releases, use DeviseInvitable 1.7.5.
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
@@ -28,7 +29,7 @@ gem install devise_invitable
|
|
|
28
29
|
Add DeviseInvitable to your Gemfile:
|
|
29
30
|
|
|
30
31
|
```ruby
|
|
31
|
-
gem 'devise_invitable', '~>
|
|
32
|
+
gem 'devise_invitable', '~> 3.0'
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
### Automatic installation
|
|
@@ -49,8 +50,8 @@ rails generate devise_invitable MODEL
|
|
|
49
50
|
|
|
50
51
|
Replace MODEL by the class name you want to add DeviseInvitable, like `User`,
|
|
51
52
|
`Admin`, etc. This will add the `:invitable` flag to your model's Devise
|
|
52
|
-
modules. The
|
|
53
|
-
|
|
53
|
+
modules. The shared invitations table is created by the install generator, so
|
|
54
|
+
the model generator does not create a migration.
|
|
54
55
|
|
|
55
56
|
### Manual installation
|
|
56
57
|
|
|
@@ -67,81 +68,57 @@ class User < ActiveRecord::Base
|
|
|
67
68
|
end
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
####
|
|
71
|
-
Add `t.invitable` to your Devise model migration:
|
|
71
|
+
#### Active Record schema
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
t.string :invitation_token
|
|
78
|
-
t.datetime :invitation_created_at
|
|
79
|
-
t.datetime :invitation_sent_at
|
|
80
|
-
t.datetime :invitation_accepted_at
|
|
81
|
-
t.integer :invitation_limit
|
|
82
|
-
t.integer :invited_by_id
|
|
83
|
-
t.string :invited_by_type
|
|
84
|
-
...
|
|
85
|
-
end
|
|
86
|
-
add_index :users, :invitation_token, unique: true
|
|
87
|
-
```
|
|
73
|
+
Invitations are stored in the shared `devise_invitations` table generated by
|
|
74
|
+
`devise_invitable:install`. It contains the token and lifecycle timestamps,
|
|
75
|
+
the configured invite keys, and polymorphic `invitable` and `invited_by`
|
|
76
|
+
associations. Invitation columns are not added to each Devise model.
|
|
88
77
|
|
|
89
|
-
|
|
90
|
-
to your model:
|
|
78
|
+
Run the generated migration:
|
|
91
79
|
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
add_column :users, :invitation_token, :string
|
|
95
|
-
add_column :users, :invitation_created_at, :datetime
|
|
96
|
-
add_column :users, :invitation_sent_at, :datetime
|
|
97
|
-
add_column :users, :invitation_accepted_at, :datetime
|
|
98
|
-
add_column :users, :invitation_limit, :integer
|
|
99
|
-
add_column :users, :invited_by_id, :integer
|
|
100
|
-
add_column :users, :invited_by_type, :string
|
|
101
|
-
add_index :users, :invitation_token, unique: true
|
|
102
|
-
end
|
|
80
|
+
```shell
|
|
81
|
+
rails db:migrate
|
|
103
82
|
```
|
|
104
83
|
|
|
105
|
-
|
|
106
|
-
`:invitation_token`, remove it:
|
|
84
|
+
#### Upgrading from resource-backed invitation columns
|
|
107
85
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
change_column :users, :invitation_token, :string, limit: nil
|
|
111
|
-
end
|
|
86
|
+
Generate one upgrade migration for every model that already uses
|
|
87
|
+
`:invitable`:
|
|
112
88
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
89
|
+
```shell
|
|
90
|
+
rails generate devise_invitable:upgrade User
|
|
91
|
+
rails db:migrate
|
|
116
92
|
```
|
|
117
93
|
|
|
118
|
-
|
|
119
|
-
If you are using Mongoid, define the following fields and indexes within your
|
|
120
|
-
invitable model:
|
|
94
|
+
Pass every configured invite key when it is not just `email`:
|
|
121
95
|
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
field :invitation_created_at, type: Time
|
|
125
|
-
field :invitation_sent_at, type: Time
|
|
126
|
-
field :invitation_accepted_at, type: Time
|
|
127
|
-
field :invitation_limit, type: Integer
|
|
128
|
-
|
|
129
|
-
index( { invitation_token: 1 }, { background: true} )
|
|
130
|
-
index( { invitation_by_id: 1 }, { background: true} )
|
|
96
|
+
```shell
|
|
97
|
+
rails generate devise_invitable:upgrade User --invite-keys email username
|
|
131
98
|
```
|
|
132
99
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
100
|
+
The generated migration creates the shared table if necessary, copies existing
|
|
101
|
+
invitation data, and removes the old invitation lifecycle and `invited_by`
|
|
102
|
+
columns. It intentionally leaves `invitation_limit` and `invitations_count`
|
|
103
|
+
alone because those describe inviters rather than individual invitations. Its
|
|
104
|
+
down direction is irreversible because multiple invitations cannot be moved
|
|
105
|
+
losslessly back onto one resource row.
|
|
106
|
+
|
|
107
|
+
## Mongoid
|
|
108
|
+
|
|
109
|
+
Mongoid fields and indexes for `DeviseInvitable::Invitation` are defined by the
|
|
110
|
+
gem. Create the indexes after deployment:
|
|
138
111
|
|
|
139
|
-
Remember to create indexes within the MongoDB database after deploying your
|
|
140
|
-
changes.
|
|
141
112
|
```shell
|
|
142
113
|
rake db:mongoid:create_indexes
|
|
143
114
|
```
|
|
144
115
|
|
|
116
|
+
To migrate legacy embedded invitation fields for a model, run:
|
|
117
|
+
|
|
118
|
+
```shell
|
|
119
|
+
rails devise_invitable:migrate_invitations[User]
|
|
120
|
+
```
|
|
121
|
+
|
|
145
122
|
## Model configuration
|
|
146
123
|
|
|
147
124
|
DeviseInvitable adds some new configuration options:
|
|
@@ -181,17 +158,25 @@ invite key as hash keys, and values that respond to the `===` operator
|
|
|
181
158
|
(including procs and regexes). The default value is looking for users by
|
|
182
159
|
email and validating with `Devise.email_regexp`.
|
|
183
160
|
|
|
184
|
-
* `validate_on_invite`:
|
|
185
|
-
invited.
|
|
161
|
+
* `validate_on_invite`: with placeholder creation enabled, force the resource
|
|
162
|
+
to be valid before it is invited. Deferred invitations validate their invite
|
|
163
|
+
keys because no resource is persisted yet.
|
|
186
164
|
|
|
187
165
|
* `resend_invitation`: resend invitation if user with invited status is
|
|
188
166
|
invited again. Enabled by default.
|
|
189
167
|
|
|
190
|
-
* `invited_by_class_name
|
|
191
|
-
|
|
168
|
+
* `invited_by_class_name` and `invited_by_foreign_key` are deprecated and
|
|
169
|
+
have no effect. This applies to both initializer configuration and options
|
|
170
|
+
passed to `devise :invitable` on a model. Invitations always use the
|
|
171
|
+
polymorphic `invited_by_id` and `invited_by_type` columns in the shared
|
|
172
|
+
invitations table.
|
|
192
173
|
|
|
193
|
-
* `
|
|
194
|
-
|
|
174
|
+
* `create_resource_on_invitation`: create a placeholder resource when the
|
|
175
|
+
invitation is sent. It is disabled by default, so only invite keys are stored
|
|
176
|
+
until acceptance. Enable it when the application needs an addressable resource
|
|
177
|
+
before signup or needs to store additional permitted resource attributes from
|
|
178
|
+
the invitation form. Arbitrary resource attributes are rejected when this is
|
|
179
|
+
disabled.
|
|
195
180
|
|
|
196
181
|
* `invited_by_counter_cache`: the column name used for counter_cache column.
|
|
197
182
|
If this is `nil` (default value), the `invited_by` association is declared
|
|
@@ -378,24 +363,45 @@ end
|
|
|
378
363
|
|
|
379
364
|
### Send an invitation
|
|
380
365
|
|
|
381
|
-
To send an invitation to a user, use the `invite!` class method.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
366
|
+
To send an invitation to a user, use the `invite!` class method. `:email` must
|
|
367
|
+
be present in the parameters hash. By default this stores an invitation without
|
|
368
|
+
creating a user. Set `create_resource_on_invitation: true` on the Devise model
|
|
369
|
+
to create a placeholder user immediately and allow other resource attributes.
|
|
385
370
|
|
|
386
371
|
```ruby
|
|
387
|
-
User.invite!(email: 'new_user@example.com'
|
|
372
|
+
User.invite!(email: 'new_user@example.com')
|
|
388
373
|
# => an invitation email will be sent to new_user@example.com
|
|
389
374
|
```
|
|
390
375
|
|
|
376
|
+
To persist additional resource attributes, enable placeholders:
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
class User < ApplicationRecord
|
|
380
|
+
devise :database_authenticatable, :invitable,
|
|
381
|
+
create_resource_on_invitation: true
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
User.invite!(email: 'new_user@example.com', name: 'John Doe')
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Multiple invitations can be sent for the same invite keys. Accepting one marks
|
|
388
|
+
that invitation accepted and supersedes the others. If the invitee registers
|
|
389
|
+
directly, all matching pending invitations are superseded rather than marked
|
|
390
|
+
accepted.
|
|
391
|
+
|
|
392
|
+
Placeholder resources expose `invitation_pending?`. Pending placeholders are
|
|
393
|
+
rejected by both Devise's password validation and
|
|
394
|
+
`active_for_authentication?` checks until an invitation is accepted or the
|
|
395
|
+
resource is claimed by direct registration.
|
|
396
|
+
|
|
391
397
|
If you want to create the invitation but not send it, you can set
|
|
392
398
|
`skip_invitation` to `true`.
|
|
393
399
|
|
|
394
400
|
```ruby
|
|
395
|
-
user = User.invite!(email: 'new_user@example.com'
|
|
401
|
+
user = User.invite!(email: 'new_user@example.com') do |u|
|
|
396
402
|
u.skip_invitation = true
|
|
397
403
|
end
|
|
398
|
-
# => the
|
|
404
|
+
# => the invitation will be stored, but its email will not be sent
|
|
399
405
|
```
|
|
400
406
|
|
|
401
407
|
When generating the `accept_user_invitation_url` yourself, you must use the
|
|
@@ -420,12 +426,13 @@ You can add `:skip_invitation` to attributes hash if `skip_invitation` is
|
|
|
420
426
|
added to `attr_accessible`.
|
|
421
427
|
|
|
422
428
|
```ruby
|
|
423
|
-
User.invite!(email: 'new_user@example.com',
|
|
424
|
-
# => the
|
|
429
|
+
User.invite!(email: 'new_user@example.com', skip_invitation: true)
|
|
430
|
+
# => the invitation will be stored, but its email will not be sent
|
|
425
431
|
```
|
|
426
432
|
|
|
427
|
-
`skip_invitation` skips sending the email
|
|
428
|
-
`
|
|
433
|
+
`skip_invitation` skips sending the email but still creates a pending
|
|
434
|
+
`DeviseInvitable::Invitation`. With placeholders enabled,
|
|
435
|
+
`invited_to_sign_up?` and `invitation_pending?` return `true` on the resource.
|
|
429
436
|
|
|
430
437
|
To check if a particular user is created by invitation, irrespective to state
|
|
431
438
|
of invitation one can use `created_by_invite?`
|
|
@@ -452,6 +459,14 @@ user = User.find(42)
|
|
|
452
459
|
user.invite!(current_user) # current user is optional to set the invited_by attribute
|
|
453
460
|
```
|
|
454
461
|
|
|
462
|
+
Whenever possible, prefer the `User.invite!` class method described above. The
|
|
463
|
+
instance method creates another invitation and does not perform the
|
|
464
|
+
class method's checks for an existing registered account. Only call it from a
|
|
465
|
+
trusted, authorized workflow after verifying that the user is eligible to be
|
|
466
|
+
invited. In particular, do not expose it through an unauthenticated endpoint,
|
|
467
|
+
as doing so could allow an attacker to disrupt or take over an existing
|
|
468
|
+
account.
|
|
469
|
+
|
|
455
470
|
### Find by invitation token
|
|
456
471
|
|
|
457
472
|
To find by invitation token use the `find_by_invitation_token` class method.
|
|
@@ -499,9 +514,9 @@ A pair of scopes to find those users that have accepted, and those that have
|
|
|
499
514
|
not accepted, invitations are defined:
|
|
500
515
|
|
|
501
516
|
```ruby
|
|
502
|
-
User.invitation_accepted # =>
|
|
503
|
-
User.invitation_not_accepted # =>
|
|
504
|
-
User.created_by_invite # =>
|
|
517
|
+
User.invitation_accepted # => resources with at least one accepted invitation
|
|
518
|
+
User.invitation_not_accepted # => resources provisioned by a pending invitation
|
|
519
|
+
User.created_by_invite # => resources created through the invitation workflow
|
|
505
520
|
```
|
|
506
521
|
|
|
507
522
|
## Integration in a Rails application
|
|
@@ -533,8 +548,9 @@ the `invitation_token` is not present or not valid, the invited is redirected
|
|
|
533
548
|
to `invalid_token_path_for(resource_name)`, which by default is
|
|
534
549
|
`after_sign_out_path_for(resource_name)`.
|
|
535
550
|
|
|
536
|
-
The controller sets the `
|
|
537
|
-
current user.
|
|
551
|
+
The controller sets the invitation's polymorphic `invited_by` association to
|
|
552
|
+
the current user. This keeps every inviter when the same address is invited
|
|
553
|
+
more than once.
|
|
538
554
|
|
|
539
555
|
## Controller filter
|
|
540
556
|
|
|
@@ -571,21 +587,16 @@ end
|
|
|
571
587
|
|
|
572
588
|
## Has many invitations
|
|
573
589
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
For the default behavior, define it like this:
|
|
590
|
+
The `DeviseInvitable::Inviter` module defines a `sent_invitations` association
|
|
591
|
+
containing every invitation sent by the resource:
|
|
578
592
|
|
|
579
593
|
```ruby
|
|
580
|
-
|
|
594
|
+
current_user.sent_invitations
|
|
595
|
+
current_admin.sent_invitations
|
|
581
596
|
```
|
|
582
597
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
```ruby
|
|
587
|
-
has_many :invitations, class_name: 'User', as: :invited_by
|
|
588
|
-
```
|
|
598
|
+
Deleting an inviter preserves its sent invitations and clears their
|
|
599
|
+
`invited_by` association.
|
|
589
600
|
|
|
590
601
|
## I18n
|
|
591
602
|
|
|
@@ -69,7 +69,7 @@ class Devise::InvitationsController < DeviseController
|
|
|
69
69
|
|
|
70
70
|
# GET /resource/invitation/remove?invitation_token=abcdef
|
|
71
71
|
def destroy
|
|
72
|
-
resource.
|
|
72
|
+
resource.revoke_invitation!
|
|
73
73
|
set_flash_message :notice, :invitation_removed if is_flashing_format?
|
|
74
74
|
redirect_to after_sign_out_path_for(resource_name)
|
|
75
75
|
end
|
|
@@ -2,16 +2,21 @@ class DeviseInvitable::RegistrationsController < Devise::RegistrationsController
|
|
|
2
2
|
protected
|
|
3
3
|
|
|
4
4
|
def build_resource(hash = {})
|
|
5
|
-
if hash[:email]
|
|
6
|
-
self.resource = resource_class.where(email: hash[:email]).first
|
|
7
|
-
if self.resource && self.resource.respond_to?(:invited_to_sign_up?) && self.resource.invited_to_sign_up?
|
|
8
|
-
self.resource.attributes = hash
|
|
9
|
-
self.resource.send_confirmation_instructions if self.resource.confirmation_required_for_invited?
|
|
10
|
-
self.resource.accept_invitation
|
|
11
|
-
else
|
|
12
|
-
self.resource = nil
|
|
13
|
-
end
|
|
14
|
-
end
|
|
5
|
+
build_invited_resource(hash) if hash[:email]
|
|
15
6
|
self.resource ||= super
|
|
7
|
+
if self.resource.respond_to?(:registering_directly=) && resource_class.pending_invitations_for(hash).any?
|
|
8
|
+
self.resource.registering_directly = true
|
|
9
|
+
end
|
|
10
|
+
self.resource
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build_invited_resource(hash)
|
|
14
|
+
self.resource = resource_class.where(email: hash[:email]).first
|
|
15
|
+
if self.resource && self.resource.respond_to?(:invited_to_sign_up?) && self.resource.invited_to_sign_up?
|
|
16
|
+
self.resource.attributes = hash
|
|
17
|
+
self.resource.send_confirmation_instructions if self.resource.confirmation_required_for_invited?
|
|
18
|
+
else
|
|
19
|
+
self.resource = nil
|
|
20
|
+
end
|
|
16
21
|
end
|
|
17
22
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
|
|
3
|
+
module DeviseInvitable
|
|
4
|
+
invitation_base =
|
|
5
|
+
if defined?(ActiveRecord::Base)
|
|
6
|
+
ActiveRecord::Base
|
|
7
|
+
else
|
|
8
|
+
Object
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class Invitation < invitation_base
|
|
12
|
+
self.table_name = 'devise_invitations' if respond_to?(:table_name=)
|
|
13
|
+
|
|
14
|
+
if defined?(Mongoid::Document) && !defined?(ActiveRecord::Base)
|
|
15
|
+
include Mongoid::Document
|
|
16
|
+
include Mongoid::Timestamps
|
|
17
|
+
|
|
18
|
+
store_in collection: 'devise_invitations'
|
|
19
|
+
|
|
20
|
+
field :invitation_token, type: String
|
|
21
|
+
field :invitation_created_at, type: Time
|
|
22
|
+
field :invitation_sent_at, type: Time
|
|
23
|
+
field :invitation_accepted_at, type: Time
|
|
24
|
+
field :superseded_at, type: Time
|
|
25
|
+
field :superseded_reason, type: String
|
|
26
|
+
field :revoked_at, type: Time
|
|
27
|
+
field :resource_created_at, type: Time
|
|
28
|
+
field :invite_key, type: Hash, default: {}
|
|
29
|
+
field :invite_key_digest, type: String
|
|
30
|
+
field :invitable_type, type: String
|
|
31
|
+
field :invitable_id
|
|
32
|
+
field :invited_by_type, type: String
|
|
33
|
+
field :invited_by_id
|
|
34
|
+
|
|
35
|
+
index({ invitation_token: 1 }, unique: true, sparse: true)
|
|
36
|
+
index({ invitable_type: 1, invitable_id: 1 })
|
|
37
|
+
index({ invited_by_type: 1, invited_by_id: 1 })
|
|
38
|
+
index({ invitable_type: 1, invite_key_digest: 1 })
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
belongs_to :invitable, polymorphic: true, optional: true
|
|
42
|
+
invited_by_options = { polymorphic: true, optional: true, autosave: true }
|
|
43
|
+
if defined?(ActiveRecord::Base) && Devise.invited_by_counter_cache
|
|
44
|
+
invited_by_options[:counter_cache] = Devise.invited_by_counter_cache
|
|
45
|
+
end
|
|
46
|
+
belongs_to :invited_by, **invited_by_options
|
|
47
|
+
|
|
48
|
+
before_validation :set_invite_key_digest
|
|
49
|
+
|
|
50
|
+
scope :pending, lambda {
|
|
51
|
+
where(invitation_accepted_at: nil, superseded_at: nil, revoked_at: nil)
|
|
52
|
+
}
|
|
53
|
+
if defined?(Mongoid::Document) && !defined?(ActiveRecord::Base)
|
|
54
|
+
scope :provisioning, -> { where(:resource_created_at.ne => nil) }
|
|
55
|
+
scope :accepted, -> { where(:invitation_accepted_at.ne => nil) }
|
|
56
|
+
else
|
|
57
|
+
scope :provisioning, -> { where.not(resource_created_at: nil) }
|
|
58
|
+
scope :accepted, -> { where.not(invitation_accepted_at: nil) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pending?
|
|
62
|
+
invitation_accepted_at.nil? && superseded_at.nil? && revoked_at.nil?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def provisioning?
|
|
66
|
+
resource_created_at.present?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def invitation_period_valid?(period)
|
|
70
|
+
period.to_i.zero? || (invitation_created_at && invitation_created_at.utc >= period.ago)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def invite_key
|
|
74
|
+
value = super
|
|
75
|
+
value.respond_to?(:with_indifferent_access) ? value.with_indifferent_access : value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.digest_for(resource_type, invite_key)
|
|
79
|
+
components = invite_key.to_h.map { |key, value| [key.to_s, value.to_s] }.sort.flatten
|
|
80
|
+
Digest::SHA256.hexdigest(([resource_type] + components).join("\0"))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def set_invite_key_digest
|
|
86
|
+
self.invite_key_digest = self.class.digest_for(invitable_type, invite_key)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -3,17 +3,32 @@ module DeviseInvitable
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
+
has_many :sent_invitations,
|
|
7
|
+
class_name: 'DeviseInvitable::Invitation',
|
|
8
|
+
as: :invited_by,
|
|
9
|
+
dependent: :nullify
|
|
10
|
+
|
|
6
11
|
extend ClassMethods
|
|
7
12
|
attr_writer :invitation_limit unless respond_to? :invitation_limit
|
|
8
13
|
end
|
|
9
14
|
|
|
10
15
|
def invitation_limit
|
|
11
|
-
|
|
16
|
+
stored_limit =
|
|
17
|
+
if invitation_limit_attribute?
|
|
18
|
+
self[:invitation_limit]
|
|
19
|
+
else
|
|
20
|
+
@invitation_limit
|
|
21
|
+
end
|
|
22
|
+
stored_limit || self.class.invitation_limit
|
|
12
23
|
end
|
|
13
24
|
|
|
14
25
|
# Return true if this user has invitations left to send
|
|
15
26
|
def has_invitations_left?
|
|
16
27
|
if self.class.invitation_limit.present?
|
|
28
|
+
unless invitation_limit_attribute?
|
|
29
|
+
return sent_invitations.count < self.class.invitation_limit
|
|
30
|
+
end
|
|
31
|
+
|
|
17
32
|
if invitation_limit
|
|
18
33
|
return invitation_limit > 0
|
|
19
34
|
else
|
|
@@ -28,11 +43,18 @@ module DeviseInvitable
|
|
|
28
43
|
|
|
29
44
|
def decrement_invitation_limit!
|
|
30
45
|
if self.class.invitation_limit.present?
|
|
46
|
+
return unless invitation_limit_attribute?
|
|
47
|
+
|
|
31
48
|
self.invitation_limit ||= self.class.invitation_limit
|
|
32
49
|
self.update_attribute(:invitation_limit, invitation_limit - 1)
|
|
33
50
|
end
|
|
34
51
|
end
|
|
35
52
|
|
|
53
|
+
def invitation_limit_attribute?
|
|
54
|
+
(respond_to?(:has_attribute?) && has_attribute?(:invitation_limit)) ||
|
|
55
|
+
(self.class.respond_to?(:fields) && self.class.fields.key?('invitation_limit'))
|
|
56
|
+
end
|
|
57
|
+
|
|
36
58
|
module ClassMethods
|
|
37
59
|
Devise::Models.config(self, :invitation_limit)
|
|
38
60
|
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module DeviseInvitable
|
|
2
|
+
class Migrator
|
|
3
|
+
BATCH_SIZE = 1_000
|
|
4
|
+
|
|
5
|
+
LEGACY_FIELDS = %w[
|
|
6
|
+
invitation_token invitation_created_at invitation_sent_at
|
|
7
|
+
invitation_accepted_at invited_by_id invited_by_type
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
def self.call(model_class)
|
|
11
|
+
new(model_class).call
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(model_class)
|
|
15
|
+
unless defined?(Mongoid::Document) && model_class < Mongoid::Document
|
|
16
|
+
raise ArgumentError, 'DeviseInvitable::Migrator only supports Mongoid models'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@model_class = model_class
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
migrated = 0
|
|
24
|
+
each_legacy_resource_batch do |resources|
|
|
25
|
+
migrated_identities = migrated_identities_for(resources)
|
|
26
|
+
resources_to_migrate = resources.reject do |resource|
|
|
27
|
+
migrated_identities.include?(migration_identity(resource))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
insert_invitations(resources_to_migrate)
|
|
31
|
+
remove_legacy_fields(resources)
|
|
32
|
+
migrated += resources_to_migrate.size
|
|
33
|
+
end
|
|
34
|
+
migrated
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :model_class
|
|
40
|
+
|
|
41
|
+
def resources_with_legacy_invitations
|
|
42
|
+
model_class.where(:invitation_created_at.ne => nil)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def each_legacy_resource_batch(&block)
|
|
46
|
+
resources_with_legacy_invitations
|
|
47
|
+
.batch_size(BATCH_SIZE)
|
|
48
|
+
.each_slice(BATCH_SIZE, &block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def migrated_identities_for(resources)
|
|
52
|
+
resource_ids = resources.map(&:id)
|
|
53
|
+
DeviseInvitable::Invitation.where(
|
|
54
|
+
invitable_type: model_class.devise_invitable_base_class.name
|
|
55
|
+
).in(
|
|
56
|
+
invitable_id: resource_ids
|
|
57
|
+
).pluck(
|
|
58
|
+
:invitable_id, :invitation_created_at
|
|
59
|
+
).to_h { |identity| [identity, true] }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def migration_identity(resource)
|
|
63
|
+
[resource.id, legacy_value(resource, 'invitation_created_at')]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def insert_invitations(resources)
|
|
67
|
+
return if resources.empty?
|
|
68
|
+
|
|
69
|
+
now = Time.now.utc
|
|
70
|
+
attributes = resources.map { |resource| invitation_attributes(resource, now) }
|
|
71
|
+
DeviseInvitable::Invitation.collection.insert_many(attributes)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def invitation_attributes(resource, now)
|
|
75
|
+
created_at = legacy_value(resource, 'invitation_created_at')
|
|
76
|
+
invite_key = invite_key_for(resource)
|
|
77
|
+
{
|
|
78
|
+
invitation_token: legacy_value(resource, 'invitation_token'),
|
|
79
|
+
invitation_created_at: created_at,
|
|
80
|
+
invitation_sent_at: legacy_value(resource, 'invitation_sent_at'),
|
|
81
|
+
invitation_accepted_at: legacy_value(resource, 'invitation_accepted_at'),
|
|
82
|
+
resource_created_at: created_at,
|
|
83
|
+
invite_key: invite_key,
|
|
84
|
+
invite_key_digest: DeviseInvitable::Invitation.digest_for(
|
|
85
|
+
model_class.devise_invitable_base_class.name,
|
|
86
|
+
invite_key
|
|
87
|
+
),
|
|
88
|
+
invitable_type: model_class.devise_invitable_base_class.name,
|
|
89
|
+
invitable_id: resource.id,
|
|
90
|
+
invited_by_type: legacy_value(resource, 'invited_by_type'),
|
|
91
|
+
invited_by_id: legacy_value(resource, 'invited_by_id'),
|
|
92
|
+
created_at: now,
|
|
93
|
+
updated_at: now
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def invite_key_for(resource)
|
|
98
|
+
model_class.invite_key_fields.each_with_object({}) do |key, values|
|
|
99
|
+
values[key] = resource.public_send(key)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def legacy_value(resource, field)
|
|
104
|
+
resource.attributes[field]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def remove_legacy_fields(resources)
|
|
108
|
+
return if resources.empty?
|
|
109
|
+
|
|
110
|
+
model_class.in(id: resources.map(&:id)).unset(*LEGACY_FIELDS)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|