Docker使ったRails環境でdeviseを導入する手順

Dockerのコマンドが写経になってしまうので、各コマンドの意味を理解するために記事化。
Docker上でGemのインストール、DBマイグレート、ルーティングと、一通りRailsでWebアプリケーションを作成する上で必要になる操作に応用できるということで、deviseの導入までの手順を記述します。
※最近の現場はDockerで開発環境を作っていることが多いので、Dockerで一から開発環境作成まで出来た方が捗るかと。
※途中参加のプロジェクトが多いので、Docker自体は触ったことあるけど、最初から作るのはやったことがないな、という人向け（つまり自分需要です）。

## 開発環境
- Docker
- Rails 5.2.4.1
- Ruby 2.5.3

こちらの記事を参考にして、Railsプロジェクトが起動するところまでは持っていきます。
[DockerでRuby on Railsの開発をしよう](https://qiita.com/saitoeku3/items/b1aa2ae143624e551aea)

## Gemインストール
こちらの記事を参照しながら設定開始。
[[*Rails*] deviseの使い方（rails5版）](https://qiita.com/cigalecigales/items/f4274088f20832252374)

### 1.Gemのインストール
Gemfileにdeviseの記述を追加したら、以下のコマンドを叩く。

```
$ docker-compose exec web bundle install
```

コマンドの意味を紐解くと、以下の通り。
**docker-compose**
各コンテナに対してコマンド発行する。詳しくは以下。
[Docker Compose](http://docs.docker.jp/compose/index.html)

**exec web**
対象のコンテナにコマンドを送る。
この場合「webコンテナに対し、コマンドを送る」という意味になる。

**bundle install**
実行されるコマンド

これでGemが入ります。

```
$ docker-compose build
```

## 2.deviseの設定

まずはdeviseをインストール。

```
$ docker-compose exec web rails g devise:install
```

「rails g devise:install」のコマンドをwebコンテナに送る。
以下のようなログが流れたら成功。

```
unning via Spring preloader in process 222
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================
```

## 3.Userモデル生成

deviseを使用するための画面を生成。

```
$ docker-compose exec web rails g devise:views
```

以下のようにログが流れて、生成されたViewが確認できる。

```
Running via Spring preloader in process 140
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_error_messages.html.erb
      create    app/views/devise/shared/_links.html.erb
      invoke  form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      invoke  erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/email_changed.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb
```

Deviseの画面は作ったので、rootの画面を生成する。
Authコントローラーを生成し、indexとshow画面を作成するためのコマンドを実行。

```
$ docker-compose exec web rails g controller Pages index show
```

画面が生成されたら、ログイン画面に相当するindexをrootに設定。
showはログイン後に遷移する画面となるので、こちらもルーティングに記述。

```ruby:config/routes.rb
Rails.application.routes.draw do
  root 'pages#index'
  get 'pages/show'
end
```

余談ですが、rootは「page/index」のように記述すると以下のエラーが出て、Dockerが立ち上がらないです。

```
roller key on routes definition, please check your routes
```

また、ここまでで生成したルーティングを確認する場合のコマンドはこちら。

```
docker-compose exec web rails routes
```

ログイン画面と、ログイン後の画面を作ったので、application.html.erbに「サインインしていなければログイン画面に飛ばす」という設定を施します。

```ruby:app/views/layouts/appliation.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>App</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <header>
      <nav>
        <% if user_signed_in? %>
          <strong><%= link_to current_user.username, auth_show_path %></strong>
          <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
        <% else %>
          <%= link_to 'ログイン', new_user_session_path %>
        <% end %>
      </nav>
    </header>

    <p class="notice"><%= notice %></p>
    <p class="alert"><%= alert %></p>
    <%= yield %>
  </body>
</html>
```

次にユーザー管理をするUserモデルを生成。

```
$ docker-compose exec web rails g devise User
```

以下のようにログが流れる。

```
Running via Spring preloader in process 299
      invoke  active_record
      create    db/migrate/20200130213032_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users
```

app/models/user.rbが生成されるので、ここを編集。
twitter認証を使いたいので、こんな感じ。

```ruby: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,
         :omniauthable, omniauth_providers: [:twitter]
end
```

※認証はtwitter認証以外を通さない設定なので、omiauth以外のモジュールは消して良いと思うのですが、今回はこのまま。

次にTwitter認証で取得されるuidとusernameを保存するカラム、そして将来、他の認証を追加した時に「なんの認証を利用したのか」を保存するproviderカラムをusersテーブルに追加。

```
$ docker-compose exec web rails g migration add_columns_to_users provider uid username
```

マイグレーションファイルができるので確認。

```ruby:20200130214734_add_columns_to_users
class AddColumnsToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :provider, :string
    add_column :users, :uid, :string
    add_column :users, :username, :string
  end
end
```

追加するカラムは全て文字列で良いので、変更なし。
DBを変更するため、マイグレーションを実行。

```
$ docker-compose exec web rails db:migrate
```

## Twitter認証を通す

TwitterのDeveloperセンターで、開発者の登録をして、アプリ作成を申請。
2017年くらいに一回やったことがあったんですが、登録がめっちゃ大変になってました。
手順についてはこちらの記事を参照。
[Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ](https://qiita.com/kngsym2018/items/2524d21455aac111cdee)

トークンが発行されたら、コールバック先の登録を行います。
ルーティングを確認し、Twitterに設定。

```
$ docker-compose exec web rails routes

                         Prefix Verb     URI Pattern                                                                              Controller#Action
               new_user_session GET      /users/sign_in(.:format)                                                                 devise/sessions#new
                   user_session POST     /users/sign_in(.:format)                                                                 devise/sessions#create
           destroy_user_session DELETE   /users/sign_out(.:format)                                                                devise/sessions#destroy
user_twitter_omniauth_authorize GET|POST /users/auth/twitter(.:format)                                                            omniauth_callbacks#passthru
 user_twitter_omniauth_callback GET|POST /users/auth/twitter/callback(.:format)                                                   omniauth_callbacks#twitter                                                           
（略）
```

確認したルーティングをTwitterのコールバックに列挙します。

<img width="497" alt="スクリーンショット 2020-02-24 7.23.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527789/95c09536-f9a4-c2be-bd05-944ab02febe7.png">

deviseのconfigにはAPIキーとコールバックを記述。

```ruby:config/initializers/devise.rb
Devise.setup do |config|
（略）
config.omniauth :twitter, "API Key", "API secret key", "https://localhost:3000/users/auth/twitter/callback"
end
```
コールバック先を記述しておかないと、`raise OAuth::Unauthorized, response 403`と怒られます。

## コールバックの設定

Twitterからコールバックがきたら受け取るコントローラーの設定をします。
まずはコールバック用のコントーラー作成。

```
$ docker-compose exec web rails g controller omniauth_callbacks
```

作られたコントーラーに対して、コールバックメソッドを記述。

```Ruby:omniauth_callbacks_controller.rb
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
  # Twitter認証
  def twitter
    @user = User.from_omniauth(request.env["omniauth.auth"].except("extra"))

    if @user.persisted?
      sign_in_and_redirect @user
    else
      session["devise.user_attributes"] = @user.attributes
      redirect_to new_user_registration_url
    end
  end
end
```

ポイントは`Devise::OmniauthCallbacksController`を継承すること。
継承することで、Deviseがコールバックで受け取った諸々を使うことができる。

`User.from_omniauth(request.env["omniauth.auth"].except("extra"))`

「Userモデルのfrom_omniauthに、request.envに入った値を引数で渡して処理してもらう」ということなので、userモデルに以下を実装。

```Ruby:user.rb
class User < ApplicationRecord
  （略）

  def self.from_omniauth(auth)
    find_or_create_by(provider: auth["provider"], uid: auth["uid"]) do |user|
      user.provider = auth["provider"]
      user.uid = auth["uid"]
      user.username = auth["info"]["nickname"]
    end
  end

  def self.new_with_session(params, session)
    if session["devise.user_attributes"]
      new(session["devise.user_attributes"]) do |user|
        user.attributes = params
      end
    else
      super
    end
  end
end
```

`from_omniauth`では、providerとuidの組み合わせで、同一があればupdate、無ければinsertを実行する。