plutonium 0.64.0 → 0.66.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 +44 -41
- data/.claude/skills/plutonium-app/SKILL.md +115 -59
- data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
- data/.claude/skills/plutonium-auth/SKILL.md +119 -53
- data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
- data/.claude/skills/plutonium-dashboard/SKILL.md +168 -0
- data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
- data/.claude/skills/plutonium-resource/SKILL.md +236 -145
- data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
- data/.claude/skills/plutonium-testing/SKILL.md +130 -33
- data/.claude/skills/plutonium-ui/SKILL.md +171 -95
- data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
- data/CHANGELOG.md +80 -0
- data/CLAUDE.md +282 -210
- data/README.md +9 -9
- data/SECURITY.md +1 -1
- data/app/assets/plutonium-charts.js +20604 -0
- data/app/assets/plutonium-charts.js.map +7 -0
- data/app/assets/plutonium-charts.min.js +41 -0
- data/app/assets/plutonium-charts.min.js.map +7 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +794 -629
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +48 -48
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/plutonium/_flash_alerts.html.erb +2 -2
- data/app/views/plutonium/_resource_header.html.erb +2 -2
- data/app/views/plutonium/_resource_sidebar.html.erb +11 -2
- data/app/views/plutonium/_toast.html.erb +2 -2
- data/app/views/rodauth/_login_form.html.erb +1 -1
- data/app/views/rodauth/_password_visibility.html.erb +1 -1
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +2 -2
- data/app/views/rodauth/create_account.html.erb +4 -4
- data/app/views/rodauth/reset_password_request.html.erb +1 -1
- data/app/views/rodauth/verify_account_resend.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +1 -1
- data/config/brakeman.ignore +4 -4
- data/config/initializers/rabl.rb +0 -40
- data/config/locales/en/api_client.yml +22 -0
- data/config/locales/en/async.yml +34 -0
- data/config/locales/en/dashboard.yml +25 -0
- data/config/locales/en/invites.yml +37 -0
- data/config/locales/en/js.yml +40 -0
- data/config/locales/en/kanban.yml +28 -0
- data/config/locales/en/plutonium.yml +26 -0
- data/config/locales/en/positioning.yml +14 -0
- data/config/locales/en/profile.yml +29 -0
- data/config/locales/en/resource.yml +60 -0
- data/config/locales/en/ui.yml +93 -0
- data/config/locales/en/wizard.yml +46 -0
- data/docs/.vitepress/config.ts +34 -1
- data/docs/.vitepress/sync-skills.mjs +6 -3
- data/docs/.vitepress/theme/components/HomeHero.vue +1 -1
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +14 -0
- data/docs/.vitepress/theme/custom.css +34 -377
- data/docs/blog/introducing-plutonium-dashboards.md +174 -0
- data/docs/blog/introducing-plutonium-i18n.md +90 -0
- data/docs/blog/introducing-plutonium.md +4 -5
- data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
- data/docs/getting-started/installation.md +5 -5
- data/docs/getting-started/tutorial/02-first-resource.md +3 -3
- data/docs/getting-started/tutorial/03-authentication.md +7 -7
- data/docs/getting-started/tutorial/04-authorization.md +21 -4
- data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
- data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
- data/docs/getting-started/tutorial/07-author-portal.md +2 -2
- data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
- data/docs/getting-started/tutorial/index.md +1 -1
- data/docs/guides/adding-resources.md +10 -7
- data/docs/guides/authentication.md +25 -25
- data/docs/guides/authorization.md +24 -24
- data/docs/guides/creating-packages.md +17 -17
- data/docs/guides/custom-actions.md +32 -32
- data/docs/guides/customizing-ui.md +29 -26
- data/docs/guides/dashboards.md +300 -0
- data/docs/guides/index.md +4 -3
- data/docs/guides/kanban.md +56 -56
- data/docs/guides/multi-tenancy.md +35 -22
- data/docs/guides/nested-resources.md +21 -21
- data/docs/guides/performance.md +3 -3
- data/docs/guides/search-filtering.md +13 -13
- data/docs/guides/testing.md +16 -12
- data/docs/guides/theming.md +32 -17
- data/docs/guides/troubleshooting.md +2 -2
- data/docs/guides/user-invites.md +17 -17
- data/docs/guides/user-profile.md +51 -24
- data/docs/guides/wizards.md +55 -55
- data/docs/public/images/blog/dashboards-card-error.png +0 -0
- data/docs/public/images/blog/dashboards-loading.png +0 -0
- data/docs/public/images/blog/dashboards-overview.png +0 -0
- data/docs/public/images/blog/dashboards-sidebar.png +0 -0
- data/docs/public/images/blog/i18n-es-admin.png +0 -0
- data/docs/public/images/guides/dashboard-card-error.png +0 -0
- data/docs/public/images/guides/dashboard-loading.png +0 -0
- data/docs/public/images/guides/dashboard-overview.png +0 -0
- data/docs/public/images/guides/dashboard-sidebar.png +0 -0
- data/docs/reference/app/generators.md +24 -24
- data/docs/reference/app/index.md +15 -18
- data/docs/reference/app/packages.md +8 -8
- data/docs/reference/app/portals.md +77 -29
- data/docs/reference/auth/accounts.md +15 -15
- data/docs/reference/auth/index.md +12 -12
- data/docs/reference/auth/profile.md +67 -29
- data/docs/reference/behavior/async-interactions.md +24 -24
- data/docs/reference/behavior/controllers.md +28 -28
- data/docs/reference/behavior/index.md +5 -5
- data/docs/reference/behavior/interactions.md +44 -44
- data/docs/reference/behavior/policies.md +48 -28
- data/docs/reference/configuration.md +88 -31
- data/docs/reference/dashboard/dsl.md +123 -0
- data/docs/reference/dashboard/index.md +35 -0
- data/docs/reference/dashboard/registration.md +76 -0
- data/docs/reference/generators/lite.md +7 -7
- data/docs/reference/i18n.md +262 -0
- data/docs/reference/index.md +11 -2
- data/docs/reference/kanban/authorization.md +9 -9
- data/docs/reference/kanban/dsl.md +33 -33
- data/docs/reference/kanban/index.md +1 -1
- data/docs/reference/kanban/positioning.md +18 -16
- data/docs/reference/resource/actions.md +51 -51
- data/docs/reference/resource/definition.md +166 -88
- data/docs/reference/resource/export.md +11 -7
- data/docs/reference/resource/index.md +16 -14
- data/docs/reference/resource/model.md +24 -24
- data/docs/reference/{positioning.md → resource/positioning.md} +78 -75
- data/docs/reference/resource/query.md +14 -12
- data/docs/reference/tenancy/entity-scoping.md +65 -35
- data/docs/reference/tenancy/index.md +11 -11
- data/docs/reference/tenancy/invites.md +20 -20
- data/docs/reference/tenancy/nested-resources.md +13 -13
- data/docs/reference/testing/index.md +116 -22
- data/docs/reference/ui/assets.md +65 -25
- data/docs/reference/ui/components.md +20 -20
- data/docs/reference/ui/displays.md +19 -22
- data/docs/reference/ui/forms.md +35 -35
- data/docs/reference/ui/index.md +17 -15
- data/docs/reference/ui/layouts.md +21 -21
- data/docs/reference/ui/pages.md +22 -22
- data/docs/reference/ui/tables.md +7 -7
- data/docs/reference/wizard/anchoring-resume.md +33 -32
- data/docs/reference/wizard/dsl.md +45 -45
- data/docs/reference/wizard/index.md +6 -6
- data/docs/reference/wizard/one-time.md +18 -18
- data/docs/reference/wizard/registration-launch.md +32 -32
- data/docs/reference/wizard/storage-config.md +23 -23
- data/esbuild.config.js +5 -2
- data/gemfiles/rails_7.gemfile.lock +2 -2
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/active_model/validations/array_validator.rb +1 -1
- data/lib/active_model/validations/attached_validator.rb +1 -1
- data/lib/active_model/validations/url_validator.rb +2 -2
- data/lib/generators/pu/core/assets/assets_generator.rb +36 -21
- data/lib/generators/pu/core/assets/templates/postcss.config.js +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +3 -2
- data/lib/generators/pu/dashboard/dashboard_generator.rb +96 -0
- data/lib/generators/pu/dashboard/templates/dashboard.rb.tt +25 -0
- data/lib/generators/pu/docker/install/install_generator.rb +58 -0
- data/lib/generators/pu/docker/install/templates/Dockerfile.dev.tt +18 -3
- data/lib/generators/pu/docker/install/templates/Dockerfile.tt +20 -5
- data/lib/generators/pu/docker/install/templates/{docker-compose.yml → docker-compose.yml.tt} +1 -1
- data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
- data/lib/generators/pu/invites/invitable_generator.rb +18 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +0 -95
- data/lib/generators/pu/lib/plutonium_generators/concerns/js_package_manager.rb +146 -0
- data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -17
- data/lib/generators/pu/pkg/package/package_generator.rb +1 -0
- data/lib/generators/pu/pkg/package/templates/config/locales/en.yml.tt +28 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +1 -0
- data/lib/generators/pu/pkg/portal/templates/config/locales/en.yml.tt +30 -0
- data/lib/generators/pu/profile/conn_generator.rb +6 -0
- data/lib/generators/pu/res/scaffold/scaffold_generator.rb +0 -8
- data/lib/generators/pu/rodauth/concerns/feature_selector.rb +0 -18
- data/lib/plutonium/action/base.rb +27 -3
- data/lib/plutonium/action/interactive.rb +17 -4
- data/lib/plutonium/api_client/concerns/create_api_client.rb +12 -11
- data/lib/plutonium/api_client/concerns/disable_api_client.rb +3 -3
- data/lib/plutonium/auth/sequel_adapter.rb +1 -1
- data/lib/plutonium/configuration.rb +5 -0
- data/lib/plutonium/core/controller.rb +25 -1
- data/lib/plutonium/core/controllers/authorizable.rb +1 -1
- data/lib/plutonium/dashboard/base.rb +114 -0
- data/lib/plutonium/dashboard/card.rb +200 -0
- data/lib/plutonium/dashboard/controller.rb +101 -0
- data/lib/plutonium/dashboard/dsl.rb +104 -0
- data/lib/plutonium/dashboard/register.rb +40 -0
- data/lib/plutonium/dashboard/route_resolution.rb +30 -0
- data/lib/plutonium/dashboard.rb +42 -0
- data/lib/plutonium/definition/actions.rb +6 -1
- data/lib/plutonium/definition/base.rb +41 -3
- data/lib/plutonium/definition/index_views.rb +1 -1
- data/lib/plutonium/definition/positioning.rb +1 -1
- data/lib/plutonium/definition/presentable.rb +2 -2
- data/lib/plutonium/helpers/assets_helper.rb +15 -2
- data/lib/plutonium/helpers/display_helper.rb +10 -2
- data/lib/plutonium/interaction/async/context.rb +6 -8
- data/lib/plutonium/interaction/async/executor.rb +6 -7
- data/lib/plutonium/interaction/async/run.rb +2 -2
- data/lib/plutonium/interaction/async/run_definition.rb +1 -1
- data/lib/plutonium/interaction/base.rb +2 -0
- data/lib/plutonium/interaction/concerns/dispatchable.rb +15 -3
- data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -3
- data/lib/plutonium/invites/concerns/invite_token.rb +25 -7
- data/lib/plutonium/invites/concerns/invite_user.rb +10 -5
- data/lib/plutonium/invites/concerns/resend_invite.rb +4 -4
- data/lib/plutonium/invites/controller.rb +11 -11
- data/lib/plutonium/kanban/column.rb +13 -3
- data/lib/plutonium/kanban/dsl.rb +7 -4
- data/lib/plutonium/models/has_cents.rb +1 -1
- data/lib/plutonium/profile/security_section.rb +10 -18
- data/lib/plutonium/query/filter.rb +10 -1
- data/lib/plutonium/query/filters/association.rb +27 -2
- data/lib/plutonium/query/filters/boolean.rb +4 -4
- data/lib/plutonium/query/filters/date.rb +4 -17
- data/lib/plutonium/query/filters/date_range.rb +2 -2
- data/lib/plutonium/query/filters/select.rb +10 -1
- data/lib/plutonium/query/filters/text.rb +9 -12
- data/lib/plutonium/railtie.rb +17 -0
- data/lib/plutonium/resource/controller.rb +10 -5
- data/lib/plutonium/resource/controllers/crud_actions.rb +17 -20
- data/lib/plutonium/resource/controllers/export_csv.rb +19 -9
- data/lib/plutonium/resource/controllers/kanban_actions.rb +16 -16
- data/lib/plutonium/resource/controllers/position_actions.rb +5 -5
- data/lib/plutonium/resource/controllers/typeahead.rb +24 -1
- data/lib/plutonium/resource/query_object.rb +30 -3
- data/lib/plutonium/resource/record/associated_with.rb +23 -2
- data/lib/plutonium/routing/dashboard_registration.rb +137 -0
- data/lib/plutonium/routing/resource_registration.rb +5 -0
- data/lib/plutonium/routing/route_set_extensions.rb +2 -1
- data/lib/plutonium/translation.rb +227 -0
- data/lib/plutonium/ui/actions_dropdown.rb +2 -2
- data/lib/plutonium/ui/breadcrumbs.rb +3 -3
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/behaviour.rb +46 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/dashboard/board.rb +80 -0
- data/lib/plutonium/ui/dashboard/card.rb +127 -0
- data/lib/plutonium/ui/dashboard/chart.rb +43 -0
- data/lib/plutonium/ui/dashboard/custom.rb +32 -0
- data/lib/plutonium/ui/dashboard/frame.rb +25 -0
- data/lib/plutonium/ui/dashboard/metric.rb +137 -0
- data/lib/plutonium/ui/dashboard/skeleton.rb +40 -0
- data/lib/plutonium/ui/display/components/attachment.rb +1 -1
- data/lib/plutonium/ui/display/components/badge.rb +7 -1
- data/lib/plutonium/ui/display/components/boolean.rb +2 -2
- data/lib/plutonium/ui/display/resource.rb +23 -16
- data/lib/plutonium/ui/export_button.rb +3 -3
- data/lib/plutonium/ui/form/components/key_value_store.rb +5 -5
- data/lib/plutonium/ui/form/components/password.rb +2 -2
- data/lib/plutonium/ui/form/components/resource_select.rb +27 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +2 -0
- data/lib/plutonium/ui/form/components/uppy.rb +11 -3
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +1 -1
- data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +1 -1
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
- data/lib/plutonium/ui/form/query.rb +8 -8
- data/lib/plutonium/ui/form/resource.rb +54 -26
- data/lib/plutonium/ui/frame_navigator_panel.rb +4 -4
- data/lib/plutonium/ui/grid/card.rb +3 -3
- data/lib/plutonium/ui/grid/resource.rb +3 -3
- data/lib/plutonium/ui/interaction/async/run_progress.rb +6 -6
- data/lib/plutonium/ui/interaction/async/running_banner.rb +2 -2
- data/lib/plutonium/ui/kanban/column.rb +6 -6
- data/lib/plutonium/ui/layout/base.rb +19 -1
- data/lib/plutonium/ui/layout/header.rb +1 -1
- data/lib/plutonium/ui/layout/icon_rail.rb +4 -4
- data/lib/plutonium/ui/layout/rodauth_layout.rb +1 -1
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/layout/topbar.rb +2 -2
- data/lib/plutonium/ui/modal/base.rb +4 -4
- data/lib/plutonium/ui/nav_grid_menu.rb +1 -1
- data/lib/plutonium/ui/nav_user.rb +2 -2
- data/lib/plutonium/ui/page/base.rb +10 -2
- data/lib/plutonium/ui/page/dashboard.rb +54 -0
- data/lib/plutonium/ui/page/edit.rb +3 -3
- data/lib/plutonium/ui/page/interactive_action.rb +2 -2
- data/lib/plutonium/ui/page/new.rb +3 -3
- data/lib/plutonium/ui/page/show.rb +2 -2
- data/lib/plutonium/ui/page/wizard.rb +22 -16
- data/lib/plutonium/ui/page/wizard_chooser.rb +10 -10
- data/lib/plutonium/ui/page/wizard_completed.rb +2 -2
- data/lib/plutonium/ui/skeleton_table.rb +1 -1
- data/lib/plutonium/ui/table/base.rb +2 -2
- data/lib/plutonium/ui/table/components/attachment.rb +1 -1
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +19 -3
- data/lib/plutonium/ui/table/components/drag_handle.rb +3 -3
- data/lib/plutonium/ui/table/components/filter_form.rb +11 -10
- data/lib/plutonium/ui/table/components/filter_pills.rb +4 -4
- data/lib/plutonium/ui/table/components/pagy_info.rb +61 -17
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +9 -5
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
- data/lib/plutonium/ui/table/components/scopes_bar.rb +6 -2
- data/lib/plutonium/ui/table/components/scopes_pills.rb +3 -3
- data/lib/plutonium/ui/table/components/selection_column.rb +1 -1
- data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
- data/lib/plutonium/ui/table/components/view_switcher.rb +12 -8
- data/lib/plutonium/ui/table/resource.rb +53 -17
- data/lib/plutonium/ui/wizard/review.rb +7 -7
- data/lib/plutonium/ui/wizard/stepper.rb +1 -1
- data/lib/plutonium/ui/wizard/summary_display.rb +6 -0
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/base.rb +2 -0
- data/lib/plutonium/wizard/driving.rb +8 -2
- data/lib/plutonium/wizard/dsl.rb +4 -2
- data/lib/plutonium/wizard/field_importer.rb +1 -8
- data/lib/plutonium/wizard/review_step.rb +8 -1
- data/lib/plutonium/wizard/step.rb +14 -3
- data/package.json +4 -2
- data/plutonium.gemspec +4 -1
- data/postcss-gem-import.cjs +34 -0
- data/postcss-gem-import.js +3 -28
- data/src/css/components.css +101 -10
- data/src/css/slim_select.css +0 -38
- data/src/css/tokens.css +24 -0
- data/src/js/controllers/attachment_input_controller.js +15 -6
- data/src/js/controllers/chart_controller.js +162 -0
- data/src/js/controllers/clipboard_controller.js +3 -2
- data/src/js/controllers/dirty_form_guard_controller.js +4 -3
- data/src/js/controllers/easymde_controller.js +3 -0
- data/src/js/controllers/filter_panel_controller.js +15 -6
- data/src/js/controllers/flatpickr_controller.js +8 -0
- data/src/js/controllers/frame_refresh_controller.js +59 -0
- data/src/js/controllers/intl_tel_input_controller.js +5 -0
- data/src/js/controllers/register_controllers.js +4 -0
- data/src/js/controllers/slim_select_controller.js +79 -176
- data/src/js/core.js +9 -1
- data/src/js/i18n.js +69 -0
- data/src/js/plutonium-charts.js +15 -0
- data/src/js/turbo/turbo_confirm.js +5 -3
- metadata +64 -16
- data/.yarnrc.yml +0 -8
- data/config/initializers/hotwire_turbo_monkey_patches.rb +0 -12
- data/lib/generators/pu/field/input/input_generator.rb +0 -32
- data/lib/generators/pu/field/input/templates/input.rb.tt +0 -15
- data/lib/generators/pu/lib/plutonium_generators/concerns/config.rb +0 -39
- data/lib/generators/pu/lib/plutonium_generators/concerns/serializer.rb +0 -39
- data/lib/generators/pu/lib/plutonium_generators/installer.rb +0 -205
- data/lib/generators/pu/res/scaffold/templates/presenter.rb.tt +0 -13
- data/lib/generators/pu/res/scaffold/templates/query_object.rb.tt +0 -17
- data/public/plutonium-assets/application.js +0 -31419
- data/public/plutonium-assets/plutonium.ico +0 -0
- data/yarn.lock +0 -6858
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Packages
|
|
2
2
|
|
|
3
|
-
Plutonium apps are organized into **packages
|
|
3
|
+
Plutonium apps are organized into **packages**: Rails engines with stricter conventions. Two flavors, hard split:
|
|
4
4
|
|
|
5
5
|
| Type | Purpose | Generator | Examples |
|
|
6
6
|
|---|---|---|---|
|
|
@@ -60,7 +60,7 @@ Each feature package gets its own base classes:
|
|
|
60
60
|
- `Blogging::ResourceDefinition`
|
|
61
61
|
- `Blogging::ResourceInteraction`
|
|
62
62
|
|
|
63
|
-
These inherit from the main app's base classes
|
|
63
|
+
These inherit from the main app's base classes; extend them for package-wide defaults.
|
|
64
64
|
|
|
65
65
|
### Creating resources inside a feature package
|
|
66
66
|
|
|
@@ -111,13 +111,13 @@ This is loaded from `config/application.rb`. Migrations from all packages are pi
|
|
|
111
111
|
|
|
112
112
|
## When to use which
|
|
113
113
|
|
|
114
|
-
**Feature packages
|
|
114
|
+
**Feature packages**: domain logic that:
|
|
115
115
|
|
|
116
116
|
- Could be reused across multiple portals (admin and customer both edit `Blogging::Post`).
|
|
117
117
|
- Has no inherent UI / auth (it's just behavior).
|
|
118
118
|
- You want isolated from other domains (`billing` should not depend on `blogging`).
|
|
119
119
|
|
|
120
|
-
**Portal packages
|
|
120
|
+
**Portal packages**: user-facing surfaces that:
|
|
121
121
|
|
|
122
122
|
- Have a specific auth flow (admin vs customer vs public).
|
|
123
123
|
- Render different views of the same underlying resources.
|
|
@@ -137,10 +137,10 @@ packages/
|
|
|
137
137
|
└── controllers, views, routes
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
The portals expose the features. A single feature can be exposed by multiple portals
|
|
140
|
+
The portals expose the features. A single feature can be exposed by multiple portals, usually with different policies and definitions per portal.
|
|
141
141
|
|
|
142
142
|
## Related
|
|
143
143
|
|
|
144
|
-
- [Portals](./portals)
|
|
145
|
-
- [Generators](./generators)
|
|
146
|
-
- [Guide: Creating Packages](/guides/creating-packages)
|
|
144
|
+
- [Portals](./portals): portal-specific configuration (mounting, auth, route registration)
|
|
145
|
+
- [Generators](./generators): `pu:pkg:package` and `pu:pkg:portal` flags
|
|
146
|
+
- [Guide: Creating Packages](/guides/creating-packages): task-oriented walkthrough
|
|
@@ -5,7 +5,7 @@ A portal is a Rails engine mixing in `Plutonium::Portal::Engine`. It defines its
|
|
|
5
5
|
## 🚨 Critical
|
|
6
6
|
|
|
7
7
|
- **Use `pu:pkg:portal` for everything.** Never hand-write the engine file, controller concern, or layout.
|
|
8
|
-
- **Pass `--auth=<name>`, `--public`, or `--byo`** for unattended runs
|
|
8
|
+
- **Pass `--auth=<name>`, `--public`, or `--byo`** for unattended runs; without one of these flags, the generator prompts.
|
|
9
9
|
- **Always connect resources with `pu:res:conn`.** Until connected, a resource has no portal routes and is invisible.
|
|
10
10
|
- **For custom routes on a registered resource, pass `as:`.** Without it, `resource_url_for` can't build URLs.
|
|
11
11
|
|
|
@@ -20,7 +20,7 @@ rails g pu:pkg:portal <name>
|
|
|
20
20
|
| Option | Description |
|
|
21
21
|
|---|---|
|
|
22
22
|
| `--auth=NAME` | Rodauth account to authenticate with (e.g. `--auth=user`) |
|
|
23
|
-
| `--public` | Public access
|
|
23
|
+
| `--public` | Public access, no authentication |
|
|
24
24
|
| `--byo` | Bring your own authentication |
|
|
25
25
|
| `--scope=CLASS` | Entity class for multi-tenancy (e.g. `--scope=Organization`) |
|
|
26
26
|
|
|
@@ -51,7 +51,9 @@ end
|
|
|
51
51
|
|
|
52
52
|
## Controller concern (auth)
|
|
53
53
|
|
|
54
|
-
Every portal has a `Concerns::Controller
|
|
54
|
+
Every portal has a `Concerns::Controller`, included by both its `ResourceController` (resource pages) and its `PlutoniumController` (dashboard and other non-resource pages). The generator wires this up; you customize for auth flow and shared before_action hooks.
|
|
55
|
+
|
|
56
|
+
Portal-wide helpers the layout calls belong here, declared with `helper_method`, so they work on the dashboard as well as resource pages. The common case is `profile_url`: `Plutonium::Auth::Rodauth` defines it as `nil`, and the avatar menu only shows a Profile link when it returns a URL. `pu:profile:conn --dest=<portal>` writes the override into this concern (see [Auth › Profile](/reference/auth/profile)).
|
|
55
57
|
|
|
56
58
|
### Rodauth
|
|
57
59
|
|
|
@@ -89,19 +91,63 @@ end
|
|
|
89
91
|
|
|
90
92
|
## Mounting
|
|
91
93
|
|
|
94
|
+
`pu:pkg:portal` writes the mount at the bottom of the portal's own `packages/<name>_portal/config/routes.rb`, at `/<name>`, wrapped in an auth constraint when `--auth` is given:
|
|
95
|
+
|
|
92
96
|
```ruby
|
|
93
|
-
# config/routes.rb
|
|
97
|
+
# packages/admin_portal/config/routes.rb (after the engine's routes.draw block)
|
|
94
98
|
Rails.application.routes.draw do
|
|
95
|
-
# Authenticated mount
|
|
96
99
|
constraints Rodauth::Rails.authenticate(:user) do
|
|
97
100
|
mount AdminPortal::Engine, at: "/admin"
|
|
98
101
|
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
With `--public` or `--byo` the mount is unconstrained and the portal handles its own auth.
|
|
106
|
+
|
|
107
|
+
To change the path, edit `at:` in place. Don't mount the engine again in `config/routes.rb`: the second `mount` reuses the route name (`admin_portal`) and Rails raises `ArgumentError: Invalid route name, already in use`.
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
Route order matters: the app's `config/routes.rb` is drawn first, then each package's routes file, then gem engines (Active Storage, Turbo).
|
|
110
|
+
|
|
111
|
+
### Mounting a portal at `/`
|
|
112
|
+
|
|
113
|
+
Two things change when a portal is mounted at `"/"`.
|
|
114
|
+
|
|
115
|
+
**Drop the `Rodauth::Rails.authenticate` constraint and authenticate in the controller concern instead.** The constraint does not fail the match for an anonymous visitor; it calls `rodauth.require_account`, which redirects to login. A constrained mount at `/` matches every path the app's own routes did not claim, so it redirects anonymous requests meant for routes drawn after it (other portals, Active Storage) and turns unknown URLs into login redirects.
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# packages/desk_portal/config/routes.rb
|
|
119
|
+
Rails.application.routes.draw do
|
|
120
|
+
mount DeskPortal::Engine, at: "/"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# packages/desk_portal/app/controllers/desk_portal/concerns/controller.rb
|
|
124
|
+
module DeskPortal
|
|
125
|
+
module Concerns
|
|
126
|
+
module Controller
|
|
127
|
+
extend ActiveSupport::Concern
|
|
128
|
+
include Plutonium::Portal::Controller
|
|
129
|
+
include Plutonium::Auth::Rodauth(:user)
|
|
130
|
+
|
|
131
|
+
included do
|
|
132
|
+
before_action { rodauth.require_account }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
102
136
|
end
|
|
103
137
|
```
|
|
104
138
|
|
|
139
|
+
**Move the engine's root off `/` but keep the name.** The app's `root` is drawn first, so the generated `root to: "dashboard#index"` is unreachable and the portal's `root_path` points at the app's home page. Plutonium's header, icon rail, breadcrumbs and wizard exits all link to `root_path`, so the portal still needs a route named `root`:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
DeskPortal::Engine.routes.draw do
|
|
143
|
+
get "dashboard", to: "dashboard#index", as: :root
|
|
144
|
+
register_resource ::Comment
|
|
145
|
+
# register resources above.
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The same applies to `register_dashboard ..., at: "/"`. Confirm with `Rails.application.routes.recognize_path("/")` (still the app's home) and `recognize_path("/dashboard")`.
|
|
150
|
+
|
|
105
151
|
## Routes & `register_resource`
|
|
106
152
|
|
|
107
153
|
Portal routes live in `packages/<name>_portal/config/routes.rb`:
|
|
@@ -126,7 +172,7 @@ For each call, Plutonium auto-generates:
|
|
|
126
172
|
- Nested routes for every registered `has_many` / `has_one` parent (prefixed `nested_`)
|
|
127
173
|
- Route names that `resource_url_for` can resolve
|
|
128
174
|
|
|
129
|
-
You list every resource the portal exposes. If a resource isn't registered, it has no URLs in that portal
|
|
175
|
+
You list every resource the portal exposes. If a resource isn't registered, it has no URLs in that portal, so `resource_url_for` will fail.
|
|
130
176
|
|
|
131
177
|
### Choosing nested associations
|
|
132
178
|
|
|
@@ -136,11 +182,11 @@ Name the associations that get nested routes, and the rest are not drawn:
|
|
|
136
182
|
register_resource ::Post, associations: %i[comments post_detail]
|
|
137
183
|
```
|
|
138
184
|
|
|
139
|
-
`associations: []` draws none, and a name that is not a routable association fails the boot. Set `config.nested_association_routes = :declared` to make naming them the rule
|
|
185
|
+
`associations: []` draws none, and a name that is not a routable association fails the boot. Set `config.nested_association_routes = :declared` to make naming them the rule; see [Tenancy › Nested resources](../tenancy/nested-resources#declaring-which-associations-get-routes).
|
|
140
186
|
|
|
141
187
|
### Singular (singleton) resources
|
|
142
188
|
|
|
143
|
-
For resources with no collection
|
|
189
|
+
For resources with no collection: a single per-user `Profile`, app-wide `Settings`, etc.:
|
|
144
190
|
|
|
145
191
|
```ruby
|
|
146
192
|
register_resource ::Profile, singular: true
|
|
@@ -178,12 +224,12 @@ end
|
|
|
178
224
|
```
|
|
179
225
|
|
|
180
226
|
::: warning Always pass `as:`
|
|
181
|
-
Without `as:`, `resource_url_for(@post, action: :preview)` fails because there's no named route
|
|
227
|
+
Without `as:`, `resource_url_for(@post, action: :preview)` fails because there's no named route, which is especially critical for nested resources.
|
|
182
228
|
:::
|
|
183
229
|
|
|
184
|
-
For most operations with business logic, prefer **interactive actions** (definition + interaction
|
|
230
|
+
For most operations with business logic, prefer **interactive actions** (definition + interaction; see [Resource › Actions](/reference/resource/actions)) over custom controller routes. Action routes wire automatically with no `register_resource` block needed.
|
|
185
231
|
|
|
186
|
-
## Connecting resources
|
|
232
|
+
## Connecting resources: `pu:res:conn`
|
|
187
233
|
|
|
188
234
|
A resource is invisible until connected to at least one portal. The generator wires up the portal-specific controller, policy, definition, and route registration.
|
|
189
235
|
|
|
@@ -191,7 +237,7 @@ A resource is invisible until connected to at least one portal. The generator wi
|
|
|
191
237
|
rails g pu:res:conn RESOURCE [RESOURCE...] --dest=PORTAL_NAME [--singular]
|
|
192
238
|
```
|
|
193
239
|
|
|
194
|
-
Pass resources directly
|
|
240
|
+
Pass resources directly to avoid interactive prompts. No `--src` needed.
|
|
195
241
|
|
|
196
242
|
```bash
|
|
197
243
|
# Main app resources
|
|
@@ -286,13 +332,13 @@ end
|
|
|
286
332
|
## Per-portal overrides
|
|
287
333
|
|
|
288
334
|
```ruby
|
|
289
|
-
# Definition
|
|
335
|
+
# Definition: how fields render per portal
|
|
290
336
|
class AdminPortal::PostDefinition < ::PostDefinition
|
|
291
337
|
scope :pending_review
|
|
292
338
|
input :internal_notes, hint: "Not shown to the author"
|
|
293
339
|
end
|
|
294
340
|
|
|
295
|
-
# Policy
|
|
341
|
+
# Policy: which fields exist, and who may act
|
|
296
342
|
# `internal_notes` appears for admins because THIS permits it,
|
|
297
343
|
# not because the definition above mentions it.
|
|
298
344
|
class AdminPortal::PostPolicy < ::PostPolicy
|
|
@@ -302,7 +348,7 @@ class AdminPortal::PostPolicy < ::PostPolicy
|
|
|
302
348
|
def permitted_attributes_for_create = %i[title content featured internal_notes]
|
|
303
349
|
end
|
|
304
350
|
|
|
305
|
-
# Controller
|
|
351
|
+
# Controller: different redirect after submit
|
|
306
352
|
module AdminPortal
|
|
307
353
|
class PostsController < ResourceController
|
|
308
354
|
private
|
|
@@ -321,20 +367,22 @@ config.after_initialize do
|
|
|
321
367
|
end
|
|
322
368
|
```
|
|
323
369
|
|
|
324
|
-
Strategies: `:path` (entity id in URL
|
|
370
|
+
Strategies: `:path` (entity id in URL, the default) or a custom method name on the portal controller concern.
|
|
325
371
|
|
|
326
372
|
For the full multi-tenancy story, see [Tenancy › Entity scoping](/reference/tenancy/entity-scoping).
|
|
327
373
|
|
|
328
374
|
## Dashboard / non-resource pages
|
|
329
375
|
|
|
376
|
+
The generated `dashboard#index` is a plain page listing the registered resources. To replace it with a [dashboard](/guides/dashboards) of metric and chart cards, run `rails g pu:dashboard Home --dest=<portal> --at=/`, which swaps the `root to:` line for a `register_dashboard ... at: "/"` registration.
|
|
377
|
+
|
|
330
378
|
```ruby
|
|
331
|
-
# config/routes.rb
|
|
379
|
+
# packages/admin_portal/config/routes.rb
|
|
332
380
|
AdminPortal::Engine.routes.draw do
|
|
333
381
|
root to: "dashboard#index"
|
|
334
382
|
get "settings", to: "settings#index"
|
|
335
383
|
end
|
|
336
384
|
|
|
337
|
-
# Controller
|
|
385
|
+
# Controller: inherit from PlutoniumController, NOT ResourceController
|
|
338
386
|
module AdminPortal
|
|
339
387
|
class DashboardController < PlutoniumController
|
|
340
388
|
def index
|
|
@@ -349,7 +397,7 @@ See [UI › Pages](/reference/ui/pages) for custom Phlex page classes.
|
|
|
349
397
|
## Multiple portals
|
|
350
398
|
|
|
351
399
|
```ruby
|
|
352
|
-
# Admin
|
|
400
|
+
# Admin: full access, entity-scoped
|
|
353
401
|
module AdminPortal
|
|
354
402
|
class Engine < Rails::Engine
|
|
355
403
|
include Plutonium::Portal::Engine
|
|
@@ -360,7 +408,7 @@ module AdminPortal
|
|
|
360
408
|
end
|
|
361
409
|
end
|
|
362
410
|
|
|
363
|
-
# Customer dashboard
|
|
411
|
+
# Customer dashboard: entity-scoped to the customer's organization
|
|
364
412
|
module DashboardPortal
|
|
365
413
|
class Engine < Rails::Engine
|
|
366
414
|
include Plutonium::Portal::Engine
|
|
@@ -371,7 +419,7 @@ module DashboardPortal
|
|
|
371
419
|
end
|
|
372
420
|
end
|
|
373
421
|
|
|
374
|
-
# Public
|
|
422
|
+
# Public: no auth, no entity scoping
|
|
375
423
|
module PublicPortal
|
|
376
424
|
class Engine < Rails::Engine
|
|
377
425
|
include Plutonium::Portal::Engine
|
|
@@ -381,9 +429,9 @@ end
|
|
|
381
429
|
|
|
382
430
|
## Related
|
|
383
431
|
|
|
384
|
-
- [Packages](./packages)
|
|
385
|
-
- [Generators](./generators)
|
|
386
|
-
- [Behavior › Controllers](/reference/behavior/controllers)
|
|
387
|
-
- [Tenancy › Entity scoping](/reference/tenancy/entity-scoping)
|
|
388
|
-
- [Auth](/reference/auth/)
|
|
389
|
-
- [UI › Layouts](/reference/ui/layouts)
|
|
432
|
+
- [Packages](./packages): feature vs portal split, structure, namespacing
|
|
433
|
+
- [Generators](./generators): full `pu:pkg:portal` / `pu:res:conn` option reference
|
|
434
|
+
- [Behavior › Controllers](/reference/behavior/controllers): controller key methods, hooks, customizations
|
|
435
|
+
- [Tenancy › Entity scoping](/reference/tenancy/entity-scoping): multi-tenancy mechanics
|
|
436
|
+
- [Auth](/reference/auth/): Rodauth account types referenced by `--auth=`
|
|
437
|
+
- [UI › Layouts](/reference/ui/layouts): customizing portal chrome
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Accounts
|
|
2
2
|
|
|
3
|
-
Rodauth account types. Pick one (or several
|
|
3
|
+
Rodauth account types. Pick one (or several; apps can have multiple side-by-side).
|
|
4
4
|
|
|
5
|
-
## Basic account
|
|
5
|
+
## Basic account: `pu:rodauth:account`
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
rails generate pu:rodauth:account user [options]
|
|
@@ -59,7 +59,7 @@ rails g pu:rodauth:account api_user --api_only --jwt --jwt_refresh
|
|
|
59
59
|
rails g pu:rodauth:account user --kitchen_sink
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
## Admin account
|
|
62
|
+
## Admin account: `pu:rodauth:admin`
|
|
63
63
|
|
|
64
64
|
Pre-configured secure admin with multi-phase login, **required** TOTP, recovery codes, lockout, active session tracking, audit logging, role-based access, invite interaction, and **no public signup**.
|
|
65
65
|
|
|
@@ -74,7 +74,7 @@ rails g pu:rodauth:admin admin --extra-attributes=name:string,department:string
|
|
|
74
74
|
| `--roles` | `super_admin,admin` | Comma-separated roles (positional enum) |
|
|
75
75
|
| `--extra_attributes` | | Additional model attributes (e.g. `name:string`) |
|
|
76
76
|
|
|
77
|
-
**Role-ordering convention:** index 0 is the most privileged. Generated invite interaction defaults new invitees to `roles[1]
|
|
77
|
+
**Role-ordering convention:** index 0 is the most privileged. Generated invite interaction defaults new invitees to `roles[1]`, so the order in `--roles=` matters.
|
|
78
78
|
|
|
79
79
|
```ruby
|
|
80
80
|
enum :role, super_admin: 0, admin: 1
|
|
@@ -82,12 +82,12 @@ enum :role, super_admin: 0, admin: 1
|
|
|
82
82
|
|
|
83
83
|
**Invite + resend.** The admin resource gets two actions:
|
|
84
84
|
|
|
85
|
-
- **Invite
|
|
86
|
-
- **Resend invitation
|
|
85
|
+
- **Invite**: invite a new admin by email; Rodauth sends a verification link and the invitee sets their own password through the verify flow.
|
|
86
|
+
- **Resend invitation**: re-send the verification email. Only shown for admins who haven't verified yet.
|
|
87
87
|
|
|
88
88
|
This uses Rodauth account verification, separate from the [Tenancy › Invites](/reference/tenancy/invites) system.
|
|
89
89
|
|
|
90
|
-
Rake task for direct admin creation (generated alongside the account
|
|
90
|
+
Rake task for direct admin creation (generated alongside the account; namespace is `rodauth`, task name is the account name):
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
93
|
EMAIL=admin@example.com rails rodauth:admin
|
|
@@ -96,7 +96,7 @@ EMAIL=admin@example.com rails rodauth:admin
|
|
|
96
96
|
|
|
97
97
|
The task creates the account and triggers a verification email; the admin sets their own password via that flow. No password is passed on the command line.
|
|
98
98
|
|
|
99
|
-
## SaaS setup
|
|
99
|
+
## SaaS setup: `pu:saas:setup` (meta-generator) {#saas-setup}
|
|
100
100
|
|
|
101
101
|
Creates the User + Entity + Membership trio AND runs:
|
|
102
102
|
|
|
@@ -152,7 +152,7 @@ class OrganizationCustomer < ApplicationRecord
|
|
|
152
152
|
end
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
## API client
|
|
155
|
+
## API client: `pu:saas:api_client`
|
|
156
156
|
|
|
157
157
|
For machine-to-machine authentication. HTTP Basic Auth with auto-generated password.
|
|
158
158
|
|
|
@@ -233,12 +233,12 @@ end
|
|
|
233
233
|
Emitted by the generators; both parts are required for concurrent portal logins.
|
|
234
234
|
|
|
235
235
|
```ruby
|
|
236
|
-
# app/rodauth/rodauth_plugin.rb
|
|
236
|
+
# app/rodauth/rodauth_plugin.rb: the shared base, once
|
|
237
237
|
enable :session_isolation
|
|
238
238
|
```
|
|
239
239
|
|
|
240
240
|
```ruby
|
|
241
|
-
# app/rodauth/<name>_rodauth_plugin.rb
|
|
241
|
+
# app/rodauth/<name>_rodauth_plugin.rb: once per account type
|
|
242
242
|
session_key_prefix "admin_" # namespaces EVERY key, account id included
|
|
243
243
|
remember_cookie_key "_admin_remember"
|
|
244
244
|
```
|
|
@@ -249,7 +249,7 @@ Do **not** also set `session_key`: explicit values bypass `convert_session_key`
|
|
|
249
249
|
|
|
250
250
|
## Related
|
|
251
251
|
|
|
252
|
-
- [Profile](./profile)
|
|
253
|
-
- [App › Portals › Controller concern (auth)](/reference/app/portals#controller-concern-auth)
|
|
254
|
-
- [Tenancy › Invites](/reference/tenancy/invites)
|
|
255
|
-
- [App › Generators › Authentication generators](/reference/app/generators#authentication-generators)
|
|
252
|
+
- [Profile](./profile): profile resource + SecuritySection component
|
|
253
|
+
- [App › Portals › Controller concern (auth)](/reference/app/portals#controller-concern-auth): wiring accounts into portal controllers
|
|
254
|
+
- [Tenancy › Invites](/reference/tenancy/invites): invitation system on top of Rodauth signup
|
|
255
|
+
- [App › Generators › Authentication generators](/reference/app/generators#authentication-generators): full generator catalog
|
|
@@ -4,18 +4,18 @@ Plutonium uses [Rodauth](http://rodauth.jeremyevans.net/) via [rodauth-rails](ht
|
|
|
4
4
|
|
|
5
5
|
## Sub-pages
|
|
6
6
|
|
|
7
|
-
- [Accounts](./accounts)
|
|
8
|
-
- [Profile](./profile)
|
|
7
|
+
- [Accounts](./accounts): Rodauth install, basic accounts, admin accounts, SaaS setup, account customization
|
|
8
|
+
- [Profile](./profile): profile resource generator, the SecuritySection component
|
|
9
9
|
|
|
10
10
|
## 🚨 Critical
|
|
11
11
|
|
|
12
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
|
|
13
|
+
- **Role index 0 is the most privileged** (`owner`, `super_admin`). Invite interactions default new invitees to **index 1**, so the order in `--roles=` matters.
|
|
14
14
|
- **`pu:saas:setup --roles=...` always prepends `owner` as index 0.** Don't include `owner` in the option.
|
|
15
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
|
|
17
|
-
- **Profile needs `pu:profile:conn` to be visible
|
|
18
|
-
- **
|
|
16
|
+
- **Profile association is always `:profile`** regardless of the model class: `current_user.profile`, `build_profile`, `params.require(:profile)`. `pu:profile:conn` hardcodes `current_user.profile`.
|
|
17
|
+
- **Profile needs `pu:profile:conn` to be visible.** Without it, there is no singular `/profile` route and `profile_url` stays `nil` (no menu link).
|
|
18
|
+
- **Check an existing profile policy for `update?`.** Older `pu:profile:conn` output lacks `def update? = true`, so the profile can't be edited once it exists. See [Profile](./profile#what-pu-profile-conn-generates).
|
|
19
19
|
|
|
20
20
|
## Install Rodauth
|
|
21
21
|
|
|
@@ -34,7 +34,7 @@ class ResourceController < PlutoniumController
|
|
|
34
34
|
end
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Multiple account types
|
|
37
|
+
Multiple account types: include the matching `:name`:
|
|
38
38
|
|
|
39
39
|
```ruby
|
|
40
40
|
class AdminController < PlutoniumController
|
|
@@ -80,9 +80,9 @@ Authorization: Bearer <access_token>
|
|
|
80
80
|
|
|
81
81
|
## Related
|
|
82
82
|
|
|
83
|
-
- [Accounts](./accounts)
|
|
84
|
-
- [Profile](./profile)
|
|
85
|
-
- [Tenancy › Invites](/reference/tenancy/invites)
|
|
86
|
-
- [App › Portals › Controller concern (auth)](/reference/app/portals#controller-concern-auth)
|
|
87
|
-
- [Guides › Authentication](/guides/authentication)
|
|
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
88
|
- [Guides › User profile](/guides/user-profile)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Profile
|
|
2
2
|
|
|
3
|
-
Manages Rodauth account settings as a Plutonium resource
|
|
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
4
|
|
|
5
5
|
## 🚨 Critical
|
|
6
6
|
|
|
7
|
-
- **Profile association is always `:profile`** regardless of the model class
|
|
8
|
-
- **Profile needs `pu:profile:conn
|
|
9
|
-
- **
|
|
7
|
+
- **Profile association is always `:profile`** regardless of the model class: `current_user.profile`, `build_profile`, `params.require(:profile)` always work. `pu:profile:conn` hardcodes `current_user.profile` in the code it generates.
|
|
8
|
+
- **Profile needs `pu:profile:conn`.** Without it: no route, no `profile_url` override, no user-menu link.
|
|
9
|
+
- **Check an existing profile policy for `update?`.** Policies generated by older versions lack `def update? = true`, which leaves the profile read-only once it exists. See [What `pu:profile:conn` generates](#what-pu-profile-conn-generates).
|
|
10
10
|
|
|
11
11
|
## Quick setup
|
|
12
12
|
|
|
@@ -42,7 +42,7 @@ rails g pu:profile:install AccountSettings bio:text --dest=main_app
|
|
|
42
42
|
|
|
43
43
|
## What gets created
|
|
44
44
|
|
|
45
|
-
By default the model is `{UserModel}Profile`
|
|
45
|
+
By default the model is `{UserModel}Profile` (`UserProfile`, `StaffUserProfile`, etc.), derived from `--user-model`.
|
|
46
46
|
|
|
47
47
|
```
|
|
48
48
|
app/models/[package/]user_profile.rb
|
|
@@ -59,10 +59,10 @@ has_one :profile, class_name: "UserProfile", dependent: :destroy
|
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
::: warning Association name is fixed
|
|
62
|
-
Even when the class is `StaffUserProfile`, the association is `:profile`. Don't rename it
|
|
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
63
|
:::
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
If you reuse a profile model whose user association has a different name (e.g. `has_one :user_profile`), add `has_one :profile, class_name: "UserProfile"` to the user model rather than editing `current_user.profile` out of the generated policy and `profile_url`. Then connect it with `pu:profile:conn UserProfile --dest=<portal>`.
|
|
66
66
|
|
|
67
67
|
## The `SecuritySection` component
|
|
68
68
|
|
|
@@ -78,7 +78,7 @@ Dynamically lists Rodauth security links based on which features are enabled on
|
|
|
78
78
|
| `active_sessions` | Active Sessions |
|
|
79
79
|
| `close_account` | Close Account |
|
|
80
80
|
|
|
81
|
-
If a feature isn't enabled on the account, its link doesn't render
|
|
81
|
+
If a feature isn't enabled on the account, its link doesn't render; no configuration needed.
|
|
82
82
|
|
|
83
83
|
To customize (e.g. add chrome, reorder), override `ShowPage#render_after_content`:
|
|
84
84
|
|
|
@@ -96,40 +96,77 @@ end
|
|
|
96
96
|
|
|
97
97
|
See [UI › Pages](/reference/ui/pages) for page-class customization.
|
|
98
98
|
|
|
99
|
-
##
|
|
99
|
+
## Connecting to a portal
|
|
100
|
+
|
|
101
|
+
`pu:profile:conn` registers the profile as a **singular** resource: `/profile` (no `:id`), and overrides the `profile_url` helper:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
rails g pu:profile:conn --dest=customer_portal
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This is what makes the profile visible. Without it, the model exists but has no route in any portal.
|
|
108
|
+
|
|
109
|
+
### What `pu:profile:conn` generates
|
|
110
|
+
|
|
111
|
+
It runs `pu:res:conn` with `--singular --policy --definition`, then injects:
|
|
112
|
+
|
|
113
|
+
- **Policy:** `relation_scope { skip_default_relation_scope!; relation.where(user: user) }` (a profile belongs to the user, not the portal's entity), `create? = user.profile.nil?`, `update? = true`, `destroy? = false`, and `permitted_attributes_for_create` minus `:user`.
|
|
114
|
+
- **Controller:** `resource_params` merged with `user: current_user`, so the owner is set server-side.
|
|
115
|
+
- **Definition:** a `ShowPage` whose `render_after_content` renders `Plutonium::Profile::SecuritySection`.
|
|
116
|
+
- **Portal controller concern:** `profile_url`, which points at the profile or, when the user has none yet, at the new-profile form.
|
|
117
|
+
|
|
118
|
+
The explicit `update?` matters: the base policy's `update?` delegates to `create?`, which is false once the profile exists. Policies generated by older versions lack it and leave the profile read-only, so add it if it's missing:
|
|
100
119
|
|
|
101
120
|
```ruby
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
def update?
|
|
122
|
+
true
|
|
123
|
+
end
|
|
124
|
+
```
|
|
104
125
|
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
## Users without a profile row
|
|
127
|
+
|
|
128
|
+
The generated `profile_url` sends a user with no profile to the new-profile form, and `create?` allows exactly one, so a missing row does not break the route. It does mean the first visit is a "create profile" form, and the `SecuritySection` only appears after it is saved. If you want the show page from the first click, create the row up front:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
class User < ApplicationRecord
|
|
132
|
+
after_create :create_profile
|
|
107
133
|
end
|
|
108
134
|
```
|
|
109
135
|
|
|
110
|
-
|
|
136
|
+
and backfill existing users:
|
|
111
137
|
|
|
112
138
|
```bash
|
|
113
|
-
rails runner "User.find_each
|
|
139
|
+
rails runner "User.find_each { |u| u.profile || u.create_profile }"
|
|
114
140
|
```
|
|
115
141
|
|
|
116
|
-
|
|
142
|
+
Skip this when the profile has required fields the user must fill in, or when existing code creates profiles itself (the unique `user_id` index makes a second insert fail).
|
|
117
143
|
|
|
118
|
-
|
|
144
|
+
## Linking to the profile
|
|
119
145
|
|
|
120
|
-
|
|
121
|
-
|
|
146
|
+
Nothing to add. `Plutonium::Auth::Rodauth` defines `profile_url` returning `nil`, and the topbar avatar menu renders a "Profile" entry whenever it returns a URL. `pu:profile:conn` overrides it in the portal's controller concern, which is what makes the link appear. Elsewhere:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
link_to("Profile", profile_url) if profile_url
|
|
122
150
|
```
|
|
123
151
|
|
|
124
|
-
|
|
152
|
+
## Account features on the profile page
|
|
125
153
|
|
|
126
|
-
|
|
154
|
+
Per-user settings that aren't part of the profile row (API tokens, connected accounts, notification preferences) are discovered from the profile page. Link them from the same `ShowPage` hook that renders `SecuritySection`, instead of adding custom items to the sidebar or overriding the topbar partial:
|
|
127
155
|
|
|
128
156
|
```ruby
|
|
129
|
-
|
|
157
|
+
class ShowPage < ShowPage
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def render_after_content
|
|
161
|
+
render Plutonium::Profile::SecuritySection.new
|
|
162
|
+
div(class: "mt-8") do
|
|
163
|
+
a(href: resource_url_for(ApiToken), class: "font-medium text-[var(--pu-text)] hover:underline") { "API tokens" }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
130
167
|
```
|
|
131
168
|
|
|
132
|
-
|
|
169
|
+
A resource like `ApiToken` belongs to the user, not the portal's entity, so connect it with `pu:res:conn ApiToken --dest=<portal> --policy` and give it the same shape the profile has: `skip_default_relation_scope!` plus `relation.where(user: user)` in the policy, and `resource_params` merged with `user: current_user` in the controller.
|
|
133
170
|
|
|
134
171
|
## Customizing the definition
|
|
135
172
|
|
|
@@ -173,13 +210,14 @@ rails g pu:profile:conn StaffUserProfile --dest=admin_portal
|
|
|
173
210
|
|
|
174
211
|
## Gotchas
|
|
175
212
|
|
|
176
|
-
-
|
|
177
|
-
-
|
|
178
|
-
- **
|
|
179
|
-
- **
|
|
213
|
+
- **Profile is read-only after creation.** The policy predates the generated `update?`, so `update?` falls back to `create?` (`user.profile.nil?`). Add `def update? = true`.
|
|
214
|
+
- **First visit shows a "create profile" form.** The user has no profile row yet. Not an error; add `after_create :create_profile` plus a backfill if they should land on the show page.
|
|
215
|
+
- **No Profile link in the user menu.** `profile_url` still returns the default `nil` because the profile isn't connected to this portal. Run `pu:profile:conn --dest=<portal>`.
|
|
216
|
+
- **Custom resource name**: pass it as the first positional argument to `pu:profile:install`. The association is still `:profile`.
|
|
217
|
+
- **SecuritySection shows nothing**: none of the relevant Rodauth features are enabled on the account. Enable `change_password`, `otp`, etc. on the Rodauth plugin.
|
|
180
218
|
|
|
181
219
|
## Related
|
|
182
220
|
|
|
183
|
-
- [Accounts](./accounts)
|
|
184
|
-
- [Resource › Definition](/reference/resource/definition)
|
|
221
|
+
- [Accounts](./accounts): Rodauth feature flags that gate SecuritySection links
|
|
222
|
+
- [Resource › Definition](/reference/resource/definition): customizing the profile definition
|
|
185
223
|
- [App › Generators › Profile generators](/reference/app/generators#profile-generators)
|