0
0

More than 1 year has passed since last update.

アプリを作る 発展的ログイン機構

Posted at

user.rb

user.rb
class User < ApplicationRecord
# 継承させる
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  # 保存する前に メアドは小文字にする
  validates :name, presence: true, length: { maximum: 50 }
  # nameカラムが存在する
  # validates(:name, presence: true)
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  # 正規表現
  validates :email, presence: true, length: { maximum: 255 },
                    # 存在性、長さの検証
                    format: { with: VALID_EMAIL_REGEX },
                    # メールフォーマットの検証
                    uniqueness: { case_sensitive: false } 
                    # :case_sensitive   大文字と小文字を区別しないで一意性を検証する
  has_secure_password
  # 仮想的なpassword属性とpassword_confirmation属性に対してバリデーションをする機能も(強制的に)追加
  validates :password, presence: true, length: { minimum: 6 }
  # 最低でも六文字異常 存在性のある文字列をパスワードととする。
  # 渡された属性が存在していることを検証する

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    # コストパラメータはテストでは最小にする
    BCrypt::Password.create(string, cost: cost)
    # stringはハッシュ化する文字列
    # cost ハッシュ化する計算コストを表す
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    # 新しいトークンを 記憶トークンにする
    update_attribute(:remember_digest, User.digest(remember_token))
    # 記憶ダイジェストの欄に記憶トークンを基にしたダイジェストを書き込む
  end

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    # 結果がfalseの時に 記憶ダイジェストが空かどうか確かめる。
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
    # 記憶ダイジェストを空に上書きする
  end
end

sessions_controller.rb

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
        # ログインさせる
        params[:session][:remember_me] == '1' ? remember(user) : forget(user)
        # 1はtureを表す
        # sessionとremember_meが有効ならば remember()でなければforget()を行う
        # forget() ユーザー情報を削除する
        # remember() ユーザー情報を記憶する
        # user.rbのrememberメソッドを使用する
        # ログインしてユーザーを保持する
        redirect_to user
        # user_url(user)を表す
        # プロフィールページへのルーティングにしています。
      else
        flash.now[:danger] = 'Invalid email/password combination' # 本当は正しくない
        # エラーメッセージを作成する
        #.nowで何かすると消えるようにする
      render 'new'
    end
      end
      def destroy
      # セッションの削除(ログアウト)
      log_out if logged_in?
      # ログイン中だったらログアウトする
      redirect_to root_url
      # 削除した後 ホーム画面に戻る
    end
  end

sessions_helper.rb

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

  # ユーザーのセッションを永続的にする
  def remember(user)
    user.remember
    # トークンを生成して記憶トークンにする
    cookies.permanent.signed[:user_id] = user.id
    # 永続化クッキー(有効期間が20年に設定されたクッキー)を設定
    #   cookies.permanent[クッキー名] = 値
    # 署名付きクッキーを設定
    #   cookies.signed[クッキー名] = 値
    cookies.permanent[:remember_token] = user.remember_token
  end

# 現在ログイン中のユーザーを返す(いる場合)
  def current_user
    if session[:user_id]
    # sessionsに含まれるなら
      @current_user ||= User.find_by(id: session[:user_id])
      # それをログイン中のユーザーcurrent_userと置く
    end
  end
  # 記憶トークンcookieに対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id])
    # セッションに情報を保存
    #    session[キー] = 値
      @current_user ||= User.find_by(id: user_id)
      # idにuser_idをもとにデータベースから探す又はcurrent_userに代入する
    elsif (user_id = cookies.signed[:user_id])
    # 署名付きのクッキーを設定できたら
      raise 
      # raise 発生させたい例外のクラス 以下
      user = User.find_by(id: user_id)
      # user_idを基に探す
      if user && user.authenticated?(cookies[:remember_token])
      # クッキーとは、クライアント側に保存されるファイル  
      #   cookies[:クッキー名] = { key: クッキー情報 }
      #   userが有効且つクッキーが認証されているか?
        log_in user
        # ログイン
        @current_user = user
        # @current_userにする
      end
    end
  end

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
  # これで判定させて表示内容を決める
    !current_user.nil?
    # ログイン中のユーザーは空じゃないか確認
  end

  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    # sessionからidを削除する
    @current_user = nil
    # ログイン済みのユーザーを空にする
  end
  # 永続的セッションを破棄する
  def forget(user)
  # forgetメソッド 
    user.forget
    # 記憶ダイジェストを空にする
    cookies.delete(:user_id)
    # クッキーをuser_idを基に削除する
    cookies.delete(:remember_token)
    # 記憶トークンを削除する
  end

  # 現在のユーザーをログアウトする
  def log_out
    forget(current_user)
    # current_userのuser_idとremember_tokenを削除する
    session.delete(:user_id)
    # セッションから削除する
    @current_user = nil
    # current_userを空にする
  end
end

user_login_test.rb

user_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
  # テストユーザー
    @user = users(:michael)
  end

  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_not is_logged_in?
    # test_helper ログイン中でない
    assert_template 'sessions/new'
    # 失敗したらまたログインページが表示されるか?
    assert_not flash.empty?
    # flashメッセージは表示されたか?
    get root_path
    # ホーム画面に行くことを要求する?
    assert flash.empty?
    # flashメッセージは表示されなかったか?
  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
    # 2番目のウィンドウでログアウトをクリックするユーザーをシミュレートする
    delete logout_path
    # 削除されるかどうか
    follow_redirect!
    # リダイレクト実行後に続いて別のリクエストを行う予定があるのであれば、follow_redirect!を呼び出す
    assert_select "a[href=?]", login_path
    # login_pathが表示されているか?
    assert_select "a[href=?]", logout_path,      count: 0
    # logout_pathが表示されていないか?
    assert_select "a[href=?]", user_path(@user), count: 0
    # user_path(@user)が表示されていないか?
  end

test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    #  有効なユーザー remember_meが有効
    assert_not_empty cookies[:remember_token]
    # クッキーとは、クライアント側に保存されるファイルのこと
    #  cookies[:クッキー名] = { key: クッキー情報 }
    # cookiesが有効か?
  end

  test "login without remembering" do
    # cookieを保存してログイン
    log_in_as(@user, remember_me: '1')
    # 有効なユーザー remember_meが有効 ログインする
    delete logout_path
    # ログアウトパスを削除
    # cookieを削除してログイン
    log_in_as(@user, remember_me: '0')
    assert_empty cookies[:remember_token]
    # 空どうか確認
    #   クライアント側に保存されているか?
    #    :remember_tokenでcookiesができるのか。
  end
end

test/models/user_test.rb

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")

    # テストユーザー
    # create_user.rbでusersテーブルに沿って作成
    # 現時点では有効
  end

  test "should be valid" do
  # 有効を検証
    assert @user.valid?
    # usersテーブルで有効なデータモデルか確認
  end

  test "name should be present" do
    @user.name = ""
    # 名前を空にする
    assert_not @user.valid?
    # 空にしたデータテーブルは無効か?
  end

  test "email should be present" do
  # 存在性の検証
    @user.email = "     "
    # メールを空にする
    assert_not @user.valid?
    # 無効か?
  end

  test "name should not be too long" do
  # 長さの検証
    @user.name = "a" * 51
    # 文字列の長さを検証
    assert_not @user.valid?
    # 無効か?
  end

  test "email should not be too long" do
    @user.email = "a" * 244 + "@example.com"
    # 文字列の長さの検証
    assert_not @user.valid?
    # 無効か?
  end

   test "email validation should accept valid addresses" do
   # メールフォーマットの検証をする
    valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org
                         first.last@foo.jp alice+bob@baz.cn]
    # メールのアドレスのドメイン名の配列
    valid_addresses.each do |valid_address|
    # いくつものアドレスを代入していく。
      @user.email = valid_address
      assert @user.valid?, "#{valid_address.inspect} should be valid"
      # ドメイン名は有効か?
      # "#{valid_address.inspect} should be valid" エラ〜メッセージ
    end
  end

  test "email validation should reject invalid addresses" do
    invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
                           foo@bar_baz.com foo@bar+baz.com]
    invalid_addresses.each do |invalid_address|
      @user.email = invalid_address
      assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
    end
  end

  test "email addresses should be unique" do
  # 一意性の検証
    duplicate_user = @user.dup
    # dup オブジェクトのコピーを作成するメソッド
    # 同じものを作って比較する
    duplicate_user.email = @user.email.upcase
    # 大文字のアドレスに上書き
    @user.save
    assert_not duplicate_user.valid?
    # 無効か?
  end

  test "password should be present (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    # パスワードに空白を入力
    assert_not @user.valid?
    # 有効にしない
  end

  test "password should have a minimum length" do
    @user.password = @user.password_confirmation = "a" * 5
    # 五文字の文字列を入力
    assert_not @user.valid?
    # 有効にしない
  end

  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
    # パスワードが空じゃないか?
  end
end

app/views/sessions/new.html.erb

app/views/sessions/new.html
.erb

<% provide(:title, "Log in") %>
<h1>ログイン</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.label :remember_me, class: "checkbox inline" do %>
      <!--パスワード記憶機能のチェックボックスを表示させる-->
        <%= f.check_box :remember_me %>
        <!--チェックボックス表示-->
        <span>Remember me on this computer</span>
                <!--<div>に似ているらしい-->
      <% end %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
      <!--ボタンを作る-->
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
    <!--文字のリンク-->
  </div>
</div>

app/assets/stylesheets/custom.scss

app/assets/stylesheets/custom.scss
@import "bootstrap-sprockets";
@import "bootstrap";
/* mixins, variables, etc. */

$gray-medium-light: #eaeaea;

@mixin box_sizing {
  -moz-box-sizing:    border-box;
  -webkit-box-sizing: border-box;
  box-sizing:         border-box;
}
// 箱のようなデザインにする

/* universal */

body {
  padding-top: 60px;
}

section {
  overflow: auto;
}

textarea {
  resize: vertical;
}

.center {
  text-align: center;
  h1 {
    margin-bottom: 10px;
  }
}

/* typography */

h1, h2, h3, h4, h5, h6 {
  line-height: 1;
}

h1 {
  font-size: 3em;
  letter-spacing: -2px;
  margin-bottom: 30px;
  text-align: center;
}

h2 {
  font-size: 1.2em;
  letter-spacing: -1px;
  margin-bottom: 30px;
  text-align: center;
  font-weight: normal;
  color: #777;
}

p {
  font-size: 1.1em;
  line-height: 1.7em;
}

/* header */

#logo {
  float: left;
  margin-right: 10px;
  font-size: 1.7em;
  color: #0f0;
  text-transform: uppercase;
  letter-spacing: -1px;
  padding-top: 9px;
  font-weight: bold;
  &:hover {
    color: #008000;
    text-decoration: none;
  }
}

/* footer */

footer {
  margin-top: 45px;
  padding-top: 5px;
  border-top: 1px solid #eaeaea;
  color: #777;
  a {
    color: #555;
    &a:hover {
      color: #222;
    }
  }
  small {
    float: left;
  }

  ul {
    float: right;
    list-style: none;
     li {
       float: left;
       margin-left: 15px;
     }
  }
}

/* miscellaneous */

.debug_dump {
  clear: both;
  float: left;
  width: 100%;
  margin-top: 45px;
  @include box_sizing;
}

/* forms */

input, textarea, select, .uneditable-input {
  border: 1px solid #bbb;
  width: 100%;
  margin-bottom: 15px;
  @include box_sizing;
}

input {
  height: auto !important;
}

#error_explanation {
  color: red;
  ul {
    color: red;
    margin: 0 0 30px 0;
  }
}

.field_with_errors {
  @extend .has-error;
  .form-control {
    color: $state-danger-text;
  }
}

.checkbox {
  margin-top: -10px;
  margin-bottom: 10px;
  span {
    margin-left: 20px;
    font-weight: normal;
  }
}

#session_remember_me {
  width: auto;
  margin-left: 0;
}

//これから勉強する

test/test_helper.rb

test/test_helper.rb
class ActiveSupport::TestCase
  fixtures :all

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

  # テストユーザーとしてログインする
  def log_in_as(user)
    session[:user_id] = user.id
    # セッションに情報を保存
    # session[キー] = 値
  end
end

class ActionDispatch::IntegrationTest

  # テストユーザーとしてログインする
  def log_in_as(user, password: 'password', remember_me: '1')
  # テストユーザーとしてログイン 有効なユーザー、 パスワード、記憶させる
    post login_path, params: { session: { email: user.email,
                                          password: password,
                                          remember_me: remember_me } }
    # 投稿する
  end
end

test/helpers/sessions_helper_test.rb

test/helpers/sessions_helper_test.rb
require 'test_helper'

class SessionsHelperTest < ActionView::TestCase

  def setup
  # テストユーザーをデータベースに記憶させる
    @user = users(:michael)
    remember(@user)
  end

  test "current_user returns right user when session is nil" do
    assert_equal @user, current_user
    # データベース上のユーザーとログイン中のユーザーが同じか?
    assert is_logged_in?
    # ログイン中か?
  end

  test "current_user returns nil when remember digest is wrong" do
    @user.update_attribute(:remember_digest, User.digest(User.new_token))
    # 記憶ダイジェストに新しくダイジェストを上書きする
    assert_nil current_user
    # assert_nil(変数 [, メッセージ])    変数がnilならば成功
    # ログイン中ではないか?
  end
end

sessions_helper.rbをテスト

.
.
.
def current_user
    if (user_id = session[:user_id])
    # セッションに情報を保存
    #    session[キー] = 値
      @current_user ||= User.find_by(id: user_id)
      # idにuser_idをもとにデータベースから探す又はcurrent_userに代入する
    elsif (user_id = cookies.signed[:user_id])
    # 署名付きのクッキーを設定できたら
      raise 
      # raise 発生させたい例外のクラス 以下
      user = User.find_by(id: user_id)
      # user_idを基に探す
      if user && user.authenticated?(cookies[:remember_token])
      # クッキーとは、クライアント側に保存されるファイル  
      #   cookies[:クッキー名] = { key: クッキー情報 }
      #   userが有効且つクッキーが認証されているか?
        log_in user
        # ログイン
        @current_user = user
        # @current_userにする
      end
    end
  end
.
.
.
ubuntu:~/environment/my_app (advanced-login) $ rails test test/helpers/sessions_helper_test.rb
Running via Spring preloader in process 8387
Run options: --seed 63395

# Running:

E

Error:
SessionsHelperTest#test_current_user_returns_right_user_when_session_is_nil:
RuntimeError: 
    app/helpers/sessions_helper.rb:20:in `current_user'
    test/helpers/sessions_helper_test.rb:11:in `block in <class:SessionsHelperTest>'


rails test test/helpers/sessions_helper_test.rb:10

test_current_user_returns_right_user_when_session_is_nil

テスト現在のユーザーは、セッションがnilのときに正しいユーザーを返します

user = User.find_by(id: user_id)

が無効になっているからかな?

続き↓

E

Error:
SessionsHelperTest#test_current_user_returns_nil_when_remember_digest_is_wrong:
RuntimeError: 
    app/helpers/sessions_helper.rb:20:in `current_user'
    test/helpers/sessions_helper_test.rb:17:in `block in <class:SessionsHelperTest>'


rails test test/helpers/sessions_helper_test.rb:15
test_current_user_returns_nil_when_remember_digest_is_wrong

記憶ダイジェストが違う時にログイン中のユーザーがnilと返される
続き↓

Finished in 0.086793s, 23.0433 runs/s, 0.0000 assertions/s.
2 runs, 0 assertions, 0 failures, 2 errors, 0 skips
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0