Ruby on Rails チュートリアル第8章をやってみて

#基本的なログイン機構
■第8章
基本的なログイン機構を構築していく。

##8.1 セッション
HTTPは状態のないプロトコル。HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない、独立したトランザクションとして扱われる。

ログインするとcreateでセッションを作成して保存。
ログアウトするとdestroyでセッションを破棄。

###8.1.1 Sessionsコントローラ
ログインとログアウトの要素を、Sessionsコントローラの特定のRESTアクションにそれぞれ対応付ける。

セッションコントローラの作成。

```rb
$ rails generate controller Sessions new
```

リソースを追加して標準的なRESTfulアクションをgetできるようにする。
以下のコードを追記。


```ruby:routes.rb
get    '/login',   to: 'sessions#new'
post   '/login',   to: 'sessions#create'
delete '/logout',  to: 'sessions#destroy'
```

Sessionsコントローラのテストで名前付きルートを使うようにする。

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

class SessionsControllerTest < ActionDispatch::IntegrationTest

  test "should get new" do
    get login_path
    assert_response :success
  end
end
```

###8.1.2 ログインフォーム
新しいセッションフォームを作成するときには、`form_for`ヘルパーに追加の情報を独自に渡さなければならない。
ログインフォームのコード

```ruby: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>
```

###8.1.3ユーザーの検索と認証
ログインでセッションを作成する場合、入力が無効な場合の処理を最初に行う。

最小限の`create`アクションをSessionsコントローラで定義し、空の`new`アクションと`destroy`アクションも作成する。

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

  def new
  end

  def create
    render 'new'
  end

  def destroy
  end
end
```

ユーザー認証に必要なあらゆる情報を`params`ハッシュから簡単に取り出せる。

Active Recordが提供する
`User.find_by`メソッドでデータベースからユーザーを探し、`has_secure_password`が提供する
`authenticate`メソッドでパスワードをチェックする。`authenticate`メソッドは認証に失敗したときにfalseを返す。

最終的にはこんな感じに。

```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
```


###8.1.4　フラッシュメッセージを表示する
セッションではActive Recordのモデルを使っていないので、ログインに失敗したときには代わりにフラッシュメッセージを表示する。

ここで書いたコードは誤りで、フラッシュメッセージが消えない状態になるので、一旦次へ。

###8.1.5 フラッシュのテスト
統合テストを作成する。

```rb
$ rails generate integration_test users_login
```

以下の流れでテストコードを実装していく

・ログイン用のパスを開く

・新しいセッションのフォームが正しく表示されたことを確認する

・わざと無効な`params`ハッシュを使ってセッション用パスにPOSTする

・新しいセッションのフォームが再度表示され、フラッシュメッセージが追加されることを確認する

・別のページ(Homeページなど)にいったん移動する

・移動先のページでフラッシュメッセージが表示されていないことを確認する

フラッシュメッセージの残留をキャッチするテスト。

```ruby: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`を`flash.now`に置き換える。後者はその後リクエストが発生したときに消滅する。

##8.2 ログイン
cookiesを使った一時セッションでユーザーをログインできるようにする。このcookiesは、ブラウザを閉じると自動的に有効期限が切れるものを使う。

ほんとはセッションの実装はめちゃくちゃ大変だけど、Rubyのモジュール機能使えば楽にできる。


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

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

ログインメソッド

```ruby:sessions_helper.rb
module SessionsHelper

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

上から3行目のコードで、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される。

```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
```

`create`アクションを完了し、ユーザーのプロフィールページにリダイレクトする準備ができた。

###8.2.2 現在のユーザー
ユーザーIDを一時セッションの中に安全に置けるようになったので、今度は、ユーザーIDを別のページで取り出すことにする。

`current_user`メソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。

ユーザーIDが存在しない状態で`find`を使うと例外が発生するので、`find_by`メソッドを使う。

```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
```

`current_user`メソッドが動作するようになったので、ユーザーがログインしているかどうかに応じてアプリケーションの動作を変更するための準備ができた。

###8.2.3 レイアウトリンクを変更する
ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまり`current_user`が`nil`ではないという状態を指すので、否定演算子`!`を使ってチェックをしていく。


```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

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end
end
```

###8.2.4 レイアウトの変更をテストする
以下の手順でテストを作成する。
・ログイン用のパスを書く

・セッション用パスに有効な情報をpostする

・ログイン用リンクが表示されなくなったことを確認する

・ログアウト用リンクが表示されていることを確認する

・プロフィール用リンクが表示されていることを確認する

上の変更の確認のため、テスト時に登録済みユーザーとしてログインしておく必要がある。

テスト用データをfixture (フィクスチャ) で作成できる。現時点のテストでは、ユーザーは1人いれば十分。

fixture向けのdigestメソッドを追加する。

以下のコードを追加。

```ruby: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
end
```

`digest`メソッドができたので、有効なユーザーを表す`fixture`を作成できるようになった。

ユーザーのログインで使うfixture。

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


このコードでテストユーザー用の有効なパスワードが作成できる。

```rb
<%= User.digest('password') %>
```

fixtureは生のパスワードを参照できないので、テスト用のfixtureでは全員同じパスワード「password」にする。

`
assert_redirected_to @user 
`

上のコードはリダイレクト先が正しいかどうかをチェックしている。

テストでも問題ないことが確認できた。

###8.2.5 ユーザー登録時にログイン
ユーザー登録中にログインを済ませておくことにする。Usersコントローラの`create`アクションに`log_in`を追加するだけで済むらしい。

##8.3 ログアウト
ログアウト機能を追加していく。ログアウト用リンクは作成済なので、ユーザーセッションを破棄するための有効なアクションをコントローラで作成する済む。

`log_out`メソッド。以下を追記。

```ruby:sessions_helper.rb
# 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
```

2行目のコードで、現在のユーザーを`nil`に設定できる。

ここで定義した`log_out`メソッドは、Sessionsコントローラの`destroy`アクションでも同様に使っていく。

以下を追記。

```ruby:sessions_controller.rb
# 現在のユーザーをログアウトする
def destroy
   log_out
   redirect_to root_url
end
```

テストの内容も弄って、無事テストGREENに。

##感想
ログイン・ログアウト機能の実装はバックエンドよりの作業なのかな？
HTTPだったりcookiesの話は応用情報の時にもちょっと触ったので懐かしいですね。
フワフワしたまま進んでいっていますが、頑張ります。