[API実装]Google とFacebook

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

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

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

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

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

~~※バリデーションに関しては詰めていないので修正は後ほど行う予定ですが、今の時点でやるべきか？~~
[記述しました_φ(･_･](https://qiita.com/i-to-to-to-mi/items/6b16bb090c7b4bfe08b8)


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


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

[API側設定はこちら](https://qiita.com/i-to-to-to-mi/items/35e71e15f575356f44ba)

```ruby:ターミナル
$ rails _5.2.4_ new mini_sns -d mysql
$ cd mi_sns
$ rails db:create
$ rails g controller users 
```

```ruby:routes.rb
 root 'users#index'  # ログインor新規登録を選ぶページ
 resources :users, only: :new  # 新規登録方法を選ぶページ
```

```ruby:Gemfile
gem 'devise'
gem 'haml-rails'
```

```ruby:ターミナル
$ bundle install
$ rails g devise:install
$ rails g devise user
$ rails g devise:views
$ rails haml:erb2haml
```
カラムは各々必要なものを設定してください

```ruby: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

#以下省略
```

```ruby:ターミナル
$ rails db:migrate
```
追加したカラムの入力欄を用意

```ruby: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"
```
追加したカラムを受け取るストロングパラメータの設定

```ruby: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
```
最後に確認用のビューを作成

```ruby: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
```

```ruby: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について](https://github.com/cookpad/omniauth-rails_csrf_protection)
[・公式ドキュメント](https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview)
[・RailsでFacebookとGoogleのOAuth連携。SNS認証の方法](https://qiita.com/nakanishi03/items/2dfe8b8431a044a01bc6)
[・[Rails5] Device SNS認証後に独自のユーザ登録フォームを実装する方法](https://qiita.com/onoxeve/items/992a803fadbd23b55fda)
[・Rails5 APIで認証付きのWebAPIを作ってみる](https://qiita.com/ochiochi/items/966b884eb17045dfb929)
[・[Rails] Facebook/Twitter/Googleでのユーザー登録をDevise & Omniauthを使って爆速で実装する](https://qiita.com/kazuooooo/items/47e7d426cbb33355590e)

####Gemを導入

```ruby:Gemfile
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
gem "omniauth-rails_csrf_protection"
```

```ruby:ターミナル
$ bundle install
```

####APIキーの環境変数設定

```ruby:ターミナル
$ 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`エンター

```ruby:ターミナル
$ source ~/.bash_profile
```
アプリ側に読み込む記述をする

```ruby: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の`uid`や`provider`を保存するためのテーブルを作成する
Userモデルとのアソシエーションのために、外部キーとして`user_id`を持たせる

```ruby:ターミナル
$ rails g model sns_credential provider:string uid:string user:references
$ rails db:migrate
```

```ruby: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
```

```ruby: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ってなに](https://qiita.com/ryoryomaru/items/7e401f9600fb11c20215)
deviseのコントローラをカスタマイズするためのコントローラを作成

```ruby:ターミナル
$ rails g devise:controllers users
```
deviseが提供するアクションの内容をカスタマイズすることができるようになった。

```ruby: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`

```ruby:ターミナル
$ 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](https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview)
[google-oauth2のGithub](https://github.com/zquestz/omniauth-google-oauth2)

```ruby: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
```

```ruby: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
```

```ruby: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認証時にはパスワード入力を不要にする

```ruby:models/user.rb
#記述を一部変更
    if user.persisted?
      sns.user = user
      sns.save
    end
    #SNS認証の時だけパスを自動生成する
    { user: user, sns: sns }
  end
```
返す値が変わったので、コントローラでも代入する部分を修正

```ruby: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
```
ビューのパスワード入力欄の表示/非表示を分ける

```ruby: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"

#コメントアウトを外す
```
インデントはこんな感じ（ミスると何も表示されなくなる。悲しい）

```ruby: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アクションの動作を変更する

```ruby: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)](https://qiita.com/yuichir43705457/items/7cfcae6546876086b849)

```ruby:.git.ignore
.env
```
この辺までで、APIの実装は大方できたので（実装続けつつ更新づるかもですが）セッションの実装うつります。


[実装完了編はこちら](https://qiita.com/i-to-to-to-mi/items/4ef8a16d87d2384251ff)