Deviseでプロフィール更新とパスワード変更フォームを分ける方法【Rails 8対応】

  • このエントリーをはてなブックマークに追加

スポンサーリンク

はじめに

Deviseでユーザー登録機能を作ったあと、「プロフィールを1つ変えるだけなのに、毎回パスワードの入力を求められる」——そんなUXに困っていませんか?

DeviseはRailsアプリケーションに認証機能を簡単に追加できる便利なGemですが、デフォルトの設定では以下のようにプロフィール更新とパスワード変更が同じフォームにまとめられています。

Deviseデフォルトの、プロフィール更新とパスワード変更が同居した編集フォーム

このままだと、ユーザーが名前やメールアドレスを更新するたびにパスワードも入力しなければならず、UX的にも良くありません。

そこでこの記事では、Deviseをカスタマイズして、プロフィール更新とパスワード変更を別々のフォームで行えるようにする方法を紹介します。これにより、ユーザーにとってより使いやすいインターフェースを提供できるようになります。

Rails 8時代でもDeviseを使う場面はある

ちなみにRails 8からは、標準の認証ジェネレータが同梱され、Gemなしでもログイン機能を作れるようになりました。とはいえ、確認メール・パスワード再設定・アカウントロックといった機能を「載せるだけ」で使いたい場面では、今もDeviseが有力な選択肢です。

Devise自体もメンテナンスが続いており、Rails 8でも問題なく利用できます。DeviseはRubyのGemでアセットパイプラインに依存しないため、Rails 8で標準になったPropshaftへ移行しても、認証まわりの挙動には影響しません。

どちらを選ぶべきか迷っている方は、Rails 8の認証ジェネレータとDeviseはどっちを使う?で判断基準をまとめているので、そちらも参考にしてください。パスワードレスなログインに興味があれば、Rails 8でマジックリンクログインを実装する方法も合わせてどうぞ。

前提条件

Railsアプリケーションの初期設定やDeviseの導入ができているboilerplate(ボイラープレート)で進めます。

もし手元にサンプルアプリケーションがない方は、以下の記事を参考にしてみてください。

現在のパスワード無しでプロフィール更新できるようにする

カスタマイズについては、Deviseのリポジトリのwikiに情報があるので、これを参考に進めていきます。

How To: Allow users to edit their account without providing a password(Devise Wiki)

ポイントは以下の4つです。

  • Devise::RegistrationsControllerを継承した独自のコントローラーを作る
  • update_resourceメソッドをオーバーライドして、update_without_passwordメソッドを使う
  • routes.rbのDeviseに関するルーティングを変更して、独自のコントローラーを使うようにする
  • app/views/devise/registrations/edit.html.erbからパスワード関連の入力欄を削除する

まずはDeviseのコントローラーを継承した独自のコントローラーを生成します。

$ rails generate devise:controllers users

      create  app/controllers/users/confirmations_controller.rb
      create  app/controllers/users/passwords_controller.rb
      create  app/controllers/users/registrations_controller.rb
      create  app/controllers/users/sessions_controller.rb
      create  app/controllers/users/unlocks_controller.rb
      create  app/controllers/users/omniauth_callbacks_controller.rb
===============================================================================

Some setup you must do manually if you haven't yet:

  Ensure you have overridden routes for generated controllers in your routes.rb.
  For example:

    Rails.application.routes.draw do
      devise_for :users, controllers: {
        sessions: 'users/sessions'
      }
    end

===============================================================================

次に、routes.rbを変更して、registrationsで独自のコントローラーを使うようにします。

  devise_for :users, controllers: {
    registrations: "users/registrations"
  }

rails routesコマンドでルーティングを確認すると、registrationsに関連するものがdevise/registrationsではなく、users/registrationsを使うように変わっているはずです。

rails routesの出力。registrations系がusers/registrationsを指すようになった様子

続いて、生成されたUsers::RegistrationsControllerを次のように変更します。

# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
  protected

  def update_resource(resource, params)
    resource.update_without_password(params)
  end
end

update_without_passwordメソッドを使うことで、現在のパスワード無しでプロフィールを更新できるようになります。

最後に、app/views/devise/registrations/edit.html.erbからパスワードの入力欄を削除します。あわせて、フォームヘルパーは現在Railsで推奨されているform_withで書いています(Deviseが生成するデフォルトはform_forですが、新しく書くならform_withが基本です)。

<h2>Edit <%= resource_name.to_s.humanize %></h2>

<%= form_with(model: resource, url: registration_path(resource_name), method: :put) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :first_name %><br>
    <%= f.text_field :first_name %>
  </div>

  <div class="field">
    <%= f.label :last_name %><br>
    <%= f.text_field :last_name %>
  </div>

  <div class="field">
    <%= f.label :email %><br>
    <%= f.email_field :email, autocomplete: "email" %>
  </div>

  <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
    <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
  <% end %>

  <div class="actions">
    <%= f.submit "Update" %>
  </div>
<% end %>

<h3>Cancel my account</h3>

<div>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { turbo_confirm: "Are you sure?" }, method: :delete %></div>

<%= link_to "Back", :back %>

なお、削除の確認ダイアログはTurbo環境ではturbo_confirmが使われます。以前よく見かけたconfirmはTurbo有効時には効かないので、turbo_confirmに統一しておきましょう。

これで、現在のパスワード無しでもプロフィール更新ができるようになりました。名前やメールアドレスを変更して「Update」を押すと、パスワードを聞かれずに更新できるはずです。

パスワード欄を外し、名前とメールアドレスだけになったプロフィール編集フォーム

ちなみに、パスワードの変更もこのページで行いたい場合は、current_passwordの入力欄だけを削除すれば実現できます。今回はプロフィールと分けたいので、次はパスワード変更専用のページを追加します。

パスワード変更ページを追加する

次に、パスワードを変更するための専用ページを追加します。こちらもwikiに情報があるので、参考にしていきます。

How To: Allow users to edit their password(Devise Wiki・Solution 3)

ページを追加するため、コントローラーとViewを作ります。ここでは、追加するページのURLをaccount/password/editにします。まずはroutes.rbに以下を追加します。

  namespace :account do
    resource :password
  end

resources(複数形)ではなくresource(単数形)を使う理由は、ログインユーザー自身のパスワードに関するルーティングだからです。idは不要で、current_userから対象を取得できます。

続いて、コントローラーを追加します。

class Account::PasswordsController < ApplicationController
  before_action :authenticate_user!

  def show
    redirect_to edit_account_password_path
  end

  def edit
  end

  def update
    if current_user.update_with_password(password_params)
      bypass_sign_in current_user
      redirect_to account_password_path, notice: "Password updated successfully"
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private

  def password_params
    params.expect(user: [:current_password, :password, :password_confirmation])
  end
end

showアクションは、ここでは編集ページにリダイレクトするだけにしています。必要に応じてカスタマイズしてください。

updateアクションのbypass_sign_inメソッドを使うと、パスワードを変更したあともログイン状態を維持できます。もし変更後にログアウトさせたい場合は、この行は不要です。

なおpassword_paramsでは、Rails 8で追加されたparams.expectを使っています。従来のparams.require(:user).permit(...)でも動きますが、Rails 8以降は不正な形式のパラメータが送られたときに500ではなく400を返すparams.expectが推奨です。詳しくはRails 8のparams.expect入門でまとめています。params.expectはRails 8以降でしか使えないので、Rails 7以前のアプリではparams.require(:user).permit(:current_password, :password, :password_confirmation)に置き換えてください。

続いて、コントローラーに対応するViewを追加します。ここでは編集ページだけを用意します。

<h2>Edit Password</h2>

<%= form_with(model: current_user, url: account_password_path, method: :put) do |f| %>
  <%= render "devise/shared/error_messages", resource: current_user %>

  <div class="field">
    <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br>
    <%= f.password_field :current_password, autocomplete: "current-password" %>
  </div>

  <div class="field">
    <%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br>
    <%= f.password_field :password, autocomplete: "new-password" %>
    <% if @minimum_password_length %>
      <br>
      <em><%= @minimum_password_length %> characters minimum</em>
    <% end %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br>
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "Update" %>
  </div>
<% end %>

最後に、ヘッダーメニューにパスワード変更ページへのリンクを追加します。

<% if user_signed_in? %>
  <li class="nav-item dropdown">
    <%= link_to "#", class: "nav-link dropdown-toggle", data: { bs_toggle: "dropdown" }, aria: { expanded: false } do %>
      <i class="bi bi-person-circle"></i>
    <% end %>
    <div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbar-dropdown">
      <%= link_to "Settings", edit_user_registration_path, class: "dropdown-item" %>
      <%= link_to "Password", account_password_path, class: "dropdown-item" %>
      <div class="dropdown-divider"></div>
      <%= button_to "Logout", destroy_user_session_path, method: :delete, class: "dropdown-item" %>
    </div>
  </li>
<% else %>
  <li class="nav-item"><%= link_to "Sign Up", new_user_registration_path, class: "nav-link" %></li>
  <li class="nav-item"><%= link_to "Login", new_user_session_path, class: "nav-link" %></li>
<% end %>

ここまでで、見た目は以下のようになります。

現在のパスワード・新しいパスワード・確認用の3項目だけを持つパスワード変更フォーム

現在のパスワードを入力したうえで新しいパスワードを入力し、パスワードが変更されるか確認してみましょう。あわせて、間違ったパスワードを入力するとエラーが表示されることも確認しておくと安心です。

問題なく動けば完了です。

まとめ

この記事では、Deviseをカスタマイズして、プロフィール更新とパスワード変更を別々のフォームに分ける方法を紹介しました。実現できたのは次の2点です。

  • ユーザーは現在のパスワード無しでプロフィール更新ができる
  • パスワード変更は専用のページで行う

Deviseのカスタマイズは少し手間がかかりますが、update_without_password と update_with_password を使い分けるだけで、UXは大きく改善します。ぜひ試してみてください。

認証まわりをもう一歩深めたい方は、Rails 8の認証ジェネレータとDeviseの違いもチェックしてみてください。プロジェクトに合った認証の選び方が見えてきます。

参考までに、今回のコミットログはこちらです。

スポンサーリンク

  • このエントリーをはてなブックマークに追加

コメント

Comments are closed.