もふもふ技術部

IT技術系mofmofメディア

【Rails】deviseを使わない自前のユーザー認証 -パスワードリセット編

deviseを使わない自前のユーザー認証 の続きで、今回はパスワードリセット機能を実装します。

下準備

1. カラムを追加します
$ rails g migration add_password_reset_to_users

20240726000000_add_password_reset_to_users.rb

class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    add_column :users, :password_reset_sent_at, :datetime
    add_column :users, :password_reset_digest, :string
  end
end
2. Userモデルに追記する

app/models/user.rb

def password_reset
  token = User.new_token
  self.password_reset_digest = User.digest(token)
  self.password_reset_sent_at = Time.zone.now
  self.save
  UserMailer.with(user: self, token: token).password_reset.deliver_now
end

def password_reset_expired?
  password_reset_sent_at < 10.minutes.ago
end

def password_update(params)
  self.password_reset_digest = nil
  self.password_reset_sent_at = nil
  self.update(params)
end

def authenticated?(attribute, token)
  digest = self.send("#{attribute}_digest")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(token)
end

class << self
  def digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  def new_token
    SecureRandom.urlsafe_base64
  end
end
3. メールを送信できるようにする(本記事では割愛)

パスワードリセットのリクエストを実装

1. ルーティングを設定する

config/routes.rb

get  '/password_reset/request', to: 'password_reset/request#new'
post '/password_reset/request', to: 'password_reset/request#create'
get  '/password_reset/requested', to: 'password_reset/request#index'
2. コントローラーの実装

app/controllers/password_reset/request_controller.rb

class PasswordReset::RequestController < ApplicationController
  skip_before_action :signed_in_user

  def new; end

  def create
    user = User.find_by(email: params[:email])
    user&.password_reset
    redirect_to password_reset_requested_url, status: :see_other
  end

  def index; end
end
3. viewsの実装

app/views/session/new.html.erb

以下を追記

<%= link_to "パスワードを忘れた", password_reset_request_path %>

app/views/password_reset/request/new.html.erb

<div>パスワードを再設定したいアカウントのメールアドレスを入力してください</div>
<%= form_with url: password_reset_request_path do |f| %>
  <div>
    <label>メールアドレス</label>
    <%= f.email_field :email, placeholder: "user@example.com", required: true %>
  </div>
  <div>
    <%= f.submit "送信する" %>
  </div>
<% end %>

app/views/password_reset/request/index.html.erb

<div>入力していただいたメールアドレス宛にパスワード再設定用のメールを送信しました。</div>
4. メーラーの実装

app/mailers/user_mailer.rb

class UserMailer < ApplicationMailer
  def password_reset
    @token = params[:token]
    mail(to: params[:user].email, subject: 'パスワード再設定')
  end
end

app/views/user_mailer/password_reset.html.erb

<div>パスワード再設定依頼を受け付けました。</div>
<div>10分以内に以下のURLより再設定を行なってください。</div>
<div><%= link_to "パスワードを再設定する", password_reset_url(token: @token, email: @user.email) %></div>

パスワードの更新を実装

1. ルーティングを設定する

config/routes.rb

get  '/password_reset/processed', to: 'password_reset#index'
get  '/password_reset/:token', to: 'password_reset#edit', as: 'password_reset'
post '/password_reset/:token', to: 'password_reset#update'
2. コントローラーの実装

app/controllers/password_reset_controller.rb

class PasswordResetController < ApplicationController
  before_action :set_user, except: :index
  before_action :valid_user, except: :index
  skip_before_action :signed_in_user

  def index; end

  def edit; end

  def update
    if @user.password_update(user_params)
      redirect_to password_reset_processed_url, status: :see_other
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private
  
  def user_params
    params.fetch(:user, {}).permit(:password, :password_confirmation)
  end

  def set_user
    @user = User.find_by(email: params[:email])
  end

  def valid_user
    unless @user.present? && @user.authenticated?(:password_reset, params[:token]) && !@user.password_reset_expired?
      redirect_to password_reset_request_url, status: :see_other and return
    end
  end
end
3. viewsの実装

app/views/password_reset/edit.html.erb

<%= form_with model: @user, url: password_reset_path(token: params[:token], email: params[:email]), local: true, method: :post do |f| %>
  <div>
    <label>パスワード</label>
    <%= f.password_field :password, placeholder: "英数字を2つ以上組み合わせよう", required: true %>
  </div>
  <div>
    <label>パスワード(確認)</label>
    <%= f.password_field :password_confirmation, placeholder: "英数字を2つ以上組み合わせよう", required: true %>
  </div>
  <div>
    <%= f.submit "更新する" %>
  </div>
<% end %>

app/views/password_reset/index.html.erb

<div>パスワードを変更しました。</div>

終わり

以上で実装は終わりです!
必要に応じて各種文言などをカスタマイズしたり、エラーメッセージを出してみたりしてみてください!