plutonium 0.50.0 → 0.52.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 (201) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +574 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +167 -302
  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 +674 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +9 -6
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +44 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1010 -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 +38 -29
  18. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
  19. data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
  20. data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
  21. data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
  22. data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
  23. data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
  24. data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
  25. data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
  26. data/docs/.vitepress/theme/custom.css +144 -0
  27. data/docs/.vitepress/theme/index.ts +58 -1
  28. data/docs/getting-started/index.md +33 -57
  29. data/docs/getting-started/installation.md +37 -80
  30. data/docs/getting-started/tutorial/02-first-resource.md +17 -8
  31. data/docs/getting-started/tutorial/03-authentication.md +31 -23
  32. data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
  33. data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
  34. data/docs/getting-started/tutorial/07-author-portal.md +8 -0
  35. data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
  36. data/docs/getting-started/tutorial/index.md +4 -5
  37. data/docs/guides/adding-resources.md +66 -377
  38. data/docs/guides/authentication.md +98 -462
  39. data/docs/guides/authorization.md +124 -370
  40. data/docs/guides/creating-packages.md +93 -298
  41. data/docs/guides/custom-actions.md +126 -441
  42. data/docs/guides/customizing-ui.md +258 -0
  43. data/docs/guides/index.md +49 -52
  44. data/docs/guides/multi-tenancy.md +123 -186
  45. data/docs/guides/nested-resources.md +137 -396
  46. data/docs/guides/search-filtering.md +127 -238
  47. data/docs/guides/testing.md +10 -5
  48. data/docs/guides/theming.md +168 -405
  49. data/docs/guides/troubleshooting.md +5 -3
  50. data/docs/guides/user-invites.md +112 -425
  51. data/docs/guides/user-profile.md +82 -241
  52. data/docs/index.md +10 -219
  53. data/docs/public/asciinema/home-scaffold.cast +305 -0
  54. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  55. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  56. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  57. data/docs/public/images/guides/nested-inputs.png +0 -0
  58. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  59. data/docs/public/images/guides/search-filtering-index.png +0 -0
  60. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  61. data/docs/public/images/guides/theming-after.png +0 -0
  62. data/docs/public/images/guides/theming-before.png +0 -0
  63. data/docs/public/images/guides/user-invites-landing.png +0 -0
  64. data/docs/public/images/guides/user-profile-edit.png +0 -0
  65. data/docs/public/images/guides/user-profile-show.png +0 -0
  66. data/docs/public/images/home-index.png +0 -0
  67. data/docs/public/images/home-new.png +0 -0
  68. data/docs/public/images/home-show.png +0 -0
  69. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  70. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  71. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  72. data/docs/public/images/tutorial/02-new-form.png +0 -0
  73. data/docs/public/images/tutorial/03-create-account.png +0 -0
  74. data/docs/public/images/tutorial/03-login.png +0 -0
  75. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  76. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  77. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  78. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  79. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  80. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  81. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  82. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  83. data/docs/reference/app/generators.md +517 -0
  84. data/docs/reference/app/index.md +158 -0
  85. data/docs/reference/app/packages.md +146 -0
  86. data/docs/reference/app/portals.md +377 -0
  87. data/docs/reference/auth/accounts.md +229 -0
  88. data/docs/reference/auth/index.md +88 -0
  89. data/docs/reference/auth/profile.md +185 -0
  90. data/docs/reference/behavior/controllers.md +395 -0
  91. data/docs/reference/behavior/index.md +22 -0
  92. data/docs/reference/behavior/interactions.md +341 -0
  93. data/docs/reference/behavior/policies.md +417 -0
  94. data/docs/reference/index.md +67 -48
  95. data/docs/reference/resource/actions.md +423 -0
  96. data/docs/reference/resource/definition.md +508 -0
  97. data/docs/reference/resource/index.md +50 -0
  98. data/docs/reference/resource/model.md +348 -0
  99. data/docs/reference/resource/query.md +305 -0
  100. data/docs/reference/tenancy/entity-scoping.md +368 -0
  101. data/docs/reference/tenancy/index.md +36 -0
  102. data/docs/reference/tenancy/invites.md +400 -0
  103. data/docs/reference/tenancy/nested-resources.md +267 -0
  104. data/docs/reference/testing/index.md +287 -0
  105. data/docs/reference/ui/assets.md +400 -0
  106. data/docs/reference/ui/components.md +165 -0
  107. data/docs/reference/ui/displays.md +104 -0
  108. data/docs/reference/ui/forms.md +284 -0
  109. data/docs/reference/ui/index.md +30 -0
  110. data/docs/reference/ui/layouts.md +106 -0
  111. data/docs/reference/ui/pages.md +189 -0
  112. data/docs/reference/ui/tables.md +121 -0
  113. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
  114. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
  115. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  116. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  117. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  118. data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
  119. data/gemfiles/rails_7.gemfile.lock +1 -1
  120. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  121. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  122. data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
  123. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  124. data/lib/generators/pu/invites/install_generator.rb +45 -0
  125. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
  126. data/lib/generators/pu/profile/conn_generator.rb +2 -2
  127. data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
  128. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
  129. data/lib/generators/pu/rodauth/account_generator.rb +2 -1
  130. data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
  131. data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
  132. data/lib/generators/pu/rodauth/views_generator.rb +0 -2
  133. data/lib/generators/pu/saas/membership/USAGE +4 -1
  134. data/lib/generators/pu/saas/setup_generator.rb +16 -4
  135. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
  136. data/lib/plutonium/definition/base.rb +1 -1
  137. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  138. data/lib/plutonium/helpers/turbo_helper.rb +30 -0
  139. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  140. data/lib/plutonium/resource/controller.rb +1 -0
  141. data/lib/plutonium/resource/controllers/crud_actions.rb +23 -5
  142. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  143. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  144. data/lib/plutonium/resource/policy.rb +7 -0
  145. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  146. data/lib/plutonium/ui/component/methods.rb +5 -0
  147. data/lib/plutonium/ui/form/base.rb +23 -3
  148. data/lib/plutonium/ui/form/components/json.rb +58 -0
  149. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  150. data/lib/plutonium/ui/form/components/secure_association.rb +103 -22
  151. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  152. data/lib/plutonium/ui/form/interaction.rb +1 -1
  153. data/lib/plutonium/ui/form/resource.rb +0 -4
  154. data/lib/plutonium/ui/form/theme.rb +1 -1
  155. data/lib/plutonium/ui/grid/resource.rb +1 -1
  156. data/lib/plutonium/ui/layout/base.rb +1 -0
  157. data/lib/plutonium/ui/page/base.rb +0 -7
  158. data/lib/plutonium/ui/page/edit.rb +1 -1
  159. data/lib/plutonium/ui/page/index.rb +4 -4
  160. data/lib/plutonium/ui/page/new.rb +1 -1
  161. data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
  162. data/lib/plutonium/ui/table/resource.rb +1 -1
  163. data/lib/plutonium/version.rb +1 -1
  164. data/lib/plutonium.rb +8 -0
  165. data/lib/tasks/release.rake +15 -1
  166. data/package.json +13 -10
  167. data/src/css/slim_select.css +4 -0
  168. data/src/js/controllers/form_controller.js +5 -4
  169. data/src/js/controllers/slim_select_controller.js +61 -0
  170. data/src/js/turbo/turbo_actions.js +33 -0
  171. data/yarn.lock +661 -544
  172. metadata +86 -33
  173. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  174. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  175. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  176. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  177. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  178. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  179. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  180. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  181. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  182. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  183. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  184. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  185. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  186. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  187. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  188. data/docs/reference/assets/index.md +0 -496
  189. data/docs/reference/controller/index.md +0 -412
  190. data/docs/reference/definition/actions.md +0 -462
  191. data/docs/reference/definition/fields.md +0 -383
  192. data/docs/reference/definition/index.md +0 -326
  193. data/docs/reference/definition/query.md +0 -351
  194. data/docs/reference/generators/index.md +0 -648
  195. data/docs/reference/interaction/index.md +0 -449
  196. data/docs/reference/model/features.md +0 -248
  197. data/docs/reference/model/index.md +0 -218
  198. data/docs/reference/policy/index.md +0 -456
  199. data/docs/reference/portal/index.md +0 -379
  200. data/docs/reference/views/forms.md +0 -411
  201. data/docs/reference/views/index.md +0 -544
@@ -0,0 +1,229 @@
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
+ | `--no-mails` | Skip mailer setup |
18
+ | `--argon2` | Use Argon2 instead of bcrypt |
19
+ | `--api_only` | JSON API only (no sessions) |
20
+
21
+ ### Feature flags
22
+
23
+ | Flag | Default | Purpose |
24
+ |---|---|---|
25
+ | `--login`, `--logout`, `--remember` | ✓ | Basic auth |
26
+ | `--create_account`, `--verify_account` | ✓ | Registration + email verification |
27
+ | `--verify_account_grace_period` | ✓ | Grace period before verification is required |
28
+ | `--reset_password`, `--reset_password_notify` | ✓ | Password reset via email + notification |
29
+ | `--change_password`, `--change_password_notify` | ✓ | Password change + notification |
30
+ | `--change_login`, `--verify_login_change` | ✓ | Email change with verification |
31
+ | `--case_insensitive_login` | ✓ | Case-insensitive email matching |
32
+ | `--internal_request` | ✓ | Internal request support |
33
+ | `--otp` | | TOTP 2FA |
34
+ | `--webauthn` | | WebAuthn / passkeys |
35
+ | `--recovery_codes` | | 2FA backup codes |
36
+ | `--lockout` | | Lock after failed attempts |
37
+ | `--active_sessions` | | Track active sessions |
38
+ | `--audit_logging` | | Log auth events |
39
+ | `--close_account` | | Allow account deletion |
40
+ | `--email_auth` | | Passwordless email login |
41
+ | `--sms_codes` | | SMS 2FA |
42
+ | `--jwt`, `--jwt_refresh` | | JWT for API auth |
43
+ | `--password_expiration` | | Force periodic password changes |
44
+ | `--disallow_password_reuse` | | Prevent reusing recent passwords |
45
+
46
+ ### Examples
47
+
48
+ ```bash
49
+ # Basic account
50
+ rails g pu:rodauth:account user
51
+
52
+ # With 2FA
53
+ rails g pu:rodauth:account user --otp --recovery_codes
54
+
55
+ # API only
56
+ rails g pu:rodauth:account api_user --api_only --jwt --jwt_refresh
57
+
58
+ # Kitchen sink
59
+ rails g pu:rodauth:account user --kitchen_sink
60
+ ```
61
+
62
+ ## Admin account — `pu:rodauth:admin`
63
+
64
+ 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**.
65
+
66
+ ```bash
67
+ rails g pu:rodauth:admin admin
68
+ rails g pu:rodauth:admin admin --roles=super_admin,admin,viewer
69
+ rails g pu:rodauth:admin admin --extra-attributes=name:string,department:string
70
+ ```
71
+
72
+ | Option | Default | Description |
73
+ |---|---|---|
74
+ | `--roles` | `super_admin,admin` | Comma-separated roles (positional enum) |
75
+ | `--extra_attributes` | | Additional model attributes (e.g. `name:string`) |
76
+
77
+ **Role-ordering convention:** index 0 is the most privileged. Generated invite interaction defaults new invitees to `roles[1]` — the order in `--roles=` matters.
78
+
79
+ ```ruby
80
+ enum :role, super_admin: 0, admin: 1
81
+ ```
82
+
83
+ Rake task for direct admin creation (generated alongside the account — namespace is `rodauth`, task name is the account name):
84
+
85
+ ```bash
86
+ EMAIL=admin@example.com rails rodauth:admin
87
+ # (run without EMAIL to be prompted)
88
+ ```
89
+
90
+ The task creates the account and triggers a verification email; the admin sets their own password via that flow. No password is passed on the command line.
91
+
92
+ ## SaaS setup — `pu:saas:setup` (meta-generator)
93
+
94
+ Creates the User + Entity + Membership trio AND runs:
95
+
96
+ - `pu:saas:portal` → a full `{Entity}Portal` scoped to the entity
97
+ - `pu:profile:setup` → profile model + association (see [Profile](./profile))
98
+ - `pu:saas:welcome` → onboarding / select-entity flow
99
+ - `pu:invites:install` → the invites package (see [Tenancy › Invites](/reference/tenancy/invites))
100
+
101
+ ::: warning Don't re-run pieces manually
102
+ 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.
103
+ :::
104
+
105
+ ```bash
106
+ rails g pu:saas:setup --user Customer --entity Organization
107
+ rails g pu:saas:setup --user Customer --entity Organization --roles=admin,member
108
+ rails g pu:saas:setup --user Customer --entity Organization --no-allow-signup
109
+ rails g pu:saas:setup --user Customer --entity Organization \
110
+ --user-attributes=name:string --entity-attributes=slug:string
111
+ ```
112
+
113
+ | Option | Default | Description |
114
+ |---|---|---|
115
+ | `--user=NAME` | (required) | User account model name |
116
+ | `--entity=NAME` | (required) | Entity model name |
117
+ | `--allow-signup` | `true` | Allow public registration |
118
+ | `--roles` | `admin,member` | Additional roles. **`owner` always prepended as index 0** |
119
+ | `--skip-entity` | | Skip entity model generation |
120
+ | `--skip-membership` | | Skip membership model generation |
121
+ | `--user-attributes`, `--entity-attributes`, `--membership-attributes` | | Extra model fields |
122
+ | `--api_client=NAME` | | Also generate an API client |
123
+ | `--api_client_roles` | `read_only,write,admin` | API client roles |
124
+
125
+ Individual SaaS generators (rarely needed): `pu:saas:user`, `pu:saas:entity`, `pu:saas:membership`, `pu:saas:portal`, `pu:saas:welcome`.
126
+
127
+ Generated user + membership models:
128
+
129
+ ```ruby
130
+ class Customer < ApplicationRecord
131
+ include Rodauth::Rails.model(:customer)
132
+ has_many :organization_customers, dependent: :destroy
133
+ has_many :organizations, through: :organization_customers
134
+ end
135
+
136
+ class OrganizationCustomer < ApplicationRecord
137
+ belongs_to :organization
138
+ belongs_to :customer
139
+ enum :role, owner: 0, admin: 1, member: 2
140
+
141
+ validates :customer, uniqueness: {
142
+ scope: :organization_id,
143
+ message: "is already a member of this organization"
144
+ }
145
+ end
146
+ ```
147
+
148
+ ## API client — `pu:saas:api_client`
149
+
150
+ For machine-to-machine authentication. HTTP Basic Auth with auto-generated password.
151
+
152
+ ```bash
153
+ rails g pu:saas:api_client ApiClient
154
+ rails g pu:saas:api_client ApiClient --entity=Organization
155
+ rails g pu:saas:api_client ApiClient --entity=Organization --roles=read_only,write,admin
156
+ ```
157
+
158
+ | Option | Default | Description |
159
+ |---|---|---|
160
+ | `--entity=NAME` | | Entity to scope API clients to |
161
+ | `--roles` | `read_only,write,admin` | Available roles |
162
+ | `--extra_attributes` | | Additional model attributes |
163
+ | `--dest` | `main_app` | Destination package |
164
+
165
+ CLI creation:
166
+
167
+ ```bash
168
+ rake api_clients:create LOGIN=my-service
169
+ rake api_clients:create LOGIN=my-service ORGANIZATION=acme ROLE=write
170
+ ```
171
+
172
+ ::: warning Credentials shown once
173
+ The auto-generated password (`SecureRandom.base64(32)`) is displayed once at creation and cannot be retrieved later.
174
+ :::
175
+
176
+ ## Common customizations
177
+
178
+ All inside the Rodauth `configure do ... end` block in `app/rodauth/<name>_rodauth_plugin.rb`.
179
+
180
+ ### Custom login redirect
181
+
182
+ ```ruby
183
+ login_redirect do
184
+ rails_account.admin? ? "/admin" : "/dashboard"
185
+ end
186
+ ```
187
+
188
+ ### Custom create-account validation + hook
189
+
190
+ ```ruby
191
+ before_create_account do
192
+ throw_error_status(422, "name", "must be present") if param("name").empty?
193
+ end
194
+
195
+ after_create_account do
196
+ Profile.create!(account_id: account_id, name: param("name"))
197
+ end
198
+ ```
199
+
200
+ ### Password requirements
201
+
202
+ ```ruby
203
+ password_minimum_length 12
204
+
205
+ password_meets_requirements? do |password|
206
+ super(password) && password.match?(/\d/) && password.match?(/[^a-zA-Z\d]/)
207
+ end
208
+ ```
209
+
210
+ ### Multi-phase login (password on a separate page)
211
+
212
+ ```ruby
213
+ use_multi_phase_login? true
214
+ ```
215
+
216
+ ### Prevent public signup (admin pattern)
217
+
218
+ ```ruby
219
+ before_create_account_route do
220
+ request.halt unless internal_request?
221
+ end
222
+ ```
223
+
224
+ ## Related
225
+
226
+ - [Profile](./profile) — profile resource + SecuritySection component
227
+ - [App › Portals › Controller concern (auth)](/reference/app/portals#controller-concern-auth) — wiring accounts into portal controllers
228
+ - [Tenancy › Invites](/reference/tenancy/invites) — invitation system on top of Rodauth signup
229
+ - [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`, `profile_url`, and `rodauth` in the controller (all available as helper methods in views too).
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)