はじめに
Deviseのデフォルトの状態だと、ユーザーのアカウントをアップデートするには、current_passwordが必要です。
ただ名前やメールアドレスといった基本情報の編集のたびにパスワードを入力させるのは、UIの面を考慮するとあまりよろしくないです。そこで、現在のパスワードを入力しなくてもプロフィールの情報を編集できるように実装します。また、パスワードを変更する場合は、現在のパスワードを入力するような仕様にします。
Devise::RegistrationsController#updateの実装について
Devise::RegistrationsController#updateでは、以下のとおり、Devise::RegistrationsController#update_resource が呼び出されています。
def update
resource_updated=update_resource(resource,account_update_params)
end
参考: devise/app/controllers/devise/registrations_controller.rb
Devise::RegistrationsController#update_resource の実装
# By default we want to require a password checks on update.
# You can overwrite this method in your own RegistrationsController.
def update_resource(resource, params)
resource.update_with_password(params)
end
参考:devise/app/controllers/devise/registrations_controller.rb
説明書きから次のことがわかります。
- デフォルトで、アップデート時にパスワードをチェックされる。
- RegistrationsControllerオーバーライドできる。
つまり、このupdate_resourceメソッドを自身の定義するRegistrationsControllerでオーバーライドすれば、ユーザー情報のアップデート時の振る舞いをカスタマイズできそうです。
update_resourceメソッドの中では、update_with_passwordメソッドが呼ばれていることがわかります。次にこのメソッドを確認します。
また、update_resourceメソッドをオーバーライドするにあたって、参考のため、そのデフォルトの挙動についても確認しておきます。
Devise::Models::DatabaseAuthenticatable#update_with_passwordの実装
# Update record attributes when :current_password matches, otherwise
# returns error on :current_password.
#
# This method also rejects the password field if it is blank (allowing
# users to change relevant information like the e-mail without changing
# their password). In case the password field is rejected, the confirmation
# is also rejected as long as it is also blank.
def update_with_password(params, *options)
if options.present?
ActiveSupport::Deprecation.warn <<-DEPRECATION.strip_heredoc
[Devise] The second argument of `DatabaseAuthenticatable#update_with_password`
(`options`) is deprecated and it will be removed in the next major version.
It was added to support a feature deprecated in Rails 4, so you can safely remove it
from your code.
DEPRECATION
end
current_password = params.delete(:current_password)
if params[:password].blank?
params.delete(:password)
params.delete(:password_confirmation) if params[:password_confirmation].blank?
end
result = if valid_password?(current_password)
update(params, *options)
else
assign_attributes(params, *options)
valid?
errors.add(:current_password, current_password.blank? ? :blank : :invalid)
false
end
clean_up_passwords
result
end
参考:devise/lib/devise/models/database_authenticatable.rb
説明書きによると、次のことがわかります。
- :current_passwordに存在した時に要素を更新する。
- :current_passwordに存在しなかった場合は、:current_passwordについて、エラーを発生させ、更新させない。
- password フイールドが空白であった場合も同様に、更新させない。ただし、パスワードの変更を伴わない、e-mailといった情報の変更は、この限りではない。
- password_confirmation フィールドが空白だった場合も同様に更新させない。
具体的に処理の中身を確認します。今回は、引数にオプションがあった場合の処理については、触れません。
上から順番に確認します。
current_password = params.delete(:current_password)
ここでは、変数current_passwordにparams.delete(:current_password)の値を代入しています。
deleteメソッドについて
key に対応する要素を取り除き、その要素を返します。
参考:instance method Hash#delete
つまり、params.delete(:current_password)では、フォームに入力された値を格納したハッシュ(params)
から:current_passwordに紐づく要素を取得していることになります。
次の処理を確認します。
if params[:password].blank?
params.delete(:password)
params.delete(:password_confirmation) if params[:password_confirmation].blank?
end
params[:password]に紐づく要素が存在しなかった場合は、:passwordと:password_confirmationをキーに持つ要素を取り除いています。
次の処理を確認します。
result = if valid_password?(current_password)
update(params, *options)
else
assign_attributes(params, *options)
valid?
errors.add(:current_password, current_password.blank? ? :blank : :invalid)
false
end
変数current_passwordがパスワードとして妥当であれば、paramsハッシュの内容で情報を更新しています。
妥当でなかった場合は、更新前の要素をassign_attributesメソッドで再度割り当てを行い、エラー原因を条件分岐させて該当するエラーを発生させています。
resultという変数に条件式の結果を代入しています。
参考:assign_attributes
参考:【Rails】errors.addって何?
最後の部分を確認します。
clean_up_passwords
result
clean_up_passwordsはdevise内部で定義されているメソッドで、パスワードの値をクリアしています。
# Set password and password confirmation to nil
def clean_up_passwords
self.password = self.password_confirmation = nil
end
参考:devise/lib/devise/models/database_authenticatable.rb
最後に、変数resultに格納されていた、条件式の結果を返しています。
ここまでで、params[:current_password]の値をdeleteメソッドで取り出して、その値で条件分岐しているということがわかりましたので、実際にメソッドを自分で定義してみます。
Users::RegistrationsController#update_resource の実装
それでは、実際にメソッドをオーバーライドします。
Controllerクラスのフォルダー構造は次のとおりです。usersフォルダーを作成して、その下にregistrations_controller.rbファイルを作成しています。
まず、Devise::RegistrationsControllerを継承したUsers::RegistrationsControllerクラスを定義します。
Users::RegistrationsControllerは、users/registrations_controller.rbというファイル構造を表しています。
参考:定数の自動読み込みと再読み込み (Zeitwerk)
update_resourceメソッドをオーバライドします。メソッド内部でカスタムメソッドのupdate_without_current_passwordメソッドを呼び出す記述を行います。
class Users::RegistrationsController < Devise::RegistrationsController
protected
def update_resource(resource, params)
resource.update_without_current_password(params)
end
end
それでは、update_without_current_passwordメソッドの処理を記述します。ご覧いただいてわかるかと思いますが、Devise定義のupdate_with_passwordメソッドの処理を少し変えただけです。
今回実現したいこと
それでは、今回実現したいことをもう一度再確認します。
実現したい実装は以下の2点でした。
- 現在のパスワード(current_password)の入力なしでユーザー情報を更新したい。
- パスワードの変更の場合は、現在のパスワードの入力を求める。
RegistrationsController#update_without_current_passwordの実装
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
validates :name, presence: true, length: { maximum: 50 }
def update_without_current_password(params, *options)
if params[:password].blank?
params.delete(:password)
params.delete(:password_confirmation) if params[:password_confirmation].blank?
params.delete(:current_password)
result = update(params, *options)
else
current_password = params.delete(:current_password)
result = if valid_password?(current_password)
update(params, *options)
else
assign_attributes(params, *options)
valid?
errors.add(:current_password, current_password.blank? ? :blank : :invalid)
false
end
end
clean_up_passwords
result
end
end
処理としては、次のような感じです。
if フォームのパスワード欄が空欄?
パラメータのパスワード情報を削除
パラメータのパスワード確認情報を削除
パラメータの現在のパスワード情報を削除
結果=self.update(名前情報,メールアドレス情報)
else #(フォームのパスワード欄に入力が有りの場合)
current_password=パラメータの現在のパスワード情報
result= if 現在のパスワードが正しい?
結果=self.update(名前情報,メールアドレス情報,パスワード情報,パスワード確認情報)
else#(フォームの現在のパスワード欄に入力誤りの場合)
更新中の値(名前情報,メールアドレス情報,パスワード情報,パスワード確認情報)をフォームの各欄に戻す
現在のパスワードがエラーになった理由を返す。
end
end
これでメソッドの記述は以上となります。
最後にルーティングを編集して、Devise::RegistrationsControllerではなく、カスタムしたRegistrationsControllerを参照するようにします。
Rails.application.routes.draw do
devise_for :users,
controllers: { registrations: 'users/registrations' }
end
Deviseのルーティングをカスタマイズするためには、
devise_forで、controllerオプションにハッシュを指定します。
routes.rbでdevise_forと記述することで、deviseがルーティングを設定してくれています。
devise_for :users,
controllers: { Deviseのコントローラー名 : 'カスタムコントローラー名' }
controllers: the controller which should be used. All routes by default points to Devise controllers.
However, if you want them to point to custom controller, you should do:
devise_for :users, controllers: { sessions: "users/sessions" }
引用:Method: ActionDispatch::Routing::Mapper#devise_for
これで、実装完了です!
まとめ
- Deviseのデフォルトの挙動を変更するためには、オーバーライドが必要。
- デフォルトのソースコードを確認して、できるだけそれを活用すれば、案外簡単に変更が可能。
- カスタムメソッドを参照するとように、routes.rbを編集する必要がある。
参考
heartcombo/devise
Method: ActionDispatch::Routing::Mapper#devise_for
Devise でユーザーがパスワードなしでアカウント情報を変更するのを許可