🔧

【Rails】devise gemのConfirmableにふれる・いじる

2023/11/09に公開

大変便利で、いつもお世話になっているdevise gem。これまではデフォルトで用意されているモジュールしか使ってこなかったのですが、今回はconfirmableの機能を追加し、さらにちょこっとイジって使うことがあったので、その忘備録として投稿させていただきます。

何をどうしたか

【何を】
Confirmableライブラリを追加し、その機能を実装すると、新しくUserレコードがDBに追加された瞬間に、入力されたemail宛に送信されるconfirmation_noticeメール(アカウント有効化メール)をいじくりました。
deviseのデフォルトの動きとしては、新しいUserレコードがDBに保存されたタイミングで、confirmableに関連したUser属性のひとつであるconfirmation_sent_atに有効化メールの送信日時が記録され、同時にconfirmation_tokenが発行されます。アカウント有効化メールには、Userレコードを有効化(confirmed_at属性に日付を付与する)するためのURLが記載されており、メールを受け取ったユーザーがそれをクリックすることで初めてアカウントが有効化され、アプリケーションが利用できるようになるというものです。

【どうしたか】
このアカウント有効化メールが送信されるタイミングをずらしました。
具体的には、新しいUserレコードがDBに追加された瞬間にはユーザーにアカウント有効化メールは送られないようにし、別のタイミングで管理者ユーザーがアカウント有効化メールを送りたいユーザーを選択し、特定のボタンをクリックしたタイミングでアカウント有効化メールが送信されるようにしました。

以下、その手順についてメモさせていただきます。
なお、devise gemの実装方法詳細については詳しく解説しておりませんので、ご了承ください。

1. Userモデルに使用するdeviseライブラリを追加する

deviseをインストールしていくと、app/models/user.rbのUserモデルのファイルに以下の構成でライブラリが追加されています。

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

ここに、今回はConfirmableとTrackableを使いたいので追記しました。また、要件の都合上、通常のusers_controller.rbの#new, #createアクションで新しいUserレコードの追加を行うため、Registerableは使用しないこととしました。

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

2. 追加したライブラリで必要となる属性をテーブルに追加する

deviseをインストールする手順の中で生成される最初のマイグレーションファイルには、たくさんの情報がコメントアウトされた状態になっています。今回追加したライブラリで必要となる属性をコメントアウト解除しておきます。(以下の ## Confirmable で始まっている部分ひとかたまりをコメントアウト解除します。また、add_index :users, :confirmation_token, unique: trueをコメントアウト解除している理由は、生成されるconfirmation_tokenの一意性を担保するためです)

# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      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
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at

      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

もし、既に最初のマイグレーションファイルをdb:migrateしてしまっているなら、Userに新しくadd_columnするためのマイグレーションファイルを用意して、以下のように書くこともできます。

class AddNewModulesToUser < ActionRecord::Migration[7.1]
	def change 
		add_column :users, :sign_in_count, :integer
		add_column :users, :current_sign_in_at, :datetime
		add_column :users, :last_sign_in_at, :datetime
		add_column :users, :current_sign_in_ip, :string
		add_column :users, :last_sign_in_ip, :string
		add_column :users, :confirmation_token, :string
		add_column :users, :confirmed_at, :datetime
		add_column :users, :confirmation_sent_at, :datetime
		add_column :users, :unconfirmed_email, :string
		 add_index :users, :confirmation_token, unique: true
	end
end

上記のどちらかが用意できたらrails db:migrateしましょう。

3. Confirmableを追加したことで起きること

追加したConfirmableの動きを確認するために、試しにアプリケーションのコンソールを立ち上げ、有効なUserオブジェクトを生成(値が必須の属性などにそれぞれ適切な値を渡し、User.newする。この時User.newを格納した変数(user)に対してuser.valid? がtrueになる状態)し、DBに保存してみましょう。

user.saveを実行すると、レコードを保存するログが流れた後に、以下のような文言がログに吐き出されます。

> user.save 
[..]
From: [..](mailto:[..])
Reply-To: [..](mailto:[..])
To: <User.newしたときに渡したemaiの値>
Message-ID: [..](mailto: [..])
Subject: [..]
[..]

<p>Welcome <User.newしたときに渡したemaiの値>!</p>

<p>You can confirm your account email through the link below:</p>

<p><a href=“http://<アプリケーションのドメイン>/users/confirmation?confirmation_token=<Confirmationトークン>>Confirm my account</a></p>

=> true

これが、新しいUserのレコードが適切にDBに保存されたときに送信されるアカウント有効化メールです。メールがユーザーのemail宛に送られています。この時のUserの各属性の状態としては、以下のようなイメージです。

  • user.confirmation_tokenが生成されている。
  • user.confirmation_sent_atにメール送信日が記録されている。
  • ユーザーがメールに記載されているURLをクリックするまでは、user.confirmed_atはnilのまま。
  • user.confirmed?も同じくfalseの状態。

4. このときやりたかったことの確認

まず、user.saveに合わせてアカウント有効化メールを送信しない (※1)ようにしたいです。
そして、それぞれの値はこんな感じにしておきたいと考えています。

  • user.confirmation_token: 生成されている状態
  • user.confirmation_sent_at: nilのまま(user.confirmation_sent_at?はfalse)(※2)
  • user.confirmed_at: nilのまま
  • user.confirmed?:false

※1 と**※2**の2つの課題に対してアプローチしていきます。

5. アカウント有効化メールを送らないようにする

deviseのコードをにらみつけていると、このあたりsend_on_create_confirmation_instructionsというコールバックメソッドが設定されています。どうやらこのメソッドによって有効化メールが送られているっぽいですね。

そして、ここsend_on_create_confirmation_instructionsの内容が記述されています。

# A callback method used to deliver confirmation
# instructions on creation. This can be overridden
# in models to map to a nice sign up e-mail.
def send_on_create_confirmation_instructions
	send_confirmation_instructions
end

send_on_create_confirmation_instructionsメソッド内のsend_confirmation_instructionsがアカウント有効化メールを送るメソッドらしいです。

なので、app/models/user.rbでこのメソッドをオーバーライドすれば良さそうと考え、試しに以下のようにsend_on_create_confirmation_instructionsメソッドを書いてみました。

# app/models/user.rb
class User < ApplicationRecord
	[...]
	def send_on_create_confirmation_instructions
		# なにもしません
	end
	 
	private
	[...]
end

そしてもう一度#3の手順と同様にコンソールで新しいUserレコードをDBに保存してみると、DBへ保存すると同時にメールの文面がログに現れることはなくなりました。confirmed?もfalseのままになっています。

これで**※1**は解決できました!

ただし、それでもuser.confirmation_sent_atには日付の値が登録されてしまっているので、※2に関してはまた別の対応が必要そうですね…。

6. 残りの問題に対処する

ではここまでの実装で残っている問題に一つずつ対処していきます。

6-1. メールは送信しないようになったがconfirmation_sent_atに日付が登録されてしまう問題

send_on_create_confirmation_instructionsメソッドをさらに以下のように修正します。

	def send_on_create_confirmation_instructions
		# なにもしません
		self.update_columns(confirmation_sent_at: nil)
	end

この部分はもともと、self.confirmation_sent_at = nilとすることで対応しようと考えたのですが、これではレコードに保存される値まで更新することはできないっぽいです。なので、update_columnsメソッドを用いて、レコードのconfirmation_sent_atの値をnilで上書きすることとしました。

6-2. confirmation_sent_atに任意のタイミングで日付を書き込むようにする

今回は、新しいユーザーがレコードに追加された瞬間には有効化メールは送られないように設定しました。代わりに、管理者となるユーザーが、ユーザーの一覧から有効化メールを送りたいユーザーを選択し、メール送信ボタンを押すことで、当該ユーザーへアカウント有効化メールを送る、といった挙動にします。なので、メール送信を実行するsend_confirmation_instructionsをオーバーライドすることで、デフォルトで実装されているメール送信の挙動はそのままに、その処理の最後に、confirmation_sent_atにメール送信時の日時を登録するという挙動を追加します。

以下のようにapp/models/user.rbでsend_confirmation_instructionsを修正します。

	def send_confirmation_instructions
		super
		self.update_columns(confirmation_sent_at: Time.now)
	end

superとすることで、既にdeviseのConfirmableモジュールで定義されている同名メソッドの挙動を引き継いでいます。そして処理の最後にupdate_columnsを実行し、今度はconfirmation_sent_atにTime.nowで日付を追加するという処理を書きました。

これで、新しいユーザーがレコードに登録された瞬間にはアカウント有効化メールをユーザーに送信することはせず、なおかつconfirmation_sent_at属性をnilに保つという要件を満たすことができました。なお、confirmation_tokenはこの時点ですでに生成されています。

6-3. そして問題はつづく

やったー、と喜ぶのも束の間、実装を続けていると、さらなる問題にぶつかりました。

どんな問題?

  • Userの既存レコードを更新できるようにするためにeditおよびupdateメソッドを実装。しかし、アプリケーション上で試してみるも、email属性の値は更新されないまま。
  • コンソールでuser.emailに=で新しい値を設定し、user.saveを試してみても、user.emailは変更前の値のまま。
  • Userの他の属性であるnameなんかはちゃんと更新される。
  • なんなら、email属性に新しい値を渡し、user.saveをすると、さっきメール送信をしないように設定したはずのアカウント有効化メールが送信されてしまっている。→user.saveをするとログにメールの内容が吐き出される。user.confirmation_sent_atにも日付が入ってしまっている…。

おそらく原因だと思われるもの

これの原因としては、Confirmableの特徴の一つである、ユーザーのemail属性の値が更新にも合わせて、アカウント有効化メールを送るという機能が存在していたためだと思われます。

このときのdeviseの動きとしては、新しいアカウントが有効化されるまでは既存のemailの値のまま維持され、新しいメールアドレスはunconfirmed_emailというstring型の属性の値として一時保管される。新しいメールアドレスでのアカウント有効化が済んだら、email属性の値を更新し、unconfirmed_emailの値をnilとするというものです。

ソースコードとしてはこのあたりがレコードの更新に伴ってアカウント有効化メールを送信する役割を担っていそうですね。

どう対処したか

このUserのemail属性の更新に付随して実行される上記の挙動は、deviseの設定を変更することで対処できました。

config/initializers/devise.rbのダラーっと縦に長いファイルを見ていると、真ん中あたりに以下の記述があります。

config.reconfirmable = true

デフォルトでtrueになっており、これをfalseにすることで、上記の挙動を行わせないように設定することができました。

まとめる

以上のように今回の要件に合わせてやりたかったことをduck-typingで探り探り進めてきたわけですが、config.reconfirmableをfalseにしたことで、今後、アプリケーションの動きや追加の要件などに干渉する可能性もあると思うので、その場合は改めて、send_reconfirmation_instructionsメソッドを要件に応じてオーバーライドする必要などがありそうですね。

他に、もっと簡単でわかりやすい方法や、間違っている箇所のご指摘があれば、お教えくださると嬉しいです!
今回は以上です。

お読みくださりありがとうございました。

Discussion