Ruby
Rails
devise

deviseで、パスワードを登録していないユーザーはパスワード追加、登録しているユーザーはパスワードを更新する

More than 3 years have passed since last update.

OmniAuthなどを使っていて、

  • OmniAuthで外部サービスと連携している場合はパスワード不要
    • パスワード不要でも、パスワード追加、更新は可能
  • OmniAuthで外部サービスと連携していない場合はパスワード必須

のような状況で、Deviseでのパスワード変更機能を作った。

Deviseでログイン機能を作ると、edit_user_registration_pathが自動に作られるはずなので、そこをカスタマイズする。

link_to('パスワードを変える', edit_user_registration_path(@user))

パスワード変更の入力フォームは以下のように書く。

registrations.html.slim
.main
  .container
    .row
      .span4.offset4
        h2
          | パスワード変更
          = resource_name.to_s.humanize
        = form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f|
          = devise_error_messages!
          - if current_user.encrypted_password.present?
            div
              = f.label :current_password, '現在のパスワード'
              br
              = f.password_field :current_password
          div
            = f.label :password, '新しいパスワード'
            br
            = f.password_field :password, :autocomplete => 'on'
          div
            = f.label :password_confirmation, '新しいパスワードをもう一度'
            br
            = f.password_field :password_confirmation
          div
            = f.submit '更新する'

User情報を更新するコントローラメソッドはDevise::RegistrationsControllerのupdate。

registration_controller.rb
 # PUT /resource
  # We need to use a copy of the resource because we don't want to change
  # the current user in place.
  def update
    self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
    prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)

    if update_resource(resource, account_update_params)
      yield resource if block_given?
      if is_flashing_format?
        flash_key = update_needs_confirmation?(resource, prev_unconfirmed_email) ?
          :update_needs_confirmation : :updated
        set_flash_message :notice, flash_key
      end
      sign_in resource_name, resource, bypass: true
      respond_with resource, location: after_update_path_for(resource)
    else
      clean_up_passwords resource
      respond_with resource
    end
  end

# ...

protected

# ...

 # 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

その中でも、実際に更新を行っているのは if update_resource(resource, account_update_params) の部分。

なので、deviseを使っているモデルでupdate_resourceをoverrideして、もしもまだパスワードを登録していないときは、パスワードを確認せずにアップデートして、パスワードを登録しているときはパスワードを確認してアップデートする。

registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
  before_filter :configure_permitted_parameters, if: :devise_controller?

 #override
  def update_resource(resource, params)
    resource.update_with_password_if_password_present(params)
  end

  def configure_permitted_parameters
     devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:email, :pen_name, :password, :password_confirmation, :current_password) }
  end

このように、DeviseのRegistrationsControllerを継承した先でoverride。

user.rb
class User < ActiveRecord::Base

# 省略  

devise :database_authenticatable, :registerable,
    :recoverable, :rememberable, :trackable, :validatable

# 省略

  def update_with_password_if_password_present(params)
    if encrypted_password.present?
      return update_with_password(params)
    else
      result = update_attributes(params)
      clean_up_passwords
      return result
    end
  end

なお、DatabaseAuthenticatableのupdate_with_passwordの実装は以下のようなもの。

database_authenticatable.rb
      # Update record attributes when :current_password matches, otherwise returns
      # error on :current_password. It also automatically rejects :password and
      # :password_confirmation if they are blank.
      def update_with_password(params, *options)
        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_attributes(params, *options)
        else
          self.assign_attributes(params, *options)
          self.valid?
          self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
          false
        end

        clean_up_passwords
        result
      end

参考