plutonium 0.45.2 → 0.46.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 +146 -0
- data/.claude/skills/plutonium-assets/SKILL.md +248 -157
- data/.claude/skills/{plutonium-rodauth → plutonium-auth}/SKILL.md +195 -229
- data/.claude/skills/plutonium-controller/SKILL.md +9 -2
- data/.claude/skills/plutonium-create-resource/SKILL.md +22 -1
- data/.claude/skills/plutonium-definition/SKILL.md +521 -7
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +317 -0
- data/.claude/skills/plutonium-forms/SKILL.md +8 -1
- data/.claude/skills/plutonium-installation/SKILL.md +25 -2
- data/.claude/skills/plutonium-interaction/SKILL.md +9 -2
- data/.claude/skills/plutonium-invites/SKILL.md +11 -7
- data/.claude/skills/plutonium-model/SKILL.md +50 -50
- data/.claude/skills/plutonium-nested-resources/SKILL.md +8 -1
- data/.claude/skills/plutonium-package/SKILL.md +8 -1
- data/.claude/skills/plutonium-policy/SKILL.md +69 -78
- data/.claude/skills/plutonium-portal/SKILL.md +26 -70
- data/.claude/skills/plutonium-views/SKILL.md +9 -2
- data/CHANGELOG.md +33 -0
- data/app/assets/plutonium.css +1 -1
- data/app/views/rodauth/_login_form.html.erb +0 -3
- data/app/views/rodauth/confirm_password.html.erb +0 -4
- data/app/views/rodauth/create_account.html.erb +0 -3
- data/app/views/rodauth/logout.html.erb +0 -3
- data/config/initializers/pagy.rb +1 -1
- data/docs/superpowers/plans/2026-04-08-plutonium-skills-overhaul.md +481 -0
- data/docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md +236 -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 +8 -0
- data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +56 -0
- data/lib/generators/pu/invites/install_generator.rb +8 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +43 -0
- data/lib/generators/pu/profile/concerns/profile_arguments.rb +10 -4
- data/lib/generators/pu/profile/conn_generator.rb +9 -12
- data/lib/generators/pu/profile/install_generator.rb +5 -2
- data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
- data/lib/generators/pu/saas/portal_generator.rb +4 -9
- data/lib/generators/pu/saas/welcome/templates/app/views/welcome/onboarding.html.erb.tt +2 -2
- data/lib/plutonium/engine.rb +18 -5
- data/lib/plutonium/ui/layout/rodauth_layout.rb +6 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- metadata +7 -8
- data/.claude/skills/plutonium/skill.md +0 -130
- data/.claude/skills/plutonium-definition-actions/SKILL.md +0 -424
- data/.claude/skills/plutonium-definition-query/SKILL.md +0 -364
- data/.claude/skills/plutonium-profile/SKILL.md +0 -276
- data/.claude/skills/plutonium-theming/SKILL.md +0 -424
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: plutonium-
|
|
3
|
-
description: Use
|
|
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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Plutonium Rodauth
|
|
6
|
+
# Plutonium Authentication (Rodauth + Profile)
|
|
7
7
|
|
|
8
|
-
|
|
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).
|
|
9
14
|
|
|
10
|
-
|
|
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.
|
|
16
|
+
|
|
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)
|
|
27
|
+
|
|
28
|
+
## Rodauth setup
|
|
11
29
|
|
|
12
30
|
### Step 1: Install Rodauth Base
|
|
13
31
|
|
|
@@ -26,8 +44,6 @@ This installs:
|
|
|
26
44
|
|
|
27
45
|
### Step 2: Create Account Type
|
|
28
46
|
|
|
29
|
-
Choose the appropriate generator for your use case:
|
|
30
|
-
|
|
31
47
|
```bash
|
|
32
48
|
# Basic user account
|
|
33
49
|
rails generate pu:rodauth:account user
|
|
@@ -39,12 +55,10 @@ rails generate pu:rodauth:admin admin
|
|
|
39
55
|
rails generate pu:saas:setup --user Customer --entity Organization
|
|
40
56
|
```
|
|
41
57
|
|
|
42
|
-
## Account
|
|
58
|
+
## Account types
|
|
43
59
|
|
|
44
60
|
### Basic Account (`pu:rodauth:account`)
|
|
45
61
|
|
|
46
|
-
Creates a standard user account with configurable features:
|
|
47
|
-
|
|
48
62
|
```bash
|
|
49
63
|
rails generate pu:rodauth:account user [options]
|
|
50
64
|
```
|
|
@@ -87,16 +101,7 @@ rails generate pu:rodauth:account user [options]
|
|
|
87
101
|
|
|
88
102
|
### Admin Account (`pu:rodauth:admin`)
|
|
89
103
|
|
|
90
|
-
Creates a secure admin account with
|
|
91
|
-
- Multi-phase login (email first, then password)
|
|
92
|
-
- TOTP two-factor authentication (required)
|
|
93
|
-
- Recovery codes
|
|
94
|
-
- Account lockout
|
|
95
|
-
- Active sessions tracking
|
|
96
|
-
- Audit logging
|
|
97
|
-
- Role-based access control
|
|
98
|
-
- Invite interaction for adding new admins
|
|
99
|
-
- No public signup (accounts created via rake task or invite)
|
|
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.
|
|
100
105
|
|
|
101
106
|
```bash
|
|
102
107
|
rails generate pu:rodauth:admin admin
|
|
@@ -111,34 +116,36 @@ rails generate pu:rodauth:admin admin --extra-attributes=name:string,department:
|
|
|
111
116
|
| `--roles` | super_admin,admin | Comma-separated roles for admin accounts |
|
|
112
117
|
| `--extra_attributes` | | Additional model attributes (e.g., name:string) |
|
|
113
118
|
|
|
114
|
-
**
|
|
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.
|
|
120
|
+
|
|
115
121
|
```ruby
|
|
116
122
|
# app/models/admin.rb
|
|
117
123
|
enum :role, super_admin: 0, admin: 1
|
|
118
124
|
```
|
|
119
125
|
|
|
120
|
-
**Generated invite interaction:**
|
|
121
126
|
```ruby
|
|
122
127
|
# app/interactions/admin/invite_interaction.rb
|
|
123
128
|
class Admin::InviteInteraction < Plutonium::Interaction::Base
|
|
124
129
|
attribute :email, :string
|
|
125
|
-
attribute :role, default: :admin
|
|
126
|
-
|
|
127
|
-
def execute
|
|
128
|
-
# Creates admin via internal request and sends invite email
|
|
129
|
-
end
|
|
130
|
+
attribute :role, default: :admin
|
|
131
|
+
# ...
|
|
130
132
|
end
|
|
131
133
|
```
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
Rake task for direct creation:
|
|
134
136
|
```bash
|
|
135
|
-
# Create admin account directly
|
|
136
137
|
rails rodauth_admin:create[admin@example.com,password123]
|
|
137
138
|
```
|
|
138
139
|
|
|
139
140
|
### SaaS Setup (`pu:saas:setup`)
|
|
140
141
|
|
|
141
|
-
|
|
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.
|
|
142
149
|
|
|
143
150
|
```bash
|
|
144
151
|
rails generate pu:saas:setup --user Customer --entity Organization
|
|
@@ -151,57 +158,30 @@ rails generate pu:saas:setup --user Customer --entity Organization --user-attrib
|
|
|
151
158
|
|
|
152
159
|
| Option | Default | Description |
|
|
153
160
|
|--------|---------|-------------|
|
|
154
|
-
| `--user=NAME` | (required) | User account model name
|
|
155
|
-
| `--entity=NAME` | (required) | Entity model name
|
|
161
|
+
| `--user=NAME` | (required) | User account model name |
|
|
162
|
+
| `--entity=NAME` | (required) | Entity model name |
|
|
156
163
|
| `--allow-signup` | true | Allow public registration |
|
|
157
|
-
| `--roles` | member
|
|
164
|
+
| `--roles` | admin,member | Additional membership roles. **`owner` is always prepended as index 0** — don't include it. |
|
|
158
165
|
| `--skip-entity` | false | Skip entity model generation |
|
|
159
166
|
| `--skip-membership` | false | Skip membership model generation |
|
|
160
167
|
| `--user-attributes` | | Additional user model attributes |
|
|
161
|
-
| `--entity-attributes` | | Additional entity model attributes
|
|
168
|
+
| `--entity-attributes` | | Additional entity model attributes |
|
|
162
169
|
| `--membership-attributes` | | Additional membership model attributes |
|
|
163
170
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
You can also run each component separately:
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
# Just the user account
|
|
170
|
-
rails g pu:saas:user Customer
|
|
171
|
-
|
|
172
|
-
# Just the entity model
|
|
173
|
-
rails g pu:saas:entity Organization
|
|
174
|
-
|
|
175
|
-
# Just the membership (requires user and entity to exist)
|
|
176
|
-
rails g pu:saas:membership --user Customer --entity Organization
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**Generated Models:**
|
|
180
|
-
|
|
181
|
-
1. **User account** - The user model with Rodauth authentication
|
|
182
|
-
2. **Entity model** - The organization/company with unique name
|
|
183
|
-
3. **Membership model** - Join table `{entity}_{user}` (e.g., `OrganizationCustomer`)
|
|
171
|
+
Individual generators: `pu:saas:user`, `pu:saas:entity`, `pu:saas:membership`.
|
|
184
172
|
|
|
185
173
|
```ruby
|
|
186
174
|
# app/models/customer.rb
|
|
187
175
|
class Customer < ApplicationRecord
|
|
188
176
|
include Rodauth::Rails.model(:customer)
|
|
189
|
-
|
|
190
177
|
has_many :organization_customers, dependent: :destroy
|
|
191
178
|
has_many :organizations, through: :organization_customers
|
|
192
179
|
end
|
|
193
180
|
|
|
194
|
-
# app/models/organization.rb
|
|
195
|
-
class Organization < ApplicationRecord
|
|
196
|
-
has_many :organization_customers, dependent: :destroy
|
|
197
|
-
has_many :customers, through: :organization_customers
|
|
198
|
-
end
|
|
199
|
-
|
|
200
181
|
# app/models/organization_customer.rb
|
|
201
182
|
class OrganizationCustomer < ApplicationRecord
|
|
202
183
|
belongs_to :organization
|
|
203
184
|
belongs_to :customer
|
|
204
|
-
|
|
205
185
|
enum :role, member: 0, owner: 1
|
|
206
186
|
|
|
207
187
|
validates :customer, uniqueness: {
|
|
@@ -211,128 +191,33 @@ class OrganizationCustomer < ApplicationRecord
|
|
|
211
191
|
end
|
|
212
192
|
```
|
|
213
193
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
The membership model includes a role enum for access control within the entity:
|
|
217
|
-
|
|
218
|
-
```ruby
|
|
219
|
-
membership = OrganizationCustomer.find_by(organization: org, customer: current_user)
|
|
220
|
-
membership.member? # Default role
|
|
221
|
-
membership.owner? # Admin role for the entity
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
## Connecting Auth to Controllers
|
|
225
|
-
|
|
226
|
-
### Include in Resource Controller
|
|
194
|
+
## Connecting auth to controllers
|
|
227
195
|
|
|
228
196
|
```ruby
|
|
229
197
|
# app/controllers/resource_controller.rb
|
|
230
198
|
class ResourceController < PlutoniumController
|
|
231
199
|
include Plutonium::Resource::Controller
|
|
232
|
-
include Plutonium::Auth::Rodauth(:user)
|
|
200
|
+
include Plutonium::Auth::Rodauth(:user)
|
|
233
201
|
end
|
|
234
202
|
```
|
|
235
203
|
|
|
236
|
-
|
|
204
|
+
Multiple account types:
|
|
237
205
|
|
|
238
206
|
```ruby
|
|
239
|
-
# app/controllers/admin_controller.rb
|
|
240
207
|
class AdminController < PlutoniumController
|
|
241
208
|
include Plutonium::Resource::Controller
|
|
242
209
|
include Plutonium::Auth::Rodauth(:admin)
|
|
243
210
|
end
|
|
244
|
-
|
|
245
|
-
# app/controllers/customer_controller.rb
|
|
246
|
-
class CustomerController < PlutoniumController
|
|
247
|
-
include Plutonium::Resource::Controller
|
|
248
|
-
include Plutonium::Auth::Rodauth(:customer)
|
|
249
|
-
end
|
|
250
211
|
```
|
|
251
212
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
Including `Plutonium::Auth::Rodauth(:name)` adds:
|
|
255
|
-
|
|
256
|
-
| Method | Description |
|
|
257
|
-
|--------|-------------|
|
|
258
|
-
| `current_user` | The authenticated account |
|
|
259
|
-
| `logout_url` | URL to logout |
|
|
260
|
-
| `plutonium-rodauth` | Access to Rodauth instance |
|
|
261
|
-
|
|
262
|
-
## Generated Files
|
|
263
|
-
|
|
264
|
-
### Account Structure
|
|
265
|
-
|
|
266
|
-
```
|
|
267
|
-
app/
|
|
268
|
-
├── controllers/
|
|
269
|
-
│ └── rodauth/
|
|
270
|
-
│ └── user_controller.rb # Account-specific controller
|
|
271
|
-
├── mailers/
|
|
272
|
-
│ └── rodauth/
|
|
273
|
-
│ └── user_mailer.rb # Account-specific mailer
|
|
274
|
-
├── models/
|
|
275
|
-
│ └── user.rb # Account model
|
|
276
|
-
├── rodauth/
|
|
277
|
-
│ ├── rodauth_app.rb # Main Roda app
|
|
278
|
-
│ ├── rodauth_plugin.rb # Base plugin
|
|
279
|
-
│ └── user_rodauth_plugin.rb # Account-specific config
|
|
280
|
-
├── policies/
|
|
281
|
-
│ └── user_policy.rb # Account policy
|
|
282
|
-
├── definitions/
|
|
283
|
-
│ └── user_definition.rb # Account definition
|
|
284
|
-
└── views/
|
|
285
|
-
├── layouts/
|
|
286
|
-
│ └── rodauth.html.erb # Auth layout
|
|
287
|
-
└── rodauth/
|
|
288
|
-
└── user_mailer/ # Email templates
|
|
289
|
-
├── reset_password.text.erb
|
|
290
|
-
├── verify_account.text.erb
|
|
291
|
-
└── ...
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
### Plugin Configuration
|
|
295
|
-
|
|
296
|
-
```ruby
|
|
297
|
-
# app/rodauth/user_rodauth_plugin.rb
|
|
298
|
-
class UserRodauthPlugin < RodauthPlugin
|
|
299
|
-
configure do
|
|
300
|
-
# Features enabled for this account
|
|
301
|
-
enable :login, :logout, :remember, :create_account, ...
|
|
302
|
-
|
|
303
|
-
# URL prefix (non-primary accounts)
|
|
304
|
-
prefix "/users"
|
|
305
|
-
|
|
306
|
-
# Password storage
|
|
307
|
-
account_password_hash_column :password_hash
|
|
308
|
-
|
|
309
|
-
# Controller for views
|
|
310
|
-
rails_controller { Rodauth::UserController }
|
|
311
|
-
|
|
312
|
-
# Model
|
|
313
|
-
rails_account_model { User }
|
|
314
|
-
|
|
315
|
-
# Redirects
|
|
316
|
-
login_redirect "/"
|
|
317
|
-
logout_redirect "/"
|
|
318
|
-
|
|
319
|
-
# Session configuration
|
|
320
|
-
session_key "_user_session"
|
|
321
|
-
remember_cookie_key "_user_remember"
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
```
|
|
213
|
+
Including `Plutonium::Auth::Rodauth(:name)` adds `current_user`, `logout_url`, and `rodauth`.
|
|
325
214
|
|
|
326
215
|
## Customization
|
|
327
216
|
|
|
328
217
|
### Custom Login Redirect
|
|
329
218
|
|
|
330
219
|
```ruby
|
|
331
|
-
# app/rodauth/user_rodauth_plugin.rb
|
|
332
220
|
configure do
|
|
333
|
-
login_redirect { "/dashboard" }
|
|
334
|
-
|
|
335
|
-
# Or dynamically based on user
|
|
336
221
|
login_redirect do
|
|
337
222
|
if rails_account.admin?
|
|
338
223
|
"/admin"
|
|
@@ -347,12 +232,10 @@ end
|
|
|
347
232
|
|
|
348
233
|
```ruby
|
|
349
234
|
configure do
|
|
350
|
-
# Add custom field validation
|
|
351
235
|
before_create_account do
|
|
352
236
|
throw_error_status(422, "name", "must be present") if param("name").empty?
|
|
353
237
|
end
|
|
354
238
|
|
|
355
|
-
# After account creation
|
|
356
239
|
after_create_account do
|
|
357
240
|
Profile.create!(account_id: account_id, name: param("name"))
|
|
358
241
|
end
|
|
@@ -363,10 +246,8 @@ end
|
|
|
363
246
|
|
|
364
247
|
```ruby
|
|
365
248
|
configure do
|
|
366
|
-
# Minimum length
|
|
367
249
|
password_minimum_length 12
|
|
368
250
|
|
|
369
|
-
# Custom complexity
|
|
370
251
|
password_meets_requirements? do |password|
|
|
371
252
|
super(password) && password.match?(/\d/) && password.match?(/[^a-zA-Z\d]/)
|
|
372
253
|
end
|
|
@@ -377,7 +258,6 @@ end
|
|
|
377
258
|
|
|
378
259
|
```ruby
|
|
379
260
|
configure do
|
|
380
|
-
# Ask for email first, then password
|
|
381
261
|
use_multi_phase_login? true
|
|
382
262
|
end
|
|
383
263
|
```
|
|
@@ -392,9 +272,7 @@ configure do
|
|
|
392
272
|
end
|
|
393
273
|
```
|
|
394
274
|
|
|
395
|
-
## Email
|
|
396
|
-
|
|
397
|
-
Emails are sent via Action Mailer. Configure delivery in your environment:
|
|
275
|
+
## Email configuration
|
|
398
276
|
|
|
399
277
|
```ruby
|
|
400
278
|
# config/environments/production.rb
|
|
@@ -407,47 +285,26 @@ config.action_mailer.smtp_settings = {
|
|
|
407
285
|
}
|
|
408
286
|
```
|
|
409
287
|
|
|
410
|
-
|
|
288
|
+
Override templates in `app/views/rodauth/user_mailer/`.
|
|
411
289
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
```erb
|
|
415
|
-
<%# app/views/rodauth/user_mailer/reset_password.text.erb %>
|
|
416
|
-
Hi <%= @account.email %>,
|
|
417
|
-
|
|
418
|
-
Someone requested a password reset for your account.
|
|
419
|
-
|
|
420
|
-
Reset your password: <%= @reset_password_url %>
|
|
421
|
-
|
|
422
|
-
If you didn't request this, ignore this email.
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
## Portal Integration
|
|
426
|
-
|
|
427
|
-
### Selecting Auth for Portal
|
|
428
|
-
|
|
429
|
-
When generating a portal, select the Rodauth account:
|
|
290
|
+
## Portal integration
|
|
430
291
|
|
|
431
292
|
```bash
|
|
432
293
|
rails generate pu:pkg:portal admin
|
|
433
|
-
# Select "Rodauth account"
|
|
434
|
-
# Choose "admin" account
|
|
294
|
+
# Select "Rodauth account" → "admin"
|
|
435
295
|
```
|
|
436
296
|
|
|
437
|
-
|
|
297
|
+
Manual:
|
|
438
298
|
|
|
439
299
|
```ruby
|
|
440
|
-
# packages/admin_portal/lib/engine.rb
|
|
441
300
|
module AdminPortal
|
|
442
301
|
class Engine < Rails::Engine
|
|
443
302
|
include Plutonium::Portal::Engine
|
|
444
303
|
|
|
445
|
-
# Require authentication
|
|
446
304
|
config.before_initialize do
|
|
447
305
|
config.to_prepare do
|
|
448
306
|
AdminPortal::ResourceController.class_eval do
|
|
449
307
|
include Plutonium::Auth::Rodauth(:admin)
|
|
450
|
-
|
|
451
308
|
before_action :require_authenticated
|
|
452
309
|
|
|
453
310
|
private
|
|
@@ -462,56 +319,165 @@ module AdminPortal
|
|
|
462
319
|
end
|
|
463
320
|
```
|
|
464
321
|
|
|
465
|
-
## API
|
|
466
|
-
|
|
467
|
-
For JSON API authentication:
|
|
322
|
+
## API authentication
|
|
468
323
|
|
|
469
324
|
```bash
|
|
470
325
|
rails generate pu:rodauth:account api_user --api_only --jwt --jwt_refresh
|
|
471
326
|
```
|
|
472
327
|
|
|
473
|
-
|
|
474
|
-
- JWT token authentication
|
|
475
|
-
- Refresh tokens
|
|
476
|
-
- No session/cookie handling
|
|
477
|
-
|
|
478
|
-
### Using JWT
|
|
479
|
-
|
|
480
|
-
```ruby
|
|
481
|
-
# Login
|
|
328
|
+
```
|
|
482
329
|
POST /api_users/login
|
|
483
|
-
Content-Type: application/json
|
|
484
|
-
|
|
485
330
|
{"login": "user@example.com", "password": "secret"}
|
|
331
|
+
# → {"access_token": "...", "refresh_token": "..."}
|
|
486
332
|
|
|
487
|
-
# Response includes JWT
|
|
488
|
-
{"access_token": "...", "refresh_token": "..."}
|
|
489
|
-
|
|
490
|
-
# Authenticated requests
|
|
491
333
|
GET /api/posts
|
|
492
334
|
Authorization: Bearer <access_token>
|
|
493
335
|
```
|
|
494
336
|
|
|
495
|
-
##
|
|
337
|
+
## Profile page
|
|
338
|
+
|
|
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.
|
|
340
|
+
|
|
341
|
+
### Quick setup
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
rails g pu:profile:setup date_of_birth:date bio:text \
|
|
345
|
+
--dest=competition \
|
|
346
|
+
--portal=competition_portal
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Step-by-step install
|
|
350
|
+
|
|
351
|
+
```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 |
|
|
359
|
+
|
|
360
|
+
With fields:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
rails g pu:profile:install \
|
|
364
|
+
bio:text \
|
|
365
|
+
avatar:attachment \
|
|
366
|
+
'timezone:string?' \
|
|
367
|
+
--dest=customer
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Custom name:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
rails g pu:profile:install AccountSettings bio:text --dest=main_app
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### What gets created
|
|
377
|
+
|
|
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.
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
app/models/[package/]user_profile.rb # {UserModel}Profile model
|
|
382
|
+
db/migrate/xxx_create_user_profiles.rb # Migration
|
|
383
|
+
app/controllers/[package/]user_profiles_controller.rb
|
|
384
|
+
app/policies/[package/]user_profile_policy.rb
|
|
385
|
+
app/definitions/[package/]user_profile_definition.rb
|
|
386
|
+
```
|
|
387
|
+
|
|
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
|
|
394
|
+
|
|
395
|
+
```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
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Dynamically checks enabled Rodauth features and displays links for:
|
|
408
|
+
|
|
409
|
+
| Feature | Label |
|
|
410
|
+
|---------|-------|
|
|
411
|
+
| `change_password` | Change Password |
|
|
412
|
+
| `change_login` | Change Email |
|
|
413
|
+
| `otp` | Two-Factor Authentication |
|
|
414
|
+
| `recovery_codes` | Recovery Codes |
|
|
415
|
+
| `webauthn` | Security Keys |
|
|
416
|
+
| `active_sessions` | Active Sessions |
|
|
417
|
+
| `close_account` | Close Account |
|
|
418
|
+
|
|
419
|
+
### After generation
|
|
420
|
+
|
|
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:
|
|
424
|
+
|
|
425
|
+
```ruby
|
|
426
|
+
class User < ApplicationRecord
|
|
427
|
+
after_create :create_profile!
|
|
428
|
+
|
|
429
|
+
private
|
|
430
|
+
|
|
431
|
+
def create_profile!
|
|
432
|
+
create_profile
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
```
|
|
496
436
|
|
|
497
|
-
|
|
437
|
+
### Customizing the profile definition
|
|
498
438
|
|
|
499
439
|
```ruby
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
|
505
452
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
453
|
+
class ShowPage < ShowPage
|
|
454
|
+
private
|
|
455
|
+
|
|
456
|
+
def render_after_content
|
|
457
|
+
render Plutonium::Profile::SecuritySection.new
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
512
461
|
```
|
|
513
462
|
|
|
514
|
-
|
|
463
|
+
### Profile link in header
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
if respond_to?(:profile_url)
|
|
467
|
+
link_to "Profile", profile_url
|
|
468
|
+
end
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Gotchas
|
|
472
|
+
|
|
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
|
|
515
481
|
|
|
516
482
|
| Feature | Description |
|
|
517
483
|
|---------|-------------|
|
|
@@ -534,12 +500,12 @@ User.create!(
|
|
|
534
500
|
| `jwt` | JWT token authentication |
|
|
535
501
|
| `jwt_refresh` | JWT refresh tokens |
|
|
536
502
|
| `close_account` | Allow account deletion |
|
|
537
|
-
| `password_expiration` | Force password changes |
|
|
538
|
-
| `disallow_password_reuse` | Prevent password reuse |
|
|
539
503
|
|
|
540
|
-
## Related
|
|
504
|
+
## Related skills
|
|
541
505
|
|
|
542
506
|
- `plutonium-installation` - Initial Plutonium setup
|
|
543
507
|
- `plutonium-portal` - Portal configuration
|
|
544
508
|
- `plutonium-policy` - Authorization after authentication
|
|
545
509
|
- `plutonium-invites` - User invitation system for multi-tenant apps
|
|
510
|
+
- `plutonium-definition` - Customizing the profile definition
|
|
511
|
+
- `plutonium-views` - Custom pages and components
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plutonium-controller
|
|
3
|
-
description: Use
|
|
3
|
+
description: Use BEFORE overriding a controller action, adding a hook, or changing redirect logic in a Plutonium controller. Also when customizing resource_params or presentation hooks.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Plutonium Controllers
|
|
7
7
|
|
|
8
|
+
## 🚨 Critical (read first)
|
|
9
|
+
- **Use generators.** `pu:res:scaffold` and `pu:res:conn` create controllers — never hand-write them.
|
|
10
|
+
- **Don't override CRUD actions.** Customize via hooks (`resource_params`, `redirect_url_after_submit`, `preferred_action_after_submit`, presentation hooks). Overriding `create`/`update` usually breaks authorization, params filtering, or both.
|
|
11
|
+
- **Prefer definitions and interactions.** UI config belongs in definitions; business logic belongs in interactions. The controller is thin by design.
|
|
12
|
+
- **Named custom routes.** When adding custom member/collection routes, always use `as:` so `resource_url_for` can build URLs — especially for nested resources.
|
|
13
|
+
- **Related skills:** `plutonium-definition` (interactive actions instead of controller actions), `plutonium-policy` (authorization), `plutonium-nested-resources` (parent/child routing), `plutonium-views` (custom page classes).
|
|
14
|
+
|
|
8
15
|
**Controllers are generated automatically** - never create them manually:
|
|
9
16
|
- `rails g pu:res:scaffold` creates the base controller
|
|
10
17
|
- `rails g pu:res:conn` creates portal-specific controllers
|
|
@@ -383,7 +390,7 @@ end
|
|
|
383
390
|
|
|
384
391
|
- `plutonium` - How controllers fit in the resource architecture
|
|
385
392
|
- `plutonium-policy` - Authorization (used by controllers)
|
|
386
|
-
- `plutonium-definition
|
|
393
|
+
- `plutonium-definition` - Interactive actions (preferred over custom controller actions)
|
|
387
394
|
- `plutonium-views` - Custom page, form, display, and table classes
|
|
388
395
|
- `plutonium-nested-resources` - Parent/child routes and scoping
|
|
389
396
|
- `plutonium-model` - Resource models
|
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plutonium-create-resource
|
|
3
|
-
description: Use
|
|
3
|
+
description: Use BEFORE running pu:res:scaffold or creating any new resource. Also when picking field types or generator options. Covers field syntax and scaffold options.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Create Resource Skill
|
|
7
7
|
|
|
8
|
+
## 🚨 Critical (read first)
|
|
9
|
+
- **Always use `pu:res:scaffold`.** Never hand-write models, migrations, policies, definitions, or controllers. Plutonium's conventions rely on files it generated.
|
|
10
|
+
- **Always pass `--dest`** (`--dest=main_app` or `--dest=package_name`) to skip the interactive destination prompt.
|
|
11
|
+
- **Quote fields with `?` or `{}`** to prevent shell expansion: `'field:type?'`, `'field:decimal{10,2}'`, `'field:decimal?{10,2}'`.
|
|
12
|
+
- **Run `pu:res:conn` next** to connect the resource to a portal — without it, the resource is invisible.
|
|
13
|
+
- **Related skills:** `plutonium-model` (model structure), `plutonium-definition` (UI config), `plutonium-policy` (authorization), `plutonium-portal` (connecting to portals).
|
|
14
|
+
|
|
8
15
|
Use the `pu:res:scaffold` generator to create complete resources in Plutonium applications.
|
|
9
16
|
|
|
17
|
+
## Quick checklist
|
|
18
|
+
|
|
19
|
+
Creating a new resource:
|
|
20
|
+
|
|
21
|
+
1. Pick a destination: `--dest=main_app` or `--dest=package_name`.
|
|
22
|
+
2. Identify field types (see field type syntax below). Quote fields with `?` or `{}`.
|
|
23
|
+
3. Run `rails g pu:res:scaffold ResourceName field:type ... --dest=<dest>`.
|
|
24
|
+
4. Review the generated migration — add cascade deletes, composite indexes, defaults.
|
|
25
|
+
5. Run `rails db:migrate`.
|
|
26
|
+
6. Run `rails g pu:res:conn ResourceName --dest=<portal_name>` to connect to a portal.
|
|
27
|
+
7. Verify routes: `bin/rails routes | grep resource_name`.
|
|
28
|
+
8. Customize the policy (`permitted_attributes_for_read`, `permitted_attributes_for_create`) as needed.
|
|
29
|
+
9. Open the portal route in the browser.
|
|
30
|
+
|
|
10
31
|
## Command Syntax
|
|
11
32
|
|
|
12
33
|
```bash
|