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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-connect-resource/SKILL.md +19 -1
  3. data/.claude/skills/plutonium-controller/SKILL.md +5 -9
  4. data/.claude/skills/plutonium-definition-query/SKILL.md +10 -2
  5. data/.claude/skills/plutonium-installation/SKILL.md +9 -7
  6. data/.claude/skills/plutonium-invites/SKILL.md +363 -0
  7. data/.claude/skills/plutonium-package/SKILL.md +2 -1
  8. data/.claude/skills/plutonium-portal/SKILL.md +30 -16
  9. data/.claude/skills/plutonium-rodauth/SKILL.md +111 -18
  10. data/CHANGELOG.md +48 -0
  11. data/app/assets/plutonium.css +1 -1
  12. data/config/initializers/sqlite_alias.rb +8 -8
  13. data/docs/.vitepress/config.ts +1 -0
  14. data/docs/getting-started/tutorial/07-author-portal.md +1 -0
  15. data/docs/getting-started/tutorial/08-customizing-ui.md +5 -2
  16. data/docs/guides/adding-resources.md +10 -0
  17. data/docs/guides/authentication.md +15 -8
  18. data/docs/guides/creating-packages.md +13 -8
  19. data/docs/guides/index.md +2 -0
  20. data/docs/guides/search-filtering.md +8 -3
  21. data/docs/guides/user-invites.md +497 -0
  22. data/docs/public/templates/base.rb +5 -1
  23. data/docs/public/templates/lite.rb +42 -0
  24. data/docs/public/templates/pluton8.rb +7 -2
  25. data/docs/reference/controller/index.md +12 -7
  26. data/docs/reference/definition/query.md +12 -3
  27. data/docs/reference/generators/index.md +70 -10
  28. data/docs/reference/portal/index.md +22 -11
  29. data/gemfiles/rails_7.gemfile.lock +1 -1
  30. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  31. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  32. data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +31 -0
  33. data/lib/generators/pu/gem/active_shrine/templates/config/initializers/shrine.rb.tt +58 -0
  34. data/lib/generators/pu/gem/annotated/templates/lib/tasks/auto_annotate_models.rake +6 -1
  35. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -0
  36. data/lib/generators/pu/invites/USAGE +27 -0
  37. data/lib/generators/pu/invites/install_generator.rb +364 -0
  38. data/lib/generators/pu/invites/invitable/USAGE +31 -0
  39. data/lib/generators/pu/invites/invitable_generator.rb +143 -0
  40. data/lib/generators/pu/invites/templates/INSTRUCTIONS +22 -0
  41. data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +24 -0
  42. data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +26 -0
  43. data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +47 -0
  44. data/lib/generators/pu/invites/templates/invitable/invitation.html.erb.tt +45 -0
  45. data/lib/generators/pu/invites/templates/invitable/invitation.text.erb.tt +15 -0
  46. data/lib/generators/pu/invites/templates/invitable/invite_user_interaction.rb.tt +33 -0
  47. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +77 -0
  48. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +68 -0
  49. data/lib/generators/pu/invites/templates/packages/invites/app/definitions/invites/user_invite_definition.rb.tt +23 -0
  50. data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/cancel_invite_interaction.rb.tt +7 -0
  51. data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/resend_invite_interaction.rb.tt +7 -0
  52. data/lib/generators/pu/invites/templates/packages/invites/app/mailers/invites/user_invite_mailer.rb.tt +34 -0
  53. data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +41 -0
  54. data/lib/generators/pu/invites/templates/packages/invites/app/policies/invites/user_invite_policy.rb.tt +33 -0
  55. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/error.html.erb.tt +24 -0
  56. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/landing.html.erb.tt +40 -0
  57. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +39 -0
  58. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +49 -0
  59. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb.tt +45 -0
  60. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.text.erb.tt +15 -0
  61. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/welcome/pending_invitation.html.erb.tt +23 -0
  62. data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +33 -0
  63. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +23 -2
  64. data/lib/generators/pu/lib/plutonium_generators/concerns/configures_sqlite.rb +130 -0
  65. data/lib/generators/pu/lib/plutonium_generators/concerns/mounts_engines.rb +72 -0
  66. data/lib/generators/pu/lib/plutonium_generators/concerns/package_selector.rb +4 -2
  67. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +7 -1
  68. data/lib/generators/pu/lite/litestream/litestream_generator.rb +105 -0
  69. data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +88 -0
  70. data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +14 -0
  71. data/lib/generators/pu/lite/setup/setup_generator.rb +54 -0
  72. data/lib/generators/pu/lite/solid_cable/solid_cable_generator.rb +65 -0
  73. data/lib/generators/pu/lite/solid_cache/solid_cache_generator.rb +66 -0
  74. data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +61 -0
  75. data/lib/generators/pu/lite/solid_queue/solid_queue_generator.rb +107 -0
  76. data/lib/generators/pu/pkg/portal/USAGE +8 -2
  77. data/lib/generators/pu/pkg/portal/portal_generator.rb +11 -1
  78. data/lib/generators/pu/pkg/portal/templates/app/controllers/concerns/controller.rb.tt +2 -0
  79. data/lib/generators/pu/pkg/portal/templates/app/controllers/plutonium_controller.rb.tt +1 -0
  80. data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +7 -0
  81. data/lib/generators/pu/pkg/portal/templates/lib/engine.rb.tt +3 -0
  82. data/lib/generators/pu/res/conn/USAGE +5 -0
  83. data/lib/generators/pu/res/conn/conn_generator.rb +30 -4
  84. data/lib/generators/pu/res/scaffold/scaffold_generator.rb +6 -3
  85. data/lib/generators/pu/res/scaffold/templates/policy.rb.tt +6 -6
  86. data/lib/generators/pu/rodauth/account_generator.rb +36 -11
  87. data/lib/generators/pu/rodauth/admin_generator.rb +55 -0
  88. data/lib/generators/pu/rodauth/install_generator.rb +1 -8
  89. data/lib/generators/pu/rodauth/templates/app/interactions/invite_admin_interaction.rb.tt +25 -0
  90. data/lib/generators/pu/rodauth/templates/app/models/account.rb.tt +6 -2
  91. data/lib/generators/pu/saas/USAGE +22 -0
  92. data/lib/generators/pu/saas/entity/USAGE +19 -0
  93. data/lib/generators/pu/saas/entity_generator.rb +55 -0
  94. data/lib/generators/pu/saas/membership/USAGE +25 -0
  95. data/lib/generators/pu/saas/membership_generator.rb +165 -0
  96. data/lib/generators/pu/saas/setup/USAGE +27 -0
  97. data/lib/generators/pu/saas/setup_generator.rb +98 -0
  98. data/lib/generators/pu/saas/user/USAGE +21 -0
  99. data/lib/generators/pu/saas/user_generator.rb +66 -0
  100. data/lib/plutonium/core/controller.rb +9 -5
  101. data/lib/plutonium/definition/base.rb +3 -1
  102. data/lib/plutonium/definition/scoping.rb +20 -0
  103. data/lib/plutonium/invites/concerns/cancel_invite.rb +44 -0
  104. data/lib/plutonium/invites/concerns/invitable.rb +98 -0
  105. data/lib/plutonium/invites/concerns/invite_token.rb +186 -0
  106. data/lib/plutonium/invites/concerns/invite_user.rb +147 -0
  107. data/lib/plutonium/invites/concerns/resend_invite.rb +66 -0
  108. data/lib/plutonium/invites/controller.rb +226 -0
  109. data/lib/plutonium/invites/pending_invite_check.rb +76 -0
  110. data/lib/plutonium/invites.rb +6 -0
  111. data/lib/plutonium/resource/controllers/queryable.rb +4 -0
  112. data/lib/plutonium/resource/query_object.rb +3 -5
  113. data/lib/plutonium/version.rb +1 -1
  114. data/package.json +1 -1
  115. metadata +64 -7
  116. data/lib/generators/pu/res/entity/entity_generator.rb +0 -158
  117. data/lib/generators/pu/rodauth/customer_generator.rb +0 -101
  118. data/public/plutonium-assets/plutonium-logo-original.png +0 -0
  119. data/public/plutonium-assets/plutonium-logo-white.png +0 -0
  120. data/public/plutonium-assets/plutonium-logo.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1628c890b14f2fb0d7dd1c269d1acd8d96da310c28f990834b0fed1c33d56cc
4
- data.tar.gz: 9e181312f1785e4ee5ce1900f4a90fc990f520cecdbc67cb166ff9f8ef7b93fb
3
+ metadata.gz: ba93910895cf945960fdec47e9ba7026df83718d63e947a68ed4eb78ccd2ff4a
4
+ data.tar.gz: fbff28e4bfefc0fb86a85ef6e8c5d8272b2669a0048e7e9465c882bcfa1bb07c
5
5
  SHA512:
6
- metadata.gz: d7de09b2961f9e00b9bf6a5deeac78bfb31ed5f9a249b7ee4c98499da9713fbd6a14422193fee1c26fb20de7fadabbb7233ca7468af997680403bbe6a9ef02b5
7
- data.tar.gz: 3859c4fa95f90cfa13f2dfc2cd728ed7013eb5891deaaf65ff3fd90e54fd3d3d9c1336776b223972711111ebb75e7f47d62865a08c98953550ce39254ae2b807
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
- Each portal can have its own controller override. Portal controllers inherit from the feature package's controller:
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
- # packages/admin_portal/app/controllers/admin_portal/posts_controller.rb
311
+ # With feature package controller:
312
312
  class AdminPortal::PostsController < ::PostsController
313
313
  include AdminPortal::Concerns::Controller
314
+ end
314
315
 
315
- private
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, default: true # Applied by default
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, default: true
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
- # Customer with entity association
130
- rails generate pu:rodauth:customer customer
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-allow_signup` | Disable public signup |
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:rodauth:customer NAME` | Create customer with entity |
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
- │ │ └── plutonium_controller.rb
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
- ### Resource Controllers
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
- Portal resource controllers inherit from the feature package's controller:
253
+ ```ruby
254
+ # With feature package controller:
255
+ class DashboardPortal::PostsController < ::PostsController
256
+ include DashboardPortal::Concerns::Controller
257
+ end
250
258
 
251
- ```
252
- ::PostsController (feature package controller)
253
-
254
- DashboardPortal::PostsController (portal-specific)
259
+ # Without feature package controller:
260
+ class DashboardPortal::PostsController < DashboardPortal::ResourceController
261
+ end
255
262
  ```
256
263
 
257
- Controllers are auto-created if not defined. When accessing `DashboardPortal::PostsController`:
264
+ ### Portal ResourceController
258
265
 
259
- 1. If file exists, use it
260
- 2. Otherwise, dynamically create inheriting from `::PostsController`
261
- 3. Include `DashboardPortal::Concerns::Controller`
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
- class DashboardPortal::PostsController < ::PostsController
313
- include DashboardPortal::Concerns::Controller
314
-
315
- private
326
+ module DashboardPortal
327
+ class PostsController < ResourceController
328
+ private
316
329
 
317
- def preferred_action_after_submit
318
- "index"
330
+ def preferred_action_after_submit
331
+ "index"
332
+ end
319
333
  end
320
334
  end
321
335
  ```