Sessionsコントローラーと対応するルーティング・ビューの作成
ログイン/ログアウトを制御するための最もメジャーな方法はセッションを使うことです。セッションは、サーバーとクライアント(サーバーにアクセスする端末のブラウザ等)の接続を担保します。セッション管理のためにSessionsコントローラーを作成しましょう。
$ bin/rails g controller Sessions new
実行コマンドの末尾の「new」はnewアクションのみ、という意味です。ログインに必要なビューはnewアクションに対応するものだけで良いため、newを指定しています。ただ、このままではルーティングにnewアクションしか定義されません。実際はログインに対応するcreateアクション、ログアウトに対応するdestroyアクションも必要となるのでルーティングを追加しましょう。
Rails.application.routes.draw do get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' …(中略)…
なお、newアクションのルーティングも/loginでアクセスできるように修正しています。
newアクションに対応するビューファイルを作成します。
…(中略)…
<%= form_for(:session, url: login_path) do |f| %>
<div class="field">
<%= f.label :email %>
<%= f.email_field :email %>
</div>
<div class="field">
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<div class="actions">
<%= f.submit "Log in" %>
</div>
<% end %>
<p>New user? <%= link_to "Sign up now!", new_user_path %></p>
…(中略)…
Usersコントローラーのnewアクションに対応するフォームのビューファイルである、app/views/users/_form.html.erbと似ていますが、form_forの引数に違いがあることが分かるでしょう。form_forメソッドは、Usersリソースのように対応するコントローラーとモデルが一致している場合は第2引数を省略できますが、Sessionsリソースでは対応するモデルがないため、第2引数に対応するURLを指定する必要があります。ここではlogin_pathが指定されているため、ルーティングで対応するSessionsコントローラーのcreateメソッド内でparams[:session]とすることにより、フォームの情報を取得することができます。
ログインの実装
それではログイン機能をSessionsコントローラーに実装していきましょう。現状はジェネレーターコマンドで作成したSessionsコントローラーに、空のnewメソッドが追加されているだけです。これに、ログインに対応するcreateメソッドの実装を行っていきます。
class SessionsController < ApplicationController
…(中略)…
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
reset_session
session[:user_id] = user.id
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
…(中略)…
先ほど解説した通り、フォームで入力されたemailはparams[:session][:email]で取得することができます。後に続くdowncaseはRubyのメソッドで、大文字を全て小文字に変換します。メールアドレスは大文字と小文字の区別がないので、フォームで大文字が入力された場合にも、小文字でusersテーブルからユーザーレコードを検索できるようにする必要があります。そのためdowncaseしています。
次のif文では該当するuserが見つかり、なおかつフォームで入力されたパスワードでログインが成功した場合にのみ、直下にあるブロックの処理が実行されます。そうでない場合、else以下の処理に続くようになっています。authenticateはhas_secure_passwordメソッドを追加すると自動的に利用できるようになるメソッドで、引数にパスワードを指定することでログインが成功したか否かを返却します。
ログインが成功した場合、Railsが提供するreset_sessionメソッドでセッションを初期化した後、session変数にユーザーIDを格納し、次の行で該当ユーザーの詳細画面にリダイレクトしています。
[解説]ルーティングの省略形
「redirect_to user」の「user」はちょっと不思議に感じるかもしれません。このコードは「redirect_to user_url(user)」と同じ意味です。これなら分かるのではないでしょうか。そう、「/users/[ユーザーID]」ですね。
userはUserモデルインスタンスなので、対応するルーティングをRailsが類推してくれているのです。
引き続きelse文以下を見てみましょう。flash.now[:danger]のflashとは、Railsが提供するメッセージングの仕組みで、簡単なメッセージを画面に表示することができます。flash.nowとすることにより現在のアクションでビューがレンダリングされた場合にのみ、設定されたメッセージを表示してくれます。後に続く[:danger]はflashメッセージの種類を定義しています。ここではログインに失敗したことを、ユーザーに気付いてもらうべき致命的なエラーとして通知するためにdangerとしています。
しかし、このままではエラーメッセージが表示されません。ビューファイルに対して、flashメッセージが設定された場合に表示するブロックを記述する必要があります。flashメッセージはビュー共通で出力したいので、ここでは共通のビューテンプレートであるlayoutsを用いて記述します。
…(中略)…
<body>
<% flash.each do |message_type, message| %>
<div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>
<%= yield %>
</body>
…(中略)…
ソースコードのようにflashオブジェクトをeachでループすることで、message_typeごとに設定されたメッセージを表示することができます。先の例ではmessage_type=dangerのメッセージが「Invalid email/password combination」となります。
また、該当メッセージのdivのclassにmessage_typeに応じたclassを設定しています。先の例ではalert-dangerクラスが付与されます。
それでは実際に/loginにアクセスしてみましょう。
あらかじめユーザー登録しておいたユーザーで、まずはパスワードを空にしたままでログインを試みます。先ほど指定した、メールアドレスとパスワードが無効であるエラーメッセージが表示されることを確認できると思います。
今度は正しいパスワードでログインを試みてみましょう。ログインが成功するとログインユーザーの詳細ページ(本稿では/users/1)にリダイレクトするはずです。
