Skip to content

Profile โ€‹

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.

๐Ÿšจ Critical โ€‹

  • 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.
  • Profile needs pu:profile:conn. Without it: no route, no profile_url override, no user-menu link.
  • 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.

Quick setup โ€‹

bash
rails g pu:profile:setup date_of_birth:date bio:text \
  --dest=competition \
  --portal=competition_portal

Meta-generator: runs pu:profile:install + pu:profile:conn in one shot.

Step-by-step โ€‹

bash
rails generate pu:profile:install bio:text avatar:attachment 'timezone:string?' \
  --dest=customer

rails db:prepare

rails generate pu:profile:conn --dest=customer_portal
OptionDefaultDescription
--dest=DEST(prompts)Target package or main_app
--user-model=NAMEUserRodauth user model

Custom resource name (first positional argument):

bash
rails g pu:profile:install AccountSettings bio:text --dest=main_app

What gets created โ€‹

By default the model is {UserModel}Profile (UserProfile, StaffUserProfile, etc.), derived from --user-model.

app/models/[package/]user_profile.rb
db/migrate/xxx_create_user_profiles.rb
app/controllers/[package/]user_profiles_controller.rb
app/policies/[package/]user_profile_policy.rb
app/definitions/[package/]user_profile_definition.rb

The generator modifies the user model:

ruby
has_one :profile, class_name: "UserProfile", dependent: :destroy

Association name is fixed

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.

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>.

The SecuritySection component โ€‹

Dynamically lists Rodauth security links based on which features are enabled on the account.

Feature enabledLink rendered
change_passwordChange Password
change_loginChange Email
otpTwo-Factor Authentication
recovery_codesRecovery Codes
webauthnSecurity Keys
active_sessionsActive Sessions
close_accountClose Account

If a feature isn't enabled on the account, its link doesn't render; no configuration needed.

To customize (e.g. add chrome, reorder), override ShowPage#render_after_content:

ruby
class UserProfileDefinition < Plutonium::Resource::Definition
  class ShowPage < ShowPage
    private

    def render_after_content
      render Plutonium::Profile::SecuritySection.new
    end
  end
end

See UI โ€บ Pages for page-class customization.

Connecting to a portal โ€‹

pu:profile:conn registers the profile as a singular resource: /profile (no :id), and overrides the profile_url helper:

bash
rails g pu:profile:conn --dest=customer_portal

This is what makes the profile visible. Without it, the model exists but has no route in any portal.

What pu:profile:conn generates โ€‹

It runs pu:res:conn with --singular --policy --definition, then injects:

  • 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.
  • Controller: resource_params merged with user: current_user, so the owner is set server-side.
  • Definition: a ShowPage whose render_after_content renders Plutonium::Profile::SecuritySection.
  • Portal controller concern: profile_url, which points at the profile or, when the user has none yet, at the new-profile form.

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:

ruby
def update?
  true
end

Users without a profile row โ€‹

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:

ruby
class User < ApplicationRecord
  after_create :create_profile
end

and backfill existing users:

bash
rails runner "User.find_each { |u| u.profile || u.create_profile }"

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).

Linking to the profile โ€‹

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:

ruby
link_to("Profile", profile_url) if profile_url

Account features on the profile page โ€‹

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:

ruby
class ShowPage < ShowPage
  private

  def render_after_content
    render Plutonium::Profile::SecuritySection.new
    div(class: "mt-8") do
      a(href: resource_url_for(ApiToken), class: "font-medium text-[var(--pu-text)] hover:underline") { "API tokens" }
    end
  end
end

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.

Customizing the definition โ€‹

The generated definition is a normal Plutonium resource definition. Customize like any other:

ruby
class UserProfileDefinition < Plutonium::Resource::Definition
  field :bio, as: :markdown
  input :avatar, as: :uppy
  field :timezone, as: :select, choices: ActiveSupport::TimeZone.all.map(&:name)

  metadata :created_at, :updated_at

  class ShowPage < ShowPage
    private

    def render_after_content
      render Plutonium::Profile::SecuritySection.new
    end
  end
end

See Resource โ€บ Definition for the full definition surface.

Multiple account types โ€‹

If your app has both User and StaffUser accounts, run pu:profile:install once per:

bash
rails g pu:profile:install --user-model=User --dest=main_app
rails g pu:profile:install --user-model=StaffUser --dest=main_app

Each gets its own *Profile model with :profile association on the respective user. Connect each to the appropriate portal:

bash
rails g pu:profile:conn UserProfile      --dest=customer_portal
rails g pu:profile:conn StaffUserProfile --dest=admin_portal

Gotchas โ€‹

  • 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.
  • 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.
  • 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>.
  • Custom resource name: pass it as the first positional argument to pu:profile:install. The association is still :profile.
  • SecuritySection shows nothing: none of the relevant Rodauth features are enabled on the account. Enable change_password, otp, etc. on the Rodauth plugin.

Released under the MIT License.