plutonium 0.45.3 → 0.46.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +146 -0
  3. data/.claude/skills/plutonium-assets/SKILL.md +248 -157
  4. data/.claude/skills/{plutonium-rodauth → plutonium-auth}/SKILL.md +195 -229
  5. data/.claude/skills/plutonium-controller/SKILL.md +9 -2
  6. data/.claude/skills/plutonium-create-resource/SKILL.md +22 -1
  7. data/.claude/skills/plutonium-definition/SKILL.md +521 -7
  8. data/.claude/skills/plutonium-entity-scoping/SKILL.md +317 -0
  9. data/.claude/skills/plutonium-forms/SKILL.md +8 -1
  10. data/.claude/skills/plutonium-installation/SKILL.md +25 -2
  11. data/.claude/skills/plutonium-interaction/SKILL.md +9 -2
  12. data/.claude/skills/plutonium-invites/SKILL.md +11 -7
  13. data/.claude/skills/plutonium-model/SKILL.md +50 -50
  14. data/.claude/skills/plutonium-nested-resources/SKILL.md +8 -1
  15. data/.claude/skills/plutonium-package/SKILL.md +8 -1
  16. data/.claude/skills/plutonium-policy/SKILL.md +69 -78
  17. data/.claude/skills/plutonium-portal/SKILL.md +26 -70
  18. data/.claude/skills/plutonium-views/SKILL.md +9 -2
  19. data/CHANGELOG.md +28 -0
  20. data/app/assets/plutonium.css +1 -1
  21. data/app/views/rodauth/_login_form.html.erb +0 -3
  22. data/app/views/rodauth/confirm_password.html.erb +0 -4
  23. data/app/views/rodauth/create_account.html.erb +0 -3
  24. data/app/views/rodauth/logout.html.erb +0 -3
  25. data/docs/superpowers/plans/2026-04-08-plutonium-skills-overhaul.md +481 -0
  26. data/docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md +236 -0
  27. data/gemfiles/rails_7.gemfile.lock +1 -1
  28. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  29. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  30. data/lib/generators/pu/core/update/update_generator.rb +8 -0
  31. data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +56 -0
  32. data/lib/generators/pu/invites/install_generator.rb +8 -1
  33. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +43 -0
  34. data/lib/generators/pu/profile/concerns/profile_arguments.rb +10 -4
  35. data/lib/generators/pu/profile/conn_generator.rb +9 -12
  36. data/lib/generators/pu/profile/install_generator.rb +5 -2
  37. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
  38. data/lib/generators/pu/saas/portal_generator.rb +4 -9
  39. data/lib/generators/pu/saas/welcome/templates/app/views/welcome/onboarding.html.erb.tt +2 -2
  40. data/lib/plutonium/engine.rb +18 -5
  41. data/lib/plutonium/ui/layout/rodauth_layout.rb +6 -1
  42. data/lib/plutonium/version.rb +1 -1
  43. data/package.json +1 -1
  44. metadata +7 -8
  45. data/.claude/skills/plutonium/skill.md +0 -130
  46. data/.claude/skills/plutonium-definition-actions/SKILL.md +0 -424
  47. data/.claude/skills/plutonium-definition-query/SKILL.md +0 -364
  48. data/.claude/skills/plutonium-profile/SKILL.md +0 -276
  49. data/.claude/skills/plutonium-theming/SKILL.md +0 -424
@@ -1,13 +1,31 @@
1
1
  ---
2
- name: plutonium-rodauth
3
- description: Use when setting up authentication - Rodauth configuration, account types, login flows, or multi-account support
2
+ name: plutonium-auth
3
+ description: Use BEFORE configuring Rodauth, account types, login flows, or building a profile / account settings page. Also when including Plutonium::Auth::Rodauth in a controller. Covers authentication and user profile pages.
4
4
  ---
5
5
 
6
- # Plutonium Rodauth Authentication
6
+ # Plutonium Authentication (Rodauth + Profile)
7
7
 
8
- Plutonium integrates with [Rodauth](http://rodauth.jeremyevans.net/) via [rodauth-rails](https://github.com/janko/rodauth-rails) for authentication. This provides a full-featured, secure authentication system.
8
+ ## 🚨 Critical (read first)
9
+ - **Use the generators.** `pu:rodauth:install`, `pu:rodauth:account`, `pu:rodauth:admin`, `pu:saas:setup`, `pu:profile:install`, `pu:profile:conn` — never hand-write Rodauth plugin files, account models, or profile resources.
10
+ - **Role index 0 is the most privileged** (`owner`, `super_admin`). Invite interactions default new invitees to index 1. `pu:saas:setup` always prepends `owner` — don't include it in `--roles`.
11
+ - **`pu:saas:setup` is a meta-generator** that also runs `pu:saas:portal`, `pu:profile:setup`, `pu:saas:welcome`, and `pu:invites:install`. Don't re-run them manually.
12
+ - **Profile association is always `:profile`** regardless of the model class — `current_user.profile`, `build_profile`, etc.
13
+ - **Related skills:** `plutonium-installation` (initial setup), `plutonium-portal` (portal auth), `plutonium-invites` (multi-tenant invitations), `plutonium-entity-scoping` (tenant scoping).
9
14
 
10
- ## Installation
15
+ Plutonium integrates with [Rodauth](http://rodauth.jeremyevans.net/) via [rodauth-rails](https://github.com/janko/rodauth-rails) for authentication. This skill covers the full auth surface: installing Rodauth, configuring account types, building login/password flows, and adding a user profile / account settings page.
16
+
17
+ ## Contents
18
+ - [Rodauth setup](#rodauth-setup)
19
+ - [Account types](#account-types)
20
+ - [Connecting auth to controllers](#connecting-auth-to-controllers)
21
+ - [Customization](#customization)
22
+ - [Email configuration](#email-configuration)
23
+ - [Portal integration](#portal-integration)
24
+ - [API authentication](#api-authentication)
25
+ - [Profile page](#profile-page)
26
+ - [Gotchas](#gotchas)
27
+
28
+ ## Rodauth setup
11
29
 
12
30
  ### Step 1: Install Rodauth Base
13
31
 
@@ -26,8 +44,6 @@ This installs:
26
44
 
27
45
  ### Step 2: Create Account Type
28
46
 
29
- Choose the appropriate generator for your use case:
30
-
31
47
  ```bash
32
48
  # Basic user account
33
49
  rails generate pu:rodauth:account user
@@ -39,12 +55,10 @@ rails generate pu:rodauth:admin admin
39
55
  rails generate pu:saas:setup --user Customer --entity Organization
40
56
  ```
41
57
 
42
- ## Account Generators
58
+ ## Account types
43
59
 
44
60
  ### Basic Account (`pu:rodauth:account`)
45
61
 
46
- Creates a standard user account with configurable features:
47
-
48
62
  ```bash
49
63
  rails generate pu:rodauth:account user [options]
50
64
  ```
@@ -87,16 +101,7 @@ rails generate pu:rodauth:account user [options]
87
101
 
88
102
  ### Admin Account (`pu:rodauth:admin`)
89
103
 
90
- Creates a secure admin account with:
91
- - Multi-phase login (email first, then password)
92
- - TOTP two-factor authentication (required)
93
- - Recovery codes
94
- - Account lockout
95
- - Active sessions tracking
96
- - Audit logging
97
- - Role-based access control
98
- - Invite interaction for adding new admins
99
- - No public signup (accounts created via rake task or invite)
104
+ Creates a secure admin account with multi-phase login, TOTP 2FA (required), recovery codes, account lockout, active session tracking, audit logging, role-based access control, invite interaction, and no public signup.
100
105
 
101
106
  ```bash
102
107
  rails generate pu:rodauth:admin admin
@@ -111,34 +116,36 @@ rails generate pu:rodauth:admin admin --extra-attributes=name:string,department:
111
116
  | `--roles` | super_admin,admin | Comma-separated roles for admin accounts |
112
117
  | `--extra_attributes` | | Additional model attributes (e.g., name:string) |
113
118
 
114
- **Generated role enum:**
119
+ **Role ordering convention:** Roles are stored as a positional enum — **index 0 is the most privileged** (`super_admin`, `owner`, etc.). The generated invite interaction defaults new invitees to `roles[1]`, so the order in `--roles=` matters.
120
+
115
121
  ```ruby
116
122
  # app/models/admin.rb
117
123
  enum :role, super_admin: 0, admin: 1
118
124
  ```
119
125
 
120
- **Generated invite interaction:**
121
126
  ```ruby
122
127
  # app/interactions/admin/invite_interaction.rb
123
128
  class Admin::InviteInteraction < Plutonium::Interaction::Base
124
129
  attribute :email, :string
125
- attribute :role, default: :admin # Second role is default
126
-
127
- def execute
128
- # Creates admin via internal request and sends invite email
129
- end
130
+ attribute :role, default: :admin
131
+ # ...
130
132
  end
131
133
  ```
132
134
 
133
- **Creates rake task:**
135
+ Rake task for direct creation:
134
136
  ```bash
135
- # Create admin account directly
136
137
  rails rodauth_admin:create[admin@example.com,password123]
137
138
  ```
138
139
 
139
140
  ### SaaS Setup (`pu:saas:setup`)
140
141
 
141
- Creates a complete multi-tenant SaaS setup with user account, entity, and membership:
142
+ > **This is a meta-generator.** In addition to creating the user + entity + membership, `pu:saas:setup` also runs:
143
+ > - `pu:saas:portal` → a full `{Entity}Portal` scoped to the entity
144
+ > - `pu:profile:setup` → a `Profile` model and association on the user
145
+ > - `pu:saas:welcome` → the onboarding / select-entity flow
146
+ > - `pu:invites:install` → the entire invites package
147
+ >
148
+ > Don't generate another entity portal after running this. Pass `--force` if re-running.
142
149
 
143
150
  ```bash
144
151
  rails generate pu:saas:setup --user Customer --entity Organization
@@ -151,57 +158,30 @@ rails generate pu:saas:setup --user Customer --entity Organization --user-attrib
151
158
 
152
159
  | Option | Default | Description |
153
160
  |--------|---------|-------------|
154
- | `--user=NAME` | (required) | User account model name (e.g., Customer) |
155
- | `--entity=NAME` | (required) | Entity model name (e.g., Organization) |
161
+ | `--user=NAME` | (required) | User account model name |
162
+ | `--entity=NAME` | (required) | Entity model name |
156
163
  | `--allow-signup` | true | Allow public registration |
157
- | `--roles` | member,owner | Comma-separated membership roles |
164
+ | `--roles` | admin,member | Additional membership roles. **`owner` is always prepended as index 0** — don't include it. |
158
165
  | `--skip-entity` | false | Skip entity model generation |
159
166
  | `--skip-membership` | false | Skip membership model generation |
160
167
  | `--user-attributes` | | Additional user model attributes |
161
- | `--entity-attributes` | | Additional entity model attributes (name is always included) |
168
+ | `--entity-attributes` | | Additional entity model attributes |
162
169
  | `--membership-attributes` | | Additional membership model attributes |
163
170
 
164
- **Individual Generators:**
165
-
166
- You can also run each component separately:
167
-
168
- ```bash
169
- # Just the user account
170
- rails g pu:saas:user Customer
171
-
172
- # Just the entity model
173
- rails g pu:saas:entity Organization
174
-
175
- # Just the membership (requires user and entity to exist)
176
- rails g pu:saas:membership --user Customer --entity Organization
177
- ```
178
-
179
- **Generated Models:**
180
-
181
- 1. **User account** - The user model with Rodauth authentication
182
- 2. **Entity model** - The organization/company with unique name
183
- 3. **Membership model** - Join table `{entity}_{user}` (e.g., `OrganizationCustomer`)
171
+ Individual generators: `pu:saas:user`, `pu:saas:entity`, `pu:saas:membership`.
184
172
 
185
173
  ```ruby
186
174
  # app/models/customer.rb
187
175
  class Customer < ApplicationRecord
188
176
  include Rodauth::Rails.model(:customer)
189
-
190
177
  has_many :organization_customers, dependent: :destroy
191
178
  has_many :organizations, through: :organization_customers
192
179
  end
193
180
 
194
- # app/models/organization.rb
195
- class Organization < ApplicationRecord
196
- has_many :organization_customers, dependent: :destroy
197
- has_many :customers, through: :organization_customers
198
- end
199
-
200
181
  # app/models/organization_customer.rb
201
182
  class OrganizationCustomer < ApplicationRecord
202
183
  belongs_to :organization
203
184
  belongs_to :customer
204
-
205
185
  enum :role, member: 0, owner: 1
206
186
 
207
187
  validates :customer, uniqueness: {
@@ -211,128 +191,33 @@ class OrganizationCustomer < ApplicationRecord
211
191
  end
212
192
  ```
213
193
 
214
- **Membership Roles:**
215
-
216
- The membership model includes a role enum for access control within the entity:
217
-
218
- ```ruby
219
- membership = OrganizationCustomer.find_by(organization: org, customer: current_user)
220
- membership.member? # Default role
221
- membership.owner? # Admin role for the entity
222
- ```
223
-
224
- ## Connecting Auth to Controllers
225
-
226
- ### Include in Resource Controller
194
+ ## Connecting auth to controllers
227
195
 
228
196
  ```ruby
229
197
  # app/controllers/resource_controller.rb
230
198
  class ResourceController < PlutoniumController
231
199
  include Plutonium::Resource::Controller
232
- include Plutonium::Auth::Rodauth(:user) # Use :user account
200
+ include Plutonium::Auth::Rodauth(:user)
233
201
  end
234
202
  ```
235
203
 
236
- ### Multiple Account Types
204
+ Multiple account types:
237
205
 
238
206
  ```ruby
239
- # app/controllers/admin_controller.rb
240
207
  class AdminController < PlutoniumController
241
208
  include Plutonium::Resource::Controller
242
209
  include Plutonium::Auth::Rodauth(:admin)
243
210
  end
244
-
245
- # app/controllers/customer_controller.rb
246
- class CustomerController < PlutoniumController
247
- include Plutonium::Resource::Controller
248
- include Plutonium::Auth::Rodauth(:customer)
249
- end
250
211
  ```
251
212
 
252
- ### What It Provides
253
-
254
- Including `Plutonium::Auth::Rodauth(:name)` adds:
255
-
256
- | Method | Description |
257
- |--------|-------------|
258
- | `current_user` | The authenticated account |
259
- | `logout_url` | URL to logout |
260
- | `plutonium-rodauth` | Access to Rodauth instance |
261
-
262
- ## Generated Files
263
-
264
- ### Account Structure
265
-
266
- ```
267
- app/
268
- ├── controllers/
269
- │ └── rodauth/
270
- │ └── user_controller.rb # Account-specific controller
271
- ├── mailers/
272
- │ └── rodauth/
273
- │ └── user_mailer.rb # Account-specific mailer
274
- ├── models/
275
- │ └── user.rb # Account model
276
- ├── rodauth/
277
- │ ├── rodauth_app.rb # Main Roda app
278
- │ ├── rodauth_plugin.rb # Base plugin
279
- │ └── user_rodauth_plugin.rb # Account-specific config
280
- ├── policies/
281
- │ └── user_policy.rb # Account policy
282
- ├── definitions/
283
- │ └── user_definition.rb # Account definition
284
- └── views/
285
- ├── layouts/
286
- │ └── rodauth.html.erb # Auth layout
287
- └── rodauth/
288
- └── user_mailer/ # Email templates
289
- ├── reset_password.text.erb
290
- ├── verify_account.text.erb
291
- └── ...
292
- ```
293
-
294
- ### Plugin Configuration
295
-
296
- ```ruby
297
- # app/rodauth/user_rodauth_plugin.rb
298
- class UserRodauthPlugin < RodauthPlugin
299
- configure do
300
- # Features enabled for this account
301
- enable :login, :logout, :remember, :create_account, ...
302
-
303
- # URL prefix (non-primary accounts)
304
- prefix "/users"
305
-
306
- # Password storage
307
- account_password_hash_column :password_hash
308
-
309
- # Controller for views
310
- rails_controller { Rodauth::UserController }
311
-
312
- # Model
313
- rails_account_model { User }
314
-
315
- # Redirects
316
- login_redirect "/"
317
- logout_redirect "/"
318
-
319
- # Session configuration
320
- session_key "_user_session"
321
- remember_cookie_key "_user_remember"
322
- end
323
- end
324
- ```
213
+ Including `Plutonium::Auth::Rodauth(:name)` adds `current_user`, `logout_url`, and `rodauth`.
325
214
 
326
215
  ## Customization
327
216
 
328
217
  ### Custom Login Redirect
329
218
 
330
219
  ```ruby
331
- # app/rodauth/user_rodauth_plugin.rb
332
220
  configure do
333
- login_redirect { "/dashboard" }
334
-
335
- # Or dynamically based on user
336
221
  login_redirect do
337
222
  if rails_account.admin?
338
223
  "/admin"
@@ -347,12 +232,10 @@ end
347
232
 
348
233
  ```ruby
349
234
  configure do
350
- # Add custom field validation
351
235
  before_create_account do
352
236
  throw_error_status(422, "name", "must be present") if param("name").empty?
353
237
  end
354
238
 
355
- # After account creation
356
239
  after_create_account do
357
240
  Profile.create!(account_id: account_id, name: param("name"))
358
241
  end
@@ -363,10 +246,8 @@ end
363
246
 
364
247
  ```ruby
365
248
  configure do
366
- # Minimum length
367
249
  password_minimum_length 12
368
250
 
369
- # Custom complexity
370
251
  password_meets_requirements? do |password|
371
252
  super(password) && password.match?(/\d/) && password.match?(/[^a-zA-Z\d]/)
372
253
  end
@@ -377,7 +258,6 @@ end
377
258
 
378
259
  ```ruby
379
260
  configure do
380
- # Ask for email first, then password
381
261
  use_multi_phase_login? true
382
262
  end
383
263
  ```
@@ -392,9 +272,7 @@ configure do
392
272
  end
393
273
  ```
394
274
 
395
- ## Email Configuration
396
-
397
- Emails are sent via Action Mailer. Configure delivery in your environment:
275
+ ## Email configuration
398
276
 
399
277
  ```ruby
400
278
  # config/environments/production.rb
@@ -407,47 +285,26 @@ config.action_mailer.smtp_settings = {
407
285
  }
408
286
  ```
409
287
 
410
- ### Custom Email Templates
288
+ Override templates in `app/views/rodauth/user_mailer/`.
411
289
 
412
- Override templates in `app/views/rodauth/user_mailer/`:
413
-
414
- ```erb
415
- <%# app/views/rodauth/user_mailer/reset_password.text.erb %>
416
- Hi <%= @account.email %>,
417
-
418
- Someone requested a password reset for your account.
419
-
420
- Reset your password: <%= @reset_password_url %>
421
-
422
- If you didn't request this, ignore this email.
423
- ```
424
-
425
- ## Portal Integration
426
-
427
- ### Selecting Auth for Portal
428
-
429
- When generating a portal, select the Rodauth account:
290
+ ## Portal integration
430
291
 
431
292
  ```bash
432
293
  rails generate pu:pkg:portal admin
433
- # Select "Rodauth account" when prompted
434
- # Choose "admin" account
294
+ # Select "Rodauth account" "admin"
435
295
  ```
436
296
 
437
- ### Manual Portal Auth Setup
297
+ Manual:
438
298
 
439
299
  ```ruby
440
- # packages/admin_portal/lib/engine.rb
441
300
  module AdminPortal
442
301
  class Engine < Rails::Engine
443
302
  include Plutonium::Portal::Engine
444
303
 
445
- # Require authentication
446
304
  config.before_initialize do
447
305
  config.to_prepare do
448
306
  AdminPortal::ResourceController.class_eval do
449
307
  include Plutonium::Auth::Rodauth(:admin)
450
-
451
308
  before_action :require_authenticated
452
309
 
453
310
  private
@@ -462,56 +319,165 @@ module AdminPortal
462
319
  end
463
320
  ```
464
321
 
465
- ## API Authentication
466
-
467
- For JSON API authentication:
322
+ ## API authentication
468
323
 
469
324
  ```bash
470
325
  rails generate pu:rodauth:account api_user --api_only --jwt --jwt_refresh
471
326
  ```
472
327
 
473
- This enables:
474
- - JWT token authentication
475
- - Refresh tokens
476
- - No session/cookie handling
477
-
478
- ### Using JWT
479
-
480
- ```ruby
481
- # Login
328
+ ```
482
329
  POST /api_users/login
483
- Content-Type: application/json
484
-
485
330
  {"login": "user@example.com", "password": "secret"}
331
+ # → {"access_token": "...", "refresh_token": "..."}
486
332
 
487
- # Response includes JWT
488
- {"access_token": "...", "refresh_token": "..."}
489
-
490
- # Authenticated requests
491
333
  GET /api/posts
492
334
  Authorization: Bearer <access_token>
493
335
  ```
494
336
 
495
- ## Internal Requests
337
+ ## Profile page
338
+
339
+ Plutonium provides a Profile resource generator for managing Rodauth account settings. Users can view/edit their profile, access Rodauth security features (change password, 2FA, etc.), and manage account settings in one place.
340
+
341
+ ### Quick setup
342
+
343
+ ```bash
344
+ rails g pu:profile:setup date_of_birth:date bio:text \
345
+ --dest=competition \
346
+ --portal=competition_portal
347
+ ```
348
+
349
+ ### Step-by-step install
350
+
351
+ ```bash
352
+ rails generate pu:profile:install --dest=main_app
353
+ ```
354
+
355
+ | Option | Default | Description |
356
+ |--------|---------|-------------|
357
+ | `--dest=DESTINATION` | (prompts) | Target package or main_app |
358
+ | `--user-model=NAME` | User | Rodauth user model name |
359
+
360
+ With fields:
361
+
362
+ ```bash
363
+ rails g pu:profile:install \
364
+ bio:text \
365
+ avatar:attachment \
366
+ 'timezone:string?' \
367
+ --dest=customer
368
+ ```
369
+
370
+ Custom name:
371
+
372
+ ```bash
373
+ rails g pu:profile:install AccountSettings bio:text --dest=main_app
374
+ ```
375
+
376
+ ### What gets created
377
+
378
+ The generator creates a standard Plutonium resource. **By default the model is named `{UserModel}Profile`** (e.g. `UserProfile`, `StaffUserProfile`) — derived from `--user-model`. Pass an explicit name as the first positional argument to override.
379
+
380
+ ```
381
+ app/models/[package/]user_profile.rb # {UserModel}Profile model
382
+ db/migrate/xxx_create_user_profiles.rb # Migration
383
+ app/controllers/[package/]user_profiles_controller.rb
384
+ app/policies/[package/]user_profile_policy.rb
385
+ app/definitions/[package/]user_profile_definition.rb
386
+ ```
387
+
388
+ And modifies:
389
+ - **User model**: Adds `has_one :profile, class_name: "{UserModel}Profile", dependent: :destroy`
390
+ > The association is **always named `:profile`** regardless of the class, so `current_user.profile` / `build_profile` / `params.require(:profile)` work uniformly.
391
+ - **Definition**: Injects custom ShowPage with SecuritySection
392
+
393
+ ### The SecuritySection component
394
+
395
+ ```ruby
396
+ class ProfileDefinition < Plutonium::Resource::Definition
397
+ class ShowPage < ShowPage
398
+ private
399
+
400
+ def render_after_content
401
+ render Plutonium::Profile::SecuritySection.new
402
+ end
403
+ end
404
+ end
405
+ ```
406
+
407
+ Dynamically checks enabled Rodauth features and displays links for:
408
+
409
+ | Feature | Label |
410
+ |---------|-------|
411
+ | `change_password` | Change Password |
412
+ | `change_login` | Change Email |
413
+ | `otp` | Two-Factor Authentication |
414
+ | `recovery_codes` | Recovery Codes |
415
+ | `webauthn` | Security Keys |
416
+ | `active_sessions` | Active Sessions |
417
+ | `close_account` | Close Account |
418
+
419
+ ### After generation
420
+
421
+ 1. `rails db:migrate`
422
+ 2. Connect to portal: `rails g pu:profile:conn --dest=customer_portal` — registers as singular resource (`/profile`) and enables `profile_url` helper.
423
+ 3. Ensure users have a profile row:
424
+
425
+ ```ruby
426
+ class User < ApplicationRecord
427
+ after_create :create_profile!
428
+
429
+ private
430
+
431
+ def create_profile!
432
+ create_profile
433
+ end
434
+ end
435
+ ```
496
436
 
497
- Create accounts programmatically:
437
+ ### Customizing the profile definition
498
438
 
499
439
  ```ruby
500
- # Using internal request
501
- Rodauth::Rails.app(:user).rodauth(:user).create_account(
502
- login: "user@example.com",
503
- password: "secure_password"
504
- )
440
+ class ProfileDefinition < Plutonium::Resource::Definition
441
+ form do |f|
442
+ f.field :bio
443
+ f.field :avatar
444
+ f.field :website
445
+ end
446
+
447
+ display do |d|
448
+ d.field :bio
449
+ d.field :avatar
450
+ d.field :website
451
+ end
505
452
 
506
- # Or via model (if allowed)
507
- User.create!(
508
- email: "user@example.com",
509
- password_hash: BCrypt::Password.create("secure_password"),
510
- status: 2 # verified
511
- )
453
+ class ShowPage < ShowPage
454
+ private
455
+
456
+ def render_after_content
457
+ render Plutonium::Profile::SecuritySection.new
458
+ end
459
+ end
460
+ end
512
461
  ```
513
462
 
514
- ## Feature Reference
463
+ ### Profile link in header
464
+
465
+ ```ruby
466
+ if respond_to?(:profile_url)
467
+ link_to "Profile", profile_url
468
+ end
469
+ ```
470
+
471
+ ## Gotchas
472
+
473
+ - **Role index 0 is the most privileged.** For admin/saas roles, index 0 is owner/super_admin. Invite interactions default new invitees to index 1.
474
+ - **`owner` is always prepended** in `pu:saas:setup --roles`. Don't include it manually.
475
+ - **Profile association is always `:profile`**, even if the model class is `StaffUserProfile`.
476
+ - **`pu:saas:setup` is a meta-generator** — it also runs `pu:saas:portal`, `pu:profile:setup`, `pu:saas:welcome`, and `pu:invites:install`. Don't rerun them separately.
477
+ - **Profile requires a connected portal** — without `pu:profile:conn`, `profile_url` is missing and the user menu link won't render.
478
+ - **Users need a profile row.** Add an `after_create` callback or use `find_or_create`.
479
+
480
+ ## Feature reference
515
481
 
516
482
  | Feature | Description |
517
483
  |---------|-------------|
@@ -534,12 +500,12 @@ User.create!(
534
500
  | `jwt` | JWT token authentication |
535
501
  | `jwt_refresh` | JWT refresh tokens |
536
502
  | `close_account` | Allow account deletion |
537
- | `password_expiration` | Force password changes |
538
- | `disallow_password_reuse` | Prevent password reuse |
539
503
 
540
- ## Related Skills
504
+ ## Related skills
541
505
 
542
506
  - `plutonium-installation` - Initial Plutonium setup
543
507
  - `plutonium-portal` - Portal configuration
544
508
  - `plutonium-policy` - Authorization after authentication
545
509
  - `plutonium-invites` - User invitation system for multi-tenant apps
510
+ - `plutonium-definition` - Customizing the profile definition
511
+ - `plutonium-views` - Custom pages and components
@@ -1,10 +1,17 @@
1
1
  ---
2
2
  name: plutonium-controller
3
- description: Use when customizing controller behavior, overriding CRUD actions, adding hooks, or changing redirect logic in Plutonium
3
+ description: Use BEFORE overriding a controller action, adding a hook, or changing redirect logic in a Plutonium controller. Also when customizing resource_params or presentation hooks.
4
4
  ---
5
5
 
6
6
  # Plutonium Controllers
7
7
 
8
+ ## 🚨 Critical (read first)
9
+ - **Use generators.** `pu:res:scaffold` and `pu:res:conn` create controllers — never hand-write them.
10
+ - **Don't override CRUD actions.** Customize via hooks (`resource_params`, `redirect_url_after_submit`, `preferred_action_after_submit`, presentation hooks). Overriding `create`/`update` usually breaks authorization, params filtering, or both.
11
+ - **Prefer definitions and interactions.** UI config belongs in definitions; business logic belongs in interactions. The controller is thin by design.
12
+ - **Named custom routes.** When adding custom member/collection routes, always use `as:` so `resource_url_for` can build URLs — especially for nested resources.
13
+ - **Related skills:** `plutonium-definition` (interactive actions instead of controller actions), `plutonium-policy` (authorization), `plutonium-nested-resources` (parent/child routing), `plutonium-views` (custom page classes).
14
+
8
15
  **Controllers are generated automatically** - never create them manually:
9
16
  - `rails g pu:res:scaffold` creates the base controller
10
17
  - `rails g pu:res:conn` creates portal-specific controllers
@@ -383,7 +390,7 @@ end
383
390
 
384
391
  - `plutonium` - How controllers fit in the resource architecture
385
392
  - `plutonium-policy` - Authorization (used by controllers)
386
- - `plutonium-definition-actions` - Interactive actions (preferred over custom controller actions)
393
+ - `plutonium-definition` - Interactive actions (preferred over custom controller actions)
387
394
  - `plutonium-views` - Custom page, form, display, and table classes
388
395
  - `plutonium-nested-resources` - Parent/child routes and scoping
389
396
  - `plutonium-model` - Resource models
@@ -1,12 +1,33 @@
1
1
  ---
2
2
  name: plutonium-create-resource
3
- description: Use when creating a new Plutonium resource - covers the pu:res:scaffold generator, field type syntax, and generator options
3
+ description: Use BEFORE running pu:res:scaffold or creating any new resource. Also when picking field types or generator options. Covers field syntax and scaffold options.
4
4
  ---
5
5
 
6
6
  # Create Resource Skill
7
7
 
8
+ ## 🚨 Critical (read first)
9
+ - **Always use `pu:res:scaffold`.** Never hand-write models, migrations, policies, definitions, or controllers. Plutonium's conventions rely on files it generated.
10
+ - **Always pass `--dest`** (`--dest=main_app` or `--dest=package_name`) to skip the interactive destination prompt.
11
+ - **Quote fields with `?` or `{}`** to prevent shell expansion: `'field:type?'`, `'field:decimal{10,2}'`, `'field:decimal?{10,2}'`.
12
+ - **Run `pu:res:conn` next** to connect the resource to a portal — without it, the resource is invisible.
13
+ - **Related skills:** `plutonium-model` (model structure), `plutonium-definition` (UI config), `plutonium-policy` (authorization), `plutonium-portal` (connecting to portals).
14
+
8
15
  Use the `pu:res:scaffold` generator to create complete resources in Plutonium applications.
9
16
 
17
+ ## Quick checklist
18
+
19
+ Creating a new resource:
20
+
21
+ 1. Pick a destination: `--dest=main_app` or `--dest=package_name`.
22
+ 2. Identify field types (see field type syntax below). Quote fields with `?` or `{}`.
23
+ 3. Run `rails g pu:res:scaffold ResourceName field:type ... --dest=<dest>`.
24
+ 4. Review the generated migration — add cascade deletes, composite indexes, defaults.
25
+ 5. Run `rails db:migrate`.
26
+ 6. Run `rails g pu:res:conn ResourceName --dest=<portal_name>` to connect to a portal.
27
+ 7. Verify routes: `bin/rails routes | grep resource_name`.
28
+ 8. Customize the policy (`permitted_attributes_for_read`, `permitted_attributes_for_create`) as needed.
29
+ 9. Open the portal route in the browser.
30
+
10
31
  ## Command Syntax
11
32
 
12
33
  ```bash