How To: Override confirmations so users can pick their own passwords as part of confirmation activation、パスワードを本登録の際に要求するを実際に動くものを作って解説していきます。

一般的なwebサービスではemailとpasswordを使って会員登録を行います。devise gemを使用した場合でもデフォルトではemailとpasswordを使って会員登録を行いますが、 要件としてpasswordは新規登録では不要な場合もあるでしょう。 deviseでもいくつかの機能をオーバーライドすることで、

  1. メールアドレスを入力して登録
  2. 届いたメールからパスワード設定画面へアクセス
  3. パスワードを設定し、アカウントを有効化 というステップを踏んだ新規登録処理を行う事ができます。

wiki 公式のhow-toを参考にやっていきますが、公式のRailsバージョンが古いことや、体系的に書かれてはいないので、ポイントとなるdeviseの機能以外は大部分が自前での実装になっています。

言語、ライブラリのバージョン

  • Ruby 2.6.3
  • Rails 6.0.2.2
  • devise 4.7.1
  • テンプレートはslimを使用

導入から基本的な実装を知りたい場合は最小構成導入からログアウトまでを参照。

前提として、deviseを使用しているmodelはUserモデルになっています。

Confirmableを使えるようにする

デフォルトの状態では新規登録において確認メールを送信するconfirmableは有効になっていないので、有効にしておきます。

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
end

デフォルトの状態では上記のようになっているので、コメントアウトされている部分を外して有効化します。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable
end

また、confirmableを使用する場合はいくつか必要なカラムがありますが、これらもデフォルトでは有効化されていないので、有効化しておきましょう。

自動生成されたmigrationファイルを確認してみます。

# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[6.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.inet     :current_sign_in_ip
      # t.inet     :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

migrationファイルはデフォルトではこのようになっています。ここでのconfirmableに関係する部分は

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

この部分です。コメントアウトを外して migrateをするか、すでに実行している場合はこの部分だけ切り出してmigrationしておきましょう。

viewとcontrollerをカスタマイズする

viewとcontrollerをカスタマイズするためには、コンソールでモデルに対応するカスタムviewテンプレートとカスタムcontrollerを作成する必要があります。

rails g devise:views モデル名  # viewのカスタムテンプレート
rails g devise:controllers モデル名 #controllerのカスタム

このように書くことでモデルに対応するカスタムテンプレートを作成することができます。

今回はUserモデルなので

$ rails g devise:views users

とすればカスタムテンプレートを生成できます。 デフォルトのテンプレートはこんな感じになっています。

h2
  | Sign up
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :email
    br
    = f.email_field :email, autofocus: true, autocomplete: "email"
  .field
    = f.label :password
    - if @minimum_password_length
      em
        | (
        = @minimum_password_length
        |  characters minimum)
    br
    = f.password_field :password, autocomplete: "new-password"
  .field
    = f.label :password_confirmation
    br
    = f.password_field :password_confirmation, autocomplete: "new-password"
  .actions
    = f.submit "Sign up"
= render "devise/shared/links"

emailとpasswordを入力するようになっています。今回の要件ではpasswordは新規登録時には不要なのでpasswordとpassword_confirmationの入力フォームを削除しておきましょう。

h2
  | Sign up
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :email
    br
    = f.email_field :email, autofocus: true, autocomplete: "email"
  .actions
    = f.submit "Sign up"
= render "devise/shared/links"

emailだけになりました。 スクリーンショット 2020-05-19 12.31.24

ただ、このままだとdeviseのvalidationが有効になっており、passwordなしでの登録ができません。なので、deviseが用意している

password_required?

をUserモデル上でオーバーライドします。

実際に定義したuserモデルは下記のようになりました。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable
         
  # confirmed? のタイミングの時だけ呼ばれるようにする
  def password_required?
    super if confirmed?
  end
end

def confirmed?
  !!confirmed_at
end

confirmed? の定義は上記。確認リンクを踏んだ後confirmed_atカラムに値が入るので、入ってた時はtrue,その場合のみpassword_required?がtrueになるという仕組み。

password_required?が使用されているのは lib/devise/models/validatable.rb の中の

validates_presence_of     :password, if: :password_required?
validates_confirmation_of :password, if: :password_required?

で使用されているので、上記を総合すると、passwordのバリデーションが有効になるのはconfirmed_atに値が入っている場合ということになる。

ここで一度emailのみで確認できるか試してみます。 devise-only-email-registration

見切れていますが「登録したメールアドレスに確認メールを送った」という旨のフラッシュが表示されました。

確認リンクを踏んだ先をpasswordの登録にする

無事登録ができるとconfirmableの機能でメールが送信されます。letter_openerなどを使って確認しましょう。

デフォルトのメールテンプレートは

p
  | Welcome 
  = @email
  | !
p
  | You can confirm your account email through the link below:
p
  = link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token)

になっています。このconfirmation_url(@resource, confirmation_token: @token)が遷移先です。 デフォルトはconfirmation#showのアクションに飛びます。そのままだと画面がなく、デフォルトはログインページにリダイレクトされるので、ここを加筆修正していきます。

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

class Users::ConfirmationsController < Devise::ConfirmationsController
  # GET /resource/confirmation/new
  # def new
  #   super
  # end

  # POST /resource/confirmation
  # def create
  #   super
  # end

  # GET /resource/confirmation?confirmation_token=abcdef
  # def show
  #   super
  # end

  # protected

  # The path used after resending confirmation instructions.
  # def after_resending_confirmation_instructions_path_for(resource_name)
  #   super(resource_name)
  # end

  # The path used after confirmation.
  # def after_confirmation_path_for(resource_name, resource)
  #   super(resource_name, resource)
  # end
end

デフォルトは全てコメントアウトされているので、deviseの素の挙動になります。 ここではリンクに仕込まれたトークンと、resources_classというクラスを抽象化した変数を使用して、遷移を制御します。showメソッドのコメントアウトを外して、オーバーライドします。

def show
  self.resource = resource_class.find_by(confirmation_token: params[:confirmation_token])
  super if !resource || resource.confirmed? 
end

resource_classはUserが入っているので、そこからtokenでレコードを引っ張ってきます。もし見つからない、あるいはすでに認証済みであればデフォルトの挙動であるsuperに移行します。 初回の場合はpasswordを設定させたいので、superの分岐には走らず、confirmation#show.html.slimに遷移することになります。

passwordを登録させる画面の作成

confirmation#show.html.slimはデフォルトでは存在しないので、自分で作成する必要があります。showページにフォームがあるのはなんだか違和感がありますが、今回はこのままいきましょう。もし気になる場合はredirect_toでeditなどに飛ばしてあげても良いと思います。

app/views/devise/confirmations/show.html.slim

h2
  | 本登録するためにパスワードを設定してください。
= form_for(resource, as: resource_name, url: users_confirmation_path, html: { method: :patch }) do |f|
  = render "devise/shared/error_messages", resource: resource
  = hidden_field_tag :confirmation_token, params[:confirmation_token]
  .field
    = f.label :password
    br
    = f.password_field :password
    - if @minimum_password_length
      br
      em
        = @minimum_password_length
        |  characters minimum
  .field
    = f.label :password_confirmation
    br
    = f.password_field :password_confirmation
  .field
  .actions
    = f.submit "Resend confirmation instructions"

確認リンクを踏んだ後にリダイレクトされるページはこのようにしました。passwordpassword_condirmationを用意しています。

これで画面表示はうまくいきますが、まだ送信ボタンを押した後に対応するルーティングを書いていないので、このままsubmitボタンを押すとエラーになってしまいます。この部分を続けて修正します。

Routesを独自アクションに対応させる

deviseに対応したルーティングを書く場合、通常通り書いてもdeviseのものとは判定されず、エラーになってしまいます。 カスタムメソッドなど、独自の動作を行うルーティングを記載する場合はdeviseが提供するいくつかのパターンを踏襲する必要があります。 今回はsubmitボタンを押した後、confirmというメソッドに遷移するようにし、その中で処理を書いていきたいと思います。

devise_scope :user do
  patch 'users/confirmation', to: 'users/confirmations#confirm'
end

devise_scope :モデル名 do ~ endという構文の中に自分で定義したルーティングを書いてあげます。 追記したroutes.rbは下記になりました。

Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    confirmations: 'users/confirmations'
  }

  devise_scope :user do
    patch 'users/confirmation', to: 'users/confirmations#confirm'
  end
`
``
`
end

これで、対応したルーティングが作成され、confirmationsコントローラーのconfirmメソッドに飛ぶことができるようになりました。このルーティングはformのurlオプションに渡している

users_confirmation_path

に対応しています。

認証ロジックを入れる

confirmメソッドにアクションを渡すことができたので、ここからパスワードを登録できるようにしていきます。

app/views/devise/confirmations/show.html.slim

h2
  | 本登録するためにパスワードを設定してください。
= form_for(resource, as: resource_name, url: users_confirmation_path, html: { method: :patch }) do |f|
  = render "devise/shared/error_messages", resource: resource
  = hidden_field_tag :confirmation_token, params[:confirmation_token]
  .field
    = f.label :password
    br
    = f.password_field :password
    - if @minimum_password_length
      br
      em
        = @minimum_password_length
        |  characters minimum
  .field
    = f.label :password_confirmation
    br
    = f.password_field :password_confirmation
  .field
  .actions
    = f.submit "Resend confirmation instructions"

復習がてらこのフォームから渡ってくる値を再確認しましょう。 このフォームからconfirmアクションに渡った際に使えるものは、

  1. hidden_fieldタグに渡したtoken
  2. password
  3. password_confirm

の3つです。これらを使って、

  • 特定のユーザーのパスワード情報(とconfirmd_at)を更新する
  • パスワードのバリデーションを確認する(空であったり再確認で間違ってたら登録させない)

の要件を満たすようにしていきたいです。

showで特定した方法と同様の方式でuserインスタンスを取得できます。

(hidden_fieldにresouceを渡せばshowで作成したuserインスタンスが取得できますが、ここでは再度DBに確認を取るようにしているという点と、tokenを使ってconfirmed_atを更新したいので、インスタンスそのものではなくtoken経由で取得するようにしています。)

self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token])

また、パスワードを当てはめるためにストロングパラメーターを設定しておきます。

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

この辺りはRailsのルールなので、見慣れている人も多いかもしれません。設定したら、その confirm_paramsを使ってupdate処理を書きます。

ここまでの処理をまとめておくと、confirmation_controllerの中身はこうなっています

# frozen_string_literal: true

class Users::ConfirmationsController < Devise::ConfirmationsController
  # GET /resource/confirmation/new
  # def new
  #   super
  # end

  # POST /resource/confirmation
  # def create
  #   super
  # end

  # GET /resource/confirmation?confirmation_token=abcdef
  def show
    self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token])
    super if resource.nil? || resource.confirmed?
  end

  def confirm
    self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token])
    resource.update(confirm_params)
  end

  # protected

  # The path used after resending confirmation instructions.
  # def after_resending_confirmation_instructions_path_for(resource_name)
  #   super(resource_name)
  # end

  # The path used after confirmation.
  # def after_confirmation_path_for(resource_name, resource)
  #   super(resource_name, resource)
  # end

  private 
  def confirm_params
    params.require(:user).permit(:password, :password_confirmation)
  end
end

このままでもupdateは一応できるのですが、パスワードの状態がどうなっていても登録できてしまい、また、confirmed_atが入らないので、希望の要件は満たせていません。パスワードをチェックするロジックと、confirmed_atを更新するロジックも入れていきます。公式のwikiに丁度良さそうなメソッドがあるので拝借します。

こちら。

def password_match?
  self.errors[:password] << "can't be blank" if password.blank?
  self.errors[:password_confirmation] << "can't be blank" if password_confirmation.blank?
  self.errors[:password_confirmation] << "does not match password" if password != password_confirmation
  password == password_confirmation && !password.blank?
end

内容としては、それぞれの条件に応じてエラーメッセージを格納し、問題なければtrueを返すメソッドです。 これをUserモデルに定義します。

app/models/user.rb

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable

  def password_required?
    confirmed? ? super : false
  end

  def password_match?
    self.errors[:password] << "can't be blank" if password.blank?
    self.errors[:password_confirmation] << "can't be blank" if password_confirmation.blank?
    self.errors[:password_confirmation] << "does not match password" if password != password_confirmation
    password == password_confirmation && !password.blank?
  end
end

これでpasswordのチェックができるようになりました。 このメソッドでチェックしてOKであれば保存するということにしたいと思うので、confirmメソッドを一部書き換えましょう。

def confirm
  self.resource =   resource_class.find_by_confirmation_token(params[:confirmation_token])
    if resource.update(confirm_params) && resource.password_match?
      #ここにconfirmed_atを更新したり、リダイレクト処理を書いたりする
    end
end

このようになります。ここでの注意点としては、update処理はpassword_confirmpasswordとマッチしていなくても更新処理は走っているという点です。ここでif分岐を行い、password_matchメソッドがfalseを返す場合はエラーメッセージを格納し、render処理を行います。一方、成功した場合はconfirmed_atに値を入れます。 update処理を走らせたくない場合は

resource.password = confirm_params[:password]

などして事前に値を入れてから

if resource.password_match? && resource.save

のようにして検証と保存の順序を逆にするといいと思います。

保存が成功した場合、confirmed_atを更新して、正しい場所にリダイレクトさせてあげる必要があります。今回は保存が成功したら、ログインしてトップページに飛ぶようにしましょう。

confirmed_atの更新には、deviseのconfirmableが提供するconfirm_by_tokenメソッドを使います。

self.resource = resource_class.confirm_by_token(params[:confirmation_token]
)

このメソッドでトークンを元にリソースを検証し、問題なければconfirmed_atプロパティに現在時刻を入れて更新します。 そのほかに、ユーザーのログインとリダイレクトしてトップページにいくこと、フラッシュメッセージなどを入れて失敗したときにrenderでshowページを表示させてあげるとこんな感じになります。

def confirm
    self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token])
    if resource.update(confirm_params) && resource.password_match?
      self.resource = resource_class.confirm_by_token(params[:confirmation_token])
      set_flash_message :notice, :confirmed
      sign_in resource
      redirect_to root_path
    else
      render :show
    end
  end

これで動作確認してみましょう。 画像はトークン付きのリンクを踏んでpasswordと確認用のpasswordフォームを入れるところです。フラッシュメッセージに Your email address has been successfully confirmed. と表示され、登録に使ったtest-user@sample.comというアドレスがcurrent_userから取得できていますね! devise-after-password-reegistration

はい、このような感じで、passwordを後から登録させ、その登録を持って本完了とする方法でした。本格的にカスタマイズしようとすると自前実装の方が楽になる分岐点が出てくるのですが、この程度であればdeviseをカスタマイズして乗っかった方が長い目でみると楽になる可能性もありますので似たような要件がある場合は是非参考にしてください!