[devise]ユーザー登録完了前に(仮登録の状態で)他のカラムを追加させる
はじめに
ruby on railsのユーザー周り(会員登録など)のgemといえばdeviseですかね・・?
一瞬で会員登録とかができるのは物凄い便利です。
そんな便利な反面、カスタマイズするのにはかなり時間がかかります。
(今回もかなり時間がかかりました。)
今回実装するのは、以下のような機能です。
# 実装したい内容
会員登録(emailとpassword)
↓
認証メール送信
↓
メールのリンクからプロフィール(今回はname)を追加するページへ
↓
プロフィールを入力して確認画面へ
↓
確認画面で送信
↓
会員登録完了(ログイン画面へ)
今回のissueは大きく二つ
- 会員登録を仮登録の状態で編集する
- 変更時に編集内容を確認するページを挟む
開発環境
- rails (6.0.3.2)
- ruby 2.6.5p114
- devise (4.7.2)
- letter_opener (1.7.0)
- mac Catalina 10.15.6
セットアップ
前提として、以下の状態を想定しています
- deviseがインストールされている
- deviseのview,controller,modelが作成されている
- Useモデルが存在する(カラムはデフォルトにnameを追加)
- letter openerをgemにインストールしている(初期設定している)
- メール認証をtrueに変更している
使い方
今回のissueは大きく二つ
- 会員登録を仮登録の状態で編集する => (解決策) confirmation_tokenを保持して、最後にconfirmation_pathに引数として与える
- 変更時に編集内容を確認するページを挟む => (解決策)hidden_fieldを入れることでデータを保持する
1つ目のissueの肝は、[confirmation_token]
仮登録完了時に送られるメールをみるとわかりやすい
<p>Welcome <%= @email %>!</p>
<p>You can confirm your account email through the link below:</p>
<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>
なんか、認証トークンとuserのデータを引数に持ってconfirmation_urlに入ったら仮登録→本登録完了になるらしい
つまり、
__これをやらずに__プロフィールを編集して、その後認証トークンを与えてconfirmation_tokenにアクセスして完了すればいい。
それでは実際にやってみる
① ルーティングを設定
まず、追加するアクションは三つ。そのルーティングを行う。
今回はこの三つ。カッコの中身がアクション名(センスなくてすみません・・・)
[入力、確認、更新]
- 入力:プロフィール入力アクション(before_create)
- 確認:入力内容確認アクション(before_confirm)
- 更新:入力内容で更新するアクション(before_update)
Rails.application.routes.draw do
devise_for :users, :controllers => {
# コントローラーを見に行くようになる
# 無しだと、そもそも見に行かずにデフォルトが実行される
# (結論:deviseのコントローラーを変更したら記述が必須)
:registrations => 'users/registrations',
:sessions => 'users/sessions',
:confirmations => 'users/confirmations'
}
devise_scope :user do
# ルーティングを指定
# アクションが追加されたらそのルーティングを指定する
get "sign_in", :to => "users/sessions#new"
get "sign_out", :to => "users/sessions#destroy"
#プロフィール編集画面(仮登録状態)
get "before_sign_up", :to => "users/registrations#before_create"
#プロフィール編集内容確認画面(仮登録状態)
post "before_sign_up_confirm", :to => "users/registrations#before_confirm"
#プロフィール編集内容のアップデート処理(仮登録→本登録に)
post "before_sign_up", :to => "users/registrations#before_update"
end
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
root 'pages#index'
get 'pages/show'
end
大きくやったことは二つ
- device_forでdeviseのcotrollerをカスタマイズ可能に
- devise_scoopeで新しいアクションのパスを指定
② プロフィール編集アクションを追加(入力、確認、更新の三つ)
続いて、アクションの追加
この三つ。カッコの中身がアクション名
- 入力:プロフィール入力アクション(before_create)
- 確認:入力内容確認アクション(before_confirm)
- 更新:入力内容で更新するアクション(before_update)
とりあえず、binding.pryで引数を見て回ったのと、superが表す内容をそれぞれ見て回った(こんなサイトで)
まずは ### **入力:プロフィール入力アクション(before_create)**
# 仮登録状態のプロフィール入力画面(メール認証後のアクション)
def before_create
# 引数のresourceを使ってユーザーを取得
@user = User.find(params["resource"])
@token = params["confirmation_token"]
end
<h2>プロフィールを登録</h2>
<%= form_for(resource, as: resource_name, url: before_sign_up_confirm_path(resource_name, confirmation_token: @token) ,html: {method: "post"}) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<%= f.hidden_field :user_id, :value => resource.id %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name, autofocus: true, autocomplete: "name" %>
</div>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email",:readonly => true %>
</div>
<div class="actions">
<%= f.submit "Sign up" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
デフォルトだったらparamsとかじゃなくてresourceで一発で呼べるんですけど、新しく追加したメソッドでresourceではとれない
余談
(厳密には、以下のような感じで取れるようにできるんですが、ログイン必須なので、仮登録では面倒そうだったので断念)
registrations_controller.rbに
**prepend_before_action :authenticate_scope! , only: [:before_create]**を追加
(余談の出来事)ということもあり、地道に実装することに。
1- 実は仮登録で、newされてるらしいので、newせずに@userにとってくる
2- confirmation_tokenは持ち続けたいので一旦@tokenへ
3- viewのhidden_fieldでuserのidを保持(これしなくても、paramsから取れる)
確認:入力内容確認アクション(before_confirm)
# 仮登録状態のプロフィール入力完了画面(プロフィール入力後のアクション)
def before_confirm
@user = User.find(params["user"]["user_id"])
@user.name = params["user"]["name"]
@token = params["confirmation_token"]
if @user.valid?
render :action => 'before_confirm'
flash.now[:success] = '確認して完了してください'
else
render :action => 'before_create'
flash.now[:alert] = '失敗しました'
end
end
<h2>確認画面</h2>
<%= form_for(@user, url: before_sign_up_path(@user, confirmation_token: @token),html: {method: "post"}) do |f| %>
<%= render "devise/shared/error_messages", resource: @user %>
<%= f.hidden_field :user_id, :value => @user.id %>
<%= f.hidden_field :email, :value => @user.email %>
<%= f.hidden_field :name, :value => @user.name %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name, autofocus: true, autocomplete: "name",:readonly => true %>
</div>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email",:readonly => true %>
</div>
<div class="actions">
<%= f.submit "Sign up" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
ここでsaveしないんですね。
@userに入れてみて、validかinvalidかを調べてるだけです。次のupdateで再び呼び出してからsaveします(ここがかなり効率悪いですよね・・・)
更新:入力内容で更新するアクション(before_update)
# 仮登録状態のプロフィール入力内容更新処理
def before_update
@user = User.find(params["user"]["user_id"])
@token = params["confirmation_token"]
@user.save
if @user.valid?
# ここが肝!
# confirmation_pathにuserデータと認証トークンを付与することで本会員登録される
redirect_to confirmation_path(@user, confirmation_token: @token)
flash[:success] = '確認して完了してください'
else
render :action => 'before_create'
flash.now[:alert] = '失敗しました'
end
end
③ mailのパスを変更
最後に、
送られてくるメールの遷移先をを変更します。
confirmation_url → before_sign_up_url
resourceもparamsで呼び出せるように追加 @resource → resource:@resource
(この辺深く理解してないのでミス多いかもです。すみません。)
<p>Welcome <%= @email %>!</p>
<p>You can confirm your account email through the link below:</p>
<p><%= link_to 'Confirm my account', before_sign_up_url(resource:@resource, confirmation_token: @token) %></p>
デモ
終わりに
gemってデフォルトで使う分にはすごい便利だけど、カスタマイズするにはgem自体をすごい理解しないといけないですよね。
特に初学者にはキツすぎる・・・
勉強不足で間違いなどあるかもしれませんが、その時は優しく指摘していただきたいです。
よろしくお願いします!