Railsチュートリアル 第8章

#セッション
HTTPはステートレス（状態を保持できない）なプロトコルです。
HTTPのリクエスト１つ１つは、それより前のリクエスト情報を利用することができません。
そのため、ブラウザのあるページから別のページに移動した時に、ユーザーのIDを保持しておく手段として、Webアプリケーションでは、セッションという半永続的な接続をコンピュータ間（ユーザーのパソコンのWebブラウザとrailsサーバなど）に別途設定します。

###実装方法
Rails でセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。
cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。
cookiesは、あるページから別のページに移動した時にもされないので、ここにユーザーID などの情報を保存できます。
アプリケーションはcookies内のデータを使って、例えばログイン中のユーザーが所有する情報をデータベースから取り出すことができます。
Rails では、sessionメソッドを使い、一時セッションを作成します。この一時セッションは、ブラウザを閉じると自動的に終了します。
また、第９章では、もっと長続きするセッションの作り方を学びます。

| HTTPリクエスト | URL | 名前付きルート | アクション名 | 用途 |
|:-:|:-:|:-:|:-:|:-:|
| GET  | /login  | login_path  | new  | 新しいセッションのページ（ログイン）  |
| POST  | /login  | login_path  | create  | 新しいセッションの作成（ログイン）  |
| DELETE  | /logout  | logout_path  | destroy  | セッションの削除（ログアウト）  |

#ログインフォーム
ユーザー登録フォームでform_forヘルパーを使ったときは、ユーザーのインスタンス変数@userを引数に取っていました。

```ruby
<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %>
```

上記のように書くだけで、「フォームのactionは/usersというURLへのPOSTリクエストである」と解釈してくれていました。
しかし、セッションにはSessionモデルというものがないので、@userのようなインスタンス変数に相当するものがありません。
そのため、ログインフォームでは下記のようにリソースの名前とそれに対応するURLを具体的に指定する必要がある。

```ruby
form_for(:session, url: login_path)
```

ログインフォーム

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

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= 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>
```

上記で生成したログインフォームのHTML

```html
<form accept-charset="UTF-8" action="/login" method="post">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input name="authenticity_token" type="hidden"
         value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
  <label for="session_email">Email</label>
  <input class="form-control" id="session_email"
         name="session[email]" type="text" />
  <label for="session_password">Password</label>
  <input id="session_password" name="session[password]"
         type="password" />
  <input class="btn btn-primary" name="commit" type="submit"
       value="Log in" />
</form>
```

フォーム送信後にparamsハッシュに入る値が、params[:session][:email]とparams[:session][:password]になることが推測できます。


#ユーザーの検索と認証

```ruby:sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      # エラーメッセージを作成する
      render 'new'
    end
  end

  def destroy
  end
end
```

上記では、送信されたメールアドレスを使い、データベースからユーザーを取り出しています。（ユーザー登録時にメールアドレスは全て小文字で登録しているので、確実にマッチさせるためにdowncaseメソッドを使っている）

find_byメソッドを使用しているので、ユーザー情報が存在する場合はユーザーオブジェクトが、存在しない場合はnilが返される。
Rubyではnilとfalse以外の全てのオブジェクトはtrueになる性質を考慮すると、&&の前後の組み合わせは下記の通りとなります。

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

| User          | Password     | a && b           |
|:-------------:|:------------:|:-----------------------------:|
| 存在しない      | 何でも良い     | (nil && [オブジェクト]) == false  |
| 有効なユーザー  | 誤ったパスワード  | (true && false) == false  |
| 有効なユーザー  | 正しいパスワード  | (true && true) == true  |

ユーザーが存在しない場合にはnilが返され、falseになるため、その時点でこのif文はfalseとなり、右の式は実行されないため、エラーにはならない。
これがもし左の式が存在しなかったら、エラーになってします。

#フラッシュ
flashとflash.nowの違い

###flash
次のアクションまで表示され、その次のアクションでは消える。
通常flashはリダイレクトを伴って利用するので、登録後にリダイレクトされた画面でメッセージが表示されており、次のアクションが実行された時点で消える。

###flash.now
次のアクションで消えます。
登録に失敗してrenderでテンプレートを表示した場合、次のアクションでは消えて欲しいので、flash.nowを使います。

#ログイン
同じログイン手法を様々な場所で使い回せるようにするために、Sessionヘルパーにlog_inというメソッドを定義する。

```ruby:sessions_helper.rb
module SessionsHelper

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

sessionメソッドで作成した一時cookieは自動的に暗号化されて保護される。
sessionメソッドで作成されあた一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了します。

log_inというメソッドを使い、ユーザのログインを行う。

```ruby:sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  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
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end
```

上記では、リダイレクトを使っていますが

```ruby
redirect_to user
```

これは、Rails では自動的に「user_url(user)」を変換しています。

#現在のユーザー
現在のログイン中のユーザー情報を返すメソッドを定義します。

```ruby:sessions_helper.rb
module SessionsHelper

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

  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
end
```

ユーザーIDが存在しない状態でfindメソッドを使うと例外が発生してしまうので、この場合はユーザーIDが存在しなかったら、nilを返してくれるfind_byメソッドを使います。

```ruby
@current_user = @current_user || User.find_by(id: session[:user_id])
@current_user ||= User.find_by(id: session[:user_id])　　　　　　　　　　⇦上記の短縮形
```

上記の意味は、@current_userに何も代入されていないときだけfind_byメソッドを呼び出しが実行されているので、既に@current_userにユーザ情報が代入されている場合はデータベースへの無駄な問い合わせが行われない。

#テスト
###fixture
テスト用のデータを作成できる。
###テストユーザー用意
fixture向けのdigestメソッドを追加する

```ruby:user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

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

有効なユーザーを表すfixtureを作成する

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

###有効な情報を使ってユーザーログインテスト

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

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' } }
    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)
  end
end
```

```ruby
＠user = users(:michael)
```

usersはfixtureのファイル名users.ymlを表し、:michaelというシンボルはユーザーを参照するためのキーを表します。

```ruby
assert_redirected_to user_url(@user.id)
assert_redirected_to @user　　　　　　　　　⇦短縮形
```

上記はリダイレクト先が正しいかをチェックしている。

```ruby
follow redirect!
```

指定のページに実際に移動する。

```ruby
assert_select "a[href=?]", login_path, count: 0
```

count: 0というオプションをassert_selectに追加すると、渡したパターンに一致するリンクが０件かどうかを確認している。


#ログアウト

```ruby:sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end
```