plutonium 0.50.0 → 0.51.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/.claude/skills/plutonium/SKILL.md +85 -102
- data/.claude/skills/plutonium-app/SKILL.md +572 -0
- data/.claude/skills/plutonium-auth/SKILL.md +163 -300
- data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
- data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
- data/.claude/skills/plutonium-testing/SKILL.md +6 -5
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +27 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1009 -1214
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +52 -51
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +37 -27
- data/docs/getting-started/index.md +22 -29
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +94 -463
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +94 -296
- data/docs/guides/custom-actions.md +121 -441
- data/docs/guides/index.md +22 -42
- data/docs/guides/multi-tenancy.md +116 -187
- data/docs/guides/nested-resources.md +103 -431
- data/docs/guides/search-filtering.md +123 -240
- data/docs/guides/testing.md +5 -4
- data/docs/guides/theming.md +157 -407
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +106 -425
- data/docs/guides/user-profile.md +76 -243
- data/docs/index.md +1 -1
- data/docs/reference/app/generators.md +517 -0
- data/docs/reference/app/index.md +158 -0
- data/docs/reference/app/packages.md +146 -0
- data/docs/reference/app/portals.md +377 -0
- data/docs/reference/auth/accounts.md +230 -0
- data/docs/reference/auth/index.md +88 -0
- data/docs/reference/auth/profile.md +185 -0
- data/docs/reference/behavior/controllers.md +395 -0
- data/docs/reference/behavior/index.md +22 -0
- data/docs/reference/behavior/interactions.md +341 -0
- data/docs/reference/behavior/policies.md +417 -0
- data/docs/reference/index.md +56 -49
- data/docs/reference/resource/actions.md +423 -0
- data/docs/reference/resource/definition.md +508 -0
- data/docs/reference/resource/index.md +50 -0
- data/docs/reference/resource/model.md +348 -0
- data/docs/reference/resource/query.md +305 -0
- data/docs/reference/tenancy/entity-scoping.md +361 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +393 -0
- data/docs/reference/tenancy/nested-resources.md +267 -0
- data/docs/reference/testing/index.md +287 -0
- data/docs/reference/ui/assets.md +400 -0
- data/docs/reference/ui/components.md +165 -0
- data/docs/reference/ui/displays.md +104 -0
- data/docs/reference/ui/forms.md +284 -0
- data/docs/reference/ui/index.md +30 -0
- data/docs/reference/ui/layouts.md +106 -0
- data/docs/reference/ui/pages.md +189 -0
- data/docs/reference/ui/tables.md +117 -0
- data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
- data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
- data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +1 -0
- data/lib/plutonium/definition/base.rb +1 -1
- data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
- data/lib/plutonium/helpers/turbo_helper.rb +11 -0
- data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
- data/lib/plutonium/resource/controller.rb +1 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
- data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
- data/lib/plutonium/resource/policy.rb +7 -0
- data/lib/plutonium/routing/mapper_extensions.rb +15 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/form/base.rb +6 -2
- data/lib/plutonium/ui/form/components/json.rb +58 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
- data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/resource.rb +0 -4
- data/lib/plutonium/ui/grid/resource.rb +1 -1
- data/lib/plutonium/ui/layout/base.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +0 -7
- data/lib/plutonium/ui/page/index.rb +4 -4
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +8 -0
- data/lib/tasks/release.rake +15 -1
- data/package.json +10 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +553 -543
- metadata +44 -33
- data/.claude/skills/plutonium-assets/SKILL.md +0 -512
- data/.claude/skills/plutonium-controller/SKILL.md +0 -396
- data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
- data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
- data/.claude/skills/plutonium-forms/SKILL.md +0 -465
- data/.claude/skills/plutonium-installation/SKILL.md +0 -331
- data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
- data/.claude/skills/plutonium-invites/SKILL.md +0 -408
- data/.claude/skills/plutonium-model/SKILL.md +0 -440
- data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
- data/.claude/skills/plutonium-package/SKILL.md +0 -198
- data/.claude/skills/plutonium-policy/SKILL.md +0 -456
- data/.claude/skills/plutonium-portal/SKILL.md +0 -410
- data/.claude/skills/plutonium-views/SKILL.md +0 -651
- data/docs/reference/assets/index.md +0 -496
- data/docs/reference/controller/index.md +0 -412
- data/docs/reference/definition/actions.md +0 -462
- data/docs/reference/definition/fields.md +0 -383
- data/docs/reference/definition/index.md +0 -326
- data/docs/reference/definition/query.md +0 -351
- data/docs/reference/generators/index.md +0 -648
- data/docs/reference/interaction/index.md +0 -449
- data/docs/reference/model/features.md +0 -248
- data/docs/reference/model/index.md +0 -218
- data/docs/reference/policy/index.md +0 -456
- data/docs/reference/portal/index.md +0 -379
- data/docs/reference/views/forms.md +0 -411
- data/docs/reference/views/index.md +0 -544
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plutonium-invites
|
|
3
|
-
description: Use BEFORE setting up user invitations, pu:invites:install, or entity membership in a multi-tenant Plutonium app. Also load plutonium-entity-scoping.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Plutonium User Invites
|
|
7
|
-
|
|
8
|
-
## 🚨 Critical (read first)
|
|
9
|
-
- **Use the generators.** `pu:invites:install` and `pu:invites:invitable` — never hand-write invite models, mailers, or controllers. Prerequisites: user model, entity model, membership model (`pu:saas:setup` creates all three).
|
|
10
|
-
- **Invite email must match the accepting user's email.** This is a security feature. Don't disable `enforce_email?` unless you fully understand the implications.
|
|
11
|
-
- **Entity scoping applies to invites** — invites are automatically filtered by the current entity. See `plutonium-entity-scoping`.
|
|
12
|
-
- **Implement `on_invite_accepted` on invitable models.** Plutonium calls it when the invite is accepted; without it, the invitable never learns about the new user.
|
|
13
|
-
- **Related skills:** `plutonium-entity-scoping` (tenant scoping for invites), `plutonium-auth` (Rodauth signup flow), `plutonium-portal` (portal connection), `plutonium-interaction` (custom invite logic).
|
|
14
|
-
|
|
15
|
-
Plutonium provides a complete user invitation system for multi-tenant applications. The system handles:
|
|
16
|
-
- Sending email invitations to new users
|
|
17
|
-
- Token-based invite acceptance flow
|
|
18
|
-
- Integration with Rodauth authentication
|
|
19
|
-
- Entity membership creation on acceptance
|
|
20
|
-
- Support for invitable models that get notified when invites are accepted
|
|
21
|
-
|
|
22
|
-
## Installation
|
|
23
|
-
|
|
24
|
-
### Prerequisites
|
|
25
|
-
|
|
26
|
-
Before installing invites, ensure you have:
|
|
27
|
-
1. A user model with Rodauth authentication
|
|
28
|
-
2. An entity model (Organization, Company, Team, etc.)
|
|
29
|
-
3. A membership model linking users to entities
|
|
30
|
-
|
|
31
|
-
Use `pu:saas:setup` to generate all three:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
rails g pu:saas:setup --user Customer --entity Organization
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Install the Invites Package
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
rails generate pu:invites:install
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**Options:**
|
|
44
|
-
|
|
45
|
-
| Option | Default | Description |
|
|
46
|
-
|--------|---------|-------------|
|
|
47
|
-
| `--entity-model=NAME` | Entity | Entity model name for scoping |
|
|
48
|
-
| `--user-model=NAME` | User | User model name |
|
|
49
|
-
| `--invite-model=NAME` | `<EntityModel><UserModel>Invite` | Invite class name. Omit for single-flow apps; set per-invocation when running the generator more than once. |
|
|
50
|
-
| `--membership-model=NAME` | EntityUser | Membership join model |
|
|
51
|
-
| `--roles=ROLES` | member,admin | Comma-separated roles |
|
|
52
|
-
| `--rodauth=NAME` | user | Rodauth configuration for signup |
|
|
53
|
-
| `--enforce-domain` | false | Require email domain to match entity |
|
|
54
|
-
|
|
55
|
-
**Example with custom models:**
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
rails g pu:invites:install \
|
|
59
|
-
--entity-model=Organization \
|
|
60
|
-
--user-model=Customer \
|
|
61
|
-
--membership-model=OrganizationMember \
|
|
62
|
-
--roles=member,manager,admin
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### What Gets Created
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
packages/invites/
|
|
69
|
-
├── app/
|
|
70
|
-
│ ├── controllers/invites/
|
|
71
|
-
│ │ ├── user_invitations_controller.rb
|
|
72
|
-
│ │ └── welcome_controller.rb
|
|
73
|
-
│ ├── definitions/invites/
|
|
74
|
-
│ │ └── user_invite_definition.rb
|
|
75
|
-
│ ├── interactions/invites/
|
|
76
|
-
│ │ ├── cancel_invite_interaction.rb
|
|
77
|
-
│ │ └── resend_invite_interaction.rb
|
|
78
|
-
│ ├── mailers/invites/
|
|
79
|
-
│ │ └── user_invite_mailer.rb
|
|
80
|
-
│ ├── models/invites/
|
|
81
|
-
│ │ └── user_invite.rb
|
|
82
|
-
│ ├── policies/invites/
|
|
83
|
-
│ │ └── user_invite_policy.rb
|
|
84
|
-
│ └── views/invites/
|
|
85
|
-
│ ├── user_invitations/
|
|
86
|
-
│ │ ├── error.html.erb
|
|
87
|
-
│ │ ├── landing.html.erb
|
|
88
|
-
│ │ ├── show.html.erb
|
|
89
|
-
│ │ └── signup.html.erb
|
|
90
|
-
│ ├── user_invite_mailer/
|
|
91
|
-
│ │ ├── invitation.html.erb
|
|
92
|
-
│ │ └── invitation.text.erb
|
|
93
|
-
│ └── welcome/
|
|
94
|
-
│ └── pending_invitation.html.erb
|
|
95
|
-
|
|
96
|
-
app/interactions/
|
|
97
|
-
├── entity/
|
|
98
|
-
│ └── invite_user_interaction.rb
|
|
99
|
-
└── user/
|
|
100
|
-
└── invite_user_interaction.rb
|
|
101
|
-
|
|
102
|
-
db/migrate/
|
|
103
|
-
└── TIMESTAMP_create_user_invites.rb
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Routes Added
|
|
107
|
-
|
|
108
|
-
```ruby
|
|
109
|
-
# Public invitation routes (unauthenticated)
|
|
110
|
-
get "welcome", to: "invites/welcome#index"
|
|
111
|
-
get "invitations/:token", to: "invites/user_invitations#show"
|
|
112
|
-
post "invitations/:token/accept", to: "invites/user_invitations#accept"
|
|
113
|
-
get "invitations/:token/signup", to: "invites/user_invitations#signup"
|
|
114
|
-
post "invitations/:token/signup", to: "invites/user_invitations#signup"
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Multiple invite flows in one app
|
|
118
|
-
|
|
119
|
-
A single app can run several independent invite flows side-by-side — for example, one for inviting customers to organizations and another for inviting funders to projects. Run `pu:invites:install` once per flow.
|
|
120
|
-
|
|
121
|
-
**Default derivation rule.** When `--invite_model` is omitted, the generator derives the class name as `<EntityModel><UserModel>Invite`. So with the defaults (`--entity_model=Organization --user_model=User`) the generated class is `Invites::OrganizationUserInvite` — there is no literal `UserInvite` default. Single-flow apps don't need to pass `--invite_model` at all.
|
|
122
|
-
|
|
123
|
-
Multi-flow apps typically vary `--entity_model` / `--user_model` per invocation; the derived names diverge automatically, so `--invite_model` is only needed when you want a custom class name.
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
rails g pu:invites:install \
|
|
127
|
-
--entity_model=FunderOrganization \
|
|
128
|
-
--user_model=SpenderAccount \
|
|
129
|
-
--invite_model=FunderInvite
|
|
130
|
-
|
|
131
|
-
rails g pu:invites:install \
|
|
132
|
-
--entity_model=Project \
|
|
133
|
-
--user_model=Member \
|
|
134
|
-
--invite_model=ProjectInvite
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Each invocation creates an independent flow: model `Invites::FunderInvite` on `funder_invites`, controller `Invites::FunderInvitationsController` on `/funder_invitations/:token`, helper `funder_invitation_path`, etc. The shared `Invites::WelcomeController` accumulates each new class into its `invite_classes` array, so `pending_invite` checks all flows in priority order (first-match wins).
|
|
138
|
-
|
|
139
|
-
Override hooks at the model level:
|
|
140
|
-
- `def user_attribute; :spender_account; end` — when `belongs_to :spender_account` instead of `:user`.
|
|
141
|
-
- `def invite_entity_attribute; :funder_organization; end` — when `belongs_to :funder_organization` instead of `:entity`.
|
|
142
|
-
|
|
143
|
-
Override hooks at the controller level (auto-generated by the install generator, shown here so you understand what it emits and can tweak it):
|
|
144
|
-
|
|
145
|
-
```ruby
|
|
146
|
-
# packages/invites/app/controllers/invites/welcome_controller.rb
|
|
147
|
-
def invite_classes
|
|
148
|
-
[::Invites::FunderInvite, ::Invites::ProjectInvite]
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# packages/invites/app/controllers/invites/funder_invitations_controller.rb
|
|
152
|
-
def invitation_path_for(token)
|
|
153
|
-
funder_invitation_path(token: token)
|
|
154
|
-
end
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## Connecting Invitables
|
|
158
|
-
|
|
159
|
-
Invitables are models that trigger invitations and get notified when they're accepted. Common examples:
|
|
160
|
-
- `Tenant` - A tenant record that needs a user assigned
|
|
161
|
-
- `TeamMember` - A membership record created by admin, waiting for user signup
|
|
162
|
-
- `ProjectCollaborator` - A project role waiting for user acceptance
|
|
163
|
-
|
|
164
|
-
### Generate an Invitable
|
|
165
|
-
|
|
166
|
-
```bash
|
|
167
|
-
rails generate pu:invites:invitable Tenant
|
|
168
|
-
rails generate pu:invites:invitable TeamMember --role=member
|
|
169
|
-
rails generate pu:invites:invitable Tenant --dest=my_package
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
**Options:**
|
|
173
|
-
|
|
174
|
-
| Option | Default | Description |
|
|
175
|
-
|--------|---------|-------------|
|
|
176
|
-
| `--role=ROLE` | member | Role to assign to invited users |
|
|
177
|
-
| `--user-model=NAME` | User | User model name |
|
|
178
|
-
| `--membership-model=NAME` | EntityUser | Membership model |
|
|
179
|
-
| `--dest=PACKAGE` | main_app | Destination package |
|
|
180
|
-
| `--[no-]email-templates` | true | Generate custom email templates |
|
|
181
|
-
|
|
182
|
-
### Implement the Callback
|
|
183
|
-
|
|
184
|
-
After generation, implement `on_invite_accepted` in your invitable model:
|
|
185
|
-
|
|
186
|
-
```ruby
|
|
187
|
-
# app/models/tenant.rb
|
|
188
|
-
class Tenant < ApplicationRecord
|
|
189
|
-
include Plutonium::Invites::Concerns::Invitable
|
|
190
|
-
|
|
191
|
-
belongs_to :entity
|
|
192
|
-
belongs_to :user, optional: true
|
|
193
|
-
|
|
194
|
-
def on_invite_accepted(user)
|
|
195
|
-
update!(user: user, status: :active)
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
## How the Flow Works
|
|
201
|
-
|
|
202
|
-
### 1. Admin Sends Invite
|
|
203
|
-
|
|
204
|
-
An admin uses the "Invite User" action on an entity or invitable:
|
|
205
|
-
|
|
206
|
-
```ruby
|
|
207
|
-
# From entity context
|
|
208
|
-
entity.invite_user(email: "user@example.com", role: :member)
|
|
209
|
-
|
|
210
|
-
# From invitable context (e.g., Tenant)
|
|
211
|
-
tenant.invite_user(email: "user@example.com")
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### 2. Email Sent
|
|
215
|
-
|
|
216
|
-
The system sends an email with a secure invitation link:
|
|
217
|
-
|
|
218
|
-
```
|
|
219
|
-
Subject: You've been invited to join Acme Corp
|
|
220
|
-
|
|
221
|
-
Click here to accept: https://app.example.com/invitations/abc123...
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### 3. User Accepts Invite
|
|
225
|
-
|
|
226
|
-
**Existing User Flow:**
|
|
227
|
-
1. User clicks invite link
|
|
228
|
-
2. User logs in (or is already logged in)
|
|
229
|
-
3. System validates email matches
|
|
230
|
-
4. Membership created, invitable notified
|
|
231
|
-
|
|
232
|
-
**New User Flow:**
|
|
233
|
-
1. User clicks invite link
|
|
234
|
-
2. User clicks "Create Account"
|
|
235
|
-
3. User signs up with the invited email
|
|
236
|
-
4. System validates email matches
|
|
237
|
-
5. Membership created, invitable notified
|
|
238
|
-
|
|
239
|
-
### 4. Pending Invite Check
|
|
240
|
-
|
|
241
|
-
After login, users are redirected to `/welcome` where pending invites are shown:
|
|
242
|
-
|
|
243
|
-
```ruby
|
|
244
|
-
# In your controller
|
|
245
|
-
include Plutonium::Invites::PendingInviteCheck
|
|
246
|
-
|
|
247
|
-
# Automatically shows pending invites after login
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
## UserInvite Model
|
|
251
|
-
|
|
252
|
-
The generated `Invites::UserInvite` model includes:
|
|
253
|
-
|
|
254
|
-
```ruby
|
|
255
|
-
class Invites::UserInvite < Invites::ResourceRecord
|
|
256
|
-
include Plutonium::Invites::Concerns::InviteToken
|
|
257
|
-
|
|
258
|
-
# Associations
|
|
259
|
-
belongs_to :entity
|
|
260
|
-
belongs_to :invited_by, polymorphic: true
|
|
261
|
-
belongs_to :user, optional: true
|
|
262
|
-
belongs_to :invitable, polymorphic: true, optional: true
|
|
263
|
-
|
|
264
|
-
# States: pending, accepted, expired, cancelled
|
|
265
|
-
enum :state, pending: 0, accepted: 1, expired: 2, cancelled: 3
|
|
266
|
-
|
|
267
|
-
# Roles
|
|
268
|
-
enum :role, member: 0, admin: 1
|
|
269
|
-
end
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### Key Methods
|
|
273
|
-
|
|
274
|
-
```ruby
|
|
275
|
-
# Find valid invite for acceptance
|
|
276
|
-
invite = Invites::UserInvite.find_for_acceptance(token)
|
|
277
|
-
|
|
278
|
-
# Accept for a user
|
|
279
|
-
invite.accept_for_user!(current_user)
|
|
280
|
-
|
|
281
|
-
# Resend invitation email
|
|
282
|
-
invite.resend!
|
|
283
|
-
|
|
284
|
-
# Cancel invitation
|
|
285
|
-
invite.cancel!
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
## Customization
|
|
289
|
-
|
|
290
|
-
### Custom Email Templates
|
|
291
|
-
|
|
292
|
-
Override templates in your package:
|
|
293
|
-
|
|
294
|
-
```erb
|
|
295
|
-
<%# packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb %>
|
|
296
|
-
<h1>Welcome to <%= @invite.entity.name %>!</h1>
|
|
297
|
-
<p><%= @invite.invited_by.email %> has invited you to join.</p>
|
|
298
|
-
<p><%= link_to "Accept Invitation", @invitation_url %></p>
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### Custom Validation
|
|
302
|
-
|
|
303
|
-
Extend the invite model:
|
|
304
|
-
|
|
305
|
-
```ruby
|
|
306
|
-
# packages/invites/app/models/invites/user_invite.rb
|
|
307
|
-
class Invites::UserInvite < Invites::ResourceRecord
|
|
308
|
-
validate :email_not_already_member
|
|
309
|
-
|
|
310
|
-
private
|
|
311
|
-
|
|
312
|
-
def email_not_already_member
|
|
313
|
-
existing = membership_model.joins(:user)
|
|
314
|
-
.where(entity: entity, users: { email: email })
|
|
315
|
-
.exists?
|
|
316
|
-
|
|
317
|
-
errors.add(:email, "is already a member") if existing
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### Domain Enforcement
|
|
323
|
-
|
|
324
|
-
Enable domain matching in the install:
|
|
325
|
-
|
|
326
|
-
```bash
|
|
327
|
-
rails g pu:invites:install --enforce-domain
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
This requires the invited email domain to match the entity's domain.
|
|
331
|
-
|
|
332
|
-
### Custom Roles
|
|
333
|
-
|
|
334
|
-
Specify roles during install:
|
|
335
|
-
|
|
336
|
-
```bash
|
|
337
|
-
rails g pu:invites:install --roles=viewer,editor,admin,owner
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
## Integration with Portals
|
|
341
|
-
|
|
342
|
-
### Connect Invites to Your Portal
|
|
343
|
-
|
|
344
|
-
```ruby
|
|
345
|
-
# packages/customer_portal/lib/engine.rb
|
|
346
|
-
module CustomerPortal
|
|
347
|
-
class Engine < Rails::Engine
|
|
348
|
-
include Plutonium::Portal::Engine
|
|
349
|
-
|
|
350
|
-
# Register the invites package for this portal
|
|
351
|
-
register_package Invites::Engine
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
### Entity-Scoped Invite Management
|
|
357
|
-
|
|
358
|
-
Invites are automatically filtered by the current entity — admins only see invites for their organization. This works because `Invites::UserInvite` has `belongs_to :entity`, which `associated_with` picks up.
|
|
359
|
-
|
|
360
|
-
> **For how entity scoping works end-to-end (model shapes, `default_relation_scope`, portal strategies), see the [plutonium-entity-scoping](../plutonium-entity-scoping/SKILL.md) skill. It is the single source of truth.**
|
|
361
|
-
|
|
362
|
-
## Troubleshooting
|
|
363
|
-
|
|
364
|
-
### Invite Not Found
|
|
365
|
-
|
|
366
|
-
- Check the token hasn't expired (default: 1 week)
|
|
367
|
-
- Verify the invite hasn't been cancelled
|
|
368
|
-
- Ensure the invite is still in `pending` state
|
|
369
|
-
|
|
370
|
-
### Email Mismatch Error
|
|
371
|
-
|
|
372
|
-
The system requires the accepting user's email to match the invited email:
|
|
373
|
-
|
|
374
|
-
```
|
|
375
|
-
"This invitation is for user@example.com. You must use an account with that email address."
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
To allow any email (not recommended for security):
|
|
379
|
-
|
|
380
|
-
```ruby
|
|
381
|
-
# In your UserInvite model
|
|
382
|
-
def enforce_email?
|
|
383
|
-
false
|
|
384
|
-
end
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### Rodauth Integration Issues
|
|
388
|
-
|
|
389
|
-
Ensure the Rodauth plugin is configured:
|
|
390
|
-
|
|
391
|
-
```ruby
|
|
392
|
-
# app/rodauth/user_rodauth_plugin.rb
|
|
393
|
-
configure do
|
|
394
|
-
login_return_to_requested_location? true
|
|
395
|
-
login_redirect "/welcome"
|
|
396
|
-
|
|
397
|
-
after_login do
|
|
398
|
-
session[:after_welcome_redirect] = session.delete(:login_redirect)
|
|
399
|
-
end
|
|
400
|
-
end
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
## Related Skills
|
|
404
|
-
|
|
405
|
-
- `plutonium-auth` - Authentication setup
|
|
406
|
-
- `plutonium-interaction` - Custom business logic
|
|
407
|
-
- `plutonium-portal` - Portal configuration
|
|
408
|
-
- `plutonium-policy` - Authorization for invite actions
|