devise_invitable 2.0.14 → 3.0.1
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 +66 -0
- data/README.md +166 -97
- data/app/controllers/devise/invitations_controller.rb +8 -2
- 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 +353 -86
- data/lib/devise_invitable/rails.rb +10 -0
- data/lib/devise_invitable/version.rb +1 -1
- data/lib/devise_invitable.rb +40 -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 +24 -3
- data/test/mailers/invitation_mail_test.rb +1 -0
- data/test/models/invitable_test.rb +40 -2
- data/test/models/separate_invitations_test.rb +181 -0
- data/test/models_test.rb +34 -0
- data/test/rails_app/app/controllers/boolean_inviter_invitations_controller.rb +11 -0
- data/test/rails_app/app/controllers/restricted_invitations_controller.rb +9 -0
- data/test/rails_app/app/models/user.rb +8 -2
- data/test/rails_app/config/initializers/devise.rb +0 -9
- data/test/rails_app/config/routes.rb +2 -0
- data/test/rails_app/db/migrate/20100401102949_create_tables.rb +22 -0
- data/test/routes_test.rb +6 -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: 96e5afbd2a5df8b9d94f1b1cbe30a2544f6c759b9c5c4ccb4f1de2a37a8f6e3a
|
|
4
|
+
data.tar.gz: ccef0234148611c07c9eae801b104da63066fd856ea3d1bd592180936e1a3042
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e81ee3d1513dded1dbaaf4685bea5fe71c6d4f08bc57bec248407488ce9f0122498a3dac8c9e80a0db7cf8812bc36c88b570d4688cad52360076d439e172a8ae
|
|
7
|
+
data.tar.gz: db4f74177f9f44b361f689a3ebb58e49e42827e07ee39f98dfb9c907a0d1e4e2704381d24e101d41014fc8a1125eb9dbdd01131e694b572580669bb7c8ae2b9d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
+
## Unreleased
|
|
2
|
+
|
|
3
|
+
## 3.0.1
|
|
4
|
+
|
|
5
|
+
- Raise `DeviseInvitable::AmbiguousInviteKeyError` instead of selecting an
|
|
6
|
+
arbitrary resource when an invite key matches multiple records
|
|
7
|
+
([#878](https://github.com/scambra/devise_invitable/issues/878))
|
|
8
|
+
- Use the signed-in resource as the inviter when a custom
|
|
9
|
+
`authenticate_inviter!` method returns a boolean success value
|
|
10
|
+
([#825](https://github.com/scambra/devise_invitable/issues/825))
|
|
11
|
+
- Do not assign attributes or modify associations on an existing resource when
|
|
12
|
+
its invitation is rejected ([#747](https://github.com/scambra/devise_invitable/issues/747))
|
|
13
|
+
- Avoid uninitialized `@accepting_invitation` warnings on supported Ruby 2.7
|
|
14
|
+
([#776](https://github.com/scambra/devise_invitable/issues/776))
|
|
15
|
+
- Document personal-data storage and retention considerations for deferred and
|
|
16
|
+
placeholder invitations ([#777](https://github.com/scambra/devise_invitable/issues/777))
|
|
17
|
+
- Add regression coverage for custom `authenticate_inviter!` methods that
|
|
18
|
+
redirect unauthorized inviters ([#893](https://github.com/scambra/devise_invitable/issues/893))
|
|
19
|
+
- Add regression coverage to ensure `devise_for` preserves a custom controller
|
|
20
|
+
`module` instead of replacing its registrations controller
|
|
21
|
+
([#877](https://github.com/scambra/devise_invitable/issues/877))
|
|
22
|
+
- Add regression coverage that instance-level `invite!` runs both the before
|
|
23
|
+
and after invitation-created callbacks
|
|
24
|
+
([#640](https://github.com/scambra/devise_invitable/issues/640))
|
|
25
|
+
|
|
26
|
+
## 3.0.0
|
|
27
|
+
|
|
28
|
+
- Drop support for Ruby < 2.7, Rails < 7.0, and Devise < 5.0
|
|
29
|
+
- Store invitations in a shared `devise_invitations` table/collection with
|
|
30
|
+
polymorphic `invitable` and `invited_by` associations, allowing multiple
|
|
31
|
+
invitations and inviters for the same resource ([#228](https://github.com/scambra/devise_invitable/issues/228),
|
|
32
|
+
[#388](https://github.com/scambra/devise_invitable/issues/388))
|
|
33
|
+
- Defer resource creation until invitation acceptance by default. Add
|
|
34
|
+
`create_resource_on_invitation` to optionally create placeholder resources
|
|
35
|
+
immediately; additional resource attributes require this option
|
|
36
|
+
- Add `invitation_pending?` and prevent pending placeholders from
|
|
37
|
+
authenticating through both password validation and
|
|
38
|
+
`active_for_authentication?`
|
|
39
|
+
- Accept only the invitation whose token was used and supersede other pending
|
|
40
|
+
invitations. Direct registration supersedes matching invitations without
|
|
41
|
+
marking them accepted
|
|
42
|
+
- Make the install generator create the shared invitations store through ORM
|
|
43
|
+
hooks. The model generator now only adds `:invitable` and no longer creates
|
|
44
|
+
per-model invitation-column migrations
|
|
45
|
+
- Add `devise_invitable:upgrade MODEL` to generate an Active Record data
|
|
46
|
+
migration from the legacy per-model columns, with `--invite-keys` support
|
|
47
|
+
- Add `devise_invitable:migrate_invitations[MODEL]` for Mongoid legacy-data
|
|
48
|
+
upgrades; Mongoid indexes continue to be created with
|
|
49
|
+
`db:mongoid:create_indexes`
|
|
50
|
+
|
|
51
|
+
## 2.0.14
|
|
52
|
+
- Fix `NameError` when `Devise.parent_mailer` is an application mailer ([#930](https://github.com/scambra/devise_invitable/pull/930))
|
|
53
|
+
|
|
54
|
+
## 2.0.13
|
|
55
|
+
- Avoid loading Action Mailer and Active Job before application initialization ([#926](https://github.com/scambra/devise_invitable/pull/926))
|
|
56
|
+
- Test against Devise 5 while retaining support for Devise 4.6 and newer
|
|
57
|
+
- Fix email interpolation in the Hong Kong and Taiwanese Chinese translations ([#927](https://github.com/scambra/devise_invitable/pull/927))
|
|
58
|
+
|
|
59
|
+
## 2.0.12
|
|
60
|
+
- Add Ruby 3.4 and Rails 8.0 compatibility coverage ([#920](https://github.com/scambra/devise_invitable/pull/920))
|
|
61
|
+
- Fix the Italian invitation instructions ([#923](https://github.com/scambra/devise_invitable/issues/923))
|
|
62
|
+
- Correct the Brazilian Portuguese locale identifier ([#924](https://github.com/scambra/devise_invitable/pull/924))
|
|
63
|
+
|
|
64
|
+
## 2.0.11
|
|
65
|
+
- Make `invitation_due_at` return `nil` for users who were not invited ([#919](https://github.com/scambra/devise_invitable/pull/919))
|
|
66
|
+
|
|
1
67
|
## 2.0.10
|
|
2
68
|
- Use invited_by_class_name and invited_by_foreign_key settings from the model
|
|
3
69
|
|
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
|
|
@@ -261,6 +246,34 @@ above is `'users/invitations'`, so our routes.rb would have this line:
|
|
|
261
246
|
devise_for :users, controllers: { invitations: 'users/invitations' }
|
|
262
247
|
```
|
|
263
248
|
|
|
249
|
+
The invitations controller prepends its authentication, invitation-limit, and
|
|
250
|
+
token-loading callbacks. These callbacks may render or redirect and halt the
|
|
251
|
+
callback chain before an ordinary callback inherited from
|
|
252
|
+
`ApplicationController` runs. If application context must be available even
|
|
253
|
+
when one of these guards halts the request, prepend that setup in the custom
|
|
254
|
+
invitations controller. For example, an around callback can set and restore
|
|
255
|
+
the locale so flash messages created by the guards use the requested locale:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
class Users::InvitationsController < Devise::InvitationsController
|
|
259
|
+
prepend_around_action :use_locale
|
|
260
|
+
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
def use_locale(&action)
|
|
264
|
+
I18n.with_locale(locale_from_request, &action)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def locale_from_request
|
|
268
|
+
params[:locale].presence_in(I18n.available_locales.map(&:to_s)) ||
|
|
269
|
+
I18n.default_locale
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The same ordering rule applies to tenant selection, request tracing, and other
|
|
275
|
+
setup that must run before Devise Invitable's controller guards.
|
|
276
|
+
|
|
264
277
|
be sure that you generate the views and put them into the controller that you
|
|
265
278
|
generated, so for this example it would be:
|
|
266
279
|
|
|
@@ -378,24 +391,75 @@ end
|
|
|
378
391
|
|
|
379
392
|
### Send an invitation
|
|
380
393
|
|
|
381
|
-
To send an invitation to a user, use the `invite!` class method.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
394
|
+
To send an invitation to a user, use the `invite!` class method. `:email` must
|
|
395
|
+
be present in the parameters hash. By default this stores an invitation without
|
|
396
|
+
creating a user. Set `create_resource_on_invitation: true` on the Devise model
|
|
397
|
+
to create a placeholder user immediately and allow other resource attributes.
|
|
385
398
|
|
|
386
399
|
```ruby
|
|
387
|
-
User.invite!(email: 'new_user@example.com'
|
|
400
|
+
User.invite!(email: 'new_user@example.com')
|
|
388
401
|
# => an invitation email will be sent to new_user@example.com
|
|
389
402
|
```
|
|
390
403
|
|
|
404
|
+
To persist additional resource attributes, enable placeholders:
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
class User < ApplicationRecord
|
|
408
|
+
devise :database_authenticatable, :invitable,
|
|
409
|
+
create_resource_on_invitation: true
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
User.invite!(email: 'new_user@example.com', name: 'John Doe')
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Multiple invitations can be sent for the same invite keys. Accepting one marks
|
|
416
|
+
that invitation accepted and supersedes the others. If the invitee registers
|
|
417
|
+
directly, all matching pending invitations are superseded rather than marked
|
|
418
|
+
accepted.
|
|
419
|
+
|
|
420
|
+
Placeholder resources expose `invitation_pending?`. Pending placeholders are
|
|
421
|
+
rejected by both Devise's password validation and
|
|
422
|
+
`active_for_authentication?` checks until an invitation is accepted or the
|
|
423
|
+
resource is claimed by direct registration.
|
|
424
|
+
|
|
425
|
+
### Privacy and data retention
|
|
426
|
+
|
|
427
|
+
Invitation keys commonly contain personal data such as an email address. With
|
|
428
|
+
the default `create_resource_on_invitation: false`, no resource row is created,
|
|
429
|
+
but the invite-key values are stored in plaintext in the
|
|
430
|
+
`devise_invitations.invite_key` JSON column so the resource can be created when
|
|
431
|
+
the invitation is accepted. `invite_key_digest` is used for matching; it does
|
|
432
|
+
not replace the stored values and should not be treated as anonymization.
|
|
433
|
+
|
|
434
|
+
With `create_resource_on_invitation: true`, the resource and any additional
|
|
435
|
+
attributes or associations supplied to `invite!` are persisted immediately.
|
|
436
|
+
Choose this mode only when the application needs a placeholder and its privacy
|
|
437
|
+
policy permits storing that data before acceptance.
|
|
438
|
+
|
|
439
|
+
`invite_for` limits token validity but does not delete invitation or resource
|
|
440
|
+
records. Applications are responsible for choosing a lawful basis for
|
|
441
|
+
processing, disclosing that processing where required, limiting invited data,
|
|
442
|
+
restricting access, protecting it at rest, and deleting pending, revoked,
|
|
443
|
+
superseded, and accepted invitation data according to their retention policy.
|
|
444
|
+
Consent is one possible lawful basis under GDPR, but it is not the only one.
|
|
445
|
+
See the [European Commission GDPR principles](https://commission.europa.eu/law/law-topic/data-protection/information-business-and-organisations/principles-gdpr_en)
|
|
446
|
+
and obtain appropriate legal advice for the application.
|
|
447
|
+
|
|
448
|
+
A one-way hash alone is insufficient for the built-in deferred-invitation
|
|
449
|
+
flow, because devise_invitable must recover the invite keys to create the
|
|
450
|
+
resource at acceptance. Applications that must not retain the email address
|
|
451
|
+
need a custom flow, such as asking the invitee to enter it during acceptance
|
|
452
|
+
and verifying it against a stored digest, or using suitably protected
|
|
453
|
+
reversible storage.
|
|
454
|
+
|
|
391
455
|
If you want to create the invitation but not send it, you can set
|
|
392
456
|
`skip_invitation` to `true`.
|
|
393
457
|
|
|
394
458
|
```ruby
|
|
395
|
-
user = User.invite!(email: 'new_user@example.com'
|
|
459
|
+
user = User.invite!(email: 'new_user@example.com') do |u|
|
|
396
460
|
u.skip_invitation = true
|
|
397
461
|
end
|
|
398
|
-
# => the
|
|
462
|
+
# => the invitation will be stored, but its email will not be sent
|
|
399
463
|
```
|
|
400
464
|
|
|
401
465
|
When generating the `accept_user_invitation_url` yourself, you must use the
|
|
@@ -420,12 +484,13 @@ You can add `:skip_invitation` to attributes hash if `skip_invitation` is
|
|
|
420
484
|
added to `attr_accessible`.
|
|
421
485
|
|
|
422
486
|
```ruby
|
|
423
|
-
User.invite!(email: 'new_user@example.com',
|
|
424
|
-
# => the
|
|
487
|
+
User.invite!(email: 'new_user@example.com', skip_invitation: true)
|
|
488
|
+
# => the invitation will be stored, but its email will not be sent
|
|
425
489
|
```
|
|
426
490
|
|
|
427
|
-
`skip_invitation` skips sending the email
|
|
428
|
-
`
|
|
491
|
+
`skip_invitation` skips sending the email but still creates a pending
|
|
492
|
+
`DeviseInvitable::Invitation`. With placeholders enabled,
|
|
493
|
+
`invited_to_sign_up?` and `invitation_pending?` return `true` on the resource.
|
|
429
494
|
|
|
430
495
|
To check if a particular user is created by invitation, irrespective to state
|
|
431
496
|
of invitation one can use `created_by_invite?`
|
|
@@ -452,6 +517,14 @@ user = User.find(42)
|
|
|
452
517
|
user.invite!(current_user) # current user is optional to set the invited_by attribute
|
|
453
518
|
```
|
|
454
519
|
|
|
520
|
+
Whenever possible, prefer the `User.invite!` class method described above. The
|
|
521
|
+
instance method creates another invitation and does not perform the
|
|
522
|
+
class method's checks for an existing registered account. Only call it from a
|
|
523
|
+
trusted, authorized workflow after verifying that the user is eligible to be
|
|
524
|
+
invited. In particular, do not expose it through an unauthenticated endpoint,
|
|
525
|
+
as doing so could allow an attacker to disrupt or take over an existing
|
|
526
|
+
account.
|
|
527
|
+
|
|
455
528
|
### Find by invitation token
|
|
456
529
|
|
|
457
530
|
To find by invitation token use the `find_by_invitation_token` class method.
|
|
@@ -499,9 +572,9 @@ A pair of scopes to find those users that have accepted, and those that have
|
|
|
499
572
|
not accepted, invitations are defined:
|
|
500
573
|
|
|
501
574
|
```ruby
|
|
502
|
-
User.invitation_accepted # =>
|
|
503
|
-
User.invitation_not_accepted # =>
|
|
504
|
-
User.created_by_invite # =>
|
|
575
|
+
User.invitation_accepted # => resources with at least one accepted invitation
|
|
576
|
+
User.invitation_not_accepted # => resources provisioned by a pending invitation
|
|
577
|
+
User.created_by_invite # => resources created through the invitation workflow
|
|
505
578
|
```
|
|
506
579
|
|
|
507
580
|
## Integration in a Rails application
|
|
@@ -533,8 +606,9 @@ the `invitation_token` is not present or not valid, the invited is redirected
|
|
|
533
606
|
to `invalid_token_path_for(resource_name)`, which by default is
|
|
534
607
|
`after_sign_out_path_for(resource_name)`.
|
|
535
608
|
|
|
536
|
-
The controller sets the `
|
|
537
|
-
current user.
|
|
609
|
+
The controller sets the invitation's polymorphic `invited_by` association to
|
|
610
|
+
the current user. This keeps every inviter when the same address is invited
|
|
611
|
+
more than once.
|
|
538
612
|
|
|
539
613
|
## Controller filter
|
|
540
614
|
|
|
@@ -571,21 +645,16 @@ end
|
|
|
571
645
|
|
|
572
646
|
## Has many invitations
|
|
573
647
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
For the default behavior, define it like this:
|
|
648
|
+
The `DeviseInvitable::Inviter` module defines a `sent_invitations` association
|
|
649
|
+
containing every invitation sent by the resource:
|
|
578
650
|
|
|
579
651
|
```ruby
|
|
580
|
-
|
|
652
|
+
current_user.sent_invitations
|
|
653
|
+
current_admin.sent_invitations
|
|
581
654
|
```
|
|
582
655
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
```ruby
|
|
587
|
-
has_many :invitations, class_name: 'User', as: :invited_by
|
|
588
|
-
```
|
|
656
|
+
Deleting an inviter preserves its sent invitations and clears their
|
|
657
|
+
`invited_by` association.
|
|
589
658
|
|
|
590
659
|
## I18n
|
|
591
660
|
|
|
@@ -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
|
|
@@ -85,7 +85,13 @@ class Devise::InvitationsController < DeviseController
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def current_inviter
|
|
88
|
-
authenticate_inviter!
|
|
88
|
+
authenticated = authenticate_inviter!
|
|
89
|
+
return authenticated if authenticated.respond_to?(:has_invitations_left?)
|
|
90
|
+
|
|
91
|
+
# Custom API/token authenticators may return true on success instead of
|
|
92
|
+
# the authenticated resource, so fall back to Devise's current scope.
|
|
93
|
+
current_resource_method = :"current_#{resource_name}"
|
|
94
|
+
send(current_resource_method) if respond_to?(current_resource_method, true)
|
|
89
95
|
end
|
|
90
96
|
|
|
91
97
|
def has_invitations_left?
|
|
@@ -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
|