Rails初学者によるRailsチュートリアル学習記録⑩ 第8章

#目次
- [1. はじめに](#1-はじめに)
- [2. 第8章の概要](#2-第8章の概要)
- [3. 学習内容](#3-学習内容)
- [4. 終わりに](#4-終わりに)

#1. はじめに
- この記事は、Rails初学者の工業大学三年生がRailsチュートリアルの学習記録を<br>つけるための記事です。
- 筆者自体がRailsやWebについて知識が少ないので、内容の解釈などに<br>間違いがある可能性があります。(その時はコメントで指摘してくださると助かります！)
- Railsチュートリアル内ではRailsの内容以外にも、gitでのバージョン管理やHerokuを使ったデプロイも<br>学習しますが、gitに関しては既に私が学習済みのため学習記録には記述しません。
- 演習の記録も省略します。

#2. 第8章の概要
前回の章でユーザー登録が可能になったので、この章ではユーザーがログイン・ログアウトを行えるようにします。
ここで実装するログインの仕組みは、ユーザーがログインしているという状態をブラウザが保持し、
ブラウザが閉じられたときにその状態を破棄するという認証システムです。
他にも、ログインしているかしていないかによって、ヘッダーの内容を変えるという認可モデルの一部を実装していきます。


1. セッションについて
    1. セッションとは
    1. セッションによってログイン機能を実現する
1. ログインフォームの作成
    1. フォームの処理
    1. ログイン機能の実装
    1. フラッシュによるエラーの表示
    1. ログアウト機能の実装
1. ログインしている時としていない時でヘッダーの内容を変える

#3. 学習内容
###1. セッションについて



####1-1. セッションとは
セッションとはブラウザ上でユーザーが入力した情報などを、他のページに移動したときに保持しておける通信のことです。
普段利用しているHTTPプロトコルは「ステートレス」なプロトコルであり、
リクエスト1つ1つがそれ以前のリクエストの情報を全く利用しないという性質があります。

実装するログイン機能は、ユーザーのIDをブラウザが保持する必要があります。
よって、ログイン機能にはセッションを利用した通信が必要です。

Railsでセッションを実装する一般的な方法はcookiesを使う方法です。
この章では<strong>sessionメソッド</strong>で一時セッションを作成します。
この一時セッションは、ブラウザが閉じられたときに自動的に終了するものです。
半永続的に続くセッションは9章で<strong>cookiesメソッド</strong>を使用して作成します。



####1-2. セッションによってログイン機能を実現する
ログイン機能を実装するために、コマンドを使用してSessionsコントローラーを作成します。
ログイン・ログアウトの処理はこのコントローラーのRESTアクションに対応付けます。
具体的には以下の表のようにアクションと処理の対応付けを行います。

| 実装する処理 | 対応するアクション |
|:-|:-:|
| ログインフォーム  | new  |
| ログイン  |  create |
|  ログアウト | delete  |

この時点で、この3つのアクションのルーティングを行います。



###2. ログインフォームの作成
####2-1. フォームの処理
ルーティングが設定できたらログインフォームから作成していくのですが、
<a href="https://qiita.com/UtimoriNI/items/16b3fd4c0e5ea7c95f87">前回(第7章)</a>で作成した、ユーザー登録用のフォームとほとんど同じなので、
ここでは前回作成したフォームとの違いを記述します。

①入力フィールドの数
ユーザー登録時には名前、メールアドレス、パスワード、パスワードの確認の4つの入力フィールドが
あったのに対して、ログインにはメールアドレスとパスワードの2つに減っています。
![登録.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285050/5d412cab-b2fa-9e33-3771-eb2763c4d97a.png)
![ログイン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285050/ea108f49-462a-768c-619d-a524c85e9179.png)

②エラーメッセージの表示方法
ユーザー登録時に入力した情報が、文字数などの条件を満たしていなかったときは、エラーメッセージを表示しました。
今回は入力した情報に誤りがあった場合にエラーメッセージを表示させたいのですが、その時に使用する方法が前回と異なります。

前回はユーザー登録時にUserモデルを使用して@userというオブジェクトを生成しており、
そのオブジェクトに値を渡していました。
この場合、モデルの属性に文字数などの条件が設定されており、それを満たしていない場合はActive Recordによって
エラーメッセージが生成されていました。

今回はSessionモデルというものを使用していないため、Active Recordによるエラーメッセージの生成が行われません。
よって、フラッシュメッセージによってエラーを表示します。

③form_withヘルパーに渡す情報
先述したSessionモデルを使用していないという違いは、form_withヘルパーを使用する場面でも影響してきます。
ユーザー登録フォームでは以下のようにform_withヘルパーを使用していました。
`<%= form_with(model: @user, local: true) do |f| %>`
Railsでは上記のように書くとフォームは「/usersというURLへのPOSTリクエスト」と自動で判定します。

しかし、モデルを使用してない場合はこのように書くことができないので、
ログインフォームでは以下のようにフォームの送信先のURLを指定する必要があります。
`<%= form_with(url: login_path, scope: :session, local: true) do |f| %>`

また、socpeに指定されている:sessionというシンボルは、
フォームの値を渡す際に使用されるハッシュの名前になります。



####2-2. ログイン機能の実装
ログイン機能の処理は以下のように行われます。
①ユーザーがメールアドレスとパスワードを入力する
↓
②メールアドレスを受け取って該当するユーザーを検索する
↓
③検索で取り出されたユーザーのパスワードと入力されたパスワードが一致するかを判断する
↓
④パスワードが正しければユーザーを使用してセッションを作成する
パスワードが間違っていた場合、ログインフォームを再表示する

①を行うためのログインフォームはこれまでで実装できたので、②以降の処理を実装していきます。
②の処理は、メールアドレスを使用したユーザーの検索なので、<strong>find_byメソッド</strong>で実装できます。
入力されたメールアドレスはユーザー登録フォームの時のように、入れ子になったハッシュとして渡されます。

form_withヘルパーで指定した`scope: :session`から入力された情報はsessionというハッシュに入ることが分かります。
このハッシュは以下のように表すことができます。
session: { passsword: "入力されたパスワード" email: "入力されたメールアドレス" }
そしてこのハッシュはparamsというハッシュの中に入っているので下のように表します。
`params[:session]`
よってデータには以下のように書いてアクセスします。
`params[:session][:email]`

以上のことから検索の処理は
`user = User.find_by(email: params[:session][:email].downcase)`のように書きます。

③は検索したユーザーのパスワードと入力されたパスワードが一致しているかを調べるため、
<strong>authenticateメソッド</strong>を使用します。
このメソッドでは引数に渡した値がユーザーのパスワードと一致したらtrueを返し、一致しなければfalseを返すので
`if user && user.authenticate(params[:session][:password])`という条件式を作れます。
この条件式でなぜuserを使用しているかというと、その前のユーザーの検索でユーザーが存在したかどうかを確かめるためです。

ようやく④でログイン処理が登場します。
とはいってもコード自体は短く、`session[:user_id] = user.id`という一行で済んでしまいます。
このログイン処理はSessionヘルパーにlog_inという名前のメソッドとして定義しておくと便利です。
パスワードが誤っていた時のページの再表示はrenderメソッドで実装できます。

ここまでの②から④までをまとめるとcreateアクションは以下のようになります

```ruby:app/controlles/sessions_controller.rb
def create
    user = User.find_by(email: params[:session][:email].downcase)
  if user && user.authenticate(params[:session][:password])
    log_in user
    redirect_to user
  else
    render 'new'
  end
end
```

####2-3. フラッシュによるエラーの表示
フラッシュは、前回ウェルカムメッセージを表示するために使用しました。
その時のコードが↓です

```ruby:app/controlles/users_controller.rb
  def create
    @user = User.new(user_params)
    if @user.save
      # 保存に成功したときの処理
      log_in @user
      flash[:success] = "Welcome to the Sample App!" # フラッシュを使用したウェルカムメッセージ
      redirect_to @user
    else
      # 保存が失敗したときの処理
      render 'new'
    end
  end
```
今回もこのようにflashハッシュにメッセージを代入するのですが、前回のコードに倣って先ほどのログイン機能のコードに
`flash[:danger] = "メールアドレスとパスワードの組み合わせに誤りがあります"`
と記述すると正しく動作しません。
具体的にどのような不具合が発生するかというと、フラッシュメッセージが表示されたまま消えなくなってしまいます。
フラッシュメッセージは別のページに遷移したり、リロードするはずなのですがそうならないのです。

原因を探すためにユーザー登録機能のコードとログイン機能のコードを比較しましょう。

```ruby:ユーザー登録機能のcreateアクションの一部
if @user.save
      # 保存に成功したときの処理
      log_in @user
      flash[:success] = "Welcome to the Sample App!" # フラッシュを使用したウェルカムメッセージ
      redirect_to @user
```

```ruby:ログイン機能のcreateアクションの一部
if user && user.authenticate(params[:session][:password])
    log_in user
    redirect_to user
  else
    flash[:danger] = "メールアドレスとパスワードの組み合わせに誤りがあります"
    render 'new'
  end
```

上記の2つのコードを比較するとflashハッシュに値を代入した後のビューの表示方法が違うことが分かります。
ユーザー登録機能は<strong>redirect_toメソッド</strong>を使用しているのに対して、ログイン機能では<strong>renderメソッド</strong>を使用しています。
これが先ほどの不具合の原因でrenderメソッドはリクエストとみなされないため、リクエスト時のメッセージがきえないのです。
この不具合を解消するには`flash.now[:danger:]`と書いて値を代入します。
flash.nowによってその後のリクエストが発生したときにメッセージが消えるようになります。



####2-4. ログアウト機能の実装
ログアウト機能はSessionsコントローラのdestroyアクションで実装します。
ログアウト処理はセッションからユーザーIDを削除します。
そのために`session.delete(:user_id)`と記述します。
この処理もログインと同じように使いまわすためにlog_outメソッドとしてsessions_helper.rbに定義しておきます。



###2. ログインしている時としていない時でヘッダーの内容を変える
ここからはユーザーがログインしている時としていない時でヘッダーの内容を変えていきます。
この機能を実現するには、現在ユーザーがログイン中かどうかを判別するメソッドが必要です。

そのメソッドの前に、現在ログイン中のユーザーの情報を扱えるようにするメソッドを説明します。
そのメソッド名はcurrent_userとして、以下のように書きます。

```ruby:app/helpers/sessions_helper.rb
# 現在ログイン中のユーザーを返す（いる場合）
def current_user
  if session[:user_id]
    @current_user ||= User.find_by(id: session[:user_id])
  end
end
```
このメソッドでは 「||=」 というRuby特有の演算子を使用しています。
上記のコードの条件式を書き直すと、
`@current_user = @current_user || User.find_by(id: session[:user_id])`となります。
意味としては@current_userが存在するときはそのままの値、存在しないときはユーザーを検索して代入するという意味です。
このメソッドにより、現在ログイン中のユーザーを@current_userというインスタンス変数で扱うことができます。

上記のメソッドが定義できたら、本題のユーザーがログイン中かどうかを判別するメソッドを定義します。
そのメソッドはlogged_in?メソッドという名前で以下のように書きます。

```ruby:app/helpers/sessions_helper.rb
# ユーザーがログインしていれば true、その他なら false を返す
def logged_in?
  !current_user.nil?
end
```
このメソッドは単純で、current_userがnilの時はfalse、それ以外はtrueを返します。
このメソッドによってユーザーがログインしているかを判別できるようになったので、
ヘッダーの内容をログイン状態に応じて変更できるようになりました。

以下に変更後のヘッダーの変更部分を示しておきます。

```html:app/views/layouts/_header.html.erb
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
  <li><%= link_to "Users", '#' %></li>
  <li class="dropdown">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown">
      Account <b class="caret"></b>
    </a>
<ul class="dropdown-menu">
  <li><%= link_to "Profile", current_user %></li>
  <li><%= link_to "Settings", '#' %></li>
  <li class="divider"></li>
  <li>
    <%= link_to "Log out", logout_path, method: :delete %>
  </li>
</ul>
</li>
<% else %>
  <li><%= link_to "Log in", login_path %></li>
<% end %>
```



#4. 終わりに
この章はログイン機能を作成してユーザー登録したユーザーがアプリケーションを使えるようになりました。
Railsでは今回実装したような認証機能を簡単に実装できるgemがありますが、
Railsチュートリアルではそのようなgemを使えば簡単に実装できる機能を1から自分で実装するので、
その分機能の仕組みがよく理解できていると思います。
内容が徐々に難しくなってきていますが、今のところ1週間に2章分というペースを維持できているので、
できる限りこのペースのまますすめていこうと思います。