🔑

[Rails] 未ログインでパスワード変更するときにdevise管理でないuserのカラムも更新する

2024/04/17に公開

前段

サービスとして、何かしらのSNSログインを廃止するとき、あるアカウントが仮にそのSNSとしか連携していなかった場合は、そのアカウントを持つユーザーは何もしないままだと、廃止以降にセッションが切れた時点で二度とアカウントにログインできなくなってしまいます。

このような時、どうすればいいのでしょうか?

OAuth連携のためにdeviseとomniauthを使っていた場合は、連携時にusersテーブルに、連携先のサーバーから取得したemailデータを入れることができるため、サービスとしては、あとはパスワードさえ設定してもらえば次回からメールアドレスとパスワードでログインしてもらえます。

なのでパスワード再設定をしてね〜と通知を送ります。

それで素早く対応してくれるユーザーばかりだといいのですが、当然ながらすぐには対応してくれないユーザーもいます。

また、対応してくれないユーザーが新たに注文や予約を行った場合、廃止によってログインができなくなると確実にトラブルとなります。

それを避けるためにも、廃止SNSでしか連携しておらず、かつパスワードの設定を行っていないユーザーは新たに注文や予約をさせないという方針が無難です。

そうなると、パスワードの設定を行ったか?がわかるboolを臨時で用意したいです。そしてそれをパスワードの設定時に更新することで、廃止SNSで連携していた人の注文や予約のブロックを解除してあげたい。

概要

前段が長くなりましたが、要は、パスワード再設定用メールのURLを未ログイン状態で踏んで、画面に従ってパスワード変更した際に、devise管理ではないusersレコードのboolを更新したいです。

こちらの、devise passwords_controller内の各種メソッドはオーバーライドできます。
https://github.com/heartcombo/devise/blob/main/lib/devise/models/recoverable.rb

今回はパスワードを更新した時にuser内の別カラムを併せて更新したいので、updateメソッドをオーバーライドします。

また、肝心の、更新先であるuserを特定できないと意味がないので、デフォルトのupdate内で最初に使われているreset_password_by_tokenメソッドの内部実装をマネします。

中身はこうです。

def reset_password_by_token(attributes = {})
  original_token       = attributes[:reset_password_token]
  reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token)

  recoverable = find_or_initialize_with_error_by(:reset_password_token, reset_password_token)

  if recoverable.persisted?
    if recoverable.reset_password_period_valid?
      recoverable.reset_password(attributes[:password], attributes[:password_confirmation])
    else
      recoverable.errors.add(:reset_password_token, :expired)
    end
  end

  recoverable.reset_password_token = original_token if recoverable.reset_password_token.present?
  recoverable
end

要するに、パスワード再設定等でURLのパラメータとしてあるreset_password_tokenをハッシュ化して、DBにあらかじめ入れられている、同じく元々tokenをハッシュ化していた値と照合して、一致したものがそのメールを受け取ったユーザーである、という特定の工程を踏んでいます。

結論

オーバーライドと、user特定の方針を踏まえて、以下のようにupdateを実装します。

class Users::PasswordsController < Devise::PasswordsController
  def update
    if params[:user].present?
      token = params[:user][:reset_password_token]
      digest = Devise.token_generator.digest(self, :reset_password_token, token)
      user = User.find_by(reset_password_token: digest)

      user.update!(has_updated_password: true) if user.present?
    end

    super
  end
end

これで、ユーザーがパスワードの設定を行ったかを把握できます。

感想

Deviseはメソッドの内部実装を漁りやすくてありがたい。
あと、SNSログインの廃止は、丁寧にやろうと思うと、単純に動線消せばいいという話ではなくなるし、ユーザーに小難しい内容を示しすぎない上で救済方法を伝える必要があるので、なかなか難しい。

Discussion