plutonium 0.39.2 → 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 (119) 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 +43 -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/definition/base.rb +3 -1
  101. data/lib/plutonium/definition/scoping.rb +20 -0
  102. data/lib/plutonium/invites/concerns/cancel_invite.rb +44 -0
  103. data/lib/plutonium/invites/concerns/invitable.rb +98 -0
  104. data/lib/plutonium/invites/concerns/invite_token.rb +186 -0
  105. data/lib/plutonium/invites/concerns/invite_user.rb +147 -0
  106. data/lib/plutonium/invites/concerns/resend_invite.rb +66 -0
  107. data/lib/plutonium/invites/controller.rb +226 -0
  108. data/lib/plutonium/invites/pending_invite_check.rb +76 -0
  109. data/lib/plutonium/invites.rb +6 -0
  110. data/lib/plutonium/resource/controllers/queryable.rb +4 -0
  111. data/lib/plutonium/resource/query_object.rb +3 -5
  112. data/lib/plutonium/version.rb +1 -1
  113. data/package.json +1 -1
  114. metadata +64 -7
  115. data/lib/generators/pu/res/entity/entity_generator.rb +0 -158
  116. data/lib/generators/pu/rodauth/customer_generator.rb +0 -101
  117. data/public/plutonium-assets/plutonium-logo-original.png +0 -0
  118. data/public/plutonium-assets/plutonium-logo-white.png +0 -0
  119. data/public/plutonium-assets/plutonium-logo.png +0 -0
@@ -254,9 +254,11 @@ Set a scope as default:
254
254
 
255
255
  ```ruby
256
256
  class PostDefinition < ResourceDefinition
257
- scope :published, default: true # Applied by default
257
+ scope :published
258
258
  scope :draft
259
259
  scope :archived
260
+
261
+ default_scope :published
260
262
  end
261
263
  ```
262
264
 
@@ -327,14 +329,17 @@ class ProductDefinition < ResourceDefinition
327
329
  filter :category, with: :association
328
330
 
329
331
  # Quick scopes (reference model scopes)
330
- scope :active, default: true
332
+ scope :active
331
333
  scope :featured
332
334
  scope(:recent) { |scope| scope.where("created_at > ?", 1.week.ago) }
333
335
 
336
+ # Default scope
337
+ default_scope :active
338
+
334
339
  # Sortable columns
335
340
  sorts :name, :price, :created_at
336
341
 
337
- # Default: newest first
342
+ # Default sort: newest first
338
343
  default_sort :created_at, :desc
339
344
  end
340
345
  ```
@@ -0,0 +1,497 @@
1
+ # User Invites
2
+
3
+ Plutonium provides a complete user invitation system for multi-tenant applications. This guide covers setting up invitations, customizing the flow, and integrating with your portals.
4
+
5
+ ## Overview
6
+
7
+ The invitation system handles:
8
+ - **Email Invitations**: Send secure invitation links to new or existing users
9
+ - **Token Validation**: Time-limited tokens with automatic expiration
10
+ - **Rodauth Integration**: Seamless signup and login flows
11
+ - **Entity Memberships**: Automatic membership creation on acceptance
12
+ - **Invitable Models**: Notify models when their invitations are accepted
13
+
14
+ ## Prerequisites
15
+
16
+ Before installing invites, ensure you have:
17
+
18
+ 1. **User Authentication**: A Rodauth user account
19
+ 2. **Entity Model**: An organization/company/team model
20
+ 3. **Membership Model**: A join model linking users to entities
21
+
22
+ The easiest way to set this up is with the SaaS generator:
23
+ ```bash
24
+ rails g pu:saas:setup --user User --entity Organization
25
+ ```
26
+
27
+ ## Installation
28
+
29
+ ### Step 1: Install the Invites Package
30
+
31
+ ```bash
32
+ rails generate pu:invites:install
33
+ ```
34
+
35
+ With custom models:
36
+
37
+ ```bash
38
+ rails g pu:invites:install \
39
+ --entity-model=Organization \
40
+ --user-model=User \
41
+ --membership-model=OrganizationUser \
42
+ --roles=member,manager,admin
43
+ ```
44
+
45
+ ### Options
46
+
47
+ | Option | Default | Description |
48
+ |--------|---------|-------------|
49
+ | `--entity-model` | Entity | Entity model for scoping invites |
50
+ | `--user-model` | User | User account model |
51
+ | `--membership-model` | EntityUser | Join model for memberships |
52
+ | `--roles` | member,admin | Available invitation roles |
53
+ | `--rodauth` | user | Rodauth configuration name |
54
+ | `--enforce-domain` | false | Require email domain matching |
55
+
56
+ ### Step 2: Run Migrations
57
+
58
+ ```bash
59
+ rails db:migrate
60
+ ```
61
+
62
+ ### Step 3: Configure Your Portal
63
+
64
+ Register the invites package in your portal:
65
+
66
+ ```ruby
67
+ # packages/customer_portal/lib/engine.rb
68
+ module CustomerPortal
69
+ class Engine < Rails::Engine
70
+ include Plutonium::Portal::Engine
71
+
72
+ register_package Invites::Engine
73
+ end
74
+ end
75
+ ```
76
+
77
+ ## Generated Files
78
+
79
+ The generator creates a complete `packages/invites/` package:
80
+
81
+ ```
82
+ packages/invites/
83
+ ├── app/
84
+ │ ├── controllers/invites/
85
+ │ │ ├── user_invitations_controller.rb # Invitation acceptance
86
+ │ │ └── welcome_controller.rb # Post-login landing
87
+ │ ├── definitions/invites/
88
+ │ │ └── user_invite_definition.rb # UI configuration
89
+ │ ├── interactions/invites/
90
+ │ │ ├── cancel_invite_interaction.rb # Cancel action
91
+ │ │ └── resend_invite_interaction.rb # Resend action
92
+ │ ├── mailers/invites/
93
+ │ │ └── user_invite_mailer.rb # Invitation emails
94
+ │ ├── models/invites/
95
+ │ │ └── user_invite.rb # Invite model
96
+ │ ├── policies/invites/
97
+ │ │ └── user_invite_policy.rb # Authorization
98
+ │ └── views/invites/
99
+ │ ├── user_invitations/ # Acceptance views
100
+ │ ├── user_invite_mailer/ # Email templates
101
+ │ └── welcome/ # Welcome page
102
+ └── lib/
103
+ └── engine.rb # Package engine
104
+ ```
105
+
106
+ ## Invitation Flow
107
+
108
+ ### Sending Invitations
109
+
110
+ Admins can invite users from the entity detail page or user management:
111
+
112
+ ```ruby
113
+ # The generated action in your entity definition
114
+ action :invite_user,
115
+ interaction: Organization::InviteUserInteraction,
116
+ category: :secondary
117
+ ```
118
+
119
+ The interaction creates an `Invites::UserInvite` record and sends an email:
120
+
121
+ ```ruby
122
+ # Generated interaction
123
+ class Organization::InviteUserInteraction < Plutonium::Interaction::Base
124
+ attribute :email, :string
125
+ attribute :role, :string, default: "member"
126
+
127
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
128
+
129
+ def execute
130
+ invite = Invites::UserInvite.create!(
131
+ entity: resource,
132
+ email: email,
133
+ role: role,
134
+ invited_by: current_user
135
+ )
136
+
137
+ succeed(invite)
138
+ .with_message("Invitation sent to #{email}")
139
+ end
140
+ end
141
+ ```
142
+
143
+ ### Accepting Invitations
144
+
145
+ #### Existing Users
146
+
147
+ 1. User receives email with invitation link
148
+ 2. Clicks link, sees invitation details
149
+ 3. If logged in with matching email, accepts directly
150
+ 4. If not logged in, redirected to login
151
+ 5. After login, redirected back to accept
152
+
153
+ #### New Users
154
+
155
+ 1. User receives email with invitation link
156
+ 2. Clicks link, sees invitation details
157
+ 3. Clicks "Create Account"
158
+ 4. Signs up with the invited email address
159
+ 5. After signup, automatically accepts invitation
160
+
161
+ ### Post-Login Welcome
162
+
163
+ After login, users land on `/welcome` where pending invitations are displayed:
164
+
165
+ ```ruby
166
+ # The WelcomeController checks for pending invites
167
+ class Invites::WelcomeController < ApplicationController
168
+ def index
169
+ @pending_invites = Invites::UserInvite
170
+ .pending
171
+ .where(email: current_user.email)
172
+
173
+ if @pending_invites.any?
174
+ render :pending_invitation
175
+ else
176
+ redirect_to session.delete(:after_welcome_redirect) || root_path
177
+ end
178
+ end
179
+ end
180
+ ```
181
+
182
+ ## Invitables
183
+
184
+ Invitables are models that trigger invitations and receive callbacks when accepted. Use this when you need to:
185
+ - Create a record that requires a user to be assigned
186
+ - Notify specific models when their invitation is accepted
187
+ - Customize invitation behavior per model type
188
+
189
+ ### Creating an Invitable
190
+
191
+ ```bash
192
+ rails g pu:invites:invitable Tenant
193
+ rails g pu:invites:invitable TeamMember --role=member
194
+ ```
195
+
196
+ ### Implementing the Callback
197
+
198
+ ```ruby
199
+ # app/models/tenant.rb
200
+ class Tenant < ApplicationRecord
201
+ include Plutonium::Invites::Concerns::Invitable
202
+
203
+ belongs_to :organization
204
+ belongs_to :user, optional: true
205
+
206
+ # Called when the invitation is accepted
207
+ def on_invite_accepted(user)
208
+ update!(
209
+ user: user,
210
+ status: :active,
211
+ activated_at: Time.current
212
+ )
213
+ end
214
+ end
215
+ ```
216
+
217
+ ### How Invitables Work
218
+
219
+ When creating an invite from an invitable:
220
+
221
+ ```ruby
222
+ # The invitable triggers the invitation
223
+ tenant.invite_user(email: "user@example.com")
224
+
225
+ # Creates UserInvite with:
226
+ # - invitable_type: "Tenant"
227
+ # - invitable_id: tenant.id
228
+ ```
229
+
230
+ When the invite is accepted:
231
+
232
+ ```ruby
233
+ # System calls:
234
+ invite.accept_for_user!(user)
235
+
236
+ # Which internally:
237
+ # 1. Creates entity membership
238
+ # 2. Calls tenant.on_invite_accepted(user)
239
+ ```
240
+
241
+ ## Customization
242
+
243
+ ### Custom Email Templates
244
+
245
+ Override the default templates:
246
+
247
+ ```erb
248
+ <%# packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb %>
249
+ <!DOCTYPE html>
250
+ <html>
251
+ <body>
252
+ <h1>Welcome to <%= @invite.entity.name %>!</h1>
253
+
254
+ <p>
255
+ <%= @invite.invited_by.email %> has invited you to join
256
+ as a <%= @invite.role %>.
257
+ </p>
258
+
259
+ <p>
260
+ <%= link_to "Accept Invitation", @invitation_url,
261
+ style: "background: #4F46E5; color: white; padding: 12px 24px;" %>
262
+ </p>
263
+
264
+ <p>This invitation expires in 7 days.</p>
265
+ </body>
266
+ </html>
267
+ ```
268
+
269
+ ### Per-Invitable Templates
270
+
271
+ Create model-specific email templates:
272
+
273
+ ```erb
274
+ <%# packages/invites/app/views/invites/user_invite_mailer/invitation_tenant.html.erb %>
275
+ <h1>You've been assigned as a tenant!</h1>
276
+ <p>Accept to access your tenant dashboard.</p>
277
+ ```
278
+
279
+ ### Custom Validation
280
+
281
+ Add validation to the invite model:
282
+
283
+ ```ruby
284
+ # packages/invites/app/models/invites/user_invite.rb
285
+ class Invites::UserInvite < Invites::ResourceRecord
286
+ validate :email_not_already_member
287
+ validate :within_invite_limit
288
+
289
+ private
290
+
291
+ def email_not_already_member
292
+ if entity.users.exists?(email: email)
293
+ errors.add(:email, "is already a member of this organization")
294
+ end
295
+ end
296
+
297
+ def within_invite_limit
298
+ pending_count = entity.user_invites.pending.count
299
+ if pending_count >= 100
300
+ errors.add(:base, "Maximum pending invitations reached")
301
+ end
302
+ end
303
+ end
304
+ ```
305
+
306
+ ### Domain Enforcement
307
+
308
+ Require invited emails to match the entity's domain:
309
+
310
+ ```bash
311
+ rails g pu:invites:install --enforce-domain
312
+ ```
313
+
314
+ Or implement custom domain logic:
315
+
316
+ ```ruby
317
+ # packages/invites/app/models/invites/user_invite.rb
318
+ def enforce_domain
319
+ entity.domain # e.g., "acme.com"
320
+ end
321
+ ```
322
+
323
+ ### Custom Expiration
324
+
325
+ Change the default expiration time:
326
+
327
+ ```ruby
328
+ # packages/invites/app/models/invites/user_invite.rb
329
+ private
330
+
331
+ def set_token_defaults
332
+ self.token ||= SecureRandom.urlsafe_base64(32)
333
+ self.expires_at ||= 3.days.from_now # Override default 1 week
334
+ end
335
+ ```
336
+
337
+ ## Managing Invitations
338
+
339
+ ### Resend Invitation
340
+
341
+ The generated `ResendInviteInteraction` allows resending:
342
+
343
+ ```ruby
344
+ # Resets expiration and sends new email
345
+ invite.resend!
346
+ ```
347
+
348
+ ### Cancel Invitation
349
+
350
+ ```ruby
351
+ invite.cancel!
352
+ # Sets state to :cancelled
353
+ ```
354
+
355
+ ### View Pending Invitations
356
+
357
+ In your admin portal:
358
+
359
+ ```ruby
360
+ # Invites are scoped to the current entity
361
+ # Admins see all pending invites for their organization
362
+ Invites::UserInvite.pending.where(entity: current_entity)
363
+ ```
364
+
365
+ ## Security Considerations
366
+
367
+ ### Token Security
368
+
369
+ - Tokens are 32-byte URL-safe base64 strings
370
+ - Tokens expire after 1 week by default
371
+ - Each invite has a unique token
372
+
373
+ ### Email Validation
374
+
375
+ By default, the accepting user's email must match the invited email:
376
+
377
+ ```ruby
378
+ def enforce_email?
379
+ true # Default: require exact match
380
+ end
381
+ ```
382
+
383
+ ### Rate Limiting
384
+
385
+ Consider adding rate limiting to prevent abuse:
386
+
387
+ ```ruby
388
+ # In your interaction
389
+ validate :rate_limit_invites
390
+
391
+ def rate_limit_invites
392
+ recent = Invites::UserInvite
393
+ .where(invited_by: current_user)
394
+ .where("created_at > ?", 1.hour.ago)
395
+ .count
396
+
397
+ if recent >= 50
398
+ errors.add(:base, "Too many invitations sent. Please wait.")
399
+ end
400
+ end
401
+ ```
402
+
403
+ ## Troubleshooting
404
+
405
+ ### "Invitation not found or expired"
406
+
407
+ - Check that the token hasn't expired (default: 1 week)
408
+ - Verify the invite is still `pending` (not cancelled or accepted)
409
+ - Ensure the URL is complete and not truncated
410
+
411
+ ### "Email mismatch" Error
412
+
413
+ The system requires the accepting user's email to match:
414
+
415
+ ```
416
+ This invitation is for user@example.com.
417
+ You must use an account with that email address.
418
+ ```
419
+
420
+ If you need to allow any email:
421
+
422
+ ```ruby
423
+ def enforce_email?
424
+ false # Not recommended for security
425
+ end
426
+ ```
427
+
428
+ ### Rodauth Not Redirecting Properly
429
+
430
+ Ensure your Rodauth plugin is configured:
431
+
432
+ ```ruby
433
+ # app/rodauth/user_rodauth_plugin.rb
434
+ configure do
435
+ login_return_to_requested_location? true
436
+ login_redirect "/welcome"
437
+
438
+ after_login do
439
+ session[:after_welcome_redirect] = session.delete(:login_redirect)
440
+ end
441
+ end
442
+ ```
443
+
444
+ ### Invitable Callback Not Called
445
+
446
+ Ensure your model includes the concern and implements the callback:
447
+
448
+ ```ruby
449
+ class Tenant < ApplicationRecord
450
+ include Plutonium::Invites::Concerns::Invitable
451
+
452
+ def on_invite_accepted(user)
453
+ # This MUST be implemented
454
+ update!(user: user)
455
+ end
456
+ end
457
+ ```
458
+
459
+ ## API Reference
460
+
461
+ ### UserInvite States
462
+
463
+ | State | Description |
464
+ |-------|-------------|
465
+ | `pending` | Awaiting acceptance |
466
+ | `accepted` | Successfully accepted |
467
+ | `expired` | Past expiration date |
468
+ | `cancelled` | Manually cancelled |
469
+
470
+ ### Key Methods
471
+
472
+ ```ruby
473
+ # Find valid invite
474
+ invite = Invites::UserInvite.find_for_acceptance(token)
475
+
476
+ # Accept invitation
477
+ invite.accept_for_user!(user)
478
+
479
+ # Resend email
480
+ invite.resend!
481
+
482
+ # Cancel
483
+ invite.cancel!
484
+
485
+ # Check state
486
+ invite.pending?
487
+ invite.accepted?
488
+ invite.expired?
489
+ invite.cancelled?
490
+ ```
491
+
492
+ ## Next Steps
493
+
494
+ - [Authentication](/guides/authentication) - Set up Rodauth
495
+ - [Authorization](/guides/authorization) - Configure policies
496
+ - [Custom Actions](/guides/custom-actions) - Add more invite actions
497
+ - [Multi-tenancy](/guides/multi-tenancy) - Entity scoping
@@ -1,6 +1,10 @@
1
1
  after_bundle do
2
2
  Bundler.with_unbundled_env do
3
- run "bundle add plutonium"
3
+ if ENV["LOCAL"]
4
+ run %(bundle add plutonium --path="/Users/stefan/Documents/plutonium/plutonium-core")
5
+ else
6
+ run "bundle add plutonium"
7
+ end
4
8
  end
5
9
 
6
10
  generate "pu:core:install"
@@ -0,0 +1,42 @@
1
+ after_bundle do
2
+ # SQLite infrastructure (replaces Redis/Postgres for simple deployments)
3
+ generate "pu:lite:setup"
4
+ git add: "."
5
+ git commit: %( -m 'setup sqlite') if `git status --porcelain`.present?
6
+
7
+ unless ENV["SKIP_SOLID_QUEUE"]
8
+ generate "pu:lite:solid_queue"
9
+ git add: "."
10
+ git commit: %( -m 'add solid_queue') if `git status --porcelain`.present?
11
+ end
12
+
13
+ unless ENV["SKIP_SOLID_CACHE"]
14
+ generate "pu:lite:solid_cache"
15
+ git add: "."
16
+ git commit: %( -m 'add solid_cache') if `git status --porcelain`.present?
17
+ end
18
+
19
+ unless ENV["SKIP_SOLID_CABLE"]
20
+ generate "pu:lite:solid_cable"
21
+ git add: "."
22
+ git commit: %( -m 'add solid_cable') if `git status --porcelain`.present?
23
+ end
24
+
25
+ unless ENV["SKIP_SOLID_ERRORS"]
26
+ generate "pu:lite:solid_errors"
27
+ git add: "."
28
+ git commit: %( -m 'add solid_errors') if `git status --porcelain`.present?
29
+ end
30
+
31
+ unless ENV["SKIP_LITESTREAM"]
32
+ generate "pu:lite:litestream"
33
+ git add: "."
34
+ git commit: %( -m 'add litestream') if `git status --porcelain`.present?
35
+ end
36
+
37
+ unless ENV["SKIP_RAILS_PULSE"]
38
+ generate "pu:lite:rails_pulse"
39
+ git add: "."
40
+ git commit: %( -m 'add rails_pulse') if `git status --porcelain`.present?
41
+ end
42
+ end
@@ -7,6 +7,11 @@ after_bundle do
7
7
  end
8
8
  rails_command "app:template LOCATION=#{template_location}"
9
9
 
10
- # Enliten!
11
- rails_command "app:template LOCATION=https://raw.githubusercontent.com/thedumbtechguy/enlitenment/main/template.rb"
10
+ # Run the lite stack setup (via rails_command so generators are available)
11
+ lite_location = if ENV["LOCAL"]
12
+ "/Users/stefan/Documents/plutonium/plutonium-core/docs/public/templates/lite.rb"
13
+ else
14
+ "https://radioactive-labs.github.io/plutonium-core/templates/lite.rb"
15
+ end
16
+ rails_command "app:template LOCATION=#{lite_location}"
12
17
  end
@@ -20,19 +20,24 @@ class PostsController < ::ResourceController
20
20
  end
21
21
  ```
22
22
 
23
- For portals, controllers inherit from the feature package's controller and include the portal's concern:
23
+ For portals:
24
24
 
25
25
  ```ruby
26
- # packages/admin_portal/app/controllers/admin_portal/posts_controller.rb
27
- class AdminPortal::PostsController < ::PostsController
28
- include AdminPortal::Concerns::Controller
26
+ # packages/admin_portal/app/controllers/admin_portal/resource_controller.rb
27
+ module AdminPortal
28
+ class ResourceController < ::ResourceController
29
+ include AdminPortal::Concerns::Controller
30
+ end
31
+ end
29
32
 
30
- # Portal-specific customizations
33
+ # packages/admin_portal/app/controllers/admin_portal/posts_controller.rb
34
+ module AdminPortal
35
+ class PostsController < ResourceController
36
+ # Portal-specific customizations
37
+ end
31
38
  end
32
39
  ```
33
40
 
34
- Controllers are auto-created if not defined. When accessing a portal resource controller, Plutonium dynamically creates it by inheriting from the feature package's controller.
35
-
36
41
  ## Built-in Actions
37
42
 
38
43
  | Action | HTTP Method | Path | Purpose |
@@ -18,6 +18,9 @@ class PostDefinition < Plutonium::Resource::Definition
18
18
  scope :published
19
19
  scope :draft
20
20
 
21
+ # Default scope
22
+ default_scope :published
23
+
21
24
  # Sorting - sortable columns
22
25
  sort :title
23
26
  sort :created_at
@@ -149,13 +152,16 @@ end
149
152
 
150
153
  ### Default Scope
151
154
 
152
- Mark a scope as the default selection:
155
+ Set a scope as the default selection:
153
156
 
154
157
  ```ruby
155
- scope :active, default: true
158
+ scope :active
156
159
  scope :archived
160
+
161
+ default_scope :active
157
162
  ```
158
163
 
164
+
159
165
  ### Inline Scope (Block Syntax)
160
166
 
161
167
  For scopes that don't exist on the model, use block syntax with the scope as an argument:
@@ -319,10 +325,13 @@ class PostDefinition < Plutonium::Resource::Definition
319
325
  scope :featured
320
326
  scope(:recent) { |scope| scope.where('created_at > ?', 1.week.ago) }
321
327
 
328
+ # Default scope
329
+ default_scope :published
330
+
322
331
  # Sortable columns
323
332
  sorts :title, :created_at, :view_count, :published_at
324
333
 
325
- # Default: newest first
334
+ # Default sort: newest first
326
335
  default_sort :created_at, :desc
327
336
  end
328
337
  ```