[Rails]SNSアカウントログイン機能の実装かつ既存userモデルと衝突しない方法

#準備
※twitterとfacebookを例にする。基本的に同じパタンなので、googleなど他のSNSも簡単に適用できる。
※deviseは使わない
※私の既存user modelとsession controllerは[Railsチュートリアル](https://railstutorial.jp/)で書いたものと同じです

##developerアカウント作成、自分のAPPを追加

###Facebook
[facebook developer](https://developers.facebook.com/?locale=ja_JP) (即時作成できる)
![facebook_meitu_1.jpg](https://qiita-image-store.s3.amazonaws.com/0/310783/873e0b20-8205-ab4f-4cac-28d3b1263159.jpeg)
右上のメニューの中から追加したAPPを選択
`Settings-Basic`から**APP ID**と**APP SECRET**を取得し、他のAPP情報も入力(ユーザ承認画面で表示される)
**PRODUCTS**のところで**Facebook Login**を追加し、`Settings`から**Valid OAuth Redirect URIs**(https://domain_name/auth/facebook/callback) を入力
※セキュリティの関係で、facebookは今httpsのurlしか承認しないので、localhostは使えないです。実行してみたいならherokuにappをデプロイすることがおすすめです。※

###Twitter
[twitter developer](https://developer.twitter.com/en/apply-for-access) (審査するには少し時間かかる)
![twitter_meitu_2.jpg](https://qiita-image-store.s3.amazonaws.com/0/310783/ebf2e46a-0c88-4ac6-e3c6-3a5460937c09.jpeg)
右上のメニューの中から追加したAPPを選択
`Keys and tokens`から**Consumer API keys (API key+API secret key)**を取得
`App details`から**Callback URL**(http://localhost:3000/auth/twitter/callback 他複数追加可)を入力、他のAPP情報も入力(ユーザ承認画面で表示される)


##gem追加

```ruby:Gemfile
gem 'omniauth'
gem 'omniauth-twitter'
gem 'omniauth-facebook'
```

##環境設定

`./config/initializers/omniauth.rb`というファイルを作成

```ruby:omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, ENV['TWITTER_ID'], ENV['TWITTER_SECRET']
  provider :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_SECRET']
end
```

追加可能なオプションのパラメータは
[omniauth-facebook (github)](https://github.com/mkdynamic/omniauth-facebook)
[omniauth-twitter (github)](https://github.com/arunagw/omniauth-twitter)
で確認できる。
例えばこんな感じ：

```ruby:omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, ENV['TWITTER_ID'], ENV['TWITTER_SECRET'],
  	{ :image_size => 'original', :authorize_params => { :lang => 'ja' } }
  provider :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_SECRET'],
  	{ display: 'page', image_size: 'normal' }
end
```
`ENV['XXX']`は環境変数です。XXXは自分で命名できます。

###localhostで設定したい場合
(特にwindowsユーザはこの方法が一番便利かなと思う)
`./config/local_env.yml`というファイルを作成 (facebookはlocalhost実行不可なので省略)

```ruby:local_env.yml
TWITTER_ID: '自分のAPP_ID'
TWITTER_SECRET: '自分のAPP_SECRET'
```

`./.gitignore`というファイルに下記のコードを追加(自分のkeyをgithubなどで公開しないように)

```common-lisp:.gitignore
/config/local_env.yml
```

###herokuで設定したい場合

```common-lisp
$ heroku config:set TWITTER_ID=自分のAPP_ID
$ heroku config:set TWITTER_SECRET=自分のAPP_SECRET
$ heroku config:set FACEBOOK_ID=自分のAPP_ID
$ heroku config:set FACEBOOK_SECRET=自分のAPP_SECRET
```


#各ファイルを更新


##ルーティング

```routes.rb
get 'auth/:provider/callback', to: 'sessions#create'
```
これが各developerアカウントのAPP設定で入力したredirect/callback URLになります

##ユーザモデル
```user.rb
#auth hashからユーザ情報を取得
#データベースにユーザが存在するならユーザ取得して情報更新する；存在しないなら新しいユーザを作成する
def self.find_or_create_from_auth(auth)
  provider = auth[:provider]
  uid = auth[:uid]
  name = auth[:info][:name]
  image = auth[:info][:image]
  #必要に応じて情報追加してください
  
  #ユーザはSNSで登録情報を変更するかもしれので、毎回データベースの情報も更新する
  self.find_or_create_by(provider: provider, uid: uid) do |user|
    user.username = name
    user.image_path = image
  end
end
```

```common-lisp
$ rails g migration add_columns_to_users
```

```ruby:20181122113757_add_columns_to_users.rb
class AddColumnsToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :uid, :string
    add_column :users, :provider, :string
  end
end
```

```common-lisp
$ rails db:migrate
```

##セッションコントローラ
```ruby:sessions_controller.rb
def create
  auth = request.env['omniauth.auth']
  if auth.present?
    user = User.find_or_create_from_auth(request.env['omniauth.auth'])
    session[:user_id] = user.id
    redirect_back_or user
  else #既存パタン
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_back_or user
    else
      flash.now[:danger] = 'メールアドレスとパスワードの組み合わせは有効ではありません'
      render 'new'
    end
  end
end
```

##ユーザモデルのバリエーション
**これだけじゃまだ動けない！**
なぜなら、user modelでは色々な**バリデーション**があるので、
`self.find_or_create_by(provider: provider, uid: uid)`という行でエラー発生。
※私のappではSNSアカウントでログインするならemailとpasswordがいらないので、バリデーションを以下のように修正した：

```ruby:user.rb
validates :username, presence: true, unless: :uid? #他省略
validates :email, presence: true, unless: :uid?
has_secure_password validations: false
validates :password, presence: true, unless: :uid?
```
`unless: :uid?`というのは、
uidデータがあればusername, email, passwordデータなしでも`User.create()`使えるようになります


##ビュー

###普通のテキストリンクでテストする場合
ログイン画面に追加

```erb:sessions/new.html.erb
<%= link_to "Twitterアカウントでログイン", "/auth/twitter" %>
<%= link_to "Facebookアカウントでログイン", "/auth/facebook" %>
```

###統一のボタンの場合
![sns_meitu_4.jpg](https://qiita-image-store.s3.amazonaws.com/0/310783/6ac36bfd-79b1-4684-8f5d-bbe06d12ff0e.jpeg)


```ruby:Gemfile
gem 'bootstrap-social-rails'
gem 'font-awesome-rails'
```

```scss:custom.scss
@import "bootstrap-social";
@import "font-awesome";
```

```erb:sessions/new.html.erb
<p>
  <%= link_to '/auth/twitter', class: "btn btn-social btn-twitter" do %>
    <span class="fa fa-twitter"></span> Twitterアカウントでログイン
  <% end %>
</p>
<p>
  <%= link_to '/auth/facebook', class: "btn btn-social btn-facebook" do %>
    <span class="fa fa-facebook"></span> Facebookアカウントでログイン
  <% end %>
</p>
<p>
  <%= link_to '/auth/google_auth2', class: "btn btn-social btn-google" do %>
    <span class="fa fa-google"></span> Googleアカウントでログイン
  <% end %>
</p>