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