大まかな流れの自己整理が目的(コード外文章中の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!







