🦓

[Rails]deviseによるユーザー認証 2/2

2023/07/17に公開

はじめに

前回の記事ではdeviseによるユーザー登録・ログイン機能ができましたので、編集機能も作成していきます。

  • プロフィール編集(パスワードなし)
  • パスワード編集(現在のパスワード+新しいパスワード)
  • パスワードリセット

過去の記事でProfilesPasswordsPassword_resetsコントローラーでそれぞれ実装しましたがdeviseを使う場合のやり方もまとめてみました。

アプリに合わせてコードをカスタマイズすることがほとんどですね。
https://zenn.dev/redheadchloe/articles/aada5c9b052415

環境:

Rails 6.1.7.3
ruby 3.0.0

コントローラーを作成する

ユーザーCRUD関連のコントローラーを生成します。
omniauth_callbacks_controller.rbをすでに作成したためスキップします。

bin/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
        skip  app/controllers/users/omniauth_callbacks_controller.rb

routes.rbを編集する

デフォルトのコントローラーをオーバーライドする必要があります。

config/routes.rb
Rails.application.routes.draw do
      devise_for :users, controllers: {
         omniauth_callbacks: 'users/omniauth_callbacks',
         registrations: 'users/registrations'
      }
end

パスワードなしで編集できるようにする

Devise のデフォルトのユーザーの編集画面(registration#edit)では、ログイン済みのユーザーが自分の情報を変更するために、現在のパスワードを入力するのが必須となっています。
これをパスワードなしで自分の情報を更新できるように変更します。

https://github.com/heartcombo/devise/blob/main/app/controllers/devise/registrations_controller.rb#L91-L95

app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_sign_up_params, only: [:create]
  before_action :configure_account_update_params, only: [:update, :edit]

  protected

  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [:user_name])
  end

  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [:user_name, :email, :description, :profile, :profile_cache])
  end

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

end

user.rbにロジックを記述する

deviseのデフォルトのpassword_required?メソッドを上書きします。

app/models/user.rb
class User < ApplicationRecord
    validates :password, presence: true, length: { minimum: 6 }, if: :password_required?

    def password_required?
        new_record? || password.present? || password_confirmation.present?
    end
end

https://github.com/heartcombo/devise/blob/main/lib/devise/models/validatable.rb#L52-L57

deviseのデフォルトの設定では次の条件のいずれかが満たされる場合にパスワードが必要となります。

!persisted?:新規にユーザーを作成する時

!password.nil?:パスワードがnilではない時

!password.confirmation.nil?:パスワード確認がnilではない時

ユーザー編集用urlを設定する

app/views/shared/_header.html.erb
<%= link_to User.human_attribute_name(:profile), edit_user_registration_path, class: 'nav-link' %>

ユーザー編集ビューを設定する

パスワードのフィールドを削除し、ユーザー名、紹介、プロフィール画像用のフィールドを追加します。

app/views/devise/registration/edit.html.erb
<h2>Edit <%= resource_name.to_s.humanize %></h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>
  <div class="form-group">
    <%= f.label :user_name %>
    <%= f.text_field :user_name, class: "form-control mb-3" %>
  </div>
  <div class="form-group">
    <%= f.label :email %>
    <%= f.email_field :email, class: "form-control mb-3" %>
  </div>
  <div class="form-group">
    <%= f.label :description %>
    <%= f.text_field :description, class: "form-control mb-3" %>
  </div>
  <div class="form-group mb-3">
    <%= f.label :profile %>
    <%= f.file_field :profile, class: "form-control js-file-select-preview", accept: 'image/*', data: { target: '#preview-target' }  %>
    <%= f.hidden_field :profile_cache %>
  </div>
  <div class='form-group mb-3'>
    <% if @user.profile.present? %>
      <%= image_tag @user.profile.url, size: '100x100'%>
    <% else %>
      <%= image_tag 'user.png', id: 'preview-target', size:'50x50', class: 'round-circle' %>
    <% end %>
  </div>
  <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
    <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
  <% end %>
  <%= f.submit  class: "btn btn-primary" %>
<% end %>
<h3>Cancel my account</h3>
<div>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %></div>
<%= link_to "Back", :back %>

パスワードなしでアカウントを編集および更新できることを確認します。
バリデーションに引っかかる場合エラーが出ることも確認します。

次パスワードの更新も見ていきます。

パスワードを更新する

devisewikiでは三つの方法を紹介されました。
https://github.com/heartcombo/devise/wiki/How-To%3A-Allow-users-to-edit-their-password

registrations#editがプロフィールの編集と更新に使いましたので三つ目の方法で実装したいと思います。
仮にプロフィールの編集および更新は他のコントローラー(Profile)で実装した場合、デフォルトのコントローラー(Devise::RegistrationsController#update)でパスワードの更新を行うのが一番スムーズにできると思います。

Userコントローラーを作成する

app/controllers/users_controller.rb
class UsersController < ApplicationController
    before_action :authenticate_user!

    def edit
        @user = current_user
    end

    def update_password
        @user = current_user
        if @user.update_with_password(password_params)
            flash[:success] = "パスワードを更新されました。もう一度ログインお願いします。"
            redirect_to new_user_session_path
        else
            flash.now[:danger] = "パスワードの更新が失敗しました。もう一度試してください。"
            render :edit, status: :unprocessable_entity
        end
    end

    private

    def password_params
        params.require(:user).permit(:password, :password_confirmation, :current_password)
    end
end

bypass_sign_in(@user) メソッドを使用して、パスワードが変更された後にユーザーを自動的に再ログインさせることができます。

ルーティングを指定する

パスをupdate_passwordにしましたが他の名前でも大丈夫です。
コントローラーのアクション名と一致する必要があることに注意しましょう。

config/routes.rb
resource :user, only: [:edit] do
  collection do
    patch 'update_password'
  end
end

update_with_passwordは、Deviseによって提供されるメソッドであり、Rails アプリケーションでユーザーのパスワードを更新するために使用されます。通常、このメソッドは、ユーザーが現在のパスワードを提供して新しいパスワードを変更する場合に使用されます。
current_password パラメーターは、ユーザーが現在のパスワードを提供するために使用されます。

update_with_password メソッドはcurrent_passwordとセットで使用する必要があります。
current_passwordなしでパスワードを更新できるようにする場合は、いつも通りのupdateメソッドで大丈夫です。

https://github.com/heartcombo/devise/blob/main/lib/devise/models/database_authenticatable.rb#L83-L115

パスワード更新へのurlを追加する

app/views/shared/_header.html.erb
<%= link_to t('users.edit.title'), edit_user_path, class: 'nav-link' %>

パスワードを更新用ビューを作成する

app/views/users/edit.html.erb
<h1><%= t('.title') %></h1>
<%= form_for(@user, :url => { :action => "update_password" } ) do |f| %>
  <div class="form-group">
    <%= f.label :current_password %>
    <%= f.password_field :current_password, class: 'form-control mb-3' %>
  </div>
  <div class="form-group">
    <%= f.label :password %><em>(<%= t('devise.shared.minimum_password_length', count: 6 ) %>)</em>
    <%= f.password_field :password, :autocomplete => "off", class: 'form-control mb-3'  %>
  </div>
  <div class="form-group">
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation, class: 'form-control mb-3' %>
  </div>
  <%= f.submit class: 'btn btn-primary' %>
<% end %>

パスワードの編集および更新できることを確認します。
バリデーションに引っかかる場合エラーが出ることも確認します。

ユーザーにメール通知を送る

パスワード更新後にユーザーにメール通知を送る場合はdeviseの設定を変えます。
app/views/devise/mailer/password_change.html.erbのメールが送られます。

config/initializers/devise.rb
Devise.setup.do |config|
-     # config.send_password_change_notification = false
+     config.send_password_change_notification = true
end

最後はパスワードのリセットを見ていきます。

パスワードリセット

deviseのパスワードリセット機能は:recoverableのモジュールで行われ、reset_password_tokenreset_password_sent_atencrypted_passwordのフィールドが必要です。
passwords#editコントローラーとpasswords/edit.html.erbを通してリセット用のメールをリクエストします。
ユーザーは受け取ったメール内のリンクをクリックして、パスワードリセットページにアクセスします。このページでは、新しいパスワードを入力し、パスワードをリセットします。Deviseはデフォルトでnew_password_pathを提供しています。
パスワードリセットページでユーザーが新しいパスワードを入力すると、パスワードが更新されます。

今回deviseのデフォルトの機能をそのままで良いので流れだけを見ていきましょう。

:require_no_authentication

edit_user_password_pathにアクセスしてみると、:require_no_authenticationとのメソッドに引っかかりましたね。ログインした状態でパスワードを更新できないようになっています。

Started GET "/users/password/edit" for ::1 at 2023-07-17 11:15:46 +0900
Processing by Devise::PasswordsController#edit as HTML
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?  [["id", 15], ["LIMIT", 1]]
Redirected to http://localhost:3000/
Filter chain halted as :require_no_authentication rendered or redirected
Completed 302 Found in 4ms (ActiveRecord: 0.1ms | Allocations: 1464)

passwords_controller.rbのソースコードを見てみると、prepend_before_action :require_no_authenticationという行がありました。

https://github.com/heartcombo/devise/blob/715192a7709a4c02127afb067e66230061b82cf2/app/controllers/devise/passwords_controller.rb#L3-L4

通常、before_action メソッドは、アクションが実行される前に特定のメソッドを実行するために使用されます。prepend_before_action メソッドは、他の before_action メソッドよりも前に実行されるため、特定のアクションの前にこのメソッドを呼び出すことができます。

require_no_authentication メソッドは、ユーザーが認証済みでない場合にのみアクションを実行することを要求します。つまり、ログインしていないユーザーのみがアクションにアクセスできるようになります。

これは、一部のアクションを非ログインユーザーに公開する必要がある場合に便利です。たとえば、新規登録ページやログインページなど、ログインしていないユーザーがアクセスできるようにしたいページで使用できます。

パスワードリセットメールの有効期間

デフォルトの有効期間では6時間となってます。

config/initializers/devise.rb
Devise.setup.do |config|
    config.reset_password_within = 6.hours
end

パスワードリセットメール

メールを日本語にカスタマイズしてみます。
deviseのメールテンプレートはapp/views/devise/mailerにあります。
reset_password_instructions.html.erbを編集します。

app/views/devise/mailer/reset_password_instructions.html.erb
<!DOCTYPE html>
<html>
  <head>
    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
  </head>
  <body>
    <p><%= @user.user_name %>様</p>
    <p>
      パスワードリセットのご申請ありがとございます。
      下のリンクをクリックし、パスワードをリセットする画面が表示されますので、新しいパスワードを設定してください。
      ※パスワードリセットはメール受信後から「6時間以内」に行ってください。
    </p>
    <p><%= link_to "パスワードをリセットする", edit_password_url(@resource, reset_password_token: @token) %></p>
  </body>
</html>

退会機能

Registrations#destroy

Deviseの退会機能はデフォルトでRegistrationsコントローラーのdestroyアクションで実現できます。

https://github.com/heartcombo/devise/blob/main/app/controllers/devise/registrations_controller.rb#L64-L71

デフォルトのビュー:

app/views/devise/registrations/edit.html.erb
<h3>Cancel my account</h3>

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

論理削除

上記の方法ではユーザーと紐付いた全てのデータも一緒に削除されますが、データを残す論理削除という扱いもあります。

公式ドキュメント:
https://github.com/heartcombo/devise/wiki/How-to:-Soft-delete-a-user-when-user-deletes-account

終わりに

deviseによるユーザーのCRUDでした。
他の機能も少しずつ試していきたいと思います。

Discussion