11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RUNTEQAdvent Calendar 2024

Day 10

【Rails7】deviseオーバーライド!:メールアドレス任意追加での認証とConfirmableの活用

Last updated at Posted at 2024-12-09

はじめに

こんにちは、こんばんは、初めまして。
プログラミングスクールRUNTEQで学習中のmassanです。

「RUNTEQ」の2024年アドベントカレンダーに参加させていただくこととなり、今回の記事を書くことにしました。

今年のテーマは『プログラミングでの"ワクワク"』ということなんですが、自分が今回のアドベントカレンダーへの参加を決めてから今日に至るまでに感じた『プログラミングでのワクワク』は『初めてのgemのカスタマイズ』だったのでそれについて記事を書いてみました。

また、deviseを使ってemail以外を用いて登録 → emailをあとから登録 → 正しいemailか確認するために確認メールを送信
というような要件を満たす実装をする際の解説を行っている記事があまりないように感じたので、自分の備忘録を兼ねて記事にしてみよう、というのもあります

つたない文章ですがよろしくお願いいたします。

対象読者

  • MVCやRailsの基本的な動作が理解できる
  • 認証機能がある程度理解できる
  • deviseを使った認証機能であとからemailを登録させたい
  • emailの登録/変更時に確認用のメールを送信したい

環境

  • Windows11
  • Ubuntu 24.04 LTS (WSL2)
  • Docker 26.1.4
  • Ruby 3.3.6
  • Rails 7.2.1
  • devise 4.9.4

今回の要件

  • ユーザー登録に必要なのはnameとpassword
  • ユーザー登録時、同時にemailも任意で登録できる
  • ユーザー登録後でもユーザー編集画面でemailを追加できる
  • 登録時や編集時にemailを追加した後、確認メールが送信され、ユーザーがメールを確認すると正式にメールアドレスが登録される

deviseの導入とemail以外での認証(下準備)

まずdebiseの導入とemail以外での認証を説明しようかなと思ったのですが、我らがRUNTEQに在籍されている方で読みやすい素晴らしい記事を書いておられる方がいたのでまずはこちらの記事をご覧ください!!&この部分の説明はこの記事に丸投げします!!

記事をご覧になられた方は次へどうぞ

紹介したの記事の中ではemailを使わずにnameだけで認証を行っていたので、今回私の記事で必要な要件とは少し違います。
上記の記事を参考に、今回の要件に必要なものを追記または変更していきます

bundle installを行った後

# Railsにdeviseの設定を追加してくれる
$ rails g devise:install

# deviseを使って認証するようにUserモデルを設定
$ rails g devise User

# deviseで使用されるviewを生成
$ rails g devise:views

# deviseの機能をオーバーライドしたいのでdeviseで使用されるコントローラーを生成
$ rails g devise:controllers users

以上を順番に実行し以下の変更を加えていきます

生成されたマイグレーションファイルを編集

# deviseを使って認証するようにUserモデルを設定
$ rails g devise User

を実行するとdeviseに必要なカラムを追加するためのマイグレーションファイルが作成されます。
今回はデフォルト設定のままだと困る部分があるので以下のように編集します

db/migrate/xxxx_add_devise_to_users.rb
# frozen_string_literal: true

class AddDeviseToUsers < ActiveRecord::Migration[7.2]
  def self.up
    change_table :users do |t|
      ## Database authenticatable
      t.string :email, null: true # ----- (1)
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
   
    # ====== 省略 =======

      # Confirmable # ----- (2)
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
    
    # ====== 省略 ======= 
    
    end

    add_index :users, :email, unique: true, where: "email IS NOT NULL AND email <> ''"  # ----- (3)
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true # ----- (2)
    # add_index :users, :unlock_token,         unique: true
  end

  def self.down # ----- (4)
    # By default, we don't want to make any assumption about how to roll back a migration when your
    # model already existed. Please edit below which fields you would like to remove in this migration.
    change_table :users do |t|
      # Database authenticatable
      t.remove :email, :encrypted_password

      # Recoverable
      t.remove :reset_password_token, :reset_password_sent_at

      # Rememberable
      t.remove :remember_created_at

      # Trackable
      # 以下のコメントアウトされたカラムは、もしself.upで有効にしたならば削除する
      # t.remove :sign_in_count, :current_sign_in_at, :last_sign_in_at, :current_sign_in_ip, :last_sign_in_ip

      # Confirmable
      t.remove :confirmation_token, :confirmed_at, :confirmation_sent_at, :unconfirmed_email

      # Lockable
      # t.remove :failed_attempts, :unlock_token, :locked_at
    end
  end
end

説明が必要そうな部分にコード内でコメントアウトで数字を割り当てておきました。
以下でその部分の説明をします

(1) 今回emailは「必須」ではないのでnullを許可する

そのままの意味です。(3)で似たような設定を追加しているのでもしかすると必要ないかもしれません

(2) Confirmable機能

今回はemail追加時、emailが正しいものであることを確認するために追加されたemailを使って確認メールを送信することが必要です。deviseでは「Confirmable」という似たような機能がオプションで追加できるのでその部分のコメントアウトを解除します。
Confirmable機能の詳しい説明は後述します

(3) 部分index

今回の要件は「登録時にemailを "任意で" 登録できる」ということです。つまり、「登録時/編集時にフォームに対しemailの入力があったりなかったりする」ということがあります。

emailの項目に入力が無い場合、「""」(空文字)がDBに送信され、保存されます。

パスワードリセット機能などを実装するためにemailにもindexが追加されているのですが、その中のunique制約が「""」(空文字)に対しても適用されてしまい、DB上でたった一人しか未入力が許可されません。

なのでNULLと「""」(空文字)に対してはindexを追加しないように記述を追加します。

(4) マイグレーションファイルをdownするための記述

deviseによって生成されたマイグレーションファイルをdownしようとするとエラーが出るので、今回このマイグレーションファイルで追加したものをdownした場合に削除できるようにこのような記述を追加する必要があります

参考記事:https://zenn.dev/hirodesu85/articles/1a391c74236931

以上の編集ができたらマイグレーション実行

$ rails db:migrate

Userモデルを編集

先ほどの$ rails g devise Userを実行するとUserモデルにdeviseを使用するために必要な記述も追加されます
今回はConfirmable機能も使用したいので「:confirmable」の記述を追加します。

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable # <---これ

  validates :email, uniqueness: true, allow_blank: true # deviceとともに追加

  validates :name,  presence: true, uniqueness: true, length: { maximum: 50 }

  # ====== 省略 ======= 

  # deviceのメソッドをオーバーライド
  def email_required?
    false
  end
end

また、このままだとemailに対してpresenceのバリデーションが適用されてしまっており、フォームが空の場合にバリデーションエラーになるので
deviseで用意されているメソッドをオーバーライドしてそのバリデーションを無効にしてあります

ソースコード参照:https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/lib/devise/models/validatable.rb#L62

生成されたdeviseのviewを編集

# deviseで使用されるviewを生成
$ rails g devise:views

以上を実行するとdeviseで使用されるviewが一通り生成されるので
その中でユーザー登録のformが記述してある
app/views/devise/registrations/new.html.erb
と、ユーザー編集のformが記述してある
app/views/devise/registrations/edit.html.erb
にname用のformを追加します(同じ記述なのでeditの方は省略)

app/views/devise/registrations/new.html.erb
<div class="mb-4">
  <%= f.label :name, class: "form-label" %>
  <%= f.text_field :name, class: "form-control", placeholder: "ユーザー名を入力(必須)", autofocus: true %>
</div>

残るはdeviseのcontrollerの編集だけ

上記の設定を済ませたらあとはdeviseの機能をオーバーライドするためにdeviseのcontrollerを編集するだけです。
ですがここで結構いろいろな学びがあったので、Confirmable機能とともに見出しを分けて詳しく書いていこうと思います。

emailの追加を任意にし、email追加時には確認メールを送信する

今回の要件ではこの見出しのように「emailの追加を任意にし、email追加時にはConfirmable機能を使って確認メールを送信する」ということが必要です。以下で順を追って説明しようと思いますがその前に、deviseのConfirmable機能について少し触れておきます

deviseのConfirmable機能

ここまでで何度か登場しているdeviseのConfirmable機能についてです

あるwebサイトではアカウント登録した際「仮登録状態」となりwebサイトの一部機能へのアクセスが制限されます。
仮登録した際に送信される「アカウント認証メール」の中に記載されているURLにアクセスすると「本登録状態」となり、すべての機能にアクセスできるようになります。

この「仮登録状態」or「本登録状態」を確認し、本登録手順とURLをメールで送信する役割を担うのがConfirmable機能です

公式参照:https://www.rubydoc.info/github/heartcombo/devise/main/Devise/Models/Confirmable

生成されたdeviseのcontrollerを編集

# deviseの機能をオーバーライドしたいのでdeviseで使用されるコントローラーを生成
$ rails g devise:controllers users

上記を実行するとdeviseの機能をオーバーライドするためのコントローラーが生成されるのでそれを編集していきます

まずは紹介した記事を参考にnameで認証できるよう、「ユーザー登録画面」と「ユーザー情報編集画面」のストロングパラメーターを設定している部分のコメントアウトを解除して書き換えます

app/controllers/users/registrations_controller.rb
# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_sign_up_params, only: [ :create ] # -----(1) コメントアウト解除
  before_action :configure_account_update_params, only: [ :update ] # -----(1) コメントアウト解除

  # ====== 省略 =======

  protected

  # If you have extra params to permit, append them to the sanitizer.
  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [ :name ]) # -----(3) ストロングパラメータ
  end

  # If you have extra params to permit, append them to the sanitizer.
  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [ :name, :email ]) # -----(2) ストロングパラメータ
  end

  # ====== 省略 =======
  
end

(1) コメントアウトを解除

ストロングパラメータを設定するのでそれを使えるようにbefore_actionの部分のコメントアウトを解除します

(2) ユーザー編集時のストロングパラメータに「:name, :email」を追加

ストロングパラメータに「:name, :email」を追加して二つの値を受け取って編集できるようにします

(3) ユーザー登録時のストロングパラメータに 「:name」のみ 追加

なぜ編集の部分で追加していたはずの「:email」が追加されていないのか?
と疑問に思うと思います。これは、今回の要件だと 「ユーザー登録時にそのユーザーのemailカラムに直接emailアドレスを保存したくないから」 です。

詳しくは後述しますが、ユーザー登録時のemail追加の挙動を変更しているため、ストロングパラメータにemailを含めてしまうと「確認メールが送信されるが、そのメールが実際に確認される前にemailカラムに未確認のemailが登録されてしまう」という問題が発生してしまい、適当なメールアドレスも登録できてしまいます
なのであえてストロングパラメータに:emailを含めず、後述する別の処理を通してからemailアドレスをemailカラムに保存するようにします。

deviseで使用するルーティング設定の変更

deviseを導入した際にルーティングに追加される記述のみではapp/controllers/users/registrations_controller.rbが認識されず使用されません
なので以下のようにdeviseのルーティング設定に記述を追加します

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

  # ====== 省略 ======

end

ユーザー登録時の機能をオーバーライド

今回の記事のメイン部分です。

今回の要件は最初に書いた通り

  • ユーザー登録に必要なのはnameとpassword
  • ユーザー登録時、同時にemailも任意で登録できる
  • ユーザー登録後でもユーザー編集画面でemailを追加できる
  • 登録時や編集時にemailを追加した後、確認メールが送信され、ユーザーがメールを確認すると正式にメールアドレスが登録される

というものです
1~3番目はこれまでの設定で達成されていますがこのままだと4番目が未達成のままです。以下に理由を説明します

ユーザー登録時にemailを登録しないとほとんどの機能にアクセスできない

そもそも先ほどの4番目の部分は不正なアカウント登録を極力弾くために正しく使用可能なメールアドレスかどうか確認するためのものですが、今回はこれを実現するために前述したConfirmable機能を応用しています

本来Confirmable機能は仮登録と本登録でアクセスできる機能を分けるためのものです

この機能をONの状態(マイグレーションの編集やモデルの編集でConfirmable機能に関する記述を行った状態)にすると仮登録状態での機能制限が適用され、登録時に追加したメールアドレスに届く「アカウント認証メール」を確認するまで本登録状態になりません。

Confirmable機能が提供する「仮登録状態」はアプリのほとんどの機能にアクセスできず、ひたすら「アカウント認証メールを確認してください」と表示されるだけになってしまいます

今回の要件ではこのレベルの「仮登録状態での機能制限」は必要なく、name, passwrodさえ登録すれば一部を除きアプリの機能へのアクセスを許可したいのです
emailを登録することでアクセスできるようになる機能についても、確認が済めばアクセスできるようにしたいので「emailが正式に登録されているかいないか」によってアクセスできるかできないかを分ければよさそうです。

よって今回はいったん、 登録が完了した時点でアカウントが認証された(本登録状態) と判断されるようにします

skip_confirmationを使って強制的に「本登録」状態にする

deviseのソースコードや関連する記事を探してみた結果、次のことが分かりました。

  • Confirmable機能の「本登録状態」とは「ユーザーがアカウント認証メールに記載されているURLにアクセスし、アカウントが認証された時間」が記録される 「confirmed_at」カラム に値が入っている状態を指す
  • この 「confirmed_at」カラム に値を入れるメソッドがdeviseに用意されていて、そのメソッドが skip_confirmation! という

この情報をもとにregistrations_controller.rbを以下のように変更します

app/controllers/users/registrations_controller.rb
# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_sign_up_params, only: [ :create ]
  before_action :configure_account_update_params, only: [ :update ]

  # ====== 省略 =======

  # POST /resource
  def create
    build_resource(sign_up_params)  # -----(1) ストロングパラメータを使用して新しいユーザーを作成

    resource.skip_confirmation! # -----(2) アカウント認証をスキップ

  # ====== 省略 =======

  end

  protected

  # If you have extra params to permit, append them to the sanitizer.
  def configure_sign_up_params # -----(1)
    devise_parameter_sanitizer.permit(:sign_up, keys: [ :name ])
  end

  # If you have extra params to permit, append them to the sanitizer.
  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [ :name, :email ])
  end

  # ====== 省略 =======

end

今回はcreateアクションのみを変更したいので

  # POST /resource
  # def create
  #   super
  # end

のように書いてある部分のコメントアウトを外して編集します
superはデフォルトの挙動を継承(?)するための記述です。今回は後述する変更も加えてデフォルトの機能を丸々書き換えるのでsuperの記述は削除します。

(1) ストロングパラメータを使用して新しいユーザーを作成

build_resource(sign_up_params)
# User.new(sign_up_params)と同じ

この部分ですがコメントアウトにもある通りUser.new(sign_up_params)と同じだと思われます
これが実行された結果resourceというローカル変数に作成されたuserのインスタンスが代入されるのでそれに対していろいろ操作を行います

sign_up_paramsの部分はprotected以下で定義してあるユーザー作成時に必要なストロングパラメータを参照するための記述です

ソースコード参照:
https://github.com/search?q=repo%3Aheartcombo%2Fdevise%20build_resource&type=code
https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/app/controllers/devise/registrations_controller.rb#L98

(2) アカウント認証をスキップ

先ほど説明したUserモデルのインスタンスが代入されているresourceに対し最初に説明したskip_confirmation!を使ってアカウント認証のスキップを適用(本登録状態に変更)します

ソースコード参照:
https://github.com/search?q=repo%3Aheartcombo%2Fdevise%20skip_confirmation!&type=code
https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/lib/devise/models/confirmable.rb#L156

以上の変更を行ったことでemailの追加無しで登録した場合でもある程度の機能にアクセスできるようになりました

ユーザー登録時にemailを追加しても確認メールが送信されない

先ほどまでの変更で実装が完了したかと思いきや、まだ要件に合った動きをしてくれません。

先ほどの部分で強制的に「本登録状態」に変更したため、「仮登録状態」での機能制限が適用されなくなっていると同時に「確認メールの送信」が行われなくなっているのです。

これはConfirmable機能の内部で「ユーザーが仮登録状態(confirmed_atカラムに値が入っていない状態)の場合に確認メールを送信する」という挙動をしているからです

ユーザー編集時の挙動を応用し確認メールを送信する

Confirmable機能をONにし、編集時の確認メール送信を許可する(デフォルトで許可になっている)とユーザー編集時にメールアドレスを変更すると「確認メール」が送信されるようになります

この時の(emailが変更された場合の)devise内部の動きが以下です

  • 新しく追加(変更)されたメールアドレスがユーザーの unconfirmed_email というカラムに一時的に保存される
  • emailが変更された場合 send_confirmation_instructions というメソッドを使用し、先ほどの unconfirmed_email カラムに保存されているメールアドレスに対して確認メールを送信する。それと同時に必要なものを生成したり保存したりして準備もしてくれる
  • ユーザーが受け取った「確認メール」に記載されているURLにアクセスすると「emailが正しいものである」と認識され、 unconfirmed_email カラムに保存されているメールアドレスを正式にemailカラムに保存する

ソースコード参照:
https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/app/controllers/devise/registrations_controller.rb#L7
https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/lib/devise/models/confirmable.rb#L316

devise/app/controllers/devise/registrations_controller.rbの中には具体的な記述はないですが、devise/lib/devise/models/confirmable.rbの中で「編集時の確認メール送信を許可する設定を行っていた場合にはupdateアクションが実行された後にsend_confirmation_instructionsを実行する」記述があります


以上の一連の動きの中で2つ目がきちんとできていれば3つ目の部分は「確認メール」に記載されているURLにアクセスすれば勝手に実行されるので、今回必要な部分は1つ目と2つ目です

よって unconfirmed_email カラムと send_confirmation_instructions メソッドを用いて先ほどのregistrations_controller.rbに以下のような記述を追加します

app/controllers/users/registrations_controller.rb
# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_sign_up_params, only: [ :create ]
  before_action :configure_account_update_params, only: [ :update ]

  # ====== 省略 =======

  # POST /resource
  def create
    build_resource(sign_up_params)  # ストロングパラメータを使用して新しいユーザーを作成

    resource.skip_confirmation! # 確認をスキップ

    if resource.save
      # ユーザーが保存された場合の処理
      sign_in(resource)  # サインインさせる
      flash[:notice] = "Welcome to MyApp!!"
      if params[:user][:email].present?
        resource.unconfirmed_email = params[:user][:email] # emailを一時保存
        resource.send_confirmation_instructions if resource.unconfirmed_email.present? # 確認メールを送信
        resource.save(validate: false)
        flash[:notice] += " メールアドレス宛てに確認メールを送信しました。メールに記載されているURLにアクセスし、メールアドレスを有効化してください(迷惑メールフォルダに送信されている場合があります)"
      end
      respond_with resource, location: after_sign_up_path_for(resource)
    else
      # エラーが発生した場合の処理のコピペ
      clean_up_passwords resource
      set_minimum_password_length
      respond_with resource
    end
  end

  # ====== 省略 =======

  protected

  # If you have extra params to permit, append them to the sanitizer.
  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [ :name ])
  end

  # If you have extra params to permit, append them to the sanitizer.
  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [ :name, :email ])
  end

  # ====== 省略 =======

end

フォームのemailに入力があった場合にunconfirmed_emailカラムにemailを一時保存、send_confirmation_instructionsを用いてunconfirmed_emailに保存されているメールアドレスに対し確認メールを送信し、その他の準備をしています。
その後、unconfirmed_emailやsend_confirmation_instructionsメソッド内で用意したのものを保存するために
resource.save(validate: false)でバリデーションを無視して保存します。

以上でregistrations_controller.rbの編集も完了です!!お疲れさまでした!

登録時、編集時に確認メールを送信させるというコードは完成しましたが、実際にメールが送信されるようにするには別途SMTPサーバーなどの設定が必要です。この記事ではその部分の解説は行いませんので、あしからず。

最後に

以上の記述をすべて行うことで初めに設定した今回の要件

  • ユーザー登録に必要なのはnameとpassword
  • ユーザー登録時、同時にemailも任意で登録できる
  • ユーザー登録後でもユーザー編集画面でemailを追加できる
  • 登録時や編集時にemailを追加した後、確認メールが送信され、ユーザーがメールを確認すると正式にメールアドレスが登録される

をすべて達成することができました。

そのおかげで私のアプリで実装したかった「email登録のあるなしでのアクセス制限」を実現することができ、アカウント登録へのハードルを下げることもできたのではないかなと思います


今回初めて「gemのソースコードを確認しつつオーバーライドし、使いやすいようにカスタマイズする」ということをやったので戸惑ったり躓くことが多く、この記事に関しても至らない点が多いと思いますが、コードを書いているときからこの記事を書き終わるまでとても楽しく良い経験になったと思います。(今回もかなりAIに頼ってしまいましたが・・・)

経験と学んだことを共有したこの記事が、少しでも読んでいただい方のお役に立てれば嬉しいです。

参考資料

コードを書いている時や記事を書いている途中に助けていただいた記事を以下にまとめて書いておきます。

Railsやdeviseのバージョンが古いものもあるのでご覧になる際には注意してください

Special Thanks!

devise公式

deviseの動きを解説してくれている記事

Rails6.1 + Devise 3.8.0 で、Deviseのデフォルトの挙動を確認してみた
Deviseのモヤモヤを解消して快適なRailsライフを送ろう!

deviseが生成するマイグレーションのdownを解説してくれている記事

Railsアプリで後からdeviseを導入したときのマイグレーションファイルをdownにしたい

導入方法を解説してくれている記事

Rails7.1環境で認証機能Rails deviseの導入方法

deviseの各種オプションを解説してくれている記事

devise の使い方(helpers, Database Authenticatable, Registerable)
【Rails】devise gemのConfirmableにふれる・いじる
【Rails×devise】例外的にメール確認なしでユーザを登録する

その他deviseを使うときに参考になりそうな記事

Rails deviseで使えるようになるヘルパーメソッド一覧
devise 日本語化
Ruby on Rails: deviseが使用する項目とユーザー情報のモデルを分離して肥大化を抑制するMEMO
[Rails]deviseとomniauthによるgoogleログイン

11
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?