コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.9 - Sign in -

# はじめに
第９回目です。
前回はサインアップ機能を作ってみましたね。今回はサインインやサインアウト機能を作ってまいります。
今後、サインインしていないと使えない機能や見れないページなども作っていきますので、アプリがユーザーのサインイン状況を知ることができてサインインしているユーザーを特定できるようにしましょう！

# 前回のソースコード
前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。

- [at946/rails_on_docker_handson at vol.08](https://github.com/at946/rails_on_docker_handson/tree/vol.08)

# セッション
RailsはRESTfulなWebアプリケーションフレームワークなのでステートレスなアプリです。
しかし、サインイン機能を実装するとなるとそのユーザーがサインイン状態であることをアプリが知る必要がありますし、アプリはそのユーザーがどのユーザーなのかを特定する必要があります。
このために使われるのがセッション（Session）です。

Railsではセッションを管理する便利な`session`メソッドが用意されています。
今回はこの`session`メソッドを使ってサインイン状態を管理し、マイページ機能、つまりサインインしているユーザーのユーザー詳細ページに遷移する機能を作っていきましょう。
サインインしていない場合はユーザーを特定できないので、マイページには遷移させずトップページにリダイレクトをかけるようにもしていきます。

このように、セッションでサインイン状態を管理できるようになれば、その状態によるページや動作の出しわけやサインインユーザーに合わせたコンテンツの出しわけが可能になります。

今回はセッションを一つのリソースと捉えて実装していきます。
つまり、サインインしたときに`sessions#create`でセッションを作成したり、サインアウトしたときに`Sessions#destroy`でセッションを削除したりさせます。

# サインインページを作ろう
セッションはSessionsコントローラーで管理してきます。
アクションとしては、

- `new`： サインインページに遷移
- `create`： セッション作成（サインイン処理）
- `destroy`： セッション削除（サインアウト処理）

が必要になってきます。
今回は`rails g`コマンドに頼らずにディレクトリやファイルを作成していきますね。

## ルーティングを作成する
まずはルーティングを作成しましょう。

```diff:config/routes.rb
  Rails.application.routes.draw do
    root 'static_pages#home'

    get   '/sign_up',  to: 'users#new',    as: :sign_up
    post  '/sign_up',  to: 'users#create', as: :create_user
    resources :users, only: [:show]
+
+   get     '/sign_in',   to: 'sessions#new',     as: :sign_in
+   post    '/sign_in',   to: 'sessions#create',  as: :create_session
+   delete  '/sign_out',  to: 'sessions#destroy', as: :sign_out
  end
```
この辺りはもう慣れてきましたよね。

## コントローラーを作成する
お次はコントローラーを作ります。（最初にコンテナを起動させて中に入っておきます。）

```
$ docker-compose up -d
$ docker-compose exec web ash
```
```
# touch app/controllers/sessions_controller.rb
```

```rb:app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
    @user = User.new
  end 

  def create
  end

  def destroy
  end
end
```
とりあえずルーティングと対応したアクションを記述しておきます。
`new`アクション、つまりサインインページではまたUserモデルを扱います（ユーザーのサインイン情報を入力してもらうので）。
そのため、`new`アクションはUsersコントローラーと同じように`@user = User.new`を記述してます。

## サインインページを作成する
さて、`new`アクションがレンダリングする`new.html.erb`をコーディングしてきましょう。

まずは、Sessionsコントローラーのビューファイルを格納するディレクトリを作成し、ビューファイルを作りましょう。

```
# mkdir app/views/sessions
# touch app/views/sessions/new.html.erb
```
さて、ビューファイルをコーディングしてみます。

```erb:app/views/sessions/new.html.erb
<div class="container">
  <h1 class="my-5">Sign in</h1>

  <%= form_with model: @user, url: create_session_path, local: true do |form| %>
    <div class="form-group">
      <%= form.label :email %>
      <%= form.text_field :email, class: "form-control" %>
    </div>
    <div class="form-group">
      <%= form.label :password %>
      <%= form.password_field :password, class: "form-control" %>
    </div>
    <div class="form-check">
      <%= check_box_tag :visible_password, :visible, false, class: "form-check-input" %>
      <%= label_tag :visible_password, "パスワードを表示する" %>
    </div>

    <div class="form-group mt-5">
      <%= form.submit "Sign in", class: "btn btn-primary form-control" %>
    </div>
  <% end %>
  <p class="text-center">登録がまだの方は<%= link_to "こちら", sign_up_path %></p>
</div>

<%= javascript_pack_tag 'visible_password' %>
```
基本的には前回のサインアップページと同じですね。今回はサインインなので`email`と`password`を入力項目に指定しました。
また、`password`はサインアップページと同様、チェックボックスで表示非表示を変更できるようにしています。

『Sign in』ボタンの下にまだサインアップが終わっていないユーザー向けにサインアップページへのリンクを追加しています。
`<%= link_to "こちら", sign_up_path %>`だけで適切な`a`タグを作成してくれるのはやはり便利ですね。

一度`http://localhost:3000/sign_in`にアクセスしておきましょう。以下のようなページが表示されたら、ここまでのコーディングは成功です！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/075ddd67-8030-a9f0-7e17-cb812028db24.png)

## サインインページへのリンクを作る
サインインページの形が出来上がってきたので、ここでサインインページへのリンクを以下のページにつけていこうと思います。

- ヘッダーの「Sign in」リンク
- サインアップページの下部に「登録済みの方はこちら」リンクを設置

### ヘッダーの「Sign in」リンク
ヘッダーの「Sign in」リンクはまだ遷移先を指定できていませんでしたね。
やっとサインインページができあがってきたので遷移先として`sign_in_path`を指定しておきます。

```diff:app/views/layouts/application.html.erb
  ...
  <header class="navbar navbar-dark navbar-expand bg-dark">
    <div class="container">
      <%= link_to "sample app", root_path, class: "navbar-brand" %>
      <ul class="navbar-nav">
        <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link" %></li>
-       <li class="nav-item"><%= link_to "Sign in", "#", class: "nav-link" %></li>
+       <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link" %></li>
      </ul>
    </div>
  </header>
  ...
```
コーディングが終わったら、一度トップページに遷移してヘッダーの「Sign in」リンクを選択してみましょう。
サインインページに遷移できたら成功です！

### サインアップページの下部に「登録済みの方はこちら」リンクを設置
サインインページで「登録がまだの方はこちら」リンクを設置したように、サインアップページにも「登録済みの方はこちら」リンクを設置してみましょう。

```diff:app/views/users/new.html.erb
  ...
  <%= form_with model: @user, url: create_user_path, local: true do |form| %>
    ...
    <div class="form-group mt-5">
      <%= form.submit "Sign up!", class: "form-control btn btn-primary" %>
    </div>
  <% end %>
+ <p class="text-center">登録済みの方は<%= link_to "こちら", sign_in_path %></p>
  ...
```
こちらもサインアップページで「こちら」をクリックしてサインインページに遷移できれば成功です！

# サインイン処理を作成する
ここまででサインインページの形ができあがりました。
次はサインイン処理（`create`）をつくっていきます！

サインイン処理ではサインインページで入力された`email`と`password`からユーザーを検索します。
ユーザーがヒットすればそのユーザーのユーザー詳細ページ（`users#show`）、ユーザーが存在しない、またはパスワードが誤っているような場合はエラーメッセージが表示されるようにしましょう。

少しおさらいですが、モデルを検索する場合は`find_by`メソッドを使うことで指定した属性に対して検索をすることができました。
さらに、`has_secure_password`をもつモデルは`authenticate`メソッドを使ってパスワード認証ができることも思い出しましょう。
この２つを組み合わせることでユーザーの検索し認証することができそうですね。

では、`create`アクションを記述していきます。

```diff:app/controllers/sessions_controller.rb
  ...
  def create
+   @user = User.new(email: params[:user][:email])
+   user = User.find_by(email: @user.email.downcase)
+   if user && user.authenticate(params[:user][:password])
+     flash[:success] = "サインインしました。"
+     redirect_to user
+   else
+     flash.now[:danger] = "#{User.human_attribute_name(:email)}または#{User.human_attribute_name(:password)}をもう一度確認してください。"
+     render :new
+   end
  end
  ...
```
少し複雑に見えるかもしれません。１行ずつ解説していきますね。

```rb
@user = User.new(email: params[:user][:email])
```
まず、`@user`に新しくUserモデルを代入しています。属性値は`email`に`params[:user][:email]`を持っています。
`params`はフォームからのリクエストの値を取得するメソッドで、フォームの`input`タグで`name="user[email]"`と定義されている値は`[:user][:email]`から取得することができます。
`@user`に`params[:user][:email]`の属性値をもつUserモデルを代入しているのは、この処理がエラー（ユーザーが見つからない or パスワード認証が通らない）の場合に今入力した`email`をデフォルトでフォームに入力された状態でサインインページを再度表示するためです。
これがないと入力した`email`をキープする方法がなくてユーザーは毎回メールアドレスを再入力する手間になってしまいます。（パスワードは特性上、キープしないように作っています）

```rb
user = User.find_by(email: @user.email.downcase)
```
先ほど`@user`に設定した`email`を使ってUserを検索しています。
Userモデルを作成するときにモデル側で`email`を小文字化してからDBに保存するようにコーディングしたことを覚えているでしょうか？
今DBには必ず全て小文字のメールアドレスが保存されているので、`find_by`で`email`を検索する時も`downcase`で検索文字列を小文字化して検索しています。
`find_by`は検索対象が存在していた場合はモデルオブジェクトを返却し、存在しない場合は`nil`を返却するメソッドであることも改めて意識しましょう。

```rb
if user && user.authenticate(params[:user][:password])
  # trueの処理
else
  # falseの処理
end
```
次に条件分岐を設けています。`&&`は「アンド条件」、「かつ」を意味していますので、

- `user`
- `user.authenticate(params[:user][:password])`

の両方を満たした場合は`true`、どちらか一方でも満たさない場合は`false`の処理に分岐します。

まず`user`の条件式をみてみます。
これは`user`が`nil`や`false`でないかどうかを検証しています。
先ほど`find_by`は検索結果があればモデルオブジェクト、なければ`nil`を返却するといいました。
なのでこの`user`の条件は`find_by`の結果その`email`をもつユーザーがそもそも存在しない場合`false`の処理を実行するようにするための条件ということになります。

次に`user.authenticate(params[:user][:password])`の条件式をみてみます。
これは順番的に`user`の条件式を満たした場合に検証される条件式です。なので`user`にはUserモデルのモデルオブジェクトが入っています。
`authenticate`メソッドは`has_secure_password`の便利機能の一つで、平文の`password`を与えるとハッシュ化してDBに格納している`password_digest`と照会し、同一であればモデルオブジェクトを、そうでなければ`false`を返却してくれるものでした。

以上より、このif文の条件式の条件分岐は以下の通りになります。

入力した`email`のユーザーが|パスワードが|実行する処理
-----|-----|-----
いない|-|falseの処理
いる|誤っている|falseの処理
いる|正しい|trueの処理

やりたいことに合致してますね。
では`true`の処理、`false`の処理をそれぞれみてみましょう。

```rb
flash[:success] = "サインインしました。"
redirect_to user
```
`true`の処理はとても単純です。
検索し認証したユーザーのユーザー詳細ページにリダイレクトさせているだけです。flashでサインインメッセージも定義してますね。

```rb
flash.now[:danger] = "#{User.human_attribute_name(:email)}または#{User.human_attribute_name(:password)}をもう一度確認してください。"
render :new
```
`false`の処理も最終的には`render :new`で再度サインインページを表示させていることがわかります。
その前に`flash.now`メソッドを使ってエラー文を用意しています。
前回のUsersコントローラーの`create`アクションでも`flash`メソッドを使ってサインアップ成功時のメッセージをユーザー詳細ページに一時的に表示させることをしました。
`flash.now`はそれの`render`時に利用する版と思ってください。`key`を`danger`にしているのは前回と同じでBootstrapの命名規則に則っています。

`flash.now`のメッセージの中身が若干複雑ですね。
まず、Rubyでは文字列の中に変数を入れる場合`#{変数}`と記述することができます。
`"aaa" + 変数 + "bbb"`のような書き方もできますが、`"aaa#{変数}bbb"`の方がよりスマートな気がしますよね。
その変数は`User.human_attribute_name(属性名)`となっています。
`human_attribute_name`メソッドは引数の属性のi18nで定義した文字列を返してくれます。ちょうど`form.label`と同じような感じですね。
なので今回の場合、`User.human_attribute_name(:email)`は『メールアドレス』、`User.human_attribute_name(:password)`は『パスワード』が変数の結果として当て込まれます。

ここまででサインイン処理（`create`アクション）をみてきました。
最後に`flash.now`のメッセージを表示するコードが今の`sessions/new.html.erb`にはないので、前回の`users/new.html.erb`に倣って記述します。

```diff:app/views/sessions/new.html.erb
  ...
  <h1 class="my-5">Sign in</h1>
+ 
+ <% flash.each do |msg_type, msg| %>
+   <div class="alert alert-<%= msg_type %>"><%= msg %></div>
+ <% end %>
+
  <%= form_with model: @user, url: create_session_path, local: true do |form| %>
...
```

# 動作確認
では、サインイン処理を動作確認してみましょう。
前回DBをリセットしてますので、もう一度`John Smith`さんをDBに登録しておきます。

```
# rails c
```

```
> User.create(name: "John Smith", email: "john@sample.com", password: "password")
```

`http://localhost:3000/sign_in`にアクセスして色々とチェックしてみましょう！

## エラー系
### `email`や`password`を入力していない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/1263a06b-79cf-b9a2-3c90-c68f94c55d29.png)
何も入力していない、またはどちらかだけでも入力していない場合は`find_by`か`authenticate`のいずれかが必ず失敗するのでエラーメッセージが表示されていますね。

### 入力した`email`のユーザーがいない
`test@test.com`みたいな適当な`email`を入力して確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/e37d8257-7610-5479-4327-541086d8f006.png)
エラーメッセージが表示されているし、`email`のテキストフィールドに入力してた`test@test.com`が残っているのも確認できましたね。

### `email`と`password`の組み合わせが異なっている
`john@sample.com`と適当なパスワード`john1234`を入力して確認してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/440b2103-d719-6bc6-5195-72218f131fc2.png)
こちらもエラーメッセージが表示されていますね。`email`に`john@sample.com`も残っています。

## 正常系
期待動作は`john@sample.com`&`password`でユーザー詳細ページに遷移することです。
やってみましょう！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/fd3ad045-c533-525c-1ae5-5a1c8ebfa8d9.png)
ちゃんと期待通りの動作になりましたね。

# セッション管理する
ここまででパスワード認証のロジックができあがりましたね。

ただ今のままでは、「認証」自体はできましたが「認証済み」という状態を管理することはできてません。
「認証済み」という状態を管理するためにセッション管理をする機能を作っていきます。

ちなみにRailsの`session`メソッドでは、半永続的で暗号化された一時Cookieが払い出されます。
これは仮にこのCookieが盗まれたとしても、それを使ってサインインを乗っ取ることはできないようになっています。
その代わりと言ってはなんですが、一度ブラウザを閉じると自動的に消える仕組みになっています。

## SessionsHelperを作成する
セッション管理するために`sign_in`メソッドや`sign_out`メソッドなどを自前で実装していきます。
これらはコントローラーの元になる`application_controller.rb`にコーディングしていきたいところですが、それでは`application_controller.rb`に多くのメソッドを記述する必要がでてくるため可読性が確保できなくなる恐れがあります。

今回は、新たにSessionsHelperを作成し、その中で`sign_in`メソッド、`sign_out`メソッドを定義します。
このSessionsHelperをApplicationControllerが読み込んでコントローラー内でメソッドを利用できるようにすることで、可読性を損なうことなく機能を実装していきましょう。

まずSessionsHelperファイルを作成します。コンソールが起動している場合は`quit`で抜け出しましょう。

```
> quit
```

```
# touch app/helpers/sessions_helper.rb
```

```rb:app/helpers/sessions_helper.rb
module SessionsHelper
end
```
ヘルパーは`module [helper_name]`で定義します。
最後に、ヘルパーをApplicationControllerから読み取るように定義しましょう。

```diff:app/controllers/application_controller.rb
  class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception
+   include SessionsHelper
  end
```

## SessionsHelperで`sign_in`メソッドを作成する
次に、SessionsHelperを編集して、`sign_in`メソッドを作成していきます。
Railsでは`session`メソッドが用意されており、簡単にセッションを管理することができます。

```diff:app/helpers/sessions_helper.rb
  module SessionsHelper
+
+   def sign_in(user)
+     session[:user_id] = user.id
+   end
+
  end
```
これだけで、`sign_in`メソッドを使って`user.id`をセッションに記録することができます。

サインインページでサインインが成功した場合に`sign_in`メソッドを呼び出してセッションを記録するようにしてみます。

```diff:app/controllers/sessions_controller.rb
  ...
  def create
    @user = User.new(email: params[:user][:email])
    user = User.find_by(email: @user.email.downcase)
    if user && user.authenticate(params[:user][:password])
      flash[:success] = "サインインしました。"
+     sign_in user
      redirect_to user
    else
      flash.now[:danger] = "#{User.human_attribute_name(:email)}または#{User.human_attribute_name(:password)}をもう一度確認してください。"
      render :new
    end
  end
  ...
```
`redirect_to`の前に`sign_in user`を追加しただけです。
SessionsControllerはApplicationControllerを継承しているのでApplicationControllerで読み込んでいるSessionsHelperを利用することができます。

## `current_user`メソッドを作成する
ここまででサインインしたときにセッション情報としてサインインユーザーの`id`をCookieに格納できるようになりました。
今度はこれを使って、サインイン中のユーザー情報をアプリが特定するための`current_user`メソッドを作成しましょう。
この`current_user`メソッドを使うことで、例えば`<%= current_user.name %>`でサインイン中のユーザーの名前を表示できるようにしたりします。

今、セッションに`user_id`のキーに対して`user.id`を格納しています。これは`session[:user_id]`で取り出すことができます。
このことから`User.find(session[:user_id])`または`User.find_by(id: session[:user_id])`でサインイン中のユーザーを特定できることを思いつきますね。
ただ、`find`メソッドはNotFoundの場合に例外が発生します。`find_by`メソッドの場合はNotFoundでも`nil`を返却するだけなのでif文などで簡単にエラー時の処理を実装できるのですが、例外の場合はちょっとクセがありますね。
今回は`find_by`メソッドで実装するようにしましょう。

では、SessionsHelperに定義します。

```diff:app/helpers/sessions_helper.rb
  module SessionsHelper

    def sign_in(user)
      session[:user_id] = user.id
    end
+
+   def current_user
+     @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
+   end
  
end
```
少しつまずきそうなコードですね。ちょっと紐解きます。

まずわかりやすいところから、一番後ろに`if session[:user_id]`と記入しています。
これはこれよりも前の文章が実行される条件式になっています。`session[:user_id]`が存在する、つまりサインイン済みの状態であればこれよりも前の文章が実行されるというわけです。
`current_user`メソッドではこの１行以外にコードはないので、`session[:user_id]`が存在しない場合は何も実行されず、`nil`が返却されるようになります。

次に見慣れない`||=`をみてみてましょう。
これは見慣れないと思いますが、少しプログラミングをかじったことがある人であれば何かの言語で`+=`のような表現を見たことがあるのではないでしょうか？
Rubyでも同じような挙動をとりますが、`a += b`は`a = a + b`と同義になりますね。
`||`はORを表す演算子ですので、`a ||= b`は`a = a || b`と同義になります。
`current_user`は上で少しお話したように、`<%= current_user.name %>`、`<%= current_user.email %>`のように使われるケースを考えています。
これが同じビューファイルで２回呼び出された場合、`User.find_by(id: session[:user_id])`とだけコーディングしていた場合は２回ともDBから情報を取得しなくてはならなくなります。
そこで`@current_user ||= User.find_by(id: session[:user_id])`としておくことで、一度目に`<%= current_user.name %>`時に`@current_user`がセットされるため二度目の`<%= current_user.email %>`の場合はDBにアクセスすることなく前回の検索時のモデルオブジェクトを再利用することができるようになります。
DBの負荷軽減やDBアクセスの回数削減によるレスポンス向上を目的として、こういった記述にしてみました。

まとめると、`current_user`メソッドは呼び出されたときに以下の動作をします。

サインイン|ユーザー|処理
-----|-----|-----
未|-|`nil`
済|NotFound|`nil`
済|Found|サインイン済みユーザーのモデルオブジェクト

書き方を変えると以下と同じですね。

```rb
def current_user
  if session[:user_id]
    if @current_user
      @current_user
    else
      User.find_by(id: session[:user_id])
    end
  end
end
```
上の書き方の方がシンプルで可読性高いですよね。

## サインイン状態を確認する`signed_in?`メソッドを作る
どんどんいきましょう。
次にサインイン状態を`true`、`false`で返却する`signed_in?`メソッドを作ってみます。
このメソッドによってユーザーのサインイン状態に合わせた処理を簡単に実装することができます。

先ほどの`current_user`メソッドはユーザーがサインインしている場合はそのユーザーのモデルオブジェクト、サインインしていない場合は`nil`を返却するメソッドでした。
そしてRailsには`nil`の場合`true`を、そうでない場合`false`を返却する`nil?`メソッドが用意されています。
さらに`!`は`true`と`false`が入れ替わる否定演算子です。

これらを組み合わせれば`signed_in?`メソッドが作れそうですね。

```diff:app/helpers/sessions_helper.rb
  module SessionsHelper

    def sign_in(user)
      session[:user_id] = user.id
    end

    def current_user
      @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
    end
+
+   def signed_in?
+     !current_user.nil?
+   end

  end
```

## サインアウトする`sign_out`メソッドを作成する
最後にサインアウトするメソッドとして`sign_out`メソッドを作ってみましょう！

```diff:app/helpers/sessions_helper.rb
  module SessionsHelper

    def sign_in(user)
      session[:user_id] = user.id
    end

    def current_user
      @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
    end

    def signed_in?
      !current_user.nil?
    end
+
+   def sign_out
+     session.delete(:user_id)
+     @current_user = nil
+   end
  
end
```
なんとなーくわかるとおもいますが、`session.delete(:user_id)`でCookieに保存していた`session[:user_id]`を削除してます。
さらにサインイン中は`@current_user`にサインインしているユーザーのモデルオブジェクトが格納されているのでこれも`nil`に初期化しています。

さてさて、ここまででサインインに関して最低限必要な全てのメソッドを作成できました。
サインインの状態に合わせてヘッダーを出しわけしてみましょう！

# ヘッダーを更新する
ヘッダーを以下の条件で出しわけしてみます。

- 未サインイン
    - `Home`: トップページに遷移する
    - `Sign in`: サインインページに遷移する
- サインイン済
    - `Profile`: サインインしたユーザーのユーザー詳細ページに遷移する
    - `Sign out`: サインアウトしてトップページに遷移する

まずは、ヘッダーを`signed_in?`メソッドを使って出しわけしてみます。

```diff:app/views/layouts/application.html.erb
  ...
  <header class="navbar navbar-dark navbar-expand bg-dark">
    <div class="container">
      <%= link_to "sample app", root_path, class: "navbar-brand" %>
      <ul class="navbar-nav">
-       <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link" %></li>
-       <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link" %></li>
+       <% if signed_in? %>
+         <%# サインイン済みの場合のリンク %>
+         <li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link" %></li>
+         <li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link" %></li>
+       <% else %>
+         <%# 未サインインの場合のリンク %>
+         <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link" %></li>
+         <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link" %></li>
+       <% end %>
      </ul>
    </div>
  </header>
...
```
今までのヘッダーは未サインインの場合のリンクになっていたので、`signed_in?`が`false`の場合の方に記述してます。

サインイン済みの場合は新たに「Profile」と「Sign out」のリンクを作っています。

```erb
<li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link" %></li>
```
「Profile」リンクの方は今までとほぼほぼ変わらない書き方ですね。少し違う点としては`root_path`、`sign_in_path`のように遷移先として`path`を指定するのではなく`current_user`というモデルオブジェクトを指定している点です。
`link_to`メソッドでは、遷移先としてモデルオブジェクトが指定された場合、そのモデルオブジェクトの参照パスに読み替えてくれます。つまり、`user_path(current_path)`と同義になります。

```erb
<li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link" %></li>
```
「Sign out」リンクの方は`method: :delete`の箇所が今までとは異なります。
通常`link_to`メソッドは`GET`メソッドでリンク先にリクエストしますが、`method: [method]`で指定することで別のメソッドでリクエストすることができます。
`sign_out_path`は`routes.rb`で`delete`メソッドでルーティングするように定義していましたので、この形で`link_to`のHTTPメソッドを指定してあげないとルーティングされなくなってしまいます。

そういえば、`sign_out_path`でルーティングされる`sessions#destroy`について、コーディングをしていませんでした。
セッションを削除する`sign_out`メソッドを実行し、トップページにリダイレクトするようにコーディングしましょう。

```diff:app/controllers/sessions_controller.rb
  ...
  def destroy
+   sign_out
+   redirect_to root_path
  end
  ...
```
ここまでで、サインイン機能、サインアウト機能、ヘッダーの出しわけについてコーディングが完了しました。
ここで一度動作確認をしていきましょう。

# サインイン機能、サインアウト機能、ヘッダー出しわけの動作確認
## サインインしていない場合
まずサインインしていない状態で`http://localhost:3000`にアクセスしてみましょう。
すでにサインインしてしまっている場合は、ブラウザを閉じるか、「Sign out」リンクを押すかしてサインアウトしましょう。

この状態ではヘッダーは「Home」リンクと「Sign in」リンクが表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/29ef2173-edf4-d02e-6f0f-6504bfca10bb.png)

「Home」リンクを選択するとトップページへ、「Sign in」リンクを選択するとサインインページに遷移することも確認できますね。

## サインインしている場合
では、サインインページから`john@sample.com`でサインインしてみましょう。
すると、`john@sample.com`のユーザー詳細ページに遷移し、ヘッダーも「Profile」リンクと「Sign out」リンクに変わっていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/618eeab4-d913-aa55-3e0e-c9b8097d5a18.png)

また、「Profile」リンクを選択することで`john@sample.com`のユーザーのユーザー詳細ページに遷移できることも確認できますね。

## サインアウトを試してみる
最後にサインアウトが正しく動作するか確認してみましょう。
サインインしている状態で、「Sign out」リンクをクリックしてみてください。

トップページに遷移して、ヘッダーが未サインイン状態の場合のヘッダーに戻っていることが確認できるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/1ad81437-0820-e415-93e3-09951f5baf4c.png)

これで、想定どおりに動作していることが確認できましたね。

# サインアップした時もサインイン状態になるようにする
今のままではサインアップした後にサインインをしないといけなくなり面倒です。
サインアップ時（`users#create`）もサインイン状態になるように更新します。

```diff:app/controllers/users_controller.rb
  ...
  def create
    @user = User.new(user_params)
    if @user.save
+     sign_in @user
      flash[:success] = "サインアップありがとう！"
      redirect_to @user
    else
      render :new
    end
  end
  ...
```
`if @user.save`の下に`sign_in @user`を追加しました。
これでDB保存が成功した場合に、そのユーザーでサインイン状態になるようになりました。

では、新たにユーザーをサインアップページで作成して、サインイン状態でプロフィールページに遷移することを確認してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/5f1dac34-d727-4fbd-805b-b821b837c40b.png)
プロフィールページに遷移してますし、ヘッダーのリンクからサインイン後の状態になっていることがわかりますね。

# サインイン状態のときに遷移できないページを作る
また、サインインしたあとにサインアップページやサインインページに遷移することは必要ありませんね。（むしろ禁止したい。）
さらに、サインイン後はトップページにもアクセスする必要はなく、ルートパスにアクセスしようとした場合、プロフィールページ（サインインユーザーのユーザー詳細ページ）に遷移させるようにしたいかもしれません。

これらを実装してみましょう。

ApplicationControllerにサインインしている場合、強制的にプロフィールページに遷移させるメソッドを作成します。

```diff:app/controllers/application_controller.rb
  class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception
    include SessionsHelper
+
+   def redirect_to_profile_if_signed_in
+     redirect_to current_user if signed_in?
+   end
  end
```
これを、トップページ（`static_pages#home`）、サインアップページ（`users#new`, `users#create`）、サインインページ（`sessions#new`, `sessions#create`）にルーティングされた直後（アクションが処理される前）に実行されるようにします。
これは`before_action`メソッドを使えば容易です。

```diff:app/controllers/static_pages_controller.rb
  class StaticPagesController < ApplicationController
+   before_action :redirect_to_profile_if_signed_in
+
    def home
    end
  end
```

```diff:app/controllers/users_controller.rb
  class UsersController < ApplicationController
+   before_action :redirect_to_profile_if_signed_in, only: [:new, :create]
+
    def show
      @user = User.find(params[:id])
    end
    ...
  end
```

```diff:app/controllers/sessions_controller.rb
  class SessionsController < ApplicationController
+   before_action :redirect_to_profile_if_signed_in, only: [:new, :create]
+
    def new
      @user = User.new
    end 
    ...
  end
```
StaticPagesControllerは`home`アクションしかなかったので、コントローラー全体に`before_action`を適用しました。
UsersControllerとSessionsControllerは`only`オプションをつけて対象のアクションの実行前にのみ`redirect_to_profile_if_signed_in`メソッドが呼び出されるようにしました。

サインイン状態にして、「トップページ」「サインアップページ」「サインインページ」にそれぞれダイレクトアクセス（URL直打ち）してみてください。
全てプロフィールページにリダイレクトされます。

また、念のためサインアウトした場合は「トップページ」「サインアップページ」「サインインページ」にそれぞれアクセスできることも確認しておきましょう！


確認できましたね？
では、今日はここまでにしておきましょう！

# 後片付け
いつものように、次回に向けてデータを消します。

```
# exit
```

```
$ docker-compose down
$ docker-compose run --rm web rails db:migrate:reset
```
DBコンテナが立ち上がった状態だと思うのでdownさせます。

```
$ docker-compose down
```

# まとめ
今回はセッションを利用してユーザーのサインイン機能、サインアウト機能を作ってみました。
さらにヘッダーの出しわけや、特定のページアクセス時にサインインしている状態だとプロフィールページにリダイレクトされる機能を作ってみました。
すでにお気づきとは思いますが、今回作成した`redirect_to_profile_if_signed_in`メソッドと逆のことをすればサインインしていないと遷移できないページを作り出すことも可能ですし、条件を`signed_in?`ではないものにすれば、特定の条件のユーザー（例えば`admin`フラグを持っているユーザーとか）しかアクセスできないページを作り出すことも可能です。

それにしても、色々と動作確認することも増えてきましたね...
このまま毎回手で目で確認していくのもしんどそうです。

次回はこれを解決するためにTDD/BDD、そしてテスト自動化に取り組んでみます！

では、次回も乞うご期待！ここまでお読みいただきありがとうございました！

Next: [コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.10 - TDD & Test Automation - - Qiita](https://qiita.com/at-946/items/b774f1d7eac7d118c408)

# ここまでのコード
- [at946/rails_on_docker_handson at vol.09](https://github.com/at946/rails_on_docker_handson/tree/vol.09)

# Reference
- [Ruby on Rails チュートリアル：実例を使って Rails を学ぼう](https://railstutorial.jp/)
- [Ruby on Railsのparamsメソッドの使い方を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン](https://techacademy.jp/magazine/19878)
- [Ruby 当たり前のtrue/false - Qiita](https://qiita.com/shizuma/items/e6310d78d9c64ea5365c)
- [[WIP/初学者]flashとflash.nowの使い分け - Qiita](https://qiita.com/shimadama/items/ea433c337d2a691ff1bc)

# Other Hands-on Links
- https://github.com/at946/rails_on_docker_handson/tree/master