Railsチュートリアル第8章で学んだこと

前回はユーザー登録できるようにしたので今回からログイン・ログアウトをやっていく

ログインにはセッションという半永続的な接続をコンピュータ間（ユーザーのパソコンのWebブラウザとRailsサーバーなど）に別途設定する。そのためにRailsではcookiesを使います。
cookiesはあるページから別のページに移動した時にも破棄されないので、ここにユーザーIDなどの情報を保存できます。アプリケーションはcookies内のデータを使って、例えばログイン中のユーザーが所有する情報をデータベースから取り出すことができるので便利。

そのためにまずはログイン・ログアウトの要素は別のコントローラーにしたいので今回はSessionsコントローラーを作る。

Usersのときはルーティングを自動的に全部のルート使えるようにresoucesを使ったが今回は基本ログインページとあとログイン、ログアウトだけでいいので個々で設定してあげる。

まずはログインフォームだが見た目はユーザー登録とそんなに変わらない。入力させる部分はemailとpasswordだけなのでむしろ楽なのかもしれない。ただ違う点はユーザー登録はフォームの送信をUserモデルに送信してたがログインはモデルに送信するのではなくあくまでセッションに送信するのでそこの送信先を変えてやる必要がある。

＜% provide(:title, "Log in") %>
＜h1>Log in</h1>
＜div class="row">
  ＜div class="col-md-6 col-md-offset-3">
    ＜%= form_with(url: login_path, scope: :session, local: true) 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>

これでparamsハッシュに入る値がparams[:session][:email]とparams[:session][:password]が送信されるようになる。

まずは入力された値でまずはemailから探し出します

user = User.find_by(email: params[:session][:email].downcase)

その後にhas_secure_passwordのおかげでauthenticateメソッドが使えるのでその機能で認証に失敗したらfalseを返すことを使います。

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

また前回はモデルに送信したおかげでエラーメッセージのメソッドが使えたが今回はモデルに送信してるわけではないので自分で作らないといけない。

flash[:danger] = 'Invalid email/password combination'

ここでいったんフラッシュがちゃんと動いてるかのテストを書く

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

に書き換えます。
flash.nowのメッセージはその後リクエストが発生したときに消滅します。

次はいよいよログイン成功の部分を作っていきます。

まずは今後ログインのヘルパーや認証のヘルパーもいろんな場面で使いたいので

コントローラーの親クラスのApplicationコントローラにSessionsHelperを呼び出せるようにします。

class ApplicationController < ActionController::Base
  include SessionsHelper
end

これで様々なコントローラでSessionsヘルパーに設定したものが使えるようになります。

ログインのメソッドを作ります。

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

ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成されます。この後のページで、session[:user_id]を使ってユーザーIDを元通りに取り出すことができます。

これでログインのメソッドができたので

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

これでとりあえずはログインはできました。
しかし今のままではほんとにログインできたかわからないので次は今ログインしてる情報を使って表示などを変えます。そのためにまずはcurrent_userメソッドを作ります。

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

これでリンク部分を分岐させることができます。

ついでにちゃんとリンクがログイン前と後で変わってるかのテストを書きます

その前にテストのためようのuserを作りたいのでそのために事前にUserモデルに渡された文字のハッシュ化をするメソッドを作っときます。

その後テスト用のfixtureに任意のnameなどを設定します。こうすることで、テストで使うことができます。

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

このテストはログインしてリダイレクト先がただしいかテストしてそのあとに実際に移動します。
そしてリンク先の表示が正しいかをassert_selectでテストします。

また今のままだとログインのときのパスワードを認証する部分をコメントアウトしてもテストにパスしてしまいます。それはemailは正しいのにパスワード間違ってたら通らないというテストを書いてないからです。なのでそのテストを書きます。

test "login with valid email/invalid password" do
    get login_path
    assert_template 'sessions/new'
    post login_path, params: { session: { email: @user.email,
                                          password: "invalid" } }
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end

これでコメントアウトしてるとREDなので戻すとちゃんとGREENになりました。

また、いまのままだとユーザーが登録したときにはログインできてないことになるので登録のときにもログインできるようにします。


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

また登録のときにちゃんとログインできたかわかんないのでテストにも追加してあげます。

そのためにテストに別途ヘルパーを追加します。これはテストのヘルパーではcurrent_userなどのヘルパーが呼び出せないからです。ただsessionメソッドは使えるのでそれを使ってヘルパーを作成します。またSessionsヘルパーのメソッドとごっちゃにならないように別名をつけるのが良き。

# テストユーザーがログイン中の場合にtrueを返す
  def is_logged_in?
    !session[:user_id].nil?
  end

これを使って登録のときのテストに追加します。

test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!
    assert_template 'users/show'
    assert is_logged_in?
  end

あとはログアウトも同じように作っていきます。そのためにログアウトメソッドを作ります。

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

このメソッドを使うと

 def destroy
    log_out
    redirect_to root_url
  end

あとはログアウトのテストも書きます。

 test "login with valid information followed by logout" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    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
    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

これでテストがパスすればログアウトも出来上がりです。

けっこうセッションとか便利で使い勝手のいいものですね。

次回は今だとページ閉じるとセッションが失われるので永続的にできるようにしていきます。