【第8章】Railsチュートリアル 5.1(第4版)　基本的なログイン機構

大まかな流れの自己整理が目的(コード外文章中のSession(s)の表記などはスルー状態)のため、内容そのものに不足・誤り等あれば追記&訂正していきますのでご指摘頂けますと幸いです:bow_tone1:
なお、筆者は[YassLabさんの動画版](https://railstutorial.jp/#screencast)で学んでいるため、本記事は「チュートリアル sample_app」+「他補足」~~個人的に「電子ページ以上に分かりやすい!」と感じた解説部分+参考記事を整理してみようと試みた劣化~~の内容寄りになってます。


#8.1 セッション
##セッション(Session)とは
ユーザがログイン後、ページ遷移しても再度ログインしなくてもいいように、「ログインしている」ことを記憶しておく機能のこと。
ページを移動しても変数の内容を保持する仕組みなので、ログイン機能以外でも利用可能。

###Sessionsリソース
「ログインしているかどうか？」が知りたく、いちいちDB書き換えは面倒...
→　今回はモデル(兼DB)は使わない!
Sessionやrailsサーバに一時的に情報を保持するやり方や、クライアント側で保持するCookieもあるので、今回はサーバ(に🔑)とブラウザに保存する。

railsサーバは「$rails server」　→　「ctrl c」で落ちてしまう...


参考
[【Rails入門説明書】sessionについて解説](https://web-camp.io/magazine/archives/18276)
[Railsのリソースとルーティングについて](http://stonebeach-dakar.hatenablog.com/entry/2016/07/14/222653)

　
まずはルーティング作成のため、トピックブランチにチェックアウト＆Sessionsコントローラを生成する。

```
$ git checkout -b basic-login
$ rails generate controller Sessions new
```

　
Usersリソースの時は専用のresourcesを使ってルーティングを自動的にフルセットで利用できるようにしたが、Sessionリソースではフルセットはいらないので、「名前付きルーティング」だけを使うためルーティングに追加する。

```ruby:routes.rb
get    '/login',   to: 'sessions#new'
post   '/login',   to: 'sessions#create'
delete '/logout',  to: 'sessions#destroy'
```
サーバ起動して、「/login」にアクセスするとnewアクションのview画面が出る。

![スクリーンショット 2020-01-05 14.35.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434223/623eecde-79b2-453c-82dd-57966e2cd356.png)

Sessionsコントローラのテストで名前付きルートを使うようにする( 「get sessions_new_url」　→　「get login_path」へ変更)。テストしてGREEN。

```ruby:sessions_controller_test.rb
require 'test_helper'

# 「::」 は 「ActionDispatch/IntegrationTest」 ディレクトリみたいなイメージ
class SessionsControllerTest < ActionDispatch::IntegrationTest
  test "should get new" do
    get login_path
    assert_response :success
  end

end
```

　

ログインフォーム
ユーザコントローラ(users_controller.rb)では、モデルがあって、それを@user：インスタンス変数に格納→それをform_forに渡す→newアクションテンプレートが動き出した。

```users_controller.rb
  def new
    @user = User.new
    # => form_for @user
  end
```

ただSessionではモデル作ってないからできない。
`POSTリクエストを「/login」に送って`createアクションが動くメソッドが欲しいので、sessionsのコントローラ、ビュー画面に下記を追加する。

```rb:sessions_controller.rb
class SessionsController < ApplicationController
  # GET /login
  def new
    # POST /login => create action
  end
  
  # POST /login
  def create
  end
end
```
　
なお、***form_forの引数***には**@user**を入れなくてもシンボル（`:session`）で対応可能であり、引数にurlを加えればok

```erb:app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <!--　↓↓↓　params[:session][:email]　-->
    <!--　↓↓↓　params[:session][:password]　-->
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>
```


参考
[POSTメソッド](http://e-words.jp/w/POST%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89.html)

　
![スクリーンショット 2020-01-05 16.56.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434223/6527b9eb-86ec-7453-fbf2-0fa8541213f4.png)

createメソッドに追記（今回はインスタンス変数@userでなくローカル変数userに情報を入れる）

```rb
def create
      #まずはUser情報が必要
    user = User.find_by(email:params[:session][:email])
      #=> User object or false
      # (【rubyの仕組み】falseとnil以外はtrue)
    if user.authenticate(params[:session][:password])
      # Sucess
    else
      # Failure (sessionモデルがないのでバリデーションが使えない)
      flash[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end
```

　　
ただ、この時点では「登録されていない(間違った)emailアドレスをパラメータに渡す（ユーザが入力する）と発生するエラー」がある。
find_byが見つからない、nil（nilオブジェクト:authenticateが適応されてない） がuserに入ってauthenticateメソッド　→　NoMethodError発生（メソッドが見つからない）

![スクリーンショット 2020-01-05 17.51.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434223/1287eb2e-f6ea-b31d-459f-36252cbb5385.png)

ここで真っ先に思いつく~~（筆者のかろじての発想）~~のが「if user == nil...」などの書き方だが、
Rubyの特徴としてuserにユーザオブジェクトが入ればtrue、`nilが入ればfalse`を利用すれば、
&&(ex. a && b 意：「aがtrueでかつ、bもtrueだったらtrue結果、どちらかfalseだったらfalse結果」 )を使って下記に表現できる。
ちなみに、rubyに限らずコンピュータ言語の&&では、左側(今回のuser)がfalseの場合は右側(user.authenticate)は判断してもしなくても必然的にfalseになるので機械側が判断せず省略する（処理を止める）。

```ruby:
if user && user.authenticate(params[:session][:password])
```


参考
[演算子式(Ruby 2.7.0 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html)

　
ここで小さなバグとして、エラーメッセージのflashの表示時間が長い（思ったより生き延びている..?)問題がある。

![スクリーンショット 2020-01-05 20.23.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434223/9e840f68-741a-4fb9-2dba-c974759d8149.png)


原因として、flashの生存期間としては、次のリクエストが来るまで
前回がredirectに対して今回はrender（リクエストを発行するのでなく、「このテンプレートを描画してね」）、つまり、失敗してflash登場後に次のリクエストが来るまで残り続ける
①表示（0リウエスト目）、②リロード（1リクエスト目）、③リロード（2リクエスト目）

すぐに直さず回帰バグを防ぐため、統合テストを行う。

```
$ rails generate integration_test users_login
```

```users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
    test "login with invalid information" do
    get login_path
    assert_template 'sessions/new'
    post login_path, params: { session: { email: "", password: "" } }
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end
end
```
　
ログインに失敗したときのテスト（意図的にログインを失敗させたときの動作）flashのバグも再現させる
1.login_pathにgetリクエストを送る
2.sessionsのnewテンプレートが呼び出される
3.login_pathに失敗させる空のパラメータ(params)を送る
4.(たぶん失敗してるので)またnewテンプレート再描画
5.invalid~のflashが出てるか？
6.その後さらにtopページにリクエスト
7.その時にflashがないか？（消えてるか？）
　

この時点ではrailsテストは失敗（RED：正しい）。
直すにはcreateメソッドのflashにメソッドとしてnowを追記する。
　

```rb

# ユーザーログイン後にユーザー情報のページにリダイレクトする
flash.now[:danger] = 'Invalid email/password combination'
```

　　
これにてテスト通過(GREEN)。
　　
#8.2 ログイン
ヘッドの部分を下記のように変更する。
ログインしてる時　　→　ログアウト（の選択項目）表示
ログアウトしてる時 　→　ログイン（の選択項目）表示

##Sessionsメソッド

Sessionsという特殊な変数(railsの機能)を使い、サーバ側に一時的に情報を保存する。
Sessionsのデータはviewとcontrollerで利用できる。
Sessionsが切れるまでの保存期間はRailsサーバが落ちるまで＋ブラウザが生き残ってる（ex.ログイン後、タブやChromeを閉じる前）まで。
　

```rb
session[:user_id] = user.id
```

＜参考＞
[【Rails】Sessionの使い方について](https://qiita.com/ozackiee/items/4ee774c81b2a0c571c05)
[Session管理とRailsのcookie store](https://www.slideshare.net/carotene4035/sessionrailscookie-store)
　
log_inメソッドと組み合わせる

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

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
end
```

　　　

Applicationコントローラのcreateメソッドに「log_in」、newへのリダイレクトを加える。

```rb

      log_in user
      redirect_to user
```

　

ユーザがProfileページへ飛ぶ時、今ログインしているユーザが誰か？(ちゃんと別の人でなく自分のユーザ情報か？)の情報を持ってこないことにははじまらない。
　→　Session情報からユーザ情報をもってくるようsessionヘルパーを編集する。

　　

ユーザーIDを一時セッションの中に安全に置けるようになったので、今度はそのユーザーIDを別のページで取り出す。
　→　current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。


```app/helpers/sessions_helper.rb
  def current_user
    # （元祖）current_user = User.find(session[:user_id])
    # view側でも引き出せるようにインスタンス変数にする
    # （改訂１）@current_user = User.find(session[:user_id])
    # findは失敗したらエラーを返すログイン中にsessionが切れる可能性はあるので、
    # 失敗しても「nil」を返すfind_byを使う。
    # （改訂２）@current_user = User.find_by(id: session[:user_id])
    # @current_user = User.find_by(id: session[:user_id])
    
    # if @current_user.nil?
    #   @current_user = User.find_by(id: session[:user_id])
    # else
    #   @current_user
    # end
    
    # 「or」演算子を使いわずか１行で
    # @current_user = @current_user || User.find_by(id: session[:user_id])
    
    # さらにRubyっぽくして完成（よく使われる形らしい）
    @current_user ||= User.find_by(id: session[:user_id])
    
  end
```
　
セッションにユーザーIDが存在しない場合、このコードは単に終了して自動的にnilを返す（何度もDB問い合わせなく処理が早い）。

レイアウトのリンクを変更する方法として、ヘルパーにメソッドを追加する。

```sessions_helper.rb
 # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end
```
　　
ヘルパーヘッダーのhtml記入。setting（設定）はダミーリンクに。
link_to はデフォルトの振る舞いとして、引数にユーザオブジェクト(cuurent_user)が加わったらユーザのProfieページに飛ばす。
Log outでdeleteのリクエストを送る。

```erb
<header class="navbar navbar-fixed-top navbar-inverse">
      <div class="container">
        <%= link_to "sample app", root_path, id: "logo" %>
        <nav>
          <ul class="nav navbar-nav navbar-right">
            <li><%= link_to "Home",   root_path %></li>
            <li><%= link_to "Help",   help_path %></li>
            <!--元<li><%= link_to "Log in", '#' %></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 %>
          </ul>
        </nav>
      </div>
    </header>
```

###  補足(エラー)

ここでログインしようとしてエラー
sessionsコントローラでlog_inメソッドが見つからない。

![スクリーンショット 2020-01-09 17.34.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434223/c212ac74-0c54-4f5e-6006-c0d61ba0c5ad.png)


ヘルパーはもともとviewで使われる。ただ、今回はコントローラでヘルパーを使いたいので、module（ヘルパー側）と対応するincludeを追記する。

```sessions_controller.rb
class SessionsController < ApplicationController
  #moduleと対応させる
  include SessionsHelper

# 省略

```

　　
Bootstrapの機能を使うため、jsに追加する。

```app/assets/javascripts/application.js
//= require jquery
//= require bootstrap
```


もう一度リロードするとログイン状態になり、右上の項目が変わる(プルダウンの起動もok)。

![スクリーンショット 2020-01-09 18.06.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434223/0eabcef4-405b-a5e4-089d-ae7153201c19.png)


###レイアウトの変更をテストする
fixture(テストを実行、成功させるための状態や前提条件の集合)をYAMLで用意する。

##YAMLファイル
YAMLファイルとは、
`構造化されたデータを表現するためのデータ形式のひとつ`(HTML,XMLなどいろんなファイルの書き方のひとつ)。
配列(先頭に「-」)、ハッシュ(キー：値)、スカラー(文字列、数値、真偽知など)で構成される。

＜ 主な用途 ＞
・各種設定ファイル
・データ保存用 (シリアライゼーション)
・データ交換用フォーマット
・ログファイル


＜参考＞
[YAMLとは｜「分かりそう」で「分からない」でも「分かった」気に ...](https://wa3.i-3-i.info/word13801.html)
[プログラマーのための YAML 入門 (初級編)](https://magazine.rubyist.net/articles/0009/0009-YAML.html)
[【Ruby入門】YAMLの使い方をわかりやすく解説！](https://www.sejuku.net/blog/20524)


　
name,email,password_digest(あとでBCryptでハッシュ化)を含むYAMLを作成する。ラベルはmichael

```test/fixtures/users.yml

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
```


　
3項演算子の作りとしては、
nilかfalseであれば（「：」から）下側のcost（精査）が、
「?」の左側の「min_cost」がそれ以外であれば上側の 「MIN_COST（簡易チェック）」が選択される。


```app/models/user.rb
# 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end
```

　
統合テストに成功してたときのメソッド・テスト文を追加する。

```test/integration/users_login_test.rb

class UsersLoginTest < ActionDispatch::IntegrationTest
    
#成功した時のメソッド（上部に追加）
def setup
    @user = users(:michael)
end

省略

 test "login with valid information" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    # @userにリダイレクトされているかチェックしてされれば以下が動く
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    # count:0は「そのリンクが存在しないよね？」のチェックができる
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
  end
```

先ほど作ったfixtureのサンプルデータにはラベルにmichaelを入れたので呼び出しができる、

さらにsignup（ユーザ登録）し終えたらログインの過程はスキップしていいので、ユーザコントローラのリファクタリングとして下記を追記する。
① createアクションに保存成功時log_inへ
② ヘルパーから使うのでincludeを冒頭に

```app/controllers/users_controller.rb
class UsersController < ApplicationController
  include SessionsHelper  #=> ヘルパー連動

省略

 def create
    @user = User.new(user_params)
      if @user.save
      log_in @user #=> ユーザー登録時にログイン
      flash[:success] = "Welcome to the Sample App!"
```
　
ただ、このタイミングで **usersコントローラとsessionsコントローラの両方で`ApplicationController`から継承されている**（下記）ので *DRY* に

```rb
 < ApplicationController
```
双方のincludeを消してapplicationコントローラのみincludeを追加する。

```application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper

省略
```

テストのヘルパーメソッド（追加）

```test/test_helper.rb
 # テストユーザーがログイン中の場合にtrueを返す
  def is_logged_in?
    !session[:user_id].nil?
  end
```
　
assert(バグが無い様に条件式を埋め込んで、明示すること)追加。

```test/integration/users_signup_test.rb
#showテンプレート表示後
assert is_logged_in? #=> signup 終えた人はログインも終わってるか？
```

#8.3 ログアウト

ユーザが「Log out」をクリックしたらDELETEアクションが反応するようにする。

＜大まかな流れ>
1.セッションからユーザーIDを削除(ユーザのログアウト)
2.ヘルパーにlog_outメソッド、sessionsコントローラにdeleteメソッドの実装
3.統合テスト

```app/helpers/sessions_helper.rb
# 現在のユーザーをログアウトする
  def log_out
    # キーを指定すると、キーに該当するバリュー(今回はユーザid)を削除してくれる
    session.delete(:user_id)
    @current_user = nil
  end
```
　
Sessionsコントローラにdestroyアクション追加（DELETEリクエストがlog_outアクションにきたら動く、rootにリダイレクト）。

```app/controllers/sessions_controller.rb
  # DELETE /logout
  def destroy
    log_out
    redirect_to root_url
  end
```

この時点でページに行き「Log out」をクリックするとホームに戻ってる。

ログアウトのテストも記述するが、ログインのテストの末尾に追記していく。

```test/integration/users_login_test.rb
# 分かるよう下記""の末尾に「 followed by logout（ログアウトもしてるよ）」追加
   test "login with valid information followed by logout" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    # ログインしてるよね？追記（なくてもok）
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    
          # ~~ 下記すべてログアウトテスト分  ~~
    # deleteリクエストをlogout_pathに送りつける
    delete logout_path
    # sessions情報が消えるので、ログインしてないですよね？
    assert_not is_logged_in?
    assert_redirected_to root_url
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path,      count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
 
  end
```

一通り終了！！

herokuへデプロイして、ログアウト動作の確認。

```
$ git checkout master
$ git merge basic-login
$ git push heroku master
```


![スクリーンショット 2020-01-10 19.45.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434223/310f4699-cc9b-f512-1b79-c8531c95ce11.png)

「Log out」クリックして、

![スクリーンショット 2020-01-10 19.49.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434223/a2d9c51a-aa59-554b-f021-4e9496bc5764.png)

無事ホーム（root）に戻ったのでok!