はじめに
第9回目です。
前回はサインアップ機能を作ってみましたね。今回はサインインやサインアウト機能を作ってまいります。
今後、サインインしていないと使えない機能や見れないページなども作っていきますので、アプリがユーザーのサインイン状況を知ることができてサインインしているユーザーを特定できるようにしましょう!
前回のソースコード
前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。
セッション
RailsはRESTfulなWebアプリケーションフレームワークなのでステートレスなアプリです。
しかし、サインイン機能を実装するとなるとそのユーザーがサインイン状態であることをアプリが知る必要がありますし、アプリはそのユーザーがどのユーザーなのかを特定する必要があります。
このために使われるのがセッション(Session)です。
Railsではセッションを管理する便利なsession
メソッドが用意されています。
今回はこのsession
メソッドを使ってサインイン状態を管理し、マイページ機能、つまりサインインしているユーザーのユーザー詳細ページに遷移する機能を作っていきましょう。
サインインしていない場合はユーザーを特定できないので、マイページには遷移させずトップページにリダイレクトをかけるようにもしていきます。
このように、セッションでサインイン状態を管理できるようになれば、その状態によるページや動作の出しわけやサインインユーザーに合わせたコンテンツの出しわけが可能になります。
今回はセッションを一つのリソースと捉えて実装していきます。
つまり、サインインしたときにsessions#create
でセッションを作成したり、サインアウトしたときにSessions#destroy
でセッションを削除したりさせます。
サインインページを作ろう
セッションはSessionsコントローラーで管理してきます。
アクションとしては、
-
new
: サインインページに遷移 -
create
: セッション作成(サインイン処理) -
destroy
: セッション削除(サインアウト処理)
が必要になってきます。
今回はrails g
コマンドに頼らずにディレクトリやファイルを作成していきますね。
ルーティングを作成する
まずはルーティングを作成しましょう。
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
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
さて、ビューファイルをコーディングしてみます。
<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' %>
基本的には前回のサインアップページと同じですね。今回はサインインなのでemail
とpassword
を入力項目に指定しました。
また、password
はサインアップページと同様、チェックボックスで表示非表示を変更できるようにしています。
『Sign in』ボタンの下にまだサインアップが終わっていないユーザー向けにサインアップページへのリンクを追加しています。
<%= link_to "こちら", sign_up_path %>
だけで適切なa
タグを作成してくれるのはやはり便利ですね。
一度http://localhost:3000/sign_in
にアクセスしておきましょう。以下のようなページが表示されたら、ここまでのコーディングは成功です!
サインインページへのリンクを作る
サインインページの形が出来上がってきたので、ここでサインインページへのリンクを以下のページにつけていこうと思います。
- ヘッダーの「Sign in」リンク
- サインアップページの下部に「登録済みの方はこちら」リンクを設置
ヘッダーの「Sign in」リンク
ヘッダーの「Sign in」リンクはまだ遷移先を指定できていませんでしたね。
やっとサインインページができあがってきたので遷移先としてsign_in_path
を指定しておきます。
...
<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」リンクを選択してみましょう。
サインインページに遷移できたら成功です!
サインアップページの下部に「登録済みの方はこちら」リンクを設置
サインインページで「登録がまだの方はこちら」リンクを設置したように、サインアップページにも「登録済みの方はこちら」リンクを設置してみましょう。
...
<%= 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
)をつくっていきます!
サインイン処理ではサインインページで入力されたemail
とpassword
からユーザーを検索します。
ユーザーがヒットすればそのユーザーのユーザー詳細ページ(users#show
)、ユーザーが存在しない、またはパスワードが誤っているような場合はエラーメッセージが表示されるようにしましょう。
少しおさらいですが、モデルを検索する場合はfind_by
メソッドを使うことで指定した属性に対して検索をすることができました。
さらに、has_secure_password
をもつモデルはauthenticate
メソッドを使ってパスワード認証ができることも思い出しましょう。
この2つを組み合わせることでユーザーの検索し認証することができそうですね。
では、create
アクションを記述していきます。
...
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モデルを代入しています。属性値はemail
にparams[:user][:email]
を持っています。
params
はフォームからのリクエストの値を取得するメソッドで、フォームのinput
タグでname="user[email]"
と定義されている値は[:user][:email]
から取得することができます。
@user
にparams[:user][:email]
の属性値をもつUserモデルを代入しているのは、この処理がエラー(ユーザーが見つからない or パスワード認証が通らない)の場合に今入力したemail
をデフォルトでフォームに入力された状態でサインインページを再度表示するためです。
これがないと入力したemail
をキープする方法がなくてユーザーは毎回メールアドレスを再入力する手間になってしまいます。(パスワードは特性上、キープしないように作っています)
user = User.find_by(email: @user.email.downcase)
先ほど@user
に設定したemail
を使ってUserを検索しています。
Userモデルを作成するときにモデル側でemail
を小文字化してからDBに保存するようにコーディングしたことを覚えているでしょうか?
今DBには必ず全て小文字のメールアドレスが保存されているので、find_by
でemail
を検索する時もdowncase
で検索文字列を小文字化して検索しています。
find_by
は検索対象が存在していた場合はモデルオブジェクトを返却し、存在しない場合はnil
を返却するメソッドであることも改めて意識しましょう。
if user && user.authenticate(params[:user][:password])
# trueの処理
else
# falseの処理
end
次に条件分岐を設けています。&&
は「アンド条件」、「かつ」を意味していますので、
user
user.authenticate(params[:user][:password])
の両方を満たした場合はtrue
、どちらか一方でも満たさない場合はfalse
の処理に分岐します。
まずuser
の条件式をみてみます。
これはuser
がnil
やfalse
でないかどうかを検証しています。
先ほど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
時に利用する版と思ってください。key
をdanger
にしているのは前回と同じで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
に倣って記述します。
...
<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
にアクセスして色々とチェックしてみましょう!
エラー系
email
やpassword
を入力していない
何も入力していない、またはどちらかだけでも入力していない場合はfind_by
かauthenticate
のいずれかが必ず失敗するのでエラーメッセージが表示されていますね。
入力したemail
のユーザーがいない
test@test.com
みたいな適当なemail
を入力して確認。
エラーメッセージが表示されているし、email
のテキストフィールドに入力してたtest@test.com
が残っているのも確認できましたね。
email
とpassword
の組み合わせが異なっている
john@sample.com
と適当なパスワードjohn1234
を入力して確認してみましょう。
こちらもエラーメッセージが表示されていますね。email
にjohn@sample.com
も残っています。
正常系
期待動作はjohn@sample.com
&password
でユーザー詳細ページに遷移することです。
やってみましょう!
ちゃんと期待通りの動作になりましたね。
セッション管理する
ここまででパスワード認証のロジックができあがりましたね。
ただ今のままでは、「認証」自体はできましたが「認証済み」という状態を管理することはできてません。
「認証済み」という状態を管理するためにセッション管理をする機能を作っていきます。
ちなみに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
module SessionsHelper
end
ヘルパーはmodule [helper_name]
で定義します。
最後に、ヘルパーをApplicationControllerから読み取るように定義しましょう。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
+ include SessionsHelper
end
SessionsHelperでsign_in
メソッドを作成する
次に、SessionsHelperを編集して、sign_in
メソッドを作成していきます。
Railsではsession
メソッドが用意されており、簡単にセッションを管理することができます。
module SessionsHelper
+
+ def sign_in(user)
+ session[:user_id] = user.id
+ end
+
end
これだけで、sign_in
メソッドを使ってuser.id
をセッションに記録することができます。
サインインページでサインインが成功した場合にsign_in
メソッドを呼び出してセッションを記録するようにしてみます。
...
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に定義します。
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 += b
はa = a + b
と同義になりますね。
||
はORを表す演算子ですので、a ||= b
はa = 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?
メソッドを作る
どんどんいきましょう。
次にサインイン状態をtrue
、false
で返却するsigned_in?
メソッドを作ってみます。
このメソッドによってユーザーのサインイン状態に合わせた処理を簡単に実装することができます。
先ほどのcurrent_user
メソッドはユーザーがサインインしている場合はそのユーザーのモデルオブジェクト、サインインしていない場合はnil
を返却するメソッドでした。
そしてRailsにはnil
の場合true
を、そうでない場合false
を返却するnil?
メソッドが用意されています。
さらに!
はtrue
とfalse
が入れ替わる否定演算子です。
これらを組み合わせればsigned_in?
メソッドが作れそうですね。
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
メソッドを作ってみましょう!
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?
メソッドを使って出しわけしてみます。
...
<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_path
、sign_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_path
はroutes.rb
でdelete
メソッドでルーティングするように定義していましたので、この形でlink_to
のHTTPメソッドを指定してあげないとルーティングされなくなってしまいます。
そういえば、sign_out_path
でルーティングされるsessions#destroy
について、コーディングをしていませんでした。
セッションを削除するsign_out
メソッドを実行し、トップページにリダイレクトするようにコーディングしましょう。
...
def destroy
+ sign_out
+ redirect_to root_path
end
...
ここまでで、サインイン機能、サインアウト機能、ヘッダーの出しわけについてコーディングが完了しました。
ここで一度動作確認をしていきましょう。
サインイン機能、サインアウト機能、ヘッダー出しわけの動作確認
サインインしていない場合
まずサインインしていない状態でhttp://localhost:3000
にアクセスしてみましょう。
すでにサインインしてしまっている場合は、ブラウザを閉じるか、「Sign out」リンクを押すかしてサインアウトしましょう。
この状態ではヘッダーは「Home」リンクと「Sign in」リンクが表示されています。
「Home」リンクを選択するとトップページへ、「Sign in」リンクを選択するとサインインページに遷移することも確認できますね。
サインインしている場合
では、サインインページからjohn@sample.com
でサインインしてみましょう。
すると、john@sample.com
のユーザー詳細ページに遷移し、ヘッダーも「Profile」リンクと「Sign out」リンクに変わっていることが確認できます。
また、「Profile」リンクを選択することでjohn@sample.com
のユーザーのユーザー詳細ページに遷移できることも確認できますね。
サインアウトを試してみる
最後にサインアウトが正しく動作するか確認してみましょう。
サインインしている状態で、「Sign out」リンクをクリックしてみてください。
トップページに遷移して、ヘッダーが未サインイン状態の場合のヘッダーに戻っていることが確認できるはずです。
これで、想定どおりに動作していることが確認できましたね。
サインアップした時もサインイン状態になるようにする
今のままではサインアップした後にサインインをしないといけなくなり面倒です。
サインアップ時(users#create
)もサインイン状態になるように更新します。
...
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保存が成功した場合に、そのユーザーでサインイン状態になるようになりました。
では、新たにユーザーをサインアップページで作成して、サインイン状態でプロフィールページに遷移することを確認してみましょう。
プロフィールページに遷移してますし、ヘッダーのリンクからサインイン後の状態になっていることがわかりますね。
サインイン状態のときに遷移できないページを作る
また、サインインしたあとにサインアップページやサインインページに遷移することは必要ありませんね。(むしろ禁止したい。)
さらに、サインイン後はトップページにもアクセスする必要はなく、ルートパスにアクセスしようとした場合、プロフィールページ(サインインユーザーのユーザー詳細ページ)に遷移させるようにしたいかもしれません。
これらを実装してみましょう。
ApplicationControllerにサインインしている場合、強制的にプロフィールページに遷移させるメソッドを作成します。
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
メソッドを使えば容易です。
class StaticPagesController < ApplicationController
+ before_action :redirect_to_profile_if_signed_in
+
def home
end
end
class UsersController < ApplicationController
+ before_action :redirect_to_profile_if_signed_in, only: [:new, :create]
+
def show
@user = User.find(params[:id])
end
...
end
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
- Ruby on Rails チュートリアル:実例を使って Rails を学ぼう
- Ruby on Railsのparamsメソッドの使い方を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン
- Ruby 当たり前のtrue/false - Qiita
- [WIP/初学者]flashとflash.nowの使い分け - Qiita