- 言語、ライブラリのバージョン
- Confirmableを使えるようにする
- 確認リンクを踏んだ先をpasswordの登録にする
- passwordを登録させる画面の作成
- Routesを独自アクションに対応させる
- 認証ロジックを入れる
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の機能以外は大部分が自前での実装になっています。
言語、ライブラリのバージョン
導入から基本的な実装を知りたい場合は最小構成導入からログアウトまでを参照。
前提として、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だけになりました。
ただ、このままだと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のみで確認できるか試してみます。
見切れていますが「登録したメールアドレスに確認メールを送った」という旨のフラッシュが表示されました。
確認リンクを踏んだ先を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"
確認リンクを踏んだ後にリダイレクトされるページはこのようにしました。password
とpassword_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アクションに渡った際に使えるものは、
- hidden_fieldタグに渡したtoken
- password
- 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_confirm
がpassword
とマッチしていなくても更新処理は走っているという点です。ここで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から取得できていますね!
はい、このような感じで、passwordを後から登録させ、その登録を持って本完了とする方法でした。本格的にカスタマイズしようとすると自前実装の方が楽になる分岐点が出てくるのですが、この程度であればdeviseをカスタマイズして乗っかった方が長い目でみると楽になる可能性もありますので似たような要件がある場合は是非参考にしてください!