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
@@ -1,107 +1,76 @@
1
1
  ---
2
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.
3
+ description: Use BEFORE installing Rodauth, configuring account types, building login/password flows, or wiring a profile / account-settings page. Covers the full auth surface — Rodauth installation, accounts, admin accounts, SaaS setup, profile resource, security section.
4
4
  ---
5
5
 
6
- # Plutonium Authentication (Rodauth + Profile)
6
+ # Plutonium Auth Rodauth + Profile
7
7
 
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).
8
+ Plutonium integrates [Rodauth](http://rodauth.jeremyevans.net/) via [rodauth-rails](https://github.com/janko/rodauth-rails). This skill covers installing Rodauth, generating account types (basic / admin / SaaS), wiring auth into controllers and portals, and the profile / account-settings resource.
14
9
 
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.
10
+ For multi-tenant invitations and membership, see [[plutonium-tenancy]] Invites. For portal-side wiring, see [[plutonium-app]] Portal Engines.
16
11
 
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)
12
+ ## 🚨 Critical (read first)
27
13
 
28
- ## Rodauth setup
14
+ - **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.
15
+ - **Role index 0 is the most privileged** (`owner`, `super_admin`). Invite interactions default new invitees to **index 1**.
16
+ - **`pu:saas:setup --roles=...` always prepends `owner` as index 0.** Don't include `owner` in the option.
17
+ - **`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.
18
+ - **Profile association is always `:profile`** regardless of the model class — `current_user.profile`, `build_profile`, `params.require(:profile)`.
19
+ - **Profile needs `pu:profile:conn` to be visible** — without it, the singular `/profile` route and `profile_url` helper don't exist.
20
+ - **Every user needs a profile row.** Add an `after_create` callback or `find_or_create_by` — otherwise `current_user.profile` is nil.
29
21
 
30
- ### Step 1: Install Rodauth Base
22
+ ---
23
+
24
+ ## Install
31
25
 
32
26
  ```bash
33
27
  rails generate pu:rodauth:install
34
28
  ```
35
29
 
36
- This installs:
37
- - Required gems (`rodauth-rails`, `bcrypt`, `sequel-activerecord_connection`)
38
- - `app/rodauth/rodauth_app.rb` - Main Roda app
39
- - `app/rodauth/rodauth_plugin.rb` - Base plugin
40
- - `app/controllers/rodauth_controller.rb` - Base controller
41
- - `config/initializers/rodauth.rb` - Configuration
42
- - `app/views/layouts/rodauth.html.erb` - Auth layout
43
- - PostgreSQL extension migration (if using PostgreSQL)
44
-
45
- ### Step 2: Create Account Type
46
-
47
- ```bash
48
- # Basic user account
49
- rails generate pu:rodauth:account user
50
-
51
- # Admin with 2FA and security features
52
- rails generate pu:rodauth:admin admin
30
+ 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.
53
31
 
54
- # SaaS user with entity/organization (multi-tenant)
55
- rails generate pu:saas:setup --user Customer --entity Organization
56
- ```
32
+ ---
57
33
 
58
34
  ## Account types
59
35
 
60
- ### Basic Account (`pu:rodauth:account`)
36
+ Pick one (or several — apps can have multiple account types side-by-side).
37
+
38
+ ### Basic account — `pu:rodauth:account`
61
39
 
62
40
  ```bash
63
41
  rails generate pu:rodauth:account user [options]
64
42
  ```
65
43
 
66
- **Options:**
67
-
68
44
  | Option | Description |
69
- |--------|-------------|
70
- | `--defaults` | Enable default features (login, logout, remember, password reset) |
71
- | `--kitchen_sink` | Enable ALL available features |
72
- | `--primary` | Mark as primary account (no URL prefix) |
45
+ |---|---|
46
+ | `--defaults` | Enables login, logout, remember, password reset |
47
+ | `--kitchen_sink` | Enables ALL features |
73
48
  | `--no-mails` | Skip mailer setup |
74
- | `--argon2` | Use Argon2 instead of bcrypt for password hashing |
75
- | `--api_only` | Configure for JSON API only (no sessions) |
76
-
77
- **Feature Options:**
78
-
79
- | Option | Default | Description |
80
- |--------|---------|-------------|
81
- | `--login` | ✓ | Login functionality |
82
- | `--logout` | ✓ | Logout functionality |
83
- | `--remember` | ✓ | "Remember me" cookies |
84
- | `--create_account` | ✓ | User registration |
85
- | `--verify_account` | | Email verification |
86
- | `--reset_password` | | Password reset via email |
87
- | `--change_password` | | Change password |
88
- | `--change_login` | | Change email |
89
- | `--verify_login_change` | ✓ | Verify email change |
90
- | `--otp` | | TOTP two-factor auth |
91
- | `--webauthn` | | WebAuthn/passkeys |
92
- | `--recovery_codes` | | Recovery codes for 2FA |
93
- | `--lockout` | | Account lockout after failed attempts |
49
+ | `--argon2` | Use Argon2 instead of bcrypt |
50
+ | `--api_only` | JSON API only (no sessions) |
51
+
52
+ ### Feature flags
53
+
54
+ | Flag | Default | Purpose |
55
+ |---|---|---|
56
+ | `--login`, `--logout`, `--remember` | ✓ | Basic auth |
57
+ | `--create_account`, `--verify_account` | ✓ | Registration + email verification |
58
+ | `--reset_password`, `--change_password` | ✓ | Password lifecycle |
59
+ | `--change_login`, `--verify_login_change` | ✓ | Email change |
60
+ | `--otp` | | TOTP 2FA |
61
+ | `--webauthn` | | WebAuthn / passkeys |
62
+ | `--recovery_codes` | | 2FA backup codes |
63
+ | `--lockout` | | Lock after failed attempts |
94
64
  | `--active_sessions` | | Track active sessions |
95
- | `--audit_logging` | | Audit authentication events |
65
+ | `--audit_logging` | | Log auth events |
96
66
  | `--close_account` | | Allow account deletion |
97
- | `--email_auth` | | Passwordless login via email |
98
- | `--sms_codes` | | SMS-based 2FA |
99
- | `--jwt` | | JWT token authentication |
100
- | `--jwt_refresh` | | JWT refresh tokens |
67
+ | `--email_auth` | | Passwordless email login |
68
+ | `--sms_codes` | | SMS 2FA |
69
+ | `--jwt`, `--jwt_refresh` | | JWT for API auth |
101
70
 
102
- ### Admin Account (`pu:rodauth:admin`)
71
+ ### Admin account `pu:rodauth:admin`
103
72
 
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.
73
+ 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**.
105
74
 
106
75
  ```bash
107
76
  rails generate pu:rodauth:admin admin
@@ -109,80 +78,70 @@ rails generate pu:rodauth:admin admin --roles=super_admin,admin,viewer
109
78
  rails generate pu:rodauth:admin admin --extra-attributes=name:string,department:string
110
79
  ```
111
80
 
112
- **Options:**
113
-
114
81
  | Option | Default | Description |
115
- |--------|---------|-------------|
116
- | `--roles` | super_admin,admin | Comma-separated roles for admin accounts |
117
- | `--extra_attributes` | | Additional model attributes (e.g., name:string) |
82
+ |---|---|---|
83
+ | `--roles` | `super_admin,admin` | Comma-separated roles (positional enum) |
84
+ | `--extra_attributes` | | Additional model attributes (e.g. `name:string`) |
118
85
 
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.
86
+ **Role-ordering convention:** index 0 is the most privileged. Generated invite interaction defaults new invitees to `roles[1]` the order in `--roles=` matters.
120
87
 
121
88
  ```ruby
122
- # app/models/admin.rb
123
89
  enum :role, super_admin: 0, admin: 1
124
90
  ```
125
91
 
126
- ```ruby
127
- # app/interactions/admin/invite_interaction.rb
128
- class Admin::InviteInteraction < Plutonium::Interaction::Base
129
- attribute :email, :string
130
- attribute :role, default: :admin
131
- # ...
132
- end
133
- ```
92
+ Rake task for direct admin creation (namespace is `rodauth`, task name is the account name):
134
93
 
135
- Rake task for direct creation:
136
94
  ```bash
137
- rails rodauth_admin:create[admin@example.com,password123]
95
+ EMAIL=admin@example.com rails rodauth:admin
96
+ # (run without EMAIL to be prompted)
138
97
  ```
139
98
 
140
- ### SaaS Setup (`pu:saas:setup`)
99
+ Creates the account and sends a verification email; the admin sets their own password through the flow. No password is passed on the command line.
100
+
101
+ ### SaaS setup — `pu:saas:setup` (meta-generator)
102
+
103
+ Creates the User + Entity + Membership trio AND runs:
141
104
 
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.
105
+ - `pu:saas:portal` a full `{Entity}Portal` scoped to the entity
106
+ - `pu:profile:setup` → profile model + association
107
+ - `pu:saas:welcome` → onboarding / select-entity flow
108
+ - `pu:invites:install` → the invites package (see [[plutonium-tenancy]])
109
+
110
+ Don't generate another entity portal after this. Pass `--force` to re-run.
149
111
 
150
112
  ```bash
151
- rails generate pu:saas:setup --user Customer --entity Organization
152
- rails generate pu:saas:setup --user Customer --entity Organization --roles=member,admin,owner
153
- rails generate pu:saas:setup --user Customer --entity Organization --no-allow-signup
154
- rails generate pu:saas:setup --user Customer --entity Organization --user-attributes=name:string --entity-attributes=slug:string
113
+ rails g pu:saas:setup --user Customer --entity Organization
114
+ rails g pu:saas:setup --user Customer --entity Organization --roles=admin,member
115
+ rails g pu:saas:setup --user Customer --entity Organization --no-allow-signup
116
+ rails g pu:saas:setup --user Customer --entity Organization \
117
+ --user-attributes=name:string --entity-attributes=slug:string
155
118
  ```
156
119
 
157
- **Options:**
158
-
159
120
  | Option | Default | Description |
160
- |--------|---------|-------------|
121
+ |---|---|---|
161
122
  | `--user=NAME` | (required) | User account model name |
162
123
  | `--entity=NAME` | (required) | Entity model name |
163
- | `--allow-signup` | true | Allow public registration |
164
- | `--roles` | admin,member | Additional membership roles. **`owner` is always prepended as index 0** — don't include it. |
165
- | `--skip-entity` | false | Skip entity model generation |
166
- | `--skip-membership` | false | Skip membership model generation |
167
- | `--user-attributes` | | Additional user model attributes |
168
- | `--entity-attributes` | | Additional entity model attributes |
169
- | `--membership-attributes` | | Additional membership model attributes |
124
+ | `--allow-signup` | `true` | Allow public registration |
125
+ | `--roles` | `admin,member` | Additional roles. **`owner` always prepended as index 0** |
126
+ | `--skip-entity` | | Skip entity model generation |
127
+ | `--skip-membership` | | Skip membership model generation |
128
+ | `--user-attributes`, `--entity-attributes`, `--membership-attributes` | | Extra model fields |
170
129
 
171
- Individual generators: `pu:saas:user`, `pu:saas:entity`, `pu:saas:membership`.
130
+ Individual generators (rarely needed): `pu:saas:user`, `pu:saas:entity`, `pu:saas:membership`.
131
+
132
+ Generated user + membership:
172
133
 
173
134
  ```ruby
174
- # app/models/customer.rb
175
135
  class Customer < ApplicationRecord
176
136
  include Rodauth::Rails.model(:customer)
177
137
  has_many :organization_customers, dependent: :destroy
178
138
  has_many :organizations, through: :organization_customers
179
139
  end
180
140
 
181
- # app/models/organization_customer.rb
182
141
  class OrganizationCustomer < ApplicationRecord
183
142
  belongs_to :organization
184
143
  belongs_to :customer
185
- enum :role, member: 0, owner: 1
144
+ enum :role, owner: 0, admin: 1, member: 2
186
145
 
187
146
  validates :customer, uniqueness: {
188
147
  scope: :organization_id,
@@ -191,17 +150,18 @@ class OrganizationCustomer < ApplicationRecord
191
150
  end
192
151
  ```
193
152
 
194
- ## Connecting auth to controllers
153
+ ---
154
+
155
+ ## Wiring auth into controllers
195
156
 
196
157
  ```ruby
197
- # app/controllers/resource_controller.rb
198
158
  class ResourceController < PlutoniumController
199
159
  include Plutonium::Resource::Controller
200
160
  include Plutonium::Auth::Rodauth(:user)
201
161
  end
202
162
  ```
203
163
 
204
- Multiple account types:
164
+ Multiple account types — include the matching `:name`:
205
165
 
206
166
  ```ruby
207
167
  class AdminController < PlutoniumController
@@ -210,72 +170,67 @@ class AdminController < PlutoniumController
210
170
  end
211
171
  ```
212
172
 
213
- Including `Plutonium::Auth::Rodauth(:name)` adds `current_user`, `logout_url`, and `rodauth`.
173
+ `Plutonium::Auth::Rodauth(:name)` exposes `current_user`, `logout_url`, and `rodauth` in the controller.
174
+
175
+ For portal wiring (`AdminPortal::Concerns::Controller`), see [[plutonium-app]] › Portal controller concern.
176
+
177
+ ---
178
+
179
+ ## Common customizations
214
180
 
215
- ## Customization
181
+ All inside the Rodauth `configure do ... end` block in `app/rodauth/<name>_rodauth_plugin.rb`.
216
182
 
217
- ### Custom Login Redirect
183
+ ### Custom login redirect
218
184
 
219
185
  ```ruby
220
- configure do
221
- login_redirect do
222
- if rails_account.admin?
223
- "/admin"
224
- else
225
- "/dashboard"
226
- end
227
- end
186
+ login_redirect do
187
+ rails_account.admin? ? "/admin" : "/dashboard"
228
188
  end
229
189
  ```
230
190
 
231
- ### Custom Validation
191
+ ### Custom create-account validation + hook
232
192
 
233
193
  ```ruby
234
- configure do
235
- before_create_account do
236
- throw_error_status(422, "name", "must be present") if param("name").empty?
237
- end
238
-
239
- after_create_account do
240
- Profile.create!(account_id: account_id, name: param("name"))
241
- end
194
+ before_create_account do
195
+ throw_error_status(422, "name", "must be present") if param("name").empty?
196
+ end
197
+
198
+ after_create_account do
199
+ Profile.create!(account_id: account_id, name: param("name"))
242
200
  end
243
201
  ```
244
202
 
245
- ### Password Requirements
203
+ ### Password requirements
246
204
 
247
205
  ```ruby
248
- configure do
249
- password_minimum_length 12
206
+ password_minimum_length 12
250
207
 
251
- password_meets_requirements? do |password|
252
- super(password) && password.match?(/\d/) && password.match?(/[^a-zA-Z\d]/)
253
- end
208
+ password_meets_requirements? do |password|
209
+ super(password) && password.match?(/\d/) && password.match?(/[^a-zA-Z\d]/)
254
210
  end
255
211
  ```
256
212
 
257
- ### Multi-Phase Login
213
+ ### Multi-phase login (password on a separate page)
258
214
 
259
215
  ```ruby
260
- configure do
261
- use_multi_phase_login? true
262
- end
216
+ use_multi_phase_login? true
263
217
  ```
264
218
 
265
- ### Prevent Public Signup
219
+ ### Prevent public signup (admin pattern)
266
220
 
267
221
  ```ruby
268
- configure do
269
- before_create_account_route do
270
- request.halt unless internal_request?
271
- end
222
+ before_create_account_route do
223
+ request.halt unless internal_request?
272
224
  end
273
225
  ```
274
226
 
227
+ ---
228
+
275
229
  ## Email configuration
276
230
 
231
+ Standard ActionMailer in `config/environments/production.rb`:
232
+
277
233
  ```ruby
278
- # config/environments/production.rb
279
234
  config.action_mailer.delivery_method = :smtp
280
235
  config.action_mailer.smtp_settings = {
281
236
  address: "smtp.example.com",
@@ -285,39 +240,9 @@ config.action_mailer.smtp_settings = {
285
240
  }
286
241
  ```
287
242
 
288
- Override templates in `app/views/rodauth/user_mailer/`.
243
+ Override templates in `app/views/rodauth/<account>_mailer/`.
289
244
 
290
- ## Portal integration
291
-
292
- ```bash
293
- rails generate pu:pkg:portal admin
294
- # Select "Rodauth account" → "admin"
295
- ```
296
-
297
- Manual:
298
-
299
- ```ruby
300
- module AdminPortal
301
- class Engine < Rails::Engine
302
- include Plutonium::Portal::Engine
303
-
304
- config.before_initialize do
305
- config.to_prepare do
306
- AdminPortal::ResourceController.class_eval do
307
- include Plutonium::Auth::Rodauth(:admin)
308
- before_action :require_authenticated
309
-
310
- private
311
-
312
- def require_authenticated
313
- redirect_to rodauth.login_path unless current_user
314
- end
315
- end
316
- end
317
- end
318
- end
319
- end
320
- ```
245
+ ---
321
246
 
322
247
  ## API authentication
323
248
 
@@ -334,40 +259,37 @@ GET /api/posts
334
259
  Authorization: Bearer <access_token>
335
260
  ```
336
261
 
337
- ## Profile page
262
+ ---
263
+
264
+ ## Profile resource
338
265
 
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.
266
+ Manages Rodauth account settings: view/edit personal fields plus links to Rodauth security features (change password, 2FA, etc.).
340
267
 
341
- ### Quick setup
268
+ ### Quick setup (with extra fields)
342
269
 
343
270
  ```bash
344
271
  rails g pu:profile:setup date_of_birth:date bio:text \
345
- --dest=competition \
346
- --portal=competition_portal
272
+ --dest=competition \
273
+ --portal=competition_portal
347
274
  ```
348
275
 
349
- ### Step-by-step install
276
+ ### Step-by-step
350
277
 
351
278
  ```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 |
279
+ rails generate pu:profile:install bio:text avatar:attachment 'timezone:string?' \
280
+ --dest=customer
359
281
 
360
- With fields:
282
+ rails db:migrate
361
283
 
362
- ```bash
363
- rails g pu:profile:install \
364
- bio:text \
365
- avatar:attachment \
366
- 'timezone:string?' \
367
- --dest=customer
284
+ rails generate pu:profile:conn --dest=customer_portal
368
285
  ```
369
286
 
370
- Custom name:
287
+ | Option | Default | Description |
288
+ |---|---|---|
289
+ | `--dest=DEST` | (prompts) | Target package or `main_app` |
290
+ | `--user-model=NAME` | `User` | Rodauth user model |
291
+
292
+ Custom resource name (first positional argument):
371
293
 
372
294
  ```bash
373
295
  rails g pu:profile:install AccountSettings bio:text --dest=main_app
@@ -375,39 +297,32 @@ rails g pu:profile:install AccountSettings bio:text --dest=main_app
375
297
 
376
298
  ### What gets created
377
299
 
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.
300
+ By default the model is `{UserModel}Profile` `UserProfile`, `StaffUserProfile`, etc. — derived from `--user-model`.
379
301
 
380
302
  ```
381
- app/models/[package/]user_profile.rb # {UserModel}Profile model
382
- db/migrate/xxx_create_user_profiles.rb # Migration
303
+ app/models/[package/]user_profile.rb
304
+ db/migrate/xxx_create_user_profiles.rb
383
305
  app/controllers/[package/]user_profiles_controller.rb
384
306
  app/policies/[package/]user_profile_policy.rb
385
307
  app/definitions/[package/]user_profile_definition.rb
386
308
  ```
387
309
 
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
310
+ The generator modifies the user model:
394
311
 
395
312
  ```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
313
+ has_one :profile, class_name: "UserProfile", dependent: :destroy
405
314
  ```
406
315
 
407
- Dynamically checks enabled Rodauth features and displays links for:
316
+ 🚨 The association is **always `:profile`**, regardless of class — `current_user.profile`, `build_profile`, `params.require(:profile)` always work.
317
+
318
+ The generated definition injects a custom `ShowPage` that renders the `SecuritySection` component.
319
+
320
+ ### The `SecuritySection` component
321
+
322
+ Dynamically lists Rodauth security links based on which features are enabled:
408
323
 
409
324
  | Feature | Label |
410
- |---------|-------|
325
+ |---|---|
411
326
  | `change_password` | Change Password |
412
327
  | `change_login` | Change Email |
413
328
  | `otp` | Two-Factor Authentication |
@@ -416,96 +331,46 @@ Dynamically checks enabled Rodauth features and displays links for:
416
331
  | `active_sessions` | Active Sessions |
417
332
  | `close_account` | Close Account |
418
333
 
419
- ### After generation
334
+ To customize the show page (e.g. wrap, reorder), override `ShowPage#render_after_content` (see [[plutonium-ui]] › Page hooks).
420
335
 
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:
336
+ ### Required: every user gets a profile
424
337
 
425
338
  ```ruby
426
339
  class User < ApplicationRecord
427
340
  after_create :create_profile!
428
341
 
429
342
  private
430
-
431
- def create_profile!
432
- create_profile
433
- end
343
+ def create_profile! = create_profile
434
344
  end
435
345
  ```
436
346
 
437
- ### Customizing the profile definition
347
+ Without this, `current_user.profile` is `nil` and the profile route errors. For existing users at migration time, run a one-off `User.find_each(&:create_profile)`.
348
+
349
+ ### Linking to the profile
438
350
 
439
351
  ```ruby
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
452
-
453
- class ShowPage < ShowPage
454
- private
455
-
456
- def render_after_content
457
- render Plutonium::Profile::SecuritySection.new
458
- end
459
- end
460
- end
352
+ link_to("Profile", profile_url) if respond_to?(:profile_url)
461
353
  ```
462
354
 
463
- ### Profile link in header
355
+ `profile_url` only exists when the profile resource is connected via `pu:profile:conn` (which registers it as a singular resource — see [[plutonium-app]] › Routes).
464
356
 
465
- ```ruby
466
- if respond_to?(:profile_url)
467
- link_to "Profile", profile_url
468
- end
469
- ```
357
+ ---
470
358
 
471
359
  ## Gotchas
472
360
 
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
481
-
482
- | Feature | Description |
483
- |---------|-------------|
484
- | `login` | Basic login/logout |
485
- | `create_account` | User registration |
486
- | `verify_account` | Email verification |
487
- | `reset_password` | Password reset via email |
488
- | `change_password` | Change password when logged in |
489
- | `change_login` | Change email address |
490
- | `verify_login_change` | Verify email change |
491
- | `remember` | "Remember me" functionality |
492
- | `otp` | TOTP two-factor authentication |
493
- | `sms_codes` | SMS-based 2FA |
494
- | `recovery_codes` | Backup codes for 2FA |
495
- | `webauthn` | WebAuthn/passkey authentication |
496
- | `lockout` | Lock account after failed attempts |
497
- | `active_sessions` | Track/manage active sessions |
498
- | `audit_logging` | Log authentication events |
499
- | `email_auth` | Passwordless email login |
500
- | `jwt` | JWT token authentication |
501
- | `jwt_refresh` | JWT refresh tokens |
502
- | `close_account` | Allow account deletion |
361
+ - **Role index 0 is the most privileged.** For admin/SaaS roles, index 0 is `owner`/`super_admin`. Generated invite interactions default invitees to index 1.
362
+ - **`owner` is always prepended** by `pu:saas:setup --roles`. Don't include it manually.
363
+ - **Profile association is always `:profile`** even when the class is `StaffUserProfile`.
364
+ - **`pu:saas:setup` runs four other generators** — don't re-run portal, profile, welcome, or invites separately.
365
+ - **Profile requires `pu:profile:conn`** without it, no route, no `profile_url`, no menu link.
366
+ - **Users need a profile row.** Add an `after_create` callback (or `find_or_create_by`) `current_user.profile` is otherwise nil.
367
+
368
+ ---
503
369
 
504
370
  ## Related skills
505
371
 
506
- - `plutonium-installation` - Initial Plutonium setup
507
- - `plutonium-portal` - Portal configuration
508
- - `plutonium-policy` - Authorization after authentication
509
- - `plutonium-invites` - User invitation system for multi-tenant apps
510
- - `plutonium-definition` - Customizing the profile definition
511
- - `plutonium-views` - Custom pages and components
372
+ - [[plutonium-app]] initial install, portal wiring, mounting auth-constrained routes
373
+ - [[plutonium-tenancy]] — invites + memberships for multi-tenant onboarding
374
+ - [[plutonium-behavior]] — policies (auth runs first, policy checks the authenticated user)
375
+ - [[plutonium-resource]] customizing the profile definition (fields, inputs, displays)
376
+ - [[plutonium-ui]] overriding the profile's `ShowPage`, theming the security section