前回はユーザー登録できるようにしたので今回からログイン・ログアウトをやっていく
ログインにはセッションという半永続的な接続をコンピュータ間(ユーザーのパソコンのWebブラウザとRailsサーバーなど)に別途設定する。そのためにRailsではcookiesを使います。
cookiesはあるページから別のページに移動した時にも破棄されないので、ここにユーザーIDなどの情報を保存できます。アプリケーションはcookies内のデータを使って、例えばログイン中のユーザーが所有する情報をデータベースから取り出すことができるので便利。
そのためにまずはログイン・ログアウトの要素は別のコントローラーにしたいので今回はSessionsコントローラーを作る。
Usersのときはルーティングを自動的に全部のルート使えるようにresoucesを使ったが今回は基本ログインページとあとログイン、ログアウトだけでいいので個々で設定してあげる。
まずはログインフォームだが見た目はユーザー登録とそんなに変わらない。入力させる部分はemailとpasswordだけなのでむしろ楽なのかもしれない。ただ違う点はユーザー登録はフォームの送信をUserモデルに送信してたがログインはモデルに送信するのではなくあくまでセッションに送信するのでそこの送信先を変えてやる必要がある。
<% provide(:title, "Log in") %>
<h1>Log in
<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>
これで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
これでテストがパスすればログアウトも出来上がりです。
けっこうセッションとか便利で使い勝手のいいものですね。
次回は今だとページ閉じるとセッションが失われるので永続的にできるようにしていきます。