[API実装]Google とFacebook


チーム課題を個人でやってみるけん_1

作成の経緯

チームのブランチで作業する前に確認したい部分があるので
実験的に個人で新たなapp作成を行い作ってみる。
今回試みるのは、セッションAPI実装部分

実装完了の定義(実装したいことと現状)
(現状)
2020/1/1時点ではSNSで新規登録するとユーザー登録画面に遷移しない
(実現したいこと)
SNSで新規登録時、名前やアドレスの情報を保持したままユーザー登録画面に遷移する

大まかな部分は実装で動くのは確認しているが
1月2日に実装部分に不備があったので修正と更新しました。

今回実装確認(変更)したい箇所は
app/models/user.rbの登録時の条件分岐箇所と
views/devise/registrations/new.html.hamlform_forのurl記述とrender部分

セッションの記述方法は別の方法もあるので今回の方法でうまく動かなかったら
今回と別の方法も考えています
(追記)
セッションとAPIを同時に進めたら事故ったのでセッションは今回作ったAPI実装のアプリに追記で作る方法に切り替えます!

※バリデーションに関しては詰めていないので修正は後ほど行う予定ですが、今の時点でやるべきか?
記述しましたφ(・

アドバイス等いただけるとすごくありがたい!!!

今回ディレクトリ名は「mi_sns」にしました

API側設定はこちら

ターミナル
$ rails _5.2.4_ new mini_sns -d mysql
$ cd mi_sns
$ rails db:create
$ rails g controller users 
routes.rb
 root 'users#index'  # ログインor新規登録を選ぶページ
 resources :users, only: :new  # 新規登録方法を選ぶページ
Gemfile
gem 'devise'
gem 'haml-rails'
ターミナル
$ bundle install
$ rails g devise:install
$ rails g devise user
$ rails g devise:views
$ rails haml:erb2haml

カラムは各々必要なものを設定してください

XXXXXXXXX_devise_create_users.rb
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :nickname, null: false, default: ''
      t.string :firstname, null: false, default: ''
      t.string :lastname, null: false, default: ''
      t.string :kana_firstname, null: false, default: ''
      t.string :kana_lastname, null: false, default: ''
      t.date   :birthday, null: false
      ## 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

#以下省略
ターミナル
$ rails db:migrate

追加したカラムの入力欄を用意

views/devise/registrations/new.html.haml
%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 :nickname
    %br/
    = f.text_field :nickname, autofocus: true
  .field
    = f.label :email
    %br/
    = f.email_field :email, autofocus: true, autocomplete: "email"
  -# - if @sns_id.present?
  -#   = hidden_field_tag :sns_auth, true
  -# - else
  .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
  .field
    = f.label :firstname
    %br/
    = f.text_field :firstname
  .field
    = f.label :lastname
    %br/
    = f.text_field :lastname
  .field
    = f.label :kana_firstname
    %br/
    = f.text_field :kana_firstname
  .field
    = f.label :kana_lastname
    %br/
    = f.text_field :kana_lastname
  .field
    = f.label :birthday
    %br/
    = f.date_select :birthday, start_year: 1950, end_year: 2019
    = f.submit "Sign up"
= render "devise/shared/links"

追加したカラムを受け取るストロングパラメータの設定

contorollers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  private

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname, :firstname, :lastname, :kana_firstname, :kana_lastname, :birthday])
  end
end

最後に確認用のビューを作成

views/users/index.html.haml
- if user_signed_in?
  = "#{current_user.nickname}でログイン中"
  = link_to 'ログアウト', destroy_user_session_path, method: :delete
- else
  ログインしていません
  = link_to '新規登録', new_user_path
  = link_to 'ログイン', new_user_session_path
views/users/new.html.haml
= link_to 'メールアドレスで登録', new_user_registration_path
= link_to 'Facebookで登録', user_facebook_omniauth_authorize_path, method: :post
= link_to 'Googleで登録', user_google_oauth2_omniauth_authorize_path, method: :post

OmniAuthによるSNS認証の実装

参考
・omniauth-rails_csrf_protectionについて
・公式ドキュメント
・RailsでFacebookとGoogleのOAuth連携。SNS認証の方法
・[Rails5] Device SNS認証後に独自のユーザ登録フォームを実装する方法
・Rails5 APIで認証付きのWebAPIを作ってみる
・[Rails] Facebook/Twitter/Googleでのユーザー登録をDevise & Omniauthを使って爆速で実装する

Gemを導入

Gemfile
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
gem "omniauth-rails_csrf_protection"
ターミナル
$ bundle install

APIキーの環境変数設定

ターミナル
$ vim ~/.bash_profile
``
i→エンターで下をコピペ

```ruby:~/.bash_profile
export FACEBOOK_CLIENT_ID='メモしたアプリID'
export FACEBOOK_CLIENT_SECRET='メモしたapp secret'
export GOOGLE_CLIENT_ID='メモしたクライアントID'
export GOOGLE_CLIENT_SECRET='メモしたクライアントシークレット'

記入後はesc:wqエンター

ターミナル
$ source ~/.bash_profile

アプリ側に読み込む記述をする

config/initializers/devise.rb
config.omniauth :facebook,ENV['FACEBOOK_CLIENT_ID'],ENV['FACEBOOK_CLIENT_SECRET']
config.omniauth :google_oauth2,ENV['GOOGLE_CLIENT_ID'],ENV['GOOGLE_CLIENT_SECRET']

SNS認証に必要なモデル、コントローラを用意

「SNS認証とユーザー登録のタイミングが異なる」仕様にするため、SNSのuidproviderを保存するためのテーブルを作成する
Userモデルとのアソシエーションのために、外部キーとしてuser_idを持たせる

ターミナル
$ rails g model sns_credential provider:string uid:string user:references
$ rails db:migrate
models/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:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:facebook, :google_oauth2]

         has_many :sns_credentials
end
models/sns_credential.rb
#変更前
class SnsCredential < ApplicationRecord
  belongs_to :user
end
#変更後
class SnsCredential < ApplicationRecord
  belongs_to :user, optional: true
end
#1/8追記
#このoptional: trueがないと外部キーのnilを許可できなくなっちゃうよ
#この記述忘れてひどく苦しんだ・・・

参考ページ
optional: trueってなに
deviseのコントローラをカスタマイズするためのコントローラを作成

ターミナル
$ rails g devise:controllers users

deviseが提供するアクションの内容をカスタマイズすることができるようになった。

config/routes.rb
#変更前
Rails.application.routes.draw do
  devise_for :users
#以下省略

#変更後
Rails.application.routes.draw do
  devise_for :users, controllers: {
    sessions: 'users/sessions',
    omniauth_callbacks: 'users/omniauth_callbacks',
    registrations: 'users/registrations'
  }
#以下省略

環境変数の読み込みができているか確認
rails c

ターミナル
$ rails c
irb(main):001:0> ENV['FACEBOOK_CLIENT_ID']
=> "1111111111111111"  # 数字だけ
irb(main):002:0> ENV['FACEBOOK_CLIENT_SECRET']
=> "1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"  # 数字と文字列
irb(main):003:0> ENV['GOOGLE_CLIENT_ID']
=> "~~~~~~~googleusercontent.com"  # 末尾がこの形
irb(main):004:0> ENV['GOOGLE_CLIENT_SECRET']
=> "1a1a1a1a1aa1_1a1a1a1_1a"       #  英数字と_(アンダーバー)が含まれる

SNS認証を実現するためのメソッドを実装

OmniAuthのGithub
google-oauth2のGithub

controllers/users/omniauth_callbacks_controller.rb
  def facebook
    authorization
  end

  def google_oauth2
    authorization
  end

  def failure
    redirect_to root_path
  end

  private

  def authorization
    #User.from_omniauthメソッドの中身はuser.rbに記述する
    # @userに代入することでdeviceのヘルパーを使える
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted? #ユーザー情報が登録済みなので、新規登録ではなくログイン処理を行う
      sign_in_and_redirect @user, event: :authentication
    else #ユーザー情報が未登録なので、新規登録画面へ遷移する
      render template: 'devise/registrations/new'
    end
  end
user.rb
#下に追記
def self.from_omniauth(auth)
  sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
  # sns認証したことがあればアソシエーションで取得
  # 無ければemailでユーザー検索して取得orビルド(保存はしない)
  user = sns.user || User.where(email: auth.info.email).first_or_initialize(
    nickname: auth.info.name,
      email: auth.info.email
  )
  # userが登録済みの場合はそのままログインの処理へ行くので、ここでsnsのuser_idを更新しておく
  if user.persisted?
    sns.user = user
    sns.save
  end
  user
end
views/devise/shared/_linkes.html.haml
#記述変更
- if devise_mapping.omniauthable?
  - resource_class.omniauth_providers.each do |provider|
    = link_to "#{OmniAuth::Utils.camelize(provider)}でログイン", omniauth_authorize_path(resource_name, provider), method: :post, target: :_blank
    %br/

SNS認証時にはパスワード入力を不要にする

models/user.rb
#記述を一部変更
    if user.persisted?
      sns.user = user
      sns.save
    end
    #SNS認証の時だけパスを自動生成する
    { user: user, sns: sns }
  end

返す値が変わったので、コントローラでも代入する部分を修正

controllers/users/omniauth_callbacks_controller.rb
# 変更前
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication #this will throw if @user is not activated
    else
      render template: 'devise/registrations/new'
    end
#変更後
    sns_info = User.from_omniauth(request.env["omniauth.auth"])
    @user = sns_info[:user]

    if @user.persisted? #ユーザー情報が登録済みなので、新規登録ではなくログイン処理を行う
      sign_in_and_redirect @user, event: :authentication#this will throw if @user is not activated
    else #ユーザー情報が未登録なので、新規登録画面へ遷移する
      #@sns_idという変数を作って新規画面で使う
      @sns_id = sns_info[:sns].id
      render template: 'devise/registrations/new'
    end
  end

ビューのパスワード入力欄の表示/非表示を分ける

views/devise/registrations/new.html.haml
  .field
    = f.label :email
    %br/
    = f.email_field :email, autofocus: true, autocomplete: "email"

#この辺りに↓↓↓↓これを入れて
  -# - if @sns_id.present?
  -#   = hidden_field_tag :sns_auth, true
  -# - else

  .field
    = f.label :password
    - if @minimum_password_length
      %em
        (#{@minimum_password_length} characters minimum)
    %br/
    = f.password_field :password, autocomplete: "new-password"

#コメントアウトを外す

インデントはこんな感じ(ミスると何も表示されなくなる。悲しい)

views/devise/registrations/new.html.haml
- if @sns_id.present?
    = hidden_field_tag :sns_auth, true
  - else
    .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"
  .field
    = f.label :firstname
    %br/
    = f.text_field :firstname
  .field
    = f.label :lastname
    %br/
    = f.text_field :lastname
  .field
    = f.label :kana_firstname
    %br/
    = f.text_field :kana_firstname
  .field
    = f.label :kana_lastname
    %br/
    = f.text_field :kana_lastname
  .field
    = f.label :birthday
    %br/
    = f.date_select :birthday, start_year: 1950, end_year: 2020
  .actions

paramsは通常devise/registrations#createアクションへ送信される
コントローラをオーバーライドするためにcontrollers/users/registrations_controller.rbを作成してあるのでこのコントローラ内でcreateアクションの動作を変更する

controllers/users/registrations_controller.rb
#こんな感じの部分を

  # POST /resource
  # def create
  #   super
  # end


# 以下コメントアウト部分を外してこう!
  def create
    if params[:sns_auth] == 'true'
      pass = Devise.friendly_token
      params[:user][:password] = pass
      params[:user][:password_confirmation] = pass
    end
    super
  end

superメソッド・・・親モデルの同名メソッドをそのまま実行

アプリケーションをGithubと連携させるためにIDとかの大事な情報を保護!

*1/4更新
下記記述はGemで'dotenv-rails'を記述して設定を
した時に有効なようです。
今回の場合はvim ~/.bash_profileで記述を
しているので下記の設定は不要のようです
[Ruby on Rails]環境変数の設定方法(.bash_profile、Dotenv-rails)

.git.ignore
.env

この辺までで、APIの実装は大方できたので(実装続けつつ更新づるかもですが)セッションの実装うつります。

実装完了編はこちら