plutonium 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +27 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1009 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +37 -27
  18. data/docs/getting-started/index.md +22 -29
  19. data/docs/getting-started/installation.md +37 -80
  20. data/docs/getting-started/tutorial/index.md +4 -5
  21. data/docs/guides/adding-resources.md +66 -377
  22. data/docs/guides/authentication.md +94 -463
  23. data/docs/guides/authorization.md +124 -370
  24. data/docs/guides/creating-packages.md +94 -296
  25. data/docs/guides/custom-actions.md +121 -441
  26. data/docs/guides/index.md +22 -42
  27. data/docs/guides/multi-tenancy.md +116 -187
  28. data/docs/guides/nested-resources.md +103 -431
  29. data/docs/guides/search-filtering.md +123 -240
  30. data/docs/guides/testing.md +5 -4
  31. data/docs/guides/theming.md +157 -407
  32. data/docs/guides/troubleshooting.md +5 -3
  33. data/docs/guides/user-invites.md +106 -425
  34. data/docs/guides/user-profile.md +76 -243
  35. data/docs/index.md +1 -1
  36. data/docs/reference/app/generators.md +517 -0
  37. data/docs/reference/app/index.md +158 -0
  38. data/docs/reference/app/packages.md +146 -0
  39. data/docs/reference/app/portals.md +377 -0
  40. data/docs/reference/auth/accounts.md +230 -0
  41. data/docs/reference/auth/index.md +88 -0
  42. data/docs/reference/auth/profile.md +185 -0
  43. data/docs/reference/behavior/controllers.md +395 -0
  44. data/docs/reference/behavior/index.md +22 -0
  45. data/docs/reference/behavior/interactions.md +341 -0
  46. data/docs/reference/behavior/policies.md +417 -0
  47. data/docs/reference/index.md +56 -49
  48. data/docs/reference/resource/actions.md +423 -0
  49. data/docs/reference/resource/definition.md +508 -0
  50. data/docs/reference/resource/index.md +50 -0
  51. data/docs/reference/resource/model.md +348 -0
  52. data/docs/reference/resource/query.md +305 -0
  53. data/docs/reference/tenancy/entity-scoping.md +361 -0
  54. data/docs/reference/tenancy/index.md +36 -0
  55. data/docs/reference/tenancy/invites.md +393 -0
  56. data/docs/reference/tenancy/nested-resources.md +267 -0
  57. data/docs/reference/testing/index.md +287 -0
  58. data/docs/reference/ui/assets.md +400 -0
  59. data/docs/reference/ui/components.md +165 -0
  60. data/docs/reference/ui/displays.md +104 -0
  61. data/docs/reference/ui/forms.md +284 -0
  62. data/docs/reference/ui/index.md +30 -0
  63. data/docs/reference/ui/layouts.md +106 -0
  64. data/docs/reference/ui/pages.md +189 -0
  65. data/docs/reference/ui/tables.md +117 -0
  66. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  67. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  68. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  69. data/gemfiles/rails_7.gemfile.lock +1 -1
  70. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  72. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  73. data/lib/generators/pu/invites/install_generator.rb +1 -0
  74. data/lib/plutonium/definition/base.rb +1 -1
  75. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  76. data/lib/plutonium/helpers/turbo_helper.rb +11 -0
  77. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  78. data/lib/plutonium/resource/controller.rb +1 -0
  79. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  80. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  81. data/lib/plutonium/resource/policy.rb +7 -0
  82. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  83. data/lib/plutonium/ui/component/methods.rb +4 -0
  84. data/lib/plutonium/ui/form/base.rb +6 -2
  85. data/lib/plutonium/ui/form/components/json.rb +58 -0
  86. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  87. data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
  88. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  89. data/lib/plutonium/ui/form/resource.rb +0 -4
  90. data/lib/plutonium/ui/grid/resource.rb +1 -1
  91. data/lib/plutonium/ui/layout/base.rb +1 -0
  92. data/lib/plutonium/ui/page/base.rb +0 -7
  93. data/lib/plutonium/ui/page/index.rb +4 -4
  94. data/lib/plutonium/ui/table/resource.rb +1 -1
  95. data/lib/plutonium/version.rb +1 -1
  96. data/lib/plutonium.rb +8 -0
  97. data/lib/tasks/release.rake +15 -1
  98. data/package.json +10 -10
  99. data/src/css/slim_select.css +4 -0
  100. data/src/js/controllers/slim_select_controller.js +61 -0
  101. data/src/js/turbo/turbo_actions.js +33 -0
  102. data/yarn.lock +553 -543
  103. metadata +44 -33
  104. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  105. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  106. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  107. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  108. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  109. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  110. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  111. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  112. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  113. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  114. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  115. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  116. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  117. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  118. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  119. data/docs/reference/assets/index.md +0 -496
  120. data/docs/reference/controller/index.md +0 -412
  121. data/docs/reference/definition/actions.md +0 -462
  122. data/docs/reference/definition/fields.md +0 -383
  123. data/docs/reference/definition/index.md +0 -326
  124. data/docs/reference/definition/query.md +0 -351
  125. data/docs/reference/generators/index.md +0 -648
  126. data/docs/reference/interaction/index.md +0 -449
  127. data/docs/reference/model/features.md +0 -248
  128. data/docs/reference/model/index.md +0 -218
  129. data/docs/reference/policy/index.md +0 -456
  130. data/docs/reference/portal/index.md +0 -379
  131. data/docs/reference/views/forms.md +0 -411
  132. data/docs/reference/views/index.md +0 -544
@@ -0,0 +1,230 @@
1
+ # Accounts
2
+
3
+ Rodauth account types. Pick one (or several — apps can have multiple side-by-side).
4
+
5
+ ## Basic account — `pu:rodauth:account`
6
+
7
+ ```bash
8
+ rails generate pu:rodauth:account user [options]
9
+ ```
10
+
11
+ ### Options
12
+
13
+ | Option | Description |
14
+ |---|---|
15
+ | `--defaults` | Enables login, logout, remember, password reset |
16
+ | `--kitchen_sink` | Enables ALL features |
17
+ | `--primary` | Mark as primary account (no URL prefix) |
18
+ | `--no-mails` | Skip mailer setup |
19
+ | `--argon2` | Use Argon2 instead of bcrypt |
20
+ | `--api_only` | JSON API only (no sessions) |
21
+
22
+ ### Feature flags
23
+
24
+ | Flag | Default | Purpose |
25
+ |---|---|---|
26
+ | `--login`, `--logout`, `--remember` | ✓ | Basic auth |
27
+ | `--create_account`, `--verify_account` | ✓ | Registration + email verification |
28
+ | `--verify_account_grace_period` | ✓ | Grace period before verification is required |
29
+ | `--reset_password`, `--reset_password_notify` | ✓ | Password reset via email + notification |
30
+ | `--change_password`, `--change_password_notify` | ✓ | Password change + notification |
31
+ | `--change_login`, `--verify_login_change` | ✓ | Email change with verification |
32
+ | `--case_insensitive_login` | ✓ | Case-insensitive email matching |
33
+ | `--internal_request` | ✓ | Internal request support |
34
+ | `--otp` | | TOTP 2FA |
35
+ | `--webauthn` | | WebAuthn / passkeys |
36
+ | `--recovery_codes` | | 2FA backup codes |
37
+ | `--lockout` | | Lock after failed attempts |
38
+ | `--active_sessions` | | Track active sessions |
39
+ | `--audit_logging` | | Log auth events |
40
+ | `--close_account` | | Allow account deletion |
41
+ | `--email_auth` | | Passwordless email login |
42
+ | `--sms_codes` | | SMS 2FA |
43
+ | `--jwt`, `--jwt_refresh` | | JWT for API auth |
44
+ | `--password_expiration` | | Force periodic password changes |
45
+ | `--disallow_password_reuse` | | Prevent reusing recent passwords |
46
+
47
+ ### Examples
48
+
49
+ ```bash
50
+ # Basic account
51
+ rails g pu:rodauth:account user
52
+
53
+ # Primary account (no URL prefix)
54
+ rails g pu:rodauth:account user --primary
55
+
56
+ # With 2FA
57
+ rails g pu:rodauth:account user --otp --recovery_codes
58
+
59
+ # API only
60
+ rails g pu:rodauth:account api_user --api_only --jwt --jwt_refresh
61
+
62
+ # Kitchen sink
63
+ rails g pu:rodauth:account user --kitchen_sink
64
+ ```
65
+
66
+ ## Admin account — `pu:rodauth:admin`
67
+
68
+ Pre-configured secure admin with multi-phase login, **required** TOTP, recovery codes, lockout, active session tracking, audit logging, role-based access, invite interaction, and **no public signup**.
69
+
70
+ ```bash
71
+ rails g pu:rodauth:admin admin
72
+ rails g pu:rodauth:admin admin --roles=super_admin,admin,viewer
73
+ rails g pu:rodauth:admin admin --extra-attributes=name:string,department:string
74
+ ```
75
+
76
+ | Option | Default | Description |
77
+ |---|---|---|
78
+ | `--roles` | `super_admin,admin` | Comma-separated roles (positional enum) |
79
+ | `--extra_attributes` | | Additional model attributes (e.g. `name:string`) |
80
+
81
+ **Role-ordering convention:** index 0 is the most privileged. Generated invite interaction defaults new invitees to `roles[1]` — the order in `--roles=` matters.
82
+
83
+ ```ruby
84
+ enum :role, super_admin: 0, admin: 1
85
+ ```
86
+
87
+ Rake task for direct admin creation:
88
+
89
+ ```bash
90
+ rails rodauth_admin:create[admin@example.com,password123]
91
+ ```
92
+
93
+ ## SaaS setup — `pu:saas:setup` (meta-generator)
94
+
95
+ Creates the User + Entity + Membership trio AND runs:
96
+
97
+ - `pu:saas:portal` → a full `{Entity}Portal` scoped to the entity
98
+ - `pu:profile:setup` → profile model + association (see [Profile](./profile))
99
+ - `pu:saas:welcome` → onboarding / select-entity flow
100
+ - `pu:invites:install` → the invites package (see [Tenancy › Invites](/reference/tenancy/invites))
101
+
102
+ ::: warning Don't re-run pieces manually
103
+ After `pu:saas:setup` runs, don't separately run `pu:saas:portal`, `pu:profile:setup`, `pu:saas:welcome`, or `pu:invites:install`. Pass `--force` to re-run the whole meta-generator.
104
+ :::
105
+
106
+ ```bash
107
+ rails g pu:saas:setup --user Customer --entity Organization
108
+ rails g pu:saas:setup --user Customer --entity Organization --roles=member,admin
109
+ rails g pu:saas:setup --user Customer --entity Organization --no-allow-signup
110
+ rails g pu:saas:setup --user Customer --entity Organization \
111
+ --user-attributes=name:string --entity-attributes=slug:string
112
+ ```
113
+
114
+ | Option | Default | Description |
115
+ |---|---|---|
116
+ | `--user=NAME` | (required) | User account model name |
117
+ | `--entity=NAME` | (required) | Entity model name |
118
+ | `--allow-signup` | `true` | Allow public registration |
119
+ | `--roles` | `admin,member` | Additional roles. **`owner` always prepended as index 0** |
120
+ | `--skip-entity` | | Skip entity model generation |
121
+ | `--skip-membership` | | Skip membership model generation |
122
+ | `--user-attributes`, `--entity-attributes`, `--membership-attributes` | | Extra model fields |
123
+ | `--api_client=NAME` | | Also generate an API client |
124
+ | `--api_client_roles` | `read_only,write,admin` | API client roles |
125
+
126
+ Individual SaaS generators (rarely needed): `pu:saas:user`, `pu:saas:entity`, `pu:saas:membership`, `pu:saas:portal`, `pu:saas:welcome`.
127
+
128
+ Generated user + membership models:
129
+
130
+ ```ruby
131
+ class Customer < ApplicationRecord
132
+ include Rodauth::Rails.model(:customer)
133
+ has_many :organization_customers, dependent: :destroy
134
+ has_many :organizations, through: :organization_customers
135
+ end
136
+
137
+ class OrganizationCustomer < ApplicationRecord
138
+ belongs_to :organization
139
+ belongs_to :customer
140
+ enum :role, owner: 0, admin: 1, member: 2
141
+
142
+ validates :customer, uniqueness: {
143
+ scope: :organization_id,
144
+ message: "is already a member of this organization"
145
+ }
146
+ end
147
+ ```
148
+
149
+ ## API client — `pu:saas:api_client`
150
+
151
+ For machine-to-machine authentication. HTTP Basic Auth with auto-generated password.
152
+
153
+ ```bash
154
+ rails g pu:saas:api_client ApiClient
155
+ rails g pu:saas:api_client ApiClient --entity=Organization
156
+ rails g pu:saas:api_client ApiClient --entity=Organization --roles=read_only,write,admin
157
+ ```
158
+
159
+ | Option | Default | Description |
160
+ |---|---|---|
161
+ | `--entity=NAME` | | Entity to scope API clients to |
162
+ | `--roles` | `read_only,write,admin` | Available roles |
163
+ | `--extra_attributes` | | Additional model attributes |
164
+ | `--dest` | `main_app` | Destination package |
165
+
166
+ CLI creation:
167
+
168
+ ```bash
169
+ rake api_clients:create LOGIN=my-service
170
+ rake api_clients:create LOGIN=my-service ORGANIZATION=acme ROLE=write
171
+ ```
172
+
173
+ ::: warning Credentials shown once
174
+ The auto-generated password (`SecureRandom.base64(32)`) is displayed once at creation and cannot be retrieved later.
175
+ :::
176
+
177
+ ## Common customizations
178
+
179
+ All inside the Rodauth `configure do ... end` block in `app/rodauth/<name>_rodauth_plugin.rb`.
180
+
181
+ ### Custom login redirect
182
+
183
+ ```ruby
184
+ login_redirect do
185
+ rails_account.admin? ? "/admin" : "/dashboard"
186
+ end
187
+ ```
188
+
189
+ ### Custom create-account validation + hook
190
+
191
+ ```ruby
192
+ before_create_account do
193
+ throw_error_status(422, "name", "must be present") if param("name").empty?
194
+ end
195
+
196
+ after_create_account do
197
+ Profile.create!(account_id: account_id, name: param("name"))
198
+ end
199
+ ```
200
+
201
+ ### Password requirements
202
+
203
+ ```ruby
204
+ password_minimum_length 12
205
+
206
+ password_meets_requirements? do |password|
207
+ super(password) && password.match?(/\d/) && password.match?(/[^a-zA-Z\d]/)
208
+ end
209
+ ```
210
+
211
+ ### Multi-phase login (password on a separate page)
212
+
213
+ ```ruby
214
+ use_multi_phase_login? true
215
+ ```
216
+
217
+ ### Prevent public signup (admin pattern)
218
+
219
+ ```ruby
220
+ before_create_account_route do
221
+ request.halt unless internal_request?
222
+ end
223
+ ```
224
+
225
+ ## Related
226
+
227
+ - [Profile](./profile) — profile resource + SecuritySection component
228
+ - [App › Portals › Controller concern (auth)](/reference/app/portals#controller-concern-auth) — wiring accounts into portal controllers
229
+ - [Tenancy › Invites](/reference/tenancy/invites) — invitation system on top of Rodauth signup
230
+ - [App › Generators › Authentication generators](/reference/app/generators#authentication-generators) — full generator catalog
@@ -0,0 +1,88 @@
1
+ # Auth Reference
2
+
3
+ Plutonium uses [Rodauth](http://rodauth.jeremyevans.net/) via [rodauth-rails](https://github.com/janko/rodauth-rails). This area covers Rodauth installation, account types, and the user profile resource.
4
+
5
+ ## Sub-pages
6
+
7
+ - [Accounts](./accounts) — Rodauth install, basic accounts, admin accounts, SaaS setup, account customization
8
+ - [Profile](./profile) — profile resource generator, the SecuritySection component
9
+
10
+ ## 🚨 Critical
11
+
12
+ - **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.
13
+ - **Role index 0 is the most privileged** (`owner`, `super_admin`). Invite interactions default new invitees to **index 1** — the order in `--roles=` matters.
14
+ - **`pu:saas:setup --roles=...` always prepends `owner` as index 0.** Don't include `owner` in the option.
15
+ - **`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 re-run those manually.
16
+ - **Profile association is always `:profile`** regardless of the model class — `current_user.profile`, `build_profile`, `params.require(:profile)`.
17
+ - **Profile needs `pu:profile:conn` to be visible** — without it, the singular `/profile` route and `profile_url` helper don't exist.
18
+ - **Every user needs a profile row.** Add an `after_create` callback or `find_or_create_by` — otherwise `current_user.profile` is nil.
19
+
20
+ ## Install Rodauth
21
+
22
+ ```bash
23
+ rails generate pu:rodauth:install
24
+ ```
25
+
26
+ Installs gems (`rodauth-rails`, `bcrypt`, `sequel-activerecord_connection`), the Roda app at `app/rodauth/rodauth_app.rb`, base plugin and controller, initializer, layout, and a PostgreSQL extension migration if applicable.
27
+
28
+ ## Wire auth into controllers
29
+
30
+ ```ruby
31
+ class ResourceController < PlutoniumController
32
+ include Plutonium::Resource::Controller
33
+ include Plutonium::Auth::Rodauth(:user)
34
+ end
35
+ ```
36
+
37
+ Multiple account types — include the matching `:name`:
38
+
39
+ ```ruby
40
+ class AdminController < PlutoniumController
41
+ include Plutonium::Resource::Controller
42
+ include Plutonium::Auth::Rodauth(:admin)
43
+ end
44
+ ```
45
+
46
+ `Plutonium::Auth::Rodauth(:name)` exposes `current_user`, `logout_url`, and `rodauth` in the controller.
47
+
48
+ For portal wiring (`AdminPortal::Concerns::Controller`), see [App › Portals](/reference/app/portals#controller-concern-auth).
49
+
50
+ ## Email configuration
51
+
52
+ Standard ActionMailer in `config/environments/production.rb`:
53
+
54
+ ```ruby
55
+ config.action_mailer.delivery_method = :smtp
56
+ config.action_mailer.smtp_settings = {
57
+ address: "smtp.example.com",
58
+ port: 587,
59
+ user_name: ENV["SMTP_USER"],
60
+ password: ENV["SMTP_PASSWORD"]
61
+ }
62
+ ```
63
+
64
+ Override templates in `app/views/rodauth/<account>_mailer/`.
65
+
66
+ ## API authentication
67
+
68
+ ```bash
69
+ rails generate pu:rodauth:account api_user --api_only --jwt --jwt_refresh
70
+ ```
71
+
72
+ ```
73
+ POST /api_users/login
74
+ {"login": "user@example.com", "password": "secret"}
75
+ # → {"access_token": "...", "refresh_token": "..."}
76
+
77
+ GET /api/posts
78
+ Authorization: Bearer <access_token>
79
+ ```
80
+
81
+ ## Related
82
+
83
+ - [Accounts](./accounts) — account types and feature flags
84
+ - [Profile](./profile) — profile resource + SecuritySection
85
+ - [Tenancy › Invites](/reference/tenancy/invites) — invitation system on top of Rodauth signup
86
+ - [App › Portals › Controller concern (auth)](/reference/app/portals#controller-concern-auth) — portal-side wiring
87
+ - [Guides › Authentication](/guides/authentication) — task-oriented walkthrough
88
+ - [Guides › User profile](/guides/user-profile)
@@ -0,0 +1,185 @@
1
+ # Profile
2
+
3
+ Manages Rodauth account settings as a Plutonium resource — users view/edit personal fields and access Rodauth security features (change password, 2FA, etc.) on one page.
4
+
5
+ ## 🚨 Critical
6
+
7
+ - **Profile association is always `:profile`** regardless of the model class — `current_user.profile`, `build_profile`, `params.require(:profile)` always work.
8
+ - **Profile needs `pu:profile:conn`** — without it, no route, no `profile_url` helper, no user-menu link.
9
+ - **Every user needs a profile row** — add an `after_create` callback (or `find_or_create_by`). Without it, `current_user.profile` is nil and the profile route errors.
10
+
11
+ ## Quick setup
12
+
13
+ ```bash
14
+ rails g pu:profile:setup date_of_birth:date bio:text \
15
+ --dest=competition \
16
+ --portal=competition_portal
17
+ ```
18
+
19
+ Meta-generator: runs `pu:profile:install` + `pu:profile:conn` in one shot.
20
+
21
+ ## Step-by-step
22
+
23
+ ```bash
24
+ rails generate pu:profile:install bio:text avatar:attachment 'timezone:string?' \
25
+ --dest=customer
26
+
27
+ rails db:migrate
28
+
29
+ rails generate pu:profile:conn --dest=customer_portal
30
+ ```
31
+
32
+ | Option | Default | Description |
33
+ |---|---|---|
34
+ | `--dest=DEST` | (prompts) | Target package or `main_app` |
35
+ | `--user-model=NAME` | `User` | Rodauth user model |
36
+
37
+ Custom resource name (first positional argument):
38
+
39
+ ```bash
40
+ rails g pu:profile:install AccountSettings bio:text --dest=main_app
41
+ ```
42
+
43
+ ## What gets created
44
+
45
+ By default the model is `{UserModel}Profile` — `UserProfile`, `StaffUserProfile`, etc. — derived from `--user-model`.
46
+
47
+ ```
48
+ app/models/[package/]user_profile.rb
49
+ db/migrate/xxx_create_user_profiles.rb
50
+ app/controllers/[package/]user_profiles_controller.rb
51
+ app/policies/[package/]user_profile_policy.rb
52
+ app/definitions/[package/]user_profile_definition.rb
53
+ ```
54
+
55
+ The generator modifies the user model:
56
+
57
+ ```ruby
58
+ has_one :profile, class_name: "UserProfile", dependent: :destroy
59
+ ```
60
+
61
+ ::: warning Association name is fixed
62
+ Even when the class is `StaffUserProfile`, the association is `:profile`. Don't rename it — `current_user.profile`, `build_profile`, `params.require(:profile)` all assume this.
63
+ :::
64
+
65
+ The generated definition injects a custom `ShowPage` that renders the `SecuritySection` component.
66
+
67
+ ## The `SecuritySection` component
68
+
69
+ Dynamically lists Rodauth security links based on which features are enabled on the account.
70
+
71
+ | Feature enabled | Link rendered |
72
+ |---|---|
73
+ | `change_password` | Change Password |
74
+ | `change_login` | Change Email |
75
+ | `otp` | Two-Factor Authentication |
76
+ | `recovery_codes` | Recovery Codes |
77
+ | `webauthn` | Security Keys |
78
+ | `active_sessions` | Active Sessions |
79
+ | `close_account` | Close Account |
80
+
81
+ If a feature isn't enabled on the account, its link doesn't render — no configuration needed.
82
+
83
+ To customize (e.g. add chrome, reorder), override `ShowPage#render_after_content`:
84
+
85
+ ```ruby
86
+ class UserProfileDefinition < Plutonium::Resource::Definition
87
+ class ShowPage < ShowPage
88
+ private
89
+
90
+ def render_after_content
91
+ render Plutonium::Profile::SecuritySection.new
92
+ end
93
+ end
94
+ end
95
+ ```
96
+
97
+ See [UI › Pages](/reference/ui/pages) for page-class customization.
98
+
99
+ ## Required: every user gets a profile
100
+
101
+ ```ruby
102
+ class User < ApplicationRecord
103
+ after_create :create_profile!
104
+
105
+ private
106
+ def create_profile! = create_profile
107
+ end
108
+ ```
109
+
110
+ Without this, `current_user.profile` returns nil and the profile route errors. For existing users at migration time, run a one-off backfill:
111
+
112
+ ```bash
113
+ rails runner "User.find_each(&:create_profile)"
114
+ ```
115
+
116
+ ## Connecting to a portal
117
+
118
+ `pu:profile:conn` registers the profile as a **singular** resource — `/profile` (no `:id`), and exposes the `profile_url` helper:
119
+
120
+ ```bash
121
+ rails g pu:profile:conn --dest=customer_portal
122
+ ```
123
+
124
+ This is what makes the profile visible. Without it, the model exists but has no route in any portal.
125
+
126
+ ## Linking to the profile
127
+
128
+ ```ruby
129
+ link_to("Profile", profile_url) if respond_to?(:profile_url)
130
+ ```
131
+
132
+ The `respond_to?` guard is defensive — only portals that ran `pu:profile:conn` have the helper.
133
+
134
+ ## Customizing the definition
135
+
136
+ The generated definition is a normal Plutonium resource definition. Customize like any other:
137
+
138
+ ```ruby
139
+ class UserProfileDefinition < Plutonium::Resource::Definition
140
+ field :bio, as: :markdown
141
+ input :avatar, as: :uppy
142
+ field :timezone, as: :select, choices: ActiveSupport::TimeZone.all.map(&:name)
143
+
144
+ metadata :created_at, :updated_at
145
+
146
+ class ShowPage < ShowPage
147
+ private
148
+
149
+ def render_after_content
150
+ render Plutonium::Profile::SecuritySection.new
151
+ end
152
+ end
153
+ end
154
+ ```
155
+
156
+ See [Resource › Definition](/reference/resource/definition) for the full definition surface.
157
+
158
+ ## Multiple account types
159
+
160
+ If your app has both `User` and `StaffUser` accounts, run `pu:profile:install` once per:
161
+
162
+ ```bash
163
+ rails g pu:profile:install --user-model=User --dest=main_app
164
+ rails g pu:profile:install --user-model=StaffUser --dest=main_app
165
+ ```
166
+
167
+ Each gets its own `*Profile` model with `:profile` association on the respective user. Connect each to the appropriate portal:
168
+
169
+ ```bash
170
+ rails g pu:profile:conn UserProfile --dest=customer_portal
171
+ rails g pu:profile:conn StaffUserProfile --dest=admin_portal
172
+ ```
173
+
174
+ ## Gotchas
175
+
176
+ - **`current_user.profile` is nil** — every user needs a profile row. Add `after_create :create_profile!` to the user model.
177
+ - **`profile_url` is undefined** — the profile isn't connected to this portal. Run `pu:profile:conn --dest=<portal>`.
178
+ - **Custom resource name** — pass it as the first positional argument to `pu:profile:install`. The association is still `:profile`.
179
+ - **SecuritySection shows nothing** — none of the relevant Rodauth features are enabled on the account. Enable `change_password`, `otp`, etc. on the Rodauth plugin.
180
+
181
+ ## Related
182
+
183
+ - [Accounts](./accounts) — Rodauth feature flags that gate SecuritySection links
184
+ - [Resource › Definition](/reference/resource/definition) — customizing the profile definition
185
+ - [App › Generators › Profile generators](/reference/app/generators#profile-generators)