1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.9 - Sign in -

Last updated at Posted at 2020-03-03

はじめに

第9回目です。
前回はサインアップ機能を作ってみましたね。今回はサインインやサインアウト機能を作ってまいります。
今後、サインインしていないと使えない機能や見れないページなども作っていきますので、アプリがユーザーのサインイン状況を知ることができてサインインしているユーザーを特定できるようにしましょう!

前回のソースコード

前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。

セッション

RailsはRESTfulなWebアプリケーションフレームワークなのでステートレスなアプリです。
しかし、サインイン機能を実装するとなるとそのユーザーがサインイン状態であることをアプリが知る必要がありますし、アプリはそのユーザーがどのユーザーなのかを特定する必要があります。
このために使われるのがセッション(Session)です。

Railsではセッションを管理する便利なsessionメソッドが用意されています。
今回はこのsessionメソッドを使ってサインイン状態を管理し、マイページ機能、つまりサインインしているユーザーのユーザー詳細ページに遷移する機能を作っていきましょう。
サインインしていない場合はユーザーを特定できないので、マイページには遷移させずトップページにリダイレクトをかけるようにもしていきます。

このように、セッションでサインイン状態を管理できるようになれば、その状態によるページや動作の出しわけやサインインユーザーに合わせたコンテンツの出しわけが可能になります。

今回はセッションを一つのリソースと捉えて実装していきます。
つまり、サインインしたときにsessions#createでセッションを作成したり、サインアウトしたときにSessions#destroyでセッションを削除したりさせます。

サインインページを作ろう

セッションはSessionsコントローラーで管理してきます。
アクションとしては、

  • new: サインインページに遷移
  • create: セッション作成(サインイン処理)
  • destroy: セッション削除(サインアウト処理)

が必要になってきます。
今回はrails gコマンドに頼らずにディレクトリやファイルを作成していきますね。

ルーティングを作成する

まずはルーティングを作成しましょう。

config/routes.rb
  Rails.application.routes.draw do
    root 'static_pages#home'

    get   '/sign_up',  to: 'users#new',    as: :sign_up
    post  '/sign_up',  to: 'users#create', as: :create_user
    resources :users, only: [:show]
+
+   get     '/sign_in',   to: 'sessions#new',     as: :sign_in
+   post    '/sign_in',   to: 'sessions#create',  as: :create_session
+   delete  '/sign_out',  to: 'sessions#destroy', as: :sign_out
  end

この辺りはもう慣れてきましたよね。

コントローラーを作成する

お次はコントローラーを作ります。(最初にコンテナを起動させて中に入っておきます。)

$ docker-compose up -d
$ docker-compose exec web ash
# touch app/controllers/sessions_controller.rb
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
    @user = User.new
  end 

  def create
  end

  def destroy
  end
end

とりあえずルーティングと対応したアクションを記述しておきます。
newアクション、つまりサインインページではまたUserモデルを扱います(ユーザーのサインイン情報を入力してもらうので)。
そのため、newアクションはUsersコントローラーと同じように@user = User.newを記述してます。

サインインページを作成する

さて、newアクションがレンダリングするnew.html.erbをコーディングしてきましょう。

まずは、Sessionsコントローラーのビューファイルを格納するディレクトリを作成し、ビューファイルを作りましょう。

# mkdir app/views/sessions
# touch app/views/sessions/new.html.erb

さて、ビューファイルをコーディングしてみます。

app/views/sessions/new.html.erb
<div class="container">
  <h1 class="my-5">Sign in</h1>

  <%= form_with model: @user, url: create_session_path, local: true do |form| %>
    <div class="form-group">
      <%= form.label :email %>
      <%= form.text_field :email, class: "form-control" %>
    </div>
    <div class="form-group">
      <%= form.label :password %>
      <%= form.password_field :password, class: "form-control" %>
    </div>
    <div class="form-check">
      <%= check_box_tag :visible_password, :visible, false, class: "form-check-input" %>
      <%= label_tag :visible_password, "パスワードを表示する" %>
    </div>

    <div class="form-group mt-5">
      <%= form.submit "Sign in", class: "btn btn-primary form-control" %>
    </div>
  <% end %>
  <p class="text-center">登録がまだの方は<%= link_to "こちら", sign_up_path %></p>
</div>

<%= javascript_pack_tag 'visible_password' %>

基本的には前回のサインアップページと同じですね。今回はサインインなのでemailpasswordを入力項目に指定しました。
また、passwordはサインアップページと同様、チェックボックスで表示非表示を変更できるようにしています。

『Sign in』ボタンの下にまだサインアップが終わっていないユーザー向けにサインアップページへのリンクを追加しています。
<%= link_to "こちら", sign_up_path %>だけで適切なaタグを作成してくれるのはやはり便利ですね。

一度http://localhost:3000/sign_inにアクセスしておきましょう。以下のようなページが表示されたら、ここまでのコーディングは成功です!
image.png

サインインページへのリンクを作る

サインインページの形が出来上がってきたので、ここでサインインページへのリンクを以下のページにつけていこうと思います。

  • ヘッダーの「Sign in」リンク
  • サインアップページの下部に「登録済みの方はこちら」リンクを設置

ヘッダーの「Sign in」リンク

ヘッダーの「Sign in」リンクはまだ遷移先を指定できていませんでしたね。
やっとサインインページができあがってきたので遷移先としてsign_in_pathを指定しておきます。

app/views/layouts/application.html.erb
  ...
  <header class="navbar navbar-dark navbar-expand bg-dark">
    <div class="container">
      <%= link_to "sample app", root_path, class: "navbar-brand" %>
      <ul class="navbar-nav">
        <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link" %></li>
-       <li class="nav-item"><%= link_to "Sign in", "#", class: "nav-link" %></li>
+       <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link" %></li>
      </ul>
    </div>
  </header>
  ...

コーディングが終わったら、一度トップページに遷移してヘッダーの「Sign in」リンクを選択してみましょう。
サインインページに遷移できたら成功です!

サインアップページの下部に「登録済みの方はこちら」リンクを設置

サインインページで「登録がまだの方はこちら」リンクを設置したように、サインアップページにも「登録済みの方はこちら」リンクを設置してみましょう。

app/views/users/new.html.erb
  ...
  <%= form_with model: @user, url: create_user_path, local: true do |form| %>
    ...
    <div class="form-group mt-5">
      <%= form.submit "Sign up!", class: "form-control btn btn-primary" %>
    </div>
  <% end %>
+ <p class="text-center">登録済みの方は<%= link_to "こちら", sign_in_path %></p>
  ...

こちらもサインアップページで「こちら」をクリックしてサインインページに遷移できれば成功です!

サインイン処理を作成する

ここまででサインインページの形ができあがりました。
次はサインイン処理(create)をつくっていきます!

サインイン処理ではサインインページで入力されたemailpasswordからユーザーを検索します。
ユーザーがヒットすればそのユーザーのユーザー詳細ページ(users#show)、ユーザーが存在しない、またはパスワードが誤っているような場合はエラーメッセージが表示されるようにしましょう。

少しおさらいですが、モデルを検索する場合はfind_byメソッドを使うことで指定した属性に対して検索をすることができました。
さらに、has_secure_passwordをもつモデルはauthenticateメソッドを使ってパスワード認証ができることも思い出しましょう。
この2つを組み合わせることでユーザーの検索し認証することができそうですね。

では、createアクションを記述していきます。

app/controllers/sessions_controller.rb
  ...
  def create
+   @user = User.new(email: params[:user][:email])
+   user = User.find_by(email: @user.email.downcase)
+   if user && user.authenticate(params[:user][:password])
+     flash[:success] = "サインインしました。"
+     redirect_to user
+   else
+     flash.now[:danger] = "#{User.human_attribute_name(:email)}または#{User.human_attribute_name(:password)}をもう一度確認してください。"
+     render :new
+   end
  end
  ...

少し複雑に見えるかもしれません。1行ずつ解説していきますね。

@user = User.new(email: params[:user][:email])

まず、@userに新しくUserモデルを代入しています。属性値はemailparams[:user][:email]を持っています。
paramsはフォームからのリクエストの値を取得するメソッドで、フォームのinputタグでname="user[email]"と定義されている値は[:user][:email]から取得することができます。
@userparams[:user][:email]の属性値をもつUserモデルを代入しているのは、この処理がエラー(ユーザーが見つからない or パスワード認証が通らない)の場合に今入力したemailをデフォルトでフォームに入力された状態でサインインページを再度表示するためです。
これがないと入力したemailをキープする方法がなくてユーザーは毎回メールアドレスを再入力する手間になってしまいます。(パスワードは特性上、キープしないように作っています)

user = User.find_by(email: @user.email.downcase)

先ほど@userに設定したemailを使ってUserを検索しています。
Userモデルを作成するときにモデル側でemailを小文字化してからDBに保存するようにコーディングしたことを覚えているでしょうか?
今DBには必ず全て小文字のメールアドレスが保存されているので、find_byemailを検索する時もdowncaseで検索文字列を小文字化して検索しています。
find_byは検索対象が存在していた場合はモデルオブジェクトを返却し、存在しない場合はnilを返却するメソッドであることも改めて意識しましょう。

if user && user.authenticate(params[:user][:password])
  # trueの処理
else
  # falseの処理
end

次に条件分岐を設けています。&&は「アンド条件」、「かつ」を意味していますので、

  • user
  • user.authenticate(params[:user][:password])

の両方を満たした場合はtrue、どちらか一方でも満たさない場合はfalseの処理に分岐します。

まずuserの条件式をみてみます。
これはusernilfalseでないかどうかを検証しています。
先ほどfind_byは検索結果があればモデルオブジェクト、なければnilを返却するといいました。
なのでこのuserの条件はfind_byの結果そのemailをもつユーザーがそもそも存在しない場合falseの処理を実行するようにするための条件ということになります。

次にuser.authenticate(params[:user][:password])の条件式をみてみます。
これは順番的にuserの条件式を満たした場合に検証される条件式です。なのでuserにはUserモデルのモデルオブジェクトが入っています。
authenticateメソッドはhas_secure_passwordの便利機能の一つで、平文のpasswordを与えるとハッシュ化してDBに格納しているpassword_digestと照会し、同一であればモデルオブジェクトを、そうでなければfalseを返却してくれるものでした。

以上より、このif文の条件式の条件分岐は以下の通りになります。

入力したemailのユーザーが パスワードが 実行する処理
いない - falseの処理
いる 誤っている falseの処理
いる 正しい trueの処理

やりたいことに合致してますね。
ではtrueの処理、falseの処理をそれぞれみてみましょう。

flash[:success] = "サインインしました。"
redirect_to user

trueの処理はとても単純です。
検索し認証したユーザーのユーザー詳細ページにリダイレクトさせているだけです。flashでサインインメッセージも定義してますね。

flash.now[:danger] = "#{User.human_attribute_name(:email)}または#{User.human_attribute_name(:password)}をもう一度確認してください。"
render :new

falseの処理も最終的にはrender :newで再度サインインページを表示させていることがわかります。
その前にflash.nowメソッドを使ってエラー文を用意しています。
前回のUsersコントローラーのcreateアクションでもflashメソッドを使ってサインアップ成功時のメッセージをユーザー詳細ページに一時的に表示させることをしました。
flash.nowはそれのrender時に利用する版と思ってください。keydangerにしているのは前回と同じでBootstrapの命名規則に則っています。

flash.nowのメッセージの中身が若干複雑ですね。
まず、Rubyでは文字列の中に変数を入れる場合#{変数}と記述することができます。
"aaa" + 変数 + "bbb"のような書き方もできますが、"aaa#{変数}bbb"の方がよりスマートな気がしますよね。
その変数はUser.human_attribute_name(属性名)となっています。
human_attribute_nameメソッドは引数の属性のi18nで定義した文字列を返してくれます。ちょうどform.labelと同じような感じですね。
なので今回の場合、User.human_attribute_name(:email)は『メールアドレス』、User.human_attribute_name(:password)は『パスワード』が変数の結果として当て込まれます。

ここまででサインイン処理(createアクション)をみてきました。
最後にflash.nowのメッセージを表示するコードが今のsessions/new.html.erbにはないので、前回のusers/new.html.erbに倣って記述します。

app/views/sessions/new.html.erb
  ...
  <h1 class="my-5">Sign in</h1>
+ 
+ <% flash.each do |msg_type, msg| %>
+   <div class="alert alert-<%= msg_type %>"><%= msg %></div>
+ <% end %>
+
  <%= form_with model: @user, url: create_session_path, local: true do |form| %>
...

動作確認

では、サインイン処理を動作確認してみましょう。
前回DBをリセットしてますので、もう一度John SmithさんをDBに登録しておきます。

# rails c
> User.create(name: "John Smith", email: "john@sample.com", password: "password")

http://localhost:3000/sign_inにアクセスして色々とチェックしてみましょう!

エラー系

emailpasswordを入力していない

image.png
何も入力していない、またはどちらかだけでも入力していない場合はfind_byauthenticateのいずれかが必ず失敗するのでエラーメッセージが表示されていますね。

入力したemailのユーザーがいない

test@test.comみたいな適当なemailを入力して確認。
image.png
エラーメッセージが表示されているし、emailのテキストフィールドに入力してたtest@test.comが残っているのも確認できましたね。

emailpasswordの組み合わせが異なっている

john@sample.comと適当なパスワードjohn1234を入力して確認してみましょう。
image.png
こちらもエラーメッセージが表示されていますね。emailjohn@sample.comも残っています。

正常系

期待動作はjohn@sample.com&passwordでユーザー詳細ページに遷移することです。
やってみましょう!
image.png
ちゃんと期待通りの動作になりましたね。

セッション管理する

ここまででパスワード認証のロジックができあがりましたね。

ただ今のままでは、「認証」自体はできましたが「認証済み」という状態を管理することはできてません。
「認証済み」という状態を管理するためにセッション管理をする機能を作っていきます。

ちなみにRailsのsessionメソッドでは、半永続的で暗号化された一時Cookieが払い出されます。
これは仮にこのCookieが盗まれたとしても、それを使ってサインインを乗っ取ることはできないようになっています。
その代わりと言ってはなんですが、一度ブラウザを閉じると自動的に消える仕組みになっています。

SessionsHelperを作成する

セッション管理するためにsign_inメソッドやsign_outメソッドなどを自前で実装していきます。
これらはコントローラーの元になるapplication_controller.rbにコーディングしていきたいところですが、それではapplication_controller.rbに多くのメソッドを記述する必要がでてくるため可読性が確保できなくなる恐れがあります。

今回は、新たにSessionsHelperを作成し、その中でsign_inメソッド、sign_outメソッドを定義します。
このSessionsHelperをApplicationControllerが読み込んでコントローラー内でメソッドを利用できるようにすることで、可読性を損なうことなく機能を実装していきましょう。

まずSessionsHelperファイルを作成します。コンソールが起動している場合はquitで抜け出しましょう。

> quit
# touch app/helpers/sessions_helper.rb
app/helpers/sessions_helper.rb
module SessionsHelper
end

ヘルパーはmodule [helper_name]で定義します。
最後に、ヘルパーをApplicationControllerから読み取るように定義しましょう。

app/controllers/application_controller.rb
  class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception
+   include SessionsHelper
  end

SessionsHelperでsign_inメソッドを作成する

次に、SessionsHelperを編集して、sign_inメソッドを作成していきます。
Railsではsessionメソッドが用意されており、簡単にセッションを管理することができます。

app/helpers/sessions_helper.rb
  module SessionsHelper
+
+   def sign_in(user)
+     session[:user_id] = user.id
+   end
+
  end

これだけで、sign_inメソッドを使ってuser.idをセッションに記録することができます。

サインインページでサインインが成功した場合にsign_inメソッドを呼び出してセッションを記録するようにしてみます。

app/controllers/sessions_controller.rb
  ...
  def create
    @user = User.new(email: params[:user][:email])
    user = User.find_by(email: @user.email.downcase)
    if user && user.authenticate(params[:user][:password])
      flash[:success] = "サインインしました。"
+     sign_in user
      redirect_to user
    else
      flash.now[:danger] = "#{User.human_attribute_name(:email)}または#{User.human_attribute_name(:password)}をもう一度確認してください。"
      render :new
    end
  end
  ...

redirect_toの前にsign_in userを追加しただけです。
SessionsControllerはApplicationControllerを継承しているのでApplicationControllerで読み込んでいるSessionsHelperを利用することができます。

current_userメソッドを作成する

ここまででサインインしたときにセッション情報としてサインインユーザーのidをCookieに格納できるようになりました。
今度はこれを使って、サインイン中のユーザー情報をアプリが特定するためのcurrent_userメソッドを作成しましょう。
このcurrent_userメソッドを使うことで、例えば<%= current_user.name %>でサインイン中のユーザーの名前を表示できるようにしたりします。

今、セッションにuser_idのキーに対してuser.idを格納しています。これはsession[:user_id]で取り出すことができます。
このことからUser.find(session[:user_id])またはUser.find_by(id: session[:user_id])でサインイン中のユーザーを特定できることを思いつきますね。
ただ、findメソッドはNotFoundの場合に例外が発生します。find_byメソッドの場合はNotFoundでもnilを返却するだけなのでif文などで簡単にエラー時の処理を実装できるのですが、例外の場合はちょっとクセがありますね。
今回はfind_byメソッドで実装するようにしましょう。

では、SessionsHelperに定義します。

app/helpers/sessions_helper.rb
  module SessionsHelper

    def sign_in(user)
      session[:user_id] = user.id
    end
+
+   def current_user
+     @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
+   end
  
end

少しつまずきそうなコードですね。ちょっと紐解きます。

まずわかりやすいところから、一番後ろにif session[:user_id]と記入しています。
これはこれよりも前の文章が実行される条件式になっています。session[:user_id]が存在する、つまりサインイン済みの状態であればこれよりも前の文章が実行されるというわけです。
current_userメソッドではこの1行以外にコードはないので、session[:user_id]が存在しない場合は何も実行されず、nilが返却されるようになります。

次に見慣れない||=をみてみてましょう。
これは見慣れないと思いますが、少しプログラミングをかじったことがある人であれば何かの言語で+=のような表現を見たことがあるのではないでしょうか?
Rubyでも同じような挙動をとりますが、a += ba = a + bと同義になりますね。
||はORを表す演算子ですので、a ||= ba = a || bと同義になります。
current_userは上で少しお話したように、<%= current_user.name %><%= current_user.email %>のように使われるケースを考えています。
これが同じビューファイルで2回呼び出された場合、User.find_by(id: session[:user_id])とだけコーディングしていた場合は2回ともDBから情報を取得しなくてはならなくなります。
そこで@current_user ||= User.find_by(id: session[:user_id])としておくことで、一度目に<%= current_user.name %>時に@current_userがセットされるため二度目の<%= current_user.email %>の場合はDBにアクセスすることなく前回の検索時のモデルオブジェクトを再利用することができるようになります。
DBの負荷軽減やDBアクセスの回数削減によるレスポンス向上を目的として、こういった記述にしてみました。

まとめると、current_userメソッドは呼び出されたときに以下の動作をします。

サインイン ユーザー 処理
- nil
NotFound nil
Found サインイン済みユーザーのモデルオブジェクト

書き方を変えると以下と同じですね。

def current_user
  if session[:user_id]
    if @current_user
      @current_user
    else
      User.find_by(id: session[:user_id])
    end
  end
end

上の書き方の方がシンプルで可読性高いですよね。

サインイン状態を確認するsigned_in?メソッドを作る

どんどんいきましょう。
次にサインイン状態をtruefalseで返却するsigned_in?メソッドを作ってみます。
このメソッドによってユーザーのサインイン状態に合わせた処理を簡単に実装することができます。

先ほどのcurrent_userメソッドはユーザーがサインインしている場合はそのユーザーのモデルオブジェクト、サインインしていない場合はnilを返却するメソッドでした。
そしてRailsにはnilの場合trueを、そうでない場合falseを返却するnil?メソッドが用意されています。
さらに!truefalseが入れ替わる否定演算子です。

これらを組み合わせればsigned_in?メソッドが作れそうですね。

app/helpers/sessions_helper.rb
  module SessionsHelper

    def sign_in(user)
      session[:user_id] = user.id
    end

    def current_user
      @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
    end
+
+   def signed_in?
+     !current_user.nil?
+   end

  end

サインアウトするsign_outメソッドを作成する

最後にサインアウトするメソッドとしてsign_outメソッドを作ってみましょう!

app/helpers/sessions_helper.rb
  module SessionsHelper

    def sign_in(user)
      session[:user_id] = user.id
    end

    def current_user
      @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
    end

    def signed_in?
      !current_user.nil?
    end
+
+   def sign_out
+     session.delete(:user_id)
+     @current_user = nil
+   end
  
end

なんとなーくわかるとおもいますが、session.delete(:user_id)でCookieに保存していたsession[:user_id]を削除してます。
さらにサインイン中は@current_userにサインインしているユーザーのモデルオブジェクトが格納されているのでこれもnilに初期化しています。

さてさて、ここまででサインインに関して最低限必要な全てのメソッドを作成できました。
サインインの状態に合わせてヘッダーを出しわけしてみましょう!

ヘッダーを更新する

ヘッダーを以下の条件で出しわけしてみます。

  • 未サインイン
    • Home: トップページに遷移する
    • Sign in: サインインページに遷移する
  • サインイン済
    • Profile: サインインしたユーザーのユーザー詳細ページに遷移する
    • Sign out: サインアウトしてトップページに遷移する

まずは、ヘッダーをsigned_in?メソッドを使って出しわけしてみます。

app/views/layouts/application.html.erb
  ...
  <header class="navbar navbar-dark navbar-expand bg-dark">
    <div class="container">
      <%= link_to "sample app", root_path, class: "navbar-brand" %>
      <ul class="navbar-nav">
-       <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link" %></li>
-       <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link" %></li>
+       <% if signed_in? %>
+         <%# サインイン済みの場合のリンク %>
+         <li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link" %></li>
+         <li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link" %></li>
+       <% else %>
+         <%# 未サインインの場合のリンク %>
+         <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link" %></li>
+         <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link" %></li>
+       <% end %>
      </ul>
    </div>
  </header>
...

今までのヘッダーは未サインインの場合のリンクになっていたので、signed_in?falseの場合の方に記述してます。

サインイン済みの場合は新たに「Profile」と「Sign out」のリンクを作っています。

<li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link" %></li>

「Profile」リンクの方は今までとほぼほぼ変わらない書き方ですね。少し違う点としてはroot_pathsign_in_pathのように遷移先としてpathを指定するのではなくcurrent_userというモデルオブジェクトを指定している点です。
link_toメソッドでは、遷移先としてモデルオブジェクトが指定された場合、そのモデルオブジェクトの参照パスに読み替えてくれます。つまり、user_path(current_path)と同義になります。

<li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link" %></li>

「Sign out」リンクの方はmethod: :deleteの箇所が今までとは異なります。
通常link_toメソッドはGETメソッドでリンク先にリクエストしますが、method: [method]で指定することで別のメソッドでリクエストすることができます。
sign_out_pathroutes.rbdeleteメソッドでルーティングするように定義していましたので、この形でlink_toのHTTPメソッドを指定してあげないとルーティングされなくなってしまいます。

そういえば、sign_out_pathでルーティングされるsessions#destroyについて、コーディングをしていませんでした。
セッションを削除するsign_outメソッドを実行し、トップページにリダイレクトするようにコーディングしましょう。

app/controllers/sessions_controller.rb
  ...
  def destroy
+   sign_out
+   redirect_to root_path
  end
  ...

ここまでで、サインイン機能、サインアウト機能、ヘッダーの出しわけについてコーディングが完了しました。
ここで一度動作確認をしていきましょう。

サインイン機能、サインアウト機能、ヘッダー出しわけの動作確認

サインインしていない場合

まずサインインしていない状態でhttp://localhost:3000にアクセスしてみましょう。
すでにサインインしてしまっている場合は、ブラウザを閉じるか、「Sign out」リンクを押すかしてサインアウトしましょう。

この状態ではヘッダーは「Home」リンクと「Sign in」リンクが表示されています。
image.png

「Home」リンクを選択するとトップページへ、「Sign in」リンクを選択するとサインインページに遷移することも確認できますね。

サインインしている場合

では、サインインページからjohn@sample.comでサインインしてみましょう。
すると、john@sample.comのユーザー詳細ページに遷移し、ヘッダーも「Profile」リンクと「Sign out」リンクに変わっていることが確認できます。
image.png

また、「Profile」リンクを選択することでjohn@sample.comのユーザーのユーザー詳細ページに遷移できることも確認できますね。

サインアウトを試してみる

最後にサインアウトが正しく動作するか確認してみましょう。
サインインしている状態で、「Sign out」リンクをクリックしてみてください。

トップページに遷移して、ヘッダーが未サインイン状態の場合のヘッダーに戻っていることが確認できるはずです。
image.png

これで、想定どおりに動作していることが確認できましたね。

サインアップした時もサインイン状態になるようにする

今のままではサインアップした後にサインインをしないといけなくなり面倒です。
サインアップ時(users#create)もサインイン状態になるように更新します。

app/controllers/users_controller.rb
  ...
  def create
    @user = User.new(user_params)
    if @user.save
+     sign_in @user
      flash[:success] = "サインアップありがとう!"
      redirect_to @user
    else
      render :new
    end
  end
  ...

if @user.saveの下にsign_in @userを追加しました。
これでDB保存が成功した場合に、そのユーザーでサインイン状態になるようになりました。

では、新たにユーザーをサインアップページで作成して、サインイン状態でプロフィールページに遷移することを確認してみましょう。
image.png
プロフィールページに遷移してますし、ヘッダーのリンクからサインイン後の状態になっていることがわかりますね。

サインイン状態のときに遷移できないページを作る

また、サインインしたあとにサインアップページやサインインページに遷移することは必要ありませんね。(むしろ禁止したい。)
さらに、サインイン後はトップページにもアクセスする必要はなく、ルートパスにアクセスしようとした場合、プロフィールページ(サインインユーザーのユーザー詳細ページ)に遷移させるようにしたいかもしれません。

これらを実装してみましょう。

ApplicationControllerにサインインしている場合、強制的にプロフィールページに遷移させるメソッドを作成します。

app/controllers/application_controller.rb
  class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception
    include SessionsHelper
+
+   def redirect_to_profile_if_signed_in
+     redirect_to current_user if signed_in?
+   end
  end

これを、トップページ(static_pages#home)、サインアップページ(users#new, users#create)、サインインページ(sessions#new, sessions#create)にルーティングされた直後(アクションが処理される前)に実行されるようにします。
これはbefore_actionメソッドを使えば容易です。

app/controllers/static_pages_controller.rb
  class StaticPagesController < ApplicationController
+   before_action :redirect_to_profile_if_signed_in
+
    def home
    end
  end
app/controllers/users_controller.rb
  class UsersController < ApplicationController
+   before_action :redirect_to_profile_if_signed_in, only: [:new, :create]
+
    def show
      @user = User.find(params[:id])
    end
    ...
  end
app/controllers/sessions_controller.rb
  class SessionsController < ApplicationController
+   before_action :redirect_to_profile_if_signed_in, only: [:new, :create]
+
    def new
      @user = User.new
    end 
    ...
  end

StaticPagesControllerはhomeアクションしかなかったので、コントローラー全体にbefore_actionを適用しました。
UsersControllerとSessionsControllerはonlyオプションをつけて対象のアクションの実行前にのみredirect_to_profile_if_signed_inメソッドが呼び出されるようにしました。

サインイン状態にして、「トップページ」「サインアップページ」「サインインページ」にそれぞれダイレクトアクセス(URL直打ち)してみてください。
全てプロフィールページにリダイレクトされます。

また、念のためサインアウトした場合は「トップページ」「サインアップページ」「サインインページ」にそれぞれアクセスできることも確認しておきましょう!

確認できましたね?
では、今日はここまでにしておきましょう!

後片付け

いつものように、次回に向けてデータを消します。

# exit
$ docker-compose down
$ docker-compose run --rm web rails db:migrate:reset

DBコンテナが立ち上がった状態だと思うのでdownさせます。

$ docker-compose down

まとめ

今回はセッションを利用してユーザーのサインイン機能、サインアウト機能を作ってみました。
さらにヘッダーの出しわけや、特定のページアクセス時にサインインしている状態だとプロフィールページにリダイレクトされる機能を作ってみました。
すでにお気づきとは思いますが、今回作成したredirect_to_profile_if_signed_inメソッドと逆のことをすればサインインしていないと遷移できないページを作り出すことも可能ですし、条件をsigned_in?ではないものにすれば、特定の条件のユーザー(例えばadminフラグを持っているユーザーとか)しかアクセスできないページを作り出すことも可能です。

それにしても、色々と動作確認することも増えてきましたね...
このまま毎回手で目で確認していくのもしんどそうです。

次回はこれを解決するためにTDD/BDD、そしてテスト自動化に取り組んでみます!

では、次回も乞うご期待!ここまでお読みいただきありがとうございました!

Next: コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.10 - TDD & Test Automation - - Qiita

ここまでのコード

Reference

Other Hands-on Links

1
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?