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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +85 -102
- data/.claude/skills/plutonium-app/SKILL.md +574 -0
- data/.claude/skills/plutonium-auth/SKILL.md +167 -302
- data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
- data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +674 -0
- data/.claude/skills/plutonium-testing/SKILL.md +9 -6
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +44 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1010 -1214
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +52 -51
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +38 -29
- data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
- data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
- data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
- data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
- data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
- data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
- data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
- data/docs/.vitepress/theme/custom.css +144 -0
- data/docs/.vitepress/theme/index.ts +58 -1
- data/docs/getting-started/index.md +33 -57
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/02-first-resource.md +17 -8
- data/docs/getting-started/tutorial/03-authentication.md +31 -23
- data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
- data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
- data/docs/getting-started/tutorial/07-author-portal.md +8 -0
- data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +98 -462
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +93 -298
- data/docs/guides/custom-actions.md +126 -441
- data/docs/guides/customizing-ui.md +258 -0
- data/docs/guides/index.md +49 -52
- data/docs/guides/multi-tenancy.md +123 -186
- data/docs/guides/nested-resources.md +137 -396
- data/docs/guides/search-filtering.md +127 -238
- data/docs/guides/testing.md +10 -5
- data/docs/guides/theming.md +168 -405
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +112 -425
- data/docs/guides/user-profile.md +82 -241
- data/docs/index.md +10 -219
- data/docs/public/asciinema/home-scaffold.cast +305 -0
- data/docs/public/images/guides/custom-actions-bulk.png +0 -0
- data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
- data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
- data/docs/public/images/guides/nested-inputs.png +0 -0
- data/docs/public/images/guides/nested-resources-tab.png +0 -0
- data/docs/public/images/guides/search-filtering-index.png +0 -0
- data/docs/public/images/guides/search-filtering-panel.png +0 -0
- data/docs/public/images/guides/theming-after.png +0 -0
- data/docs/public/images/guides/theming-before.png +0 -0
- data/docs/public/images/guides/user-invites-landing.png +0 -0
- data/docs/public/images/guides/user-profile-edit.png +0 -0
- data/docs/public/images/guides/user-profile-show.png +0 -0
- data/docs/public/images/home-index.png +0 -0
- data/docs/public/images/home-new.png +0 -0
- data/docs/public/images/home-show.png +0 -0
- data/docs/public/images/tutorial/02-empty-index.png +0 -0
- data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
- data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
- data/docs/public/images/tutorial/02-new-form.png +0 -0
- data/docs/public/images/tutorial/03-create-account.png +0 -0
- data/docs/public/images/tutorial/03-login.png +0 -0
- data/docs/public/images/tutorial/04-admin-index.png +0 -0
- data/docs/public/images/tutorial/05-actions-menu.png +0 -0
- data/docs/public/images/tutorial/05-row-actions.png +0 -0
- data/docs/public/images/tutorial/06-comments-tab.png +0 -0
- data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
- data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
- data/docs/public/images/tutorial/07-author-portal.png +0 -0
- data/docs/public/images/tutorial/08-customized-index.png +0 -0
- data/docs/reference/app/generators.md +517 -0
- data/docs/reference/app/index.md +158 -0
- data/docs/reference/app/packages.md +146 -0
- data/docs/reference/app/portals.md +377 -0
- data/docs/reference/auth/accounts.md +229 -0
- data/docs/reference/auth/index.md +88 -0
- data/docs/reference/auth/profile.md +185 -0
- data/docs/reference/behavior/controllers.md +395 -0
- data/docs/reference/behavior/index.md +22 -0
- data/docs/reference/behavior/interactions.md +341 -0
- data/docs/reference/behavior/policies.md +417 -0
- data/docs/reference/index.md +67 -48
- data/docs/reference/resource/actions.md +423 -0
- data/docs/reference/resource/definition.md +508 -0
- data/docs/reference/resource/index.md +50 -0
- data/docs/reference/resource/model.md +348 -0
- data/docs/reference/resource/query.md +305 -0
- data/docs/reference/tenancy/entity-scoping.md +368 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +400 -0
- data/docs/reference/tenancy/nested-resources.md +267 -0
- data/docs/reference/testing/index.md +287 -0
- data/docs/reference/ui/assets.md +400 -0
- data/docs/reference/ui/components.md +165 -0
- data/docs/reference/ui/displays.md +104 -0
- data/docs/reference/ui/forms.md +284 -0
- data/docs/reference/ui/index.md +30 -0
- data/docs/reference/ui/layouts.md +106 -0
- data/docs/reference/ui/pages.md +189 -0
- data/docs/reference/ui/tables.md +121 -0
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
- data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
- data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
- data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
- data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
- data/lib/generators/pu/core/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +45 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
- data/lib/generators/pu/profile/conn_generator.rb +2 -2
- data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
- data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
- data/lib/generators/pu/rodauth/account_generator.rb +2 -1
- data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
- data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
- data/lib/generators/pu/rodauth/views_generator.rb +0 -2
- data/lib/generators/pu/saas/membership/USAGE +4 -1
- data/lib/generators/pu/saas/setup_generator.rb +16 -4
- data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
- data/lib/plutonium/definition/base.rb +1 -1
- data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
- data/lib/plutonium/helpers/turbo_helper.rb +30 -0
- data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
- data/lib/plutonium/resource/controller.rb +1 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +23 -5
- data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
- data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
- data/lib/plutonium/resource/policy.rb +7 -0
- data/lib/plutonium/routing/mapper_extensions.rb +15 -0
- data/lib/plutonium/ui/component/methods.rb +5 -0
- data/lib/plutonium/ui/form/base.rb +23 -3
- data/lib/plutonium/ui/form/components/json.rb +58 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
- data/lib/plutonium/ui/form/components/secure_association.rb +103 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/interaction.rb +1 -1
- data/lib/plutonium/ui/form/resource.rb +0 -4
- data/lib/plutonium/ui/form/theme.rb +1 -1
- data/lib/plutonium/ui/grid/resource.rb +1 -1
- data/lib/plutonium/ui/layout/base.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +0 -7
- data/lib/plutonium/ui/page/edit.rb +1 -1
- data/lib/plutonium/ui/page/index.rb +4 -4
- data/lib/plutonium/ui/page/new.rb +1 -1
- data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +8 -0
- data/lib/tasks/release.rake +15 -1
- data/package.json +13 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/form_controller.js +5 -4
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +661 -544
- metadata +86 -33
- data/.claude/skills/plutonium-assets/SKILL.md +0 -512
- data/.claude/skills/plutonium-controller/SKILL.md +0 -396
- data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
- data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
- data/.claude/skills/plutonium-forms/SKILL.md +0 -465
- data/.claude/skills/plutonium-installation/SKILL.md +0 -331
- data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
- data/.claude/skills/plutonium-invites/SKILL.md +0 -408
- data/.claude/skills/plutonium-model/SKILL.md +0 -440
- data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
- data/.claude/skills/plutonium-package/SKILL.md +0 -198
- data/.claude/skills/plutonium-policy/SKILL.md +0 -456
- data/.claude/skills/plutonium-portal/SKILL.md +0 -410
- data/.claude/skills/plutonium-views/SKILL.md +0 -651
- data/docs/reference/assets/index.md +0 -496
- data/docs/reference/controller/index.md +0 -412
- data/docs/reference/definition/actions.md +0 -462
- data/docs/reference/definition/fields.md +0 -383
- data/docs/reference/definition/index.md +0 -326
- data/docs/reference/definition/query.md +0 -351
- data/docs/reference/generators/index.md +0 -648
- data/docs/reference/interaction/index.md +0 -449
- data/docs/reference/model/features.md +0 -248
- data/docs/reference/model/index.md +0 -218
- data/docs/reference/policy/index.md +0 -456
- data/docs/reference/portal/index.md +0 -379
- data/docs/reference/views/forms.md +0 -411
- data/docs/reference/views/index.md +0 -544
|
@@ -1,107 +1,76 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plutonium-auth
|
|
3
|
-
description: Use BEFORE
|
|
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
|
|
6
|
+
# Plutonium Auth — Rodauth + Profile
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
10
|
+
For multi-tenant invitations and membership, see [[plutonium-tenancy]] › Invites. For portal-side wiring, see [[plutonium-app]] › Portal Engines.
|
|
16
11
|
|
|
17
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Install
|
|
31
25
|
|
|
32
26
|
```bash
|
|
33
27
|
rails generate pu:rodauth:install
|
|
34
28
|
```
|
|
35
29
|
|
|
36
|
-
|
|
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
|
-
|
|
55
|
-
rails generate pu:saas:setup --user Customer --entity Organization
|
|
56
|
-
```
|
|
32
|
+
---
|
|
57
33
|
|
|
58
34
|
## Account types
|
|
59
35
|
|
|
60
|
-
|
|
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` |
|
|
71
|
-
| `--kitchen_sink` |
|
|
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
|
|
75
|
-
| `--api_only` |
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
|
80
|
-
|
|
81
|
-
| `--login` | ✓ |
|
|
82
|
-
| `--
|
|
83
|
-
| `--
|
|
84
|
-
| `--
|
|
85
|
-
| `--
|
|
86
|
-
| `--
|
|
87
|
-
| `--
|
|
88
|
-
| `--
|
|
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` | |
|
|
65
|
+
| `--audit_logging` | | Log auth events |
|
|
96
66
|
| `--close_account` | | Allow account deletion |
|
|
97
|
-
| `--email_auth` | | Passwordless login
|
|
98
|
-
| `--sms_codes` | | SMS
|
|
99
|
-
| `--jwt` | | JWT
|
|
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
|
|
71
|
+
### Admin account — `pu:rodauth:admin`
|
|
103
72
|
|
|
104
|
-
|
|
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
|
|
117
|
-
| `--extra_attributes` | | Additional model attributes (e.g
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
95
|
+
EMAIL=admin@example.com rails rodauth:admin
|
|
96
|
+
# (run without EMAIL to be prompted)
|
|
138
97
|
```
|
|
139
98
|
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
152
|
-
rails
|
|
153
|
-
rails
|
|
154
|
-
rails
|
|
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
|
|
165
|
-
| `--skip-entity` |
|
|
166
|
-
| `--skip-membership` |
|
|
167
|
-
| `--user-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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
+
All inside the Rodauth `configure do ... end` block in `app/rodauth/<name>_rodauth_plugin.rb`.
|
|
216
182
|
|
|
217
|
-
### Custom
|
|
183
|
+
### Custom login redirect
|
|
218
184
|
|
|
219
185
|
```ruby
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
191
|
+
### Custom create-account validation + hook
|
|
232
192
|
|
|
233
193
|
```ruby
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
203
|
+
### Password requirements
|
|
246
204
|
|
|
247
205
|
```ruby
|
|
248
|
-
|
|
249
|
-
password_minimum_length 12
|
|
206
|
+
password_minimum_length 12
|
|
250
207
|
|
|
251
|
-
|
|
252
|
-
|
|
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-
|
|
213
|
+
### Multi-phase login (password on a separate page)
|
|
258
214
|
|
|
259
215
|
```ruby
|
|
260
|
-
|
|
261
|
-
use_multi_phase_login? true
|
|
262
|
-
end
|
|
216
|
+
use_multi_phase_login? true
|
|
263
217
|
```
|
|
264
218
|
|
|
265
|
-
### Prevent
|
|
219
|
+
### Prevent public signup (admin pattern)
|
|
266
220
|
|
|
267
221
|
```ruby
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
243
|
+
Override templates in `app/views/rodauth/<account>_mailer/`.
|
|
289
244
|
|
|
290
|
-
|
|
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
|
-
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Profile resource
|
|
338
265
|
|
|
339
|
-
|
|
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
|
-
|
|
346
|
-
|
|
272
|
+
--dest=competition \
|
|
273
|
+
--portal=competition_portal
|
|
347
274
|
```
|
|
348
275
|
|
|
349
|
-
### Step-by-step
|
|
276
|
+
### Step-by-step
|
|
350
277
|
|
|
351
278
|
```bash
|
|
352
|
-
rails generate pu:profile:install
|
|
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
|
-
|
|
282
|
+
rails db:migrate
|
|
361
283
|
|
|
362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
382
|
-
db/migrate/xxx_create_user_profiles.rb
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
334
|
+
To customize the show page (e.g. wrap, reorder), override `ShowPage#render_after_content` (see [[plutonium-ui]] › Page hooks).
|
|
420
335
|
|
|
421
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
474
|
-
- **`owner` is always prepended**
|
|
475
|
-
- **Profile association is always `:profile
|
|
476
|
-
- **`pu:saas:setup`
|
|
477
|
-
- **Profile requires
|
|
478
|
-
- **Users need a profile row.** Add an `after_create` callback or
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
-
|
|
507
|
-
-
|
|
508
|
-
-
|
|
509
|
-
-
|
|
510
|
-
-
|
|
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
|