はじめに
スクールの成果打つでの機能実装を備忘録にしました。
かなり難しかったので誰かのお役に立てれば幸いです。
ウィザードとはいくつかにページを分けて情報を登録する形式です。↓

今回は画像と同じように3つのページに分けて情報を登録していきます。
登録方法は
・メールアドレス
・facebook
・google
この3つの方法で登録することができるようにします。
環境
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 で認証させる
facebook
https://developers.facebook.com/
omniauth-facebookでユーザー情報を取得する
ここで注意なのですが、外部APIを使用した時、手順通りやっても上手く行かないなんて事があります。(経験値です。)
何度試しても上手くいかない。どの記事をみてもやり方は同じなのに。そんな時は焦らず時間を置いてみてください。APIにURLを登録するとフィットするのに時間がかかるのか、しばらく経つと正常にうごく事があります。私は色々触って時間がかかりましたが、結局次の日になっていると動いていました。
omniauth gemのインストール
次に以下のgemをインストールします。
gem 'omniauth-rails_csrf_protection' #omniauthよりも安全
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
deviseの設定
scopeなど色々オプションを付けれるみたいですが、今回は必要ありません。もしAPIとの接続が上手くいかなかったり、必要なデータを取れない場合はKEYに問題がある可能性が高いです。
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']
# 以下略
ルーティング
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
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を編集していきます。
それぞれ順番にみていきます。
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となっています。
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を入力しなくてもいい事にします。
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文をみていきます。
if session[:provider].present? && session[:uid].present?
これはすなわちsnsで登録する場合という条件分岐で
trueの場合はpasswordを自動生成
falseの場合はパラメータから手動入力した値を受け取ります。
そして2ページ目に電話番号を入力後posrした時にcreateでようやくUser.createでuserを生成しました。
同じタイミングでSnsCredentialも生成しており、こちらも先程の説明同様、これより以前のタイミングでcreateやsaveしてしまうと、SnsCredentialのレコードだけ登録されてしまうので注意が必要です。
フロント
あとはSNS登録の遷移ボタンとpasswordを自動入力した時は非表示にするため以下の記述を行います。
=link_to "fecebookで登録する", user_facebook_omniauth_authorize_path, method: :post
=link_to "googleで登録する", user_google_oauth2_omniauth_authorize_path, method: :post
- 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とバリデーションを追加する事でひとまず回避しています。
もし解決策ありましたら教えてください。
以上です。
お付き合い頂きありがとうございました。