大まかな流れの自己整理が目的(コード外文章中のSession(s)の表記などはスルー状態)のため、内容そのものに不足・誤り等あれば追記&訂正していきますのでご指摘頂けますと幸いです
なお、筆者はYassLabさんの動画版で学んでいるため、本記事は「チュートリアル sample_app」+「他補足」個人的に「電子ページ以上に分かりやすい!」と感じた解説部分+参考記事を整理してみようと試みた劣化の内容寄りになってます。
8.1 セッション
セッション(Session)とは
ユーザがログイン後、ページ遷移しても再度ログインしなくてもいいように、「ログインしている」ことを記憶しておく機能のこと。
ページを移動しても変数の内容を保持する仕組みなので、ログイン機能以外でも利用可能。
Sessionsリソース
「ログインしているかどうか?」が知りたく、いちいちDB書き換えは面倒...
→ 今回はモデル(兼DB)は使わない!
Sessionやrailsサーバに一時的に情報を保持するやり方や、クライアント側で保持するCookieもあるので、今回はサーバ(に🔑)とブラウザに保存する。
railsサーバは「$rails server」 → 「ctrl c」で落ちてしまう...
参考
【Rails入門説明書】sessionについて解説
Railsのリソースとルーティングについて
まずはルーティング作成のため、トピックブランチにチェックアウト&Sessionsコントローラを生成する。
$ git checkout -b basic-login
$ rails generate controller Sessions new
Usersリソースの時は専用のresourcesを使ってルーティングを自動的にフルセットで利用できるようにしたが、Sessionリソースではフルセットはいらないので、「名前付きルーティング」だけを使うためルーティングに追加する。
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
サーバ起動して、「/login」にアクセスするとnewアクションのview画面が出る。
Sessionsコントローラのテストで名前付きルートを使うようにする( 「get sessions_new_url」 → 「get login_path」へ変更)。テストしてGREEN。
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アクションテンプレートが動き出した。
def new
@user = User.new
# => form_for @user
end
ただSessionではモデル作ってないからできない。
POSTリクエストを「/login」に送って
createアクションが動くメソッドが欲しいので、sessionsのコントローラ、ビュー画面に下記を追加する。
class SessionsController < ApplicationController
# GET /login
def new
# POST /login => create action
end
# POST /login
def create
end
end
なお、form_forの引数には@userを入れなくてもシンボル(:session
)で対応可能であり、引数にurlを加えればok
<% 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メソッド
createメソッドに追記(今回はインスタンス変数@userでなくローカル変数userに情報を入れる)
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発生(メソッドが見つからない)
ここで真っ先に思いつく(筆者のかろじての発想)のが「if user == nil...」などの書き方だが、
Rubyの特徴としてuserにユーザオブジェクトが入ればtrue、nilが入ればfalse
を利用すれば、
&&(ex. a && b 意:「aがtrueでかつ、bもtrueだったらtrue結果、どちらかfalseだったらfalse結果」 )を使って下記に表現できる。
ちなみに、rubyに限らずコンピュータ言語の&&では、左側(今回のuser)がfalseの場合は右側(user.authenticate)は判断してもしなくても必然的にfalseになるので機械側が判断せず省略する(処理を止める)。
if user && user.authenticate(params[:session][:password])
参考
演算子式(Ruby 2.7.0 リファレンスマニュアル)
ここで小さなバグとして、エラーメッセージのflashの表示時間が長い(思ったより生き延びている..?)問題がある。
原因として、flashの生存期間としては、次のリクエストが来るまで
前回がredirectに対して今回はrender(リクエストを発行するのでなく、「このテンプレートを描画してね」)、つまり、失敗してflash登場後に次のリクエストが来るまで残り続ける
①表示(0リウエスト目)、②リロード(1リクエスト目)、③リロード(2リクエスト目)
すぐに直さず回帰バグを防ぐため、統合テストを行う。
$ rails generate integration_test users_login
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を追記する。
# ユーザーログイン後にユーザー情報のページにリダイレクトする
flash.now[:danger] = 'Invalid email/password combination'
これにてテスト通過(GREEN)。
8.2 ログイン
ヘッドの部分を下記のように変更する。
ログインしてる時 → ログアウト(の選択項目)表示
ログアウトしてる時 → ログイン(の選択項目)表示
Sessionsメソッド
Sessionsという特殊な変数(railsの機能)を使い、サーバ側に一時的に情報を保存する。
Sessionsのデータはviewとcontrollerで利用できる。
Sessionsが切れるまでの保存期間はRailsサーバが落ちるまで+ブラウザが生き残ってる(ex.ログイン後、タブやChromeを閉じる前)まで。
session[:user_id] = user.id
<参考>
【Rails】Sessionの使い方について
Session管理とRailsのcookie store
log_inメソッドと組み合わせる
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
end
Applicationコントローラのcreateメソッドに「log_in」、newへのリダイレクトを加える。
log_in user
redirect_to user
ユーザがProfileページへ飛ぶ時、今ログインしているユーザが誰か?(ちゃんと別の人でなく自分のユーザ情報か?)の情報を持ってこないことにははじまらない。
→ Session情報からユーザ情報をもってくるようsessionヘルパーを編集する。
ユーザーIDを一時セッションの中に安全に置けるようになったので、今度はそのユーザーIDを別のページで取り出す。
→ current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。
def current_user
# (元祖)current_user = User.find(session[:user_id])
# view側でも引き出せるようにインスタンス変数にする
# (改訂1)@current_user = User.find(session[:user_id])
# findは失敗したらエラーを返すログイン中にsessionが切れる可能性はあるので、
# 失敗しても「nil」を返すfind_byを使う。
# (改訂2)@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」演算子を使いわずか1行で
# @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問い合わせなく処理が早い)。
レイアウトのリンクを変更する方法として、ヘルパーにメソッドを追加する。
# ユーザーがログインしていればtrue、その他ならfalseを返す
def logged_in?
!current_user.nil?
end
ヘルパーヘッダーのhtml記入。setting(設定)はダミーリンクに。
link_to はデフォルトの振る舞いとして、引数にユーザオブジェクト(cuurent_user)が加わったらユーザのProfieページに飛ばす。
Log outでdeleteのリクエストを送る。
<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メソッドが見つからない。
ヘルパーはもともとviewで使われる。ただ、今回はコントローラでヘルパーを使いたいので、module(ヘルパー側)と対応するincludeを追記する。
class SessionsController < ApplicationController
#moduleと対応させる
include SessionsHelper
# 省略
Bootstrapの機能を使うため、jsに追加する。
//= require jquery
//= require bootstrap
もう一度リロードするとログイン状態になり、右上の項目が変わる(プルダウンの起動もok)。
レイアウトの変更をテストする
fixture(テストを実行、成功させるための状態や前提条件の集合)をYAMLで用意する。
YAMLファイル
YAMLファイルとは、
構造化されたデータを表現するためのデータ形式のひとつ
(HTML,XMLなどいろんなファイルの書き方のひとつ)。
配列(先頭に「-」)、ハッシュ(キー:値)、スカラー(文字列、数値、真偽知など)で構成される。
< 主な用途 >
・各種設定ファイル
・データ保存用 (シリアライゼーション)
・データ交換用フォーマット
・ログファイル
<参考>
YAMLとは|「分かりそう」で「分からない」でも「分かった」気に ...
プログラマーのための YAML 入門 (初級編)
【Ruby入門】YAMLの使い方をわかりやすく解説!
name,email,password_digest(あとでBCryptでハッシュ化)を含むYAMLを作成する。ラベルはmichael
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
3項演算子の作りとしては、
nilかfalseであれば(「:」から)下側のcost(精査)が、
「?」の左側の「min_cost」がそれ以外であれば上側の 「MIN_COST(簡易チェック)」が選択される。
# 渡された文字列のハッシュ値を返す
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
統合テストに成功してたときのメソッド・テスト文を追加する。
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を冒頭に
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 に
< ApplicationController
双方のincludeを消してapplicationコントローラのみincludeを追加する。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
省略
テストのヘルパーメソッド(追加)
# テストユーザーがログイン中の場合にtrueを返す
def is_logged_in?
!session[:user_id].nil?
end
assert(バグが無い様に条件式を埋め込んで、明示すること)追加。
#showテンプレート表示後
assert is_logged_in? #=> signup 終えた人はログインも終わってるか?
8.3 ログアウト
ユーザが「Log out」をクリックしたらDELETEアクションが反応するようにする。
<大まかな流れ>
1.セッションからユーザーIDを削除(ユーザのログアウト)
2.ヘルパーにlog_outメソッド、sessionsコントローラにdeleteメソッドの実装
3.統合テスト
# 現在のユーザーをログアウトする
def log_out
# キーを指定すると、キーに該当するバリュー(今回はユーザid)を削除してくれる
session.delete(:user_id)
@current_user = nil
end
Sessionsコントローラにdestroyアクション追加(DELETEリクエストがlog_outアクションにきたら動く、rootにリダイレクト)。
# DELETE /logout
def destroy
log_out
redirect_to root_url
end
この時点でページに行き「Log out」をクリックするとホームに戻ってる。
ログアウトのテストも記述するが、ログインのテストの末尾に追記していく。
# 分かるよう下記""の末尾に「 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
「Log out」クリックして、
無事ホーム(root)に戻ったのでok!