Devise + omniauth(facebook,google) を使ったウィザード形式でのuser登録方法

#はじめに
スクールの成果打つでの機能実装を備忘録にしました。
かなり難しかったので誰かのお役に立てれば幸いです。


ウィザードとはいくつかにページを分けて情報を登録する形式です。↓
[![Screenshot from Gyazo](https://gyazo.com/ddf3a984f9730edcd36e103247299045/raw)](https://gyazo.com/ddf3a984f9730edcd36e103247299045)

<img width="1025" alt="スクリーンショット 2020-03-21 22.01.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509166/75806080-e903-954c-b9ed-32afbd010092.png">

今回は画像と同じように３つのページに分けて情報を登録していきます。

登録方法は

・メールアドレス
・facebook
・google

この３つの方法で登録することができるようにします。

## 環境
Rails 5.2.4.1
ruby 2.5.1

※前提条件としてdeviseでコントローラーを生成している。userに追加したカラムを許可していることとします。

## 手順
・APIの設定(google/facebook)
・omniauth gemのインストール
・deviseの設定
・コントローラー、モデルの設定
・フロントの遷移ボタンを作成

それではわかりづらい点もあるかと思いますがよろしくお願いいたします。

## APIの設定
こちらは記事がたくさん出ているので割愛させていただきます。
参考

google
https://console.developers.google.com/project
[爆速ッ!! gem omniauth-google-oauth2 で認証させる](https://qiita.com/bino98/items/596b5cffeca7c104bd90)

facebook
https://developers.facebook.com/
[omniauth-facebookでユーザー情報を取得する](https://qiita.com/tanaka512/items/08641aa0f9b968d6320f)


ここで注意なのですが、外部APIを使用した時、手順通りやっても上手く行かないなんて事があります。（経験値です。）
何度試しても上手くいかない。どの記事をみてもやり方は同じなのに。そんな時は焦らず時間を置いてみてください。APIにURLを登録するとフィットするのに時間がかかるのか、しばらく経つと正常にうごく事があります。私は色々触って時間がかかりましたが、結局次の日になっていると動いていました。


##omniauth gemのインストール

次に以下のgemをインストールします。

```
gem 'omniauth-rails_csrf_protection' #omniauthよりも安全
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
```

##deviseの設定
scopeなど色々オプションを付けれるみたいですが、今回は必要ありません。もしAPIとの接続が上手くいかなかったり、必要なデータを取れない場合はKEYに問題がある可能性が高いです。

```ruby:devise.rb
Devise.setup do |config|
  config.omniauth :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
  config.omniauth :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']
#以下略
```

##ルーティング

```ruby:routes.rb
devise_for :users, controllers: {
    registrations: "users/registrations",
    omniauth_callbacks: 'users/omniauth_callbacks'#こちらを追加します。
  }
```

##SnsCredentialを作成

次にAPIから情報を取得した時に生成されるproviderカラムとuidカラムを保存させるモデルを作成します。

```
rails g modle sns_credential provider:string uid:string user:references
```

```ruby:sns_credential.rb
class SnsCredential < ApplicationRecord
  belongs_to :user
end
```

ここで少しAPIでの情報取得方法について深掘りしてみます。
以降の内容で設定していくのですが、SNSで情報を取り出す際にproviderとuidというカラムが生成され、providerにはfacebookやgoogleoauth2というったvalueが入り、uidにはランダムな文字列が入ります。
SNSでの新規登録やログイン時にはこれを使って条件分岐させていきます。

snscredentialモデルを作らずuserにproviderやuidを追加させる方法もあるのですが、こちらの方が応用が効きそうでした。

##コントローラ、モデルを設定
次にメインとなるomniauth_callbacks_controller.rb、user.rbとdeviseのregistrations_controllerを編集していきます。
それぞれ順番にみていきます。

```ruby:omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

  def facebook
    authorization
  end

  def google_oauth2
    authorization
  end

  def failure
    redirect_to root_path
  end

  private

  def authorization
    @omniauth = request.env['omniauth.auth']
    info = User.find_oauth(@omniauth)
    @user = info[:user]
    if @user.persisted? # SNSで登録済みの場合ログイン処理
      sign_in_and_redirect @user, event: :authentication
    else 
      @sns = info[:sns]
      session[:provider] = @sns[:provider]
      session[:uid] = @sns[:uid]
      
      render template: "devise/registrations/new" 
    end
  end

end
```
ご覧のとおりSNSによって aurhorizationメソッドをSNS毎に使用、失敗した時はルートに戻るように設定しています。

authorizationの中身をみていきます。

まず、omniauthを使用して帰ってきたSNSのアカウントのデータはrequest.env['omniauth.auth']という形で取得する事ができます。
binding.pryなどで中身をみにいきます。

```
[1] pry(#<Users::OmniauthCallbacksController>)> request.env['omniauth.auth']
=> {"provider"=>"google_oauth2",
 "uid"=>"文字列",
 "info"=>
  {"name"=>"hogehoge",
   "email"=>"hogehoge@gmail.com",
   #以下略
```
以上のようにprovider、uid、infoにはハッシュでアカウントに登録されているデータが入っています。
今回使用するのはnameとemailのみです。
次にuser.rb内に記述したfind_oauthメソッドを使用します。引数として上記で取得したproviderなどのデータを@omniauthに入れて持っていきます。


こちらがuser.rbです。

※中略の上はアソシエーションとdeviseのオプションです。
今回の使用では2ページ目に入力する電話番号はphoneモデルを作成して登録しているので、has_one :phoneとなっています。

```ruby:user.rb
  devise :database_authenticatable, :registerable,
          :recoverable, :rememberable, :validatable,
          :omniauthable, omniauth_providers: %i[facebook google_oauth2]

  has_one :phone
  has_many :sns_credentials, dependent: :destroy
  
  #中略(バリデーション)
  
  # oauth認証メソッド
  def self.without_sns_data(auth)
    user = User.where(email: auth.info.email).first

      if user.present?
        sns = SnsCredential.create(
          uid: auth.uid,
          provider: auth.provider,
          user_id: user.id
        )
      else
        user = User.new(
          nickname: auth.info.name,
          email: auth.info.email,
        )
        sns = SnsCredential.new(
          uid: auth.uid,
          provider: auth.provider
        )
      end
      return { user: user ,sns: sns}
    end

  def self.with_sns_data(auth, snscredential)
    user = User.where(id: snscredential.user_id).first
    unless user.present?
      user = User.new(
        nickname: auth.info.name,
        email: auth.info.email,
      )
    end
    return {user: user}
  end

  def self.find_oauth(auth)
    uid = auth.uid
    provider = auth.provider
    snscredential = SnsCredential.where(uid: uid, provider: provider).first
    if snscredential.present?
      user = with_sns_data(auth, snscredential)[:user]
      sns = snscredential
    else
      user = without_sns_data(auth)[:user]
      sns = without_sns_data(auth)[:sns]
    end
    return { user: user ,sns: sns}
  end

```

find_oauthの中身です。
uid、providerに引数で持ってきた値を代入してSnsCredentialを取得します。
snscredentialを取得できた場合はwith_sns_dataを用いてuserとsnsを
取得できなかった場合はwithout_sns_dataを用いて新たにUserとSnsCredentialを生成して同様にuserとsnsに代入。
それらの値を返しomniauth_callbacks_controllerに戻ります。↑

snsで登録した事がある場合と初めての場合を条件分岐させsessionにproviderとuidを代入し新規登録の1ページ目に戻ります。


registrations_controller.rbです。
こちらはSNS認証を使う場合とSNSを使わずに手動で入力させる場合に分岐させていきます。
SNSを用いる場合はpasswordを入力しなくてもいい事にします。

```ruby:registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  # GET /resource/sign_up
  #1ページ目
  def new
    @user = User.new
  end

  # POST /resource
  #1ページ目post
  def create
    session[:nickname] = params[:user][:nickname]
    session[:first_name] = params[:user][:first_name]
    session[:last_name] = params[:user][:last_name]
    session[:first_name_kana] = params[:user][:first_name_kana]
    session[:last_name_kana] = params[:user][:last_name_kana]
    session[:birthday] = birthday_join
    session[:email] = params[:user][:email]
    session[:password] = params[:user][:password]
    @user = User.new(
      nickname: session[:nickname],
      first_name: session[:first_name],
      last_name: session[:last_name],
      first_name_kana: session[:first_name_kana],
      last_name_kana: session[:last_name_kana],
      birthday: session[:birthday],
      email: session[:email],
    )
    #SNSで登録する場合
    if session[:provider].present? && session[:uid].present?
      # パスワードは自動生成する
      password = Devise.friendly_token.first(7)
      @user.password = password
      session[:password] = password
    #メールアドレスで登録する場合
    else
      @user.password = session[:password]
    end
    @phone = @user.build_phone
    # バリデーションチェック
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    render :new_phone
  end

  #2ページ目post
  def create_phone
    @user = User.create(
      nickname: session[:nickname],
      first_name: session[:first_name],
      last_name: session[:last_name],
      first_name_kana: session[:first_name_kana],
      last_name_kana: session[:last_name_kana],
      birthday: session[:birthday],
      email: session[:email],
      password: session[:password]
    )
    @phone = Phone.new(phone_params)
    unless @phone.valid?
      flash.now[:alert] = @phone.errors.full_messages
      render :new_phone and return
    end
    @phone.save
    if session[:provider].present? && session[:uid].present?
      @sns = SnsCredential.create(
        user_id: @user.id,
        uid: session[:uid],
        provider: session[:provider]
      )
    end
    sign_in(:user, @user)
  end

  protected

  def phone_params
    params.require(:phone).permit(:phonenumber).merge(user_id: @user.id)
  end

  #birthdayのパラメータをData型として生成する。
  def birthday_join
    params[:user][:last_name_kana] = Date.new(
      params[:user]["birthday(1i)"].to_i,
      params[:user]["birthday(2i)"].to_i,
      params[:user]["birthday(3i)"].to_i
    )
  end

end
```
それでは順番にみていきます。

1ページ目の情報入力ページでは
userのインスタンスを生成

createメソッドでは
sessionに入力したパラメータを代入していきます。

sessionを使うと代入した情報をページ遷移後でも保持することができます。

ここで注意したいのが、この時点でuserをcreateやsaveしてしまうともしユーザーが情報入力を中断してしまうと、そのデータが残ってしまい、2ページ目で入力するはずだったphone(電話番号）を持たないuserが登録されてしまうという事です。

そのためここではあくまでsessionに入れておくだけです。
そのsessionを用いてインスタンスを生成します。

その下のif文をみていきます。

```ruby:
if session[:provider].present? && session[:uid].present?
```

これはすなわちsnsで登録する場合という条件分岐で
trueの場合はpasswordを自動生成
falseの場合はパラメータから手動入力した値を受け取ります。

そして2ページ目に電話番号を入力後posrした時にcreateでようやくUser.createでuserを生成しました。
同じタイミングでSnsCredentialも生成しており、こちらも先程の説明同様、これより以前のタイミングでcreateやsaveしてしまうと、SnsCredentialのレコードだけ登録されてしまうので注意が必要です。

## フロント
あとはSNS登録の遷移ボタンとpasswordを自動入力した時は非表示にするため以下の記述を行います。

```ruby:ログイン方法選ページ
        =link_to "fecebookで登録する", user_facebook_omniauth_authorize_path, method: :post
        =link_to "googleで登録する", user_google_oauth2_omniauth_authorize_path, method: :post
```

```ruby:devise/registrations/new.html.haml
- if @sns.nil?
  .ubody__body--label
    = f.label :password
    %span
      必須
    %br/
    = f.password_field :password, autocomplete: "new-password", placeholder: "7文字以上の半角英数字"
```

以上でfacebook、googleでのSNS登録、メールアドレスでの手動登録を実装する事ができました。


## 問題点
機能自体は正常に動作するのですが、以下の問題点があります。

・SNS登録でヴァリデーションに引っかかりリダイレクトした場合パスワード入力フォームが表示されてしまう。
　こちらはフォームを空白のまま登録すればパスワードは自動生成されます。

・同じくSNS登録で情報入力を中断した場合sessionにuidが残ってしまうため、意図せずしてuidが登録されてしまう事がある。
　こちらはSnsCredentialに　validates :uid, uniqueness: trueとバリデーションを追加する事でひとまず回避しています。

もし解決策ありましたら教えてください。


以上です。
お付き合い頂きありがとうございました。