plutonium 0.39.1 → 0.40.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-connect-resource/SKILL.md +19 -1
- data/.claude/skills/plutonium-controller/SKILL.md +5 -9
- data/.claude/skills/plutonium-definition-query/SKILL.md +10 -2
- data/.claude/skills/plutonium-installation/SKILL.md +9 -7
- data/.claude/skills/plutonium-invites/SKILL.md +363 -0
- data/.claude/skills/plutonium-package/SKILL.md +2 -1
- data/.claude/skills/plutonium-portal/SKILL.md +30 -16
- data/.claude/skills/plutonium-rodauth/SKILL.md +111 -18
- data/CHANGELOG.md +48 -0
- data/app/assets/plutonium.css +1 -1
- data/config/initializers/sqlite_alias.rb +8 -8
- data/docs/.vitepress/config.ts +1 -0
- data/docs/getting-started/tutorial/07-author-portal.md +1 -0
- data/docs/getting-started/tutorial/08-customizing-ui.md +5 -2
- data/docs/guides/adding-resources.md +10 -0
- data/docs/guides/authentication.md +15 -8
- data/docs/guides/creating-packages.md +13 -8
- data/docs/guides/index.md +2 -0
- data/docs/guides/search-filtering.md +8 -3
- data/docs/guides/user-invites.md +497 -0
- data/docs/public/templates/base.rb +5 -1
- data/docs/public/templates/lite.rb +42 -0
- data/docs/public/templates/pluton8.rb +7 -2
- data/docs/reference/controller/index.md +12 -7
- data/docs/reference/definition/query.md +12 -3
- data/docs/reference/generators/index.md +70 -10
- data/docs/reference/portal/index.md +22 -11
- 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/gem/active_shrine/active_shrine_generator.rb +31 -0
- data/lib/generators/pu/gem/active_shrine/templates/config/initializers/shrine.rb.tt +58 -0
- data/lib/generators/pu/gem/annotated/templates/lib/tasks/auto_annotate_models.rake +6 -1
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -0
- data/lib/generators/pu/invites/USAGE +27 -0
- data/lib/generators/pu/invites/install_generator.rb +364 -0
- data/lib/generators/pu/invites/invitable/USAGE +31 -0
- data/lib/generators/pu/invites/invitable_generator.rb +143 -0
- data/lib/generators/pu/invites/templates/INSTRUCTIONS +22 -0
- data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +24 -0
- data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +26 -0
- data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +47 -0
- data/lib/generators/pu/invites/templates/invitable/invitation.html.erb.tt +45 -0
- data/lib/generators/pu/invites/templates/invitable/invitation.text.erb.tt +15 -0
- data/lib/generators/pu/invites/templates/invitable/invite_user_interaction.rb.tt +33 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +77 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +68 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/definitions/invites/user_invite_definition.rb.tt +23 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/cancel_invite_interaction.rb.tt +7 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/resend_invite_interaction.rb.tt +7 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/mailers/invites/user_invite_mailer.rb.tt +34 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +41 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/policies/invites/user_invite_policy.rb.tt +33 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/error.html.erb.tt +24 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/landing.html.erb.tt +40 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +39 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +49 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb.tt +45 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.text.erb.tt +15 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/welcome/pending_invitation.html.erb.tt +23 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +33 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +23 -2
- data/lib/generators/pu/lib/plutonium_generators/concerns/configures_sqlite.rb +130 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/mounts_engines.rb +72 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/package_selector.rb +4 -2
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +7 -1
- data/lib/generators/pu/lite/litestream/litestream_generator.rb +105 -0
- data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +88 -0
- data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +14 -0
- data/lib/generators/pu/lite/setup/setup_generator.rb +54 -0
- data/lib/generators/pu/lite/solid_cable/solid_cable_generator.rb +65 -0
- data/lib/generators/pu/lite/solid_cache/solid_cache_generator.rb +66 -0
- data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +61 -0
- data/lib/generators/pu/lite/solid_queue/solid_queue_generator.rb +107 -0
- data/lib/generators/pu/pkg/portal/USAGE +8 -2
- data/lib/generators/pu/pkg/portal/portal_generator.rb +11 -1
- data/lib/generators/pu/pkg/portal/templates/app/controllers/concerns/controller.rb.tt +2 -0
- data/lib/generators/pu/pkg/portal/templates/app/controllers/plutonium_controller.rb.tt +1 -0
- data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +7 -0
- data/lib/generators/pu/pkg/portal/templates/lib/engine.rb.tt +3 -0
- data/lib/generators/pu/res/conn/USAGE +5 -0
- data/lib/generators/pu/res/conn/conn_generator.rb +30 -4
- data/lib/generators/pu/res/scaffold/scaffold_generator.rb +6 -3
- data/lib/generators/pu/res/scaffold/templates/policy.rb.tt +6 -6
- data/lib/generators/pu/rodauth/account_generator.rb +36 -11
- data/lib/generators/pu/rodauth/admin_generator.rb +55 -0
- data/lib/generators/pu/rodauth/install_generator.rb +1 -8
- data/lib/generators/pu/rodauth/templates/app/interactions/invite_admin_interaction.rb.tt +25 -0
- data/lib/generators/pu/rodauth/templates/app/models/account.rb.tt +6 -2
- data/lib/generators/pu/saas/USAGE +22 -0
- data/lib/generators/pu/saas/entity/USAGE +19 -0
- data/lib/generators/pu/saas/entity_generator.rb +55 -0
- data/lib/generators/pu/saas/membership/USAGE +25 -0
- data/lib/generators/pu/saas/membership_generator.rb +165 -0
- data/lib/generators/pu/saas/setup/USAGE +27 -0
- data/lib/generators/pu/saas/setup_generator.rb +98 -0
- data/lib/generators/pu/saas/user/USAGE +21 -0
- data/lib/generators/pu/saas/user_generator.rb +66 -0
- data/lib/plutonium/core/controller.rb +9 -5
- data/lib/plutonium/definition/base.rb +3 -1
- data/lib/plutonium/definition/scoping.rb +20 -0
- data/lib/plutonium/invites/concerns/cancel_invite.rb +44 -0
- data/lib/plutonium/invites/concerns/invitable.rb +98 -0
- data/lib/plutonium/invites/concerns/invite_token.rb +186 -0
- data/lib/plutonium/invites/concerns/invite_user.rb +147 -0
- data/lib/plutonium/invites/concerns/resend_invite.rb +66 -0
- data/lib/plutonium/invites/controller.rb +226 -0
- data/lib/plutonium/invites/pending_invite_check.rb +76 -0
- data/lib/plutonium/invites.rb +6 -0
- data/lib/plutonium/resource/controllers/queryable.rb +4 -0
- data/lib/plutonium/resource/query_object.rb +3 -5
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- metadata +64 -7
- data/lib/generators/pu/res/entity/entity_generator.rb +0 -158
- data/lib/generators/pu/rodauth/customer_generator.rb +0 -101
- data/public/plutonium-assets/plutonium-logo-original.png +0 -0
- data/public/plutonium-assets/plutonium-logo-white.png +0 -0
- data/public/plutonium-assets/plutonium-logo.png +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba93910895cf945960fdec47e9ba7026df83718d63e947a68ed4eb78ccd2ff4a
|
|
4
|
+
data.tar.gz: fbff28e4bfefc0fb86a85ef6e8c5d8272b2669a0048e7e9465c882bcfa1bb07c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c6bf1a8172e6898415f7643e9170e9ca8541980b49346e05a74264b8d19deb94ff3f8b42f5cd1c7a256aa3bb35f112ee542d687b20dfc361a770da5e77b6414
|
|
7
|
+
data.tar.gz: a70bdce9041d0e729a65f225c4211a97be8c4c97e378b9138079edcd3282a8a33cae73e2f230b9054f2041ba8dffe6035f32aab4e3ae87fb27ba7b8646cb781e
|
|
@@ -10,11 +10,18 @@ Use the `pu:res:conn` generator to connect resources to portals. This is require
|
|
|
10
10
|
## Command Syntax
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
rails g pu:res:conn RESOURCE [RESOURCE...] --dest=PORTAL_NAME
|
|
13
|
+
rails g pu:res:conn RESOURCE [RESOURCE...] --dest=PORTAL_NAME [--singular]
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
**Always specify resources directly** - this avoids interactive prompts. The `--src` option is only needed for interactive mode and can be ignored.
|
|
17
17
|
|
|
18
|
+
### Options
|
|
19
|
+
|
|
20
|
+
| Option | Description |
|
|
21
|
+
|--------|-------------|
|
|
22
|
+
| `--dest=NAME` | Target portal package (required) |
|
|
23
|
+
| `--singular` | Register as a singular resource (e.g., profile, dashboard) |
|
|
24
|
+
|
|
18
25
|
## Usage Patterns
|
|
19
26
|
|
|
20
27
|
### Main App Resources (not in a package)
|
|
@@ -39,6 +46,16 @@ rails g pu:res:conn Blogging::Post Blogging::Comment --dest=admin_portal
|
|
|
39
46
|
rails g pu:res:conn Property PropertyAmenity Unit Tenant --dest=admin_portal
|
|
40
47
|
```
|
|
41
48
|
|
|
49
|
+
### Singular Resources
|
|
50
|
+
|
|
51
|
+
For resources that represent a single record per user (e.g., profile, dashboard, settings):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
rails g pu:res:conn Profile --dest=customer_portal --singular
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This registers the resource with `singular: true`, generating routes like `/profile` instead of `/profiles/:id`.
|
|
58
|
+
|
|
42
59
|
## What Gets Generated
|
|
43
60
|
|
|
44
61
|
For a resource `Post` connected to `admin_portal`:
|
|
@@ -89,6 +106,7 @@ end
|
|
|
89
106
|
```ruby
|
|
90
107
|
# In packages/admin_portal/config/routes.rb
|
|
91
108
|
register_resource ::Post
|
|
109
|
+
register_resource ::Profile, singular: true # With --singular
|
|
92
110
|
```
|
|
93
111
|
|
|
94
112
|
## Typical Workflow
|
|
@@ -305,23 +305,19 @@ end
|
|
|
305
305
|
|
|
306
306
|
## Portal-Specific Controllers
|
|
307
307
|
|
|
308
|
-
|
|
308
|
+
Portal controllers inherit from the feature package's controller if one exists (and include the portal's `Concerns::Controller`). If no feature package controller exists, they inherit from the portal's `ResourceController`.
|
|
309
309
|
|
|
310
310
|
```ruby
|
|
311
|
-
#
|
|
311
|
+
# With feature package controller:
|
|
312
312
|
class AdminPortal::PostsController < ::PostsController
|
|
313
313
|
include AdminPortal::Concerns::Controller
|
|
314
|
+
end
|
|
314
315
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def preferred_action_after_submit
|
|
318
|
-
"index" # Admin prefers list view
|
|
319
|
-
end
|
|
316
|
+
# Without feature package controller:
|
|
317
|
+
class AdminPortal::PostsController < AdminPortal::ResourceController
|
|
320
318
|
end
|
|
321
319
|
```
|
|
322
320
|
|
|
323
|
-
Controllers are auto-created if not defined. When accessing a portal resource, Plutonium dynamically creates the controller by inheriting from the feature package's controller.
|
|
324
|
-
|
|
325
321
|
For non-resource portal pages (dashboard, settings), inherit from `PlutoniumController`:
|
|
326
322
|
|
|
327
323
|
```ruby
|
|
@@ -27,6 +27,9 @@ class PostDefinition < ResourceDefinition
|
|
|
27
27
|
scope :published
|
|
28
28
|
scope :draft
|
|
29
29
|
|
|
30
|
+
# Default scope
|
|
31
|
+
default_scope :published
|
|
32
|
+
|
|
30
33
|
# Sorting - sortable columns
|
|
31
34
|
sort :title
|
|
32
35
|
sort :created_at
|
|
@@ -246,9 +249,11 @@ Set a scope as default to apply it when no scope is explicitly selected:
|
|
|
246
249
|
|
|
247
250
|
```ruby
|
|
248
251
|
class PostDefinition < ResourceDefinition
|
|
249
|
-
scope :published
|
|
252
|
+
scope :published
|
|
250
253
|
scope :draft
|
|
251
254
|
scope :archived
|
|
255
|
+
|
|
256
|
+
default_scope :published # Applied by default
|
|
252
257
|
end
|
|
253
258
|
```
|
|
254
259
|
|
|
@@ -329,10 +334,13 @@ class ProductDefinition < ResourceDefinition
|
|
|
329
334
|
filter :category, with: :association
|
|
330
335
|
|
|
331
336
|
# Quick scopes
|
|
332
|
-
scope :active
|
|
337
|
+
scope :active
|
|
333
338
|
scope :featured
|
|
334
339
|
scope(:recent) { |scope| scope.where('created_at > ?', 1.week.ago) }
|
|
335
340
|
|
|
341
|
+
# Default scope
|
|
342
|
+
default_scope :active
|
|
343
|
+
|
|
336
344
|
# Sortable columns
|
|
337
345
|
sorts :name, :price, :created_at
|
|
338
346
|
|
|
@@ -124,10 +124,10 @@ rails generate pu:rodauth:install
|
|
|
124
124
|
rails generate pu:rodauth:account user
|
|
125
125
|
|
|
126
126
|
# Admin with 2FA, lockout, audit logging
|
|
127
|
-
rails generate pu:rodauth:admin
|
|
127
|
+
rails generate pu:rodauth:admin admin
|
|
128
128
|
|
|
129
|
-
#
|
|
130
|
-
rails generate pu:
|
|
129
|
+
# SaaS user with entity/organization (multi-tenant)
|
|
130
|
+
rails generate pu:saas:setup --user Customer --entity Organization
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
### Account Options
|
|
@@ -136,8 +136,7 @@ rails generate pu:rodauth:customer customer
|
|
|
136
136
|
|--------|-------------|
|
|
137
137
|
| `--defaults` | Enable common features (login, logout, remember, reset_password) |
|
|
138
138
|
| `--kitchen_sink` | Enable all available features |
|
|
139
|
-
| `--no-
|
|
140
|
-
| `--entity=Organization` | Create associated entity model |
|
|
139
|
+
| `--no-allow-signup` | Disable public signup |
|
|
141
140
|
|
|
142
141
|
### Connect Auth to Controllers
|
|
143
142
|
|
|
@@ -279,8 +278,11 @@ For models that already exist in your app:
|
|
|
279
278
|
| `pu:core:install` | Initial Plutonium setup |
|
|
280
279
|
| `pu:rodauth:install` | Setup Rodauth authentication |
|
|
281
280
|
| `pu:rodauth:account NAME` | Create user account type |
|
|
282
|
-
| `pu:rodauth:admin` | Create admin account with 2FA |
|
|
283
|
-
| `pu:
|
|
281
|
+
| `pu:rodauth:admin NAME` | Create admin account with 2FA |
|
|
282
|
+
| `pu:saas:setup` | Create SaaS user + entity + membership |
|
|
283
|
+
| `pu:saas:user NAME` | Create SaaS user account |
|
|
284
|
+
| `pu:saas:entity NAME` | Create entity model |
|
|
285
|
+
| `pu:saas:membership` | Create membership join table |
|
|
284
286
|
| `pu:pkg:package NAME` | Create feature package |
|
|
285
287
|
| `pu:pkg:portal NAME` | Create portal package |
|
|
286
288
|
| `pu:res:scaffold NAME` | Create resource (model, policy, definition, controller) |
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plutonium-invites
|
|
3
|
+
description: Plutonium user invites - invitation system for multi-tenant apps with entity memberships
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Plutonium User Invites
|
|
7
|
+
|
|
8
|
+
Plutonium provides a complete user invitation system for multi-tenant applications. The system handles:
|
|
9
|
+
- Sending email invitations to new users
|
|
10
|
+
- Token-based invite acceptance flow
|
|
11
|
+
- Integration with Rodauth authentication
|
|
12
|
+
- Entity membership creation on acceptance
|
|
13
|
+
- Support for invitable models that get notified when invites are accepted
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### Prerequisites
|
|
18
|
+
|
|
19
|
+
Before installing invites, ensure you have:
|
|
20
|
+
1. A user model with Rodauth authentication
|
|
21
|
+
2. An entity model (Organization, Company, Team, etc.)
|
|
22
|
+
3. A membership model linking users to entities
|
|
23
|
+
|
|
24
|
+
Use `pu:saas:setup` to generate all three:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
rails g pu:saas:setup --user Customer --entity Organization
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Install the Invites Package
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
rails generate pu:invites:install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Options:**
|
|
37
|
+
|
|
38
|
+
| Option | Default | Description |
|
|
39
|
+
|--------|---------|-------------|
|
|
40
|
+
| `--entity-model=NAME` | Entity | Entity model name for scoping |
|
|
41
|
+
| `--user-model=NAME` | User | User model name |
|
|
42
|
+
| `--membership-model=NAME` | EntityUser | Membership join model |
|
|
43
|
+
| `--roles=ROLES` | member,admin | Comma-separated roles |
|
|
44
|
+
| `--rodauth=NAME` | user | Rodauth configuration for signup |
|
|
45
|
+
| `--enforce-domain` | false | Require email domain to match entity |
|
|
46
|
+
|
|
47
|
+
**Example with custom models:**
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
rails g pu:invites:install \
|
|
51
|
+
--entity-model=Organization \
|
|
52
|
+
--user-model=Customer \
|
|
53
|
+
--membership-model=OrganizationMember \
|
|
54
|
+
--roles=member,manager,admin
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### What Gets Created
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
packages/invites/
|
|
61
|
+
├── app/
|
|
62
|
+
│ ├── controllers/invites/
|
|
63
|
+
│ │ ├── user_invitations_controller.rb
|
|
64
|
+
│ │ └── welcome_controller.rb
|
|
65
|
+
│ ├── definitions/invites/
|
|
66
|
+
│ │ └── user_invite_definition.rb
|
|
67
|
+
│ ├── interactions/invites/
|
|
68
|
+
│ │ ├── cancel_invite_interaction.rb
|
|
69
|
+
│ │ └── resend_invite_interaction.rb
|
|
70
|
+
│ ├── mailers/invites/
|
|
71
|
+
│ │ └── user_invite_mailer.rb
|
|
72
|
+
│ ├── models/invites/
|
|
73
|
+
│ │ └── user_invite.rb
|
|
74
|
+
│ ├── policies/invites/
|
|
75
|
+
│ │ └── user_invite_policy.rb
|
|
76
|
+
│ └── views/invites/
|
|
77
|
+
│ ├── user_invitations/
|
|
78
|
+
│ │ ├── error.html.erb
|
|
79
|
+
│ │ ├── landing.html.erb
|
|
80
|
+
│ │ ├── show.html.erb
|
|
81
|
+
│ │ └── signup.html.erb
|
|
82
|
+
│ ├── user_invite_mailer/
|
|
83
|
+
│ │ ├── invitation.html.erb
|
|
84
|
+
│ │ └── invitation.text.erb
|
|
85
|
+
│ └── welcome/
|
|
86
|
+
│ └── pending_invitation.html.erb
|
|
87
|
+
|
|
88
|
+
app/interactions/
|
|
89
|
+
├── entity/
|
|
90
|
+
│ └── invite_user_interaction.rb
|
|
91
|
+
└── user/
|
|
92
|
+
└── invite_user_interaction.rb
|
|
93
|
+
|
|
94
|
+
db/migrate/
|
|
95
|
+
└── TIMESTAMP_create_user_invites.rb
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Routes Added
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# Public invitation routes (unauthenticated)
|
|
102
|
+
get "welcome", to: "invites/welcome#index"
|
|
103
|
+
get "invitations/:token", to: "invites/user_invitations#show"
|
|
104
|
+
post "invitations/:token/accept", to: "invites/user_invitations#accept"
|
|
105
|
+
get "invitations/:token/signup", to: "invites/user_invitations#signup"
|
|
106
|
+
post "invitations/:token/signup", to: "invites/user_invitations#signup"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Connecting Invitables
|
|
110
|
+
|
|
111
|
+
Invitables are models that trigger invitations and get notified when they're accepted. Common examples:
|
|
112
|
+
- `Tenant` - A tenant record that needs a user assigned
|
|
113
|
+
- `TeamMember` - A membership record created by admin, waiting for user signup
|
|
114
|
+
- `ProjectCollaborator` - A project role waiting for user acceptance
|
|
115
|
+
|
|
116
|
+
### Generate an Invitable
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
rails generate pu:invites:invitable Tenant
|
|
120
|
+
rails generate pu:invites:invitable TeamMember --role=member
|
|
121
|
+
rails generate pu:invites:invitable Tenant --dest=my_package
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Options:**
|
|
125
|
+
|
|
126
|
+
| Option | Default | Description |
|
|
127
|
+
|--------|---------|-------------|
|
|
128
|
+
| `--role=ROLE` | member | Role to assign to invited users |
|
|
129
|
+
| `--user-model=NAME` | User | User model name |
|
|
130
|
+
| `--membership-model=NAME` | EntityUser | Membership model |
|
|
131
|
+
| `--dest=PACKAGE` | main_app | Destination package |
|
|
132
|
+
| `--[no-]email-templates` | true | Generate custom email templates |
|
|
133
|
+
|
|
134
|
+
### Implement the Callback
|
|
135
|
+
|
|
136
|
+
After generation, implement `on_invite_accepted` in your invitable model:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# app/models/tenant.rb
|
|
140
|
+
class Tenant < ApplicationRecord
|
|
141
|
+
include Plutonium::Invites::Concerns::Invitable
|
|
142
|
+
|
|
143
|
+
belongs_to :entity
|
|
144
|
+
belongs_to :user, optional: true
|
|
145
|
+
|
|
146
|
+
def on_invite_accepted(user)
|
|
147
|
+
update!(user: user, status: :active)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## How the Flow Works
|
|
153
|
+
|
|
154
|
+
### 1. Admin Sends Invite
|
|
155
|
+
|
|
156
|
+
An admin uses the "Invite User" action on an entity or invitable:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# From entity context
|
|
160
|
+
entity.invite_user(email: "user@example.com", role: :member)
|
|
161
|
+
|
|
162
|
+
# From invitable context (e.g., Tenant)
|
|
163
|
+
tenant.invite_user(email: "user@example.com")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 2. Email Sent
|
|
167
|
+
|
|
168
|
+
The system sends an email with a secure invitation link:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
Subject: You've been invited to join Acme Corp
|
|
172
|
+
|
|
173
|
+
Click here to accept: https://app.example.com/invitations/abc123...
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 3. User Accepts Invite
|
|
177
|
+
|
|
178
|
+
**Existing User Flow:**
|
|
179
|
+
1. User clicks invite link
|
|
180
|
+
2. User logs in (or is already logged in)
|
|
181
|
+
3. System validates email matches
|
|
182
|
+
4. Membership created, invitable notified
|
|
183
|
+
|
|
184
|
+
**New User Flow:**
|
|
185
|
+
1. User clicks invite link
|
|
186
|
+
2. User clicks "Create Account"
|
|
187
|
+
3. User signs up with the invited email
|
|
188
|
+
4. System validates email matches
|
|
189
|
+
5. Membership created, invitable notified
|
|
190
|
+
|
|
191
|
+
### 4. Pending Invite Check
|
|
192
|
+
|
|
193
|
+
After login, users are redirected to `/welcome` where pending invites are shown:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# In your controller
|
|
197
|
+
include Plutonium::Invites::PendingInviteCheck
|
|
198
|
+
|
|
199
|
+
# Automatically shows pending invites after login
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## UserInvite Model
|
|
203
|
+
|
|
204
|
+
The generated `Invites::UserInvite` model includes:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
class Invites::UserInvite < Invites::ResourceRecord
|
|
208
|
+
include Plutonium::Invites::Concerns::InviteToken
|
|
209
|
+
|
|
210
|
+
# Associations
|
|
211
|
+
belongs_to :entity
|
|
212
|
+
belongs_to :invited_by, polymorphic: true
|
|
213
|
+
belongs_to :user, optional: true
|
|
214
|
+
belongs_to :invitable, polymorphic: true, optional: true
|
|
215
|
+
|
|
216
|
+
# States: pending, accepted, expired, cancelled
|
|
217
|
+
enum :state, pending: 0, accepted: 1, expired: 2, cancelled: 3
|
|
218
|
+
|
|
219
|
+
# Roles
|
|
220
|
+
enum :role, member: 0, admin: 1
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Key Methods
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# Find valid invite for acceptance
|
|
228
|
+
invite = Invites::UserInvite.find_for_acceptance(token)
|
|
229
|
+
|
|
230
|
+
# Accept for a user
|
|
231
|
+
invite.accept_for_user!(current_user)
|
|
232
|
+
|
|
233
|
+
# Resend invitation email
|
|
234
|
+
invite.resend!
|
|
235
|
+
|
|
236
|
+
# Cancel invitation
|
|
237
|
+
invite.cancel!
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Customization
|
|
241
|
+
|
|
242
|
+
### Custom Email Templates
|
|
243
|
+
|
|
244
|
+
Override templates in your package:
|
|
245
|
+
|
|
246
|
+
```erb
|
|
247
|
+
<%# packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb %>
|
|
248
|
+
<h1>Welcome to <%= @invite.entity.name %>!</h1>
|
|
249
|
+
<p><%= @invite.invited_by.email %> has invited you to join.</p>
|
|
250
|
+
<p><%= link_to "Accept Invitation", @invitation_url %></p>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Custom Validation
|
|
254
|
+
|
|
255
|
+
Extend the invite model:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
# packages/invites/app/models/invites/user_invite.rb
|
|
259
|
+
class Invites::UserInvite < Invites::ResourceRecord
|
|
260
|
+
validate :email_not_already_member
|
|
261
|
+
|
|
262
|
+
private
|
|
263
|
+
|
|
264
|
+
def email_not_already_member
|
|
265
|
+
existing = membership_model.joins(:user)
|
|
266
|
+
.where(entity: entity, users: { email: email })
|
|
267
|
+
.exists?
|
|
268
|
+
|
|
269
|
+
errors.add(:email, "is already a member") if existing
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Domain Enforcement
|
|
275
|
+
|
|
276
|
+
Enable domain matching in the install:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
rails g pu:invites:install --enforce-domain
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
This requires the invited email domain to match the entity's domain.
|
|
283
|
+
|
|
284
|
+
### Custom Roles
|
|
285
|
+
|
|
286
|
+
Specify roles during install:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
rails g pu:invites:install --roles=viewer,editor,admin,owner
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Integration with Portals
|
|
293
|
+
|
|
294
|
+
### Connect Invites to Your Portal
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
# packages/customer_portal/lib/engine.rb
|
|
298
|
+
module CustomerPortal
|
|
299
|
+
class Engine < Rails::Engine
|
|
300
|
+
include Plutonium::Portal::Engine
|
|
301
|
+
|
|
302
|
+
# Register the invites package for this portal
|
|
303
|
+
register_package Invites::Engine
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Entity-Scoped Invite Management
|
|
309
|
+
|
|
310
|
+
The `Invites::UserInvite` definition automatically scopes to the current entity:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
# In your portal, invites are automatically filtered by entity_scope
|
|
314
|
+
# Admins only see invites for their organization
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Troubleshooting
|
|
318
|
+
|
|
319
|
+
### Invite Not Found
|
|
320
|
+
|
|
321
|
+
- Check the token hasn't expired (default: 1 week)
|
|
322
|
+
- Verify the invite hasn't been cancelled
|
|
323
|
+
- Ensure the invite is still in `pending` state
|
|
324
|
+
|
|
325
|
+
### Email Mismatch Error
|
|
326
|
+
|
|
327
|
+
The system requires the accepting user's email to match the invited email:
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
"This invitation is for user@example.com. You must use an account with that email address."
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
To allow any email (not recommended for security):
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
# In your UserInvite model
|
|
337
|
+
def enforce_email?
|
|
338
|
+
false
|
|
339
|
+
end
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Rodauth Integration Issues
|
|
343
|
+
|
|
344
|
+
Ensure the Rodauth plugin is configured:
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
# app/rodauth/user_rodauth_plugin.rb
|
|
348
|
+
configure do
|
|
349
|
+
login_return_to_requested_location? true
|
|
350
|
+
login_redirect "/welcome"
|
|
351
|
+
|
|
352
|
+
after_login do
|
|
353
|
+
session[:after_welcome_redirect] = session.delete(:login_redirect)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Related Skills
|
|
359
|
+
|
|
360
|
+
- `plutonium-rodauth` - Authentication setup
|
|
361
|
+
- `plutonium-interaction` - Custom business logic
|
|
362
|
+
- `plutonium-portal` - Portal configuration
|
|
363
|
+
- `plutonium-policy` - Authorization for invite actions
|
|
@@ -74,7 +74,8 @@ packages/admin_portal/
|
|
|
74
74
|
│ ├── controllers/admin_portal/
|
|
75
75
|
│ │ ├── concerns/controller.rb
|
|
76
76
|
│ │ ├── dashboard_controller.rb
|
|
77
|
-
│ │
|
|
77
|
+
│ │ ├── plutonium_controller.rb
|
|
78
|
+
│ │ └── resource_controller.rb
|
|
78
79
|
│ ├── definitions/admin_portal/ # Portal-specific overrides
|
|
79
80
|
│ ├── policies/admin_portal/ # Portal-specific overrides
|
|
80
81
|
│ └── views/
|
|
@@ -20,12 +20,16 @@ rails g pu:pkg:portal dashboard
|
|
|
20
20
|
| `--auth=NAME` | Rodauth account to authenticate with (e.g., `--auth=user`) |
|
|
21
21
|
| `--public` | Grant public access (no authentication) |
|
|
22
22
|
| `--byo` | Bring your own authentication |
|
|
23
|
+
| `--scope=CLASS` | Entity class to scope to for multi-tenancy (e.g., `--scope=Organization`) |
|
|
23
24
|
|
|
24
25
|
```bash
|
|
25
26
|
# Non-interactive examples
|
|
26
27
|
rails g pu:pkg:portal admin --auth=admin
|
|
27
28
|
rails g pu:pkg:portal api --public
|
|
28
29
|
rails g pu:pkg:portal custom --byo
|
|
30
|
+
|
|
31
|
+
# With entity scoping (multi-tenancy)
|
|
32
|
+
rails g pu:pkg:portal admin --auth=admin --scope=Organization
|
|
29
33
|
```
|
|
30
34
|
|
|
31
35
|
Without flags, the generator prompts interactively:
|
|
@@ -244,21 +248,31 @@ end
|
|
|
244
248
|
|
|
245
249
|
## Controller Hierarchy
|
|
246
250
|
|
|
247
|
-
|
|
251
|
+
Portal controllers inherit from the feature package's controller if one exists (and include the portal's `Concerns::Controller`). If no feature package controller exists, they inherit from the portal's `ResourceController`.
|
|
248
252
|
|
|
249
|
-
|
|
253
|
+
```ruby
|
|
254
|
+
# With feature package controller:
|
|
255
|
+
class DashboardPortal::PostsController < ::PostsController
|
|
256
|
+
include DashboardPortal::Concerns::Controller
|
|
257
|
+
end
|
|
250
258
|
|
|
251
|
-
|
|
252
|
-
::PostsController
|
|
253
|
-
|
|
254
|
-
DashboardPortal::PostsController (portal-specific)
|
|
259
|
+
# Without feature package controller:
|
|
260
|
+
class DashboardPortal::PostsController < DashboardPortal::ResourceController
|
|
261
|
+
end
|
|
255
262
|
```
|
|
256
263
|
|
|
257
|
-
|
|
264
|
+
### Portal ResourceController
|
|
258
265
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
266
|
+
The portal's `ResourceController` serves as the base class for resource controllers when no feature package controller exists. It includes the portal's `Concerns::Controller` so individual resource controllers don't need to.
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
# packages/dashboard_portal/app/controllers/dashboard_portal/resource_controller.rb
|
|
270
|
+
module DashboardPortal
|
|
271
|
+
class ResourceController < ::ResourceController
|
|
272
|
+
include DashboardPortal::Concerns::Controller
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
```
|
|
262
276
|
|
|
263
277
|
### Non-Resource Controllers
|
|
264
278
|
|
|
@@ -309,13 +323,13 @@ end
|
|
|
309
323
|
|
|
310
324
|
```ruby
|
|
311
325
|
# packages/dashboard_portal/app/controllers/dashboard_portal/posts_controller.rb
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
private
|
|
326
|
+
module DashboardPortal
|
|
327
|
+
class PostsController < ResourceController
|
|
328
|
+
private
|
|
316
329
|
|
|
317
|
-
|
|
318
|
-
|
|
330
|
+
def preferred_action_after_submit
|
|
331
|
+
"index"
|
|
332
|
+
end
|
|
319
333
|
end
|
|
320
334
|
end
|
|
321
335
|
```
|