【Rails】ログイン機能を実装する

[TODO](https://qiita.com/d0ne1s/items/5e63dde992f20f25b8bb)アプリにログイン機能を実装します。

・[【Rails】バリデーションを実装する](https://qiita.com/d0ne1s/items/822f66d16d65ae28162b)
・[【Rails】パーシャルを利用する](https://qiita.com/d0ne1s/items/154a5faa5ae09466e637)

##Userモデルを作成
`name`、`email`、`password_digest`という属性を持つUserモデルを作成します。`password`ではなく、必ず`password_digest`という属性を設定してください。

```terminal
$ rails g model User name:string email:string password_digest:string
```
```terminal
$ rails db:migrate
```
##ハッシュ化したパスワードで認証ができるようにする
パスワードを平文で保存することは危険です。ハッシュ化したパスワードをDBに保存し、ユーザーのログイン時に入力されたパスワードをハッシュ化した値と比較するようにしましょう。
###`bcrypt`を追加
ハッシュ化を行うためのgemを追加します。

```ruby:/Gemfile
source 'https://rubygems.org'

gem 'rails',   '5.1.6'
gem 'bcrypt',  '3.1.12'
.
.
.
```
```terminal
$ bundle install
```
###Userモデルに`has_secure_password`を追加
`password_digest`という属性を持つモデルに`has_secure_password`と書き込むことで、以下の3つが可能になります。

1. セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存できるようになる。
2. 仮想的な属性`password`と`password_confirmation`が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される。
3. `authenticateメソッド`が使えるようになる (引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalseを返すメソッド) 。

```ruby:/app/models/user.rb
class User < ApplicationRecord
  has_secure_password
end
```

###ログイン用のダミーデータを作成
`password_digest`という属性がある場合、ユーザー登録時には`password`、`password_confirmation`の2つの属性を登録します。

```terminal
$ rails c
```
```terminal
irb(main):001:0> User.create(name: "sampleTarou", email: "tarou@example.com", password: "hogehoge", password_confirmation: "hogehoge")
```


##セッション管理の基盤を作成
セッションを利用してユーザーを判別する仕組みを作ります。
Sessionsコントローラを生成するときに(密かに)自動生成される`セッション用ヘルパーモジュール`を利用すると、`sessionメソッド`が使えるようになり、セッション管理の仕組みを簡単に実装することができます。

具体的には、ログイン時に
`session[:user_id] = user.id`でブラウザのcookieにハッシュ化したユーザーidを保存し、ページ遷移の度に
`@current_user = User.find_by(id: session[:user_id])`でidを元にログイン中のユーザーの情報を取得できるようになります。

###Sessionsコントローラーを作成
`new`、`create`、`destroy`アクションを持ったSessionsコントローラーを作成します。
viewが必要なのはログインページを表示するアクションである`new`アクションだけなので、余計なファイルを生成しないために`create`、`destroy`アクションは手動で追加します。

```terminal
$ rails g controller Sessions new
```
```ruby:/app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
  end

  def destroy
  end
end
```
ルーティングを設定します。

```ruby:/config/routes.rb
Rails.application.routes.draw do
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
.
.
end
```

###ログインページを作成
セッションにはSessionモデルというものがなく、`@task`のようなインスタンス変数に相当するものもありません。

本来`form_for(@task)`と書くだけで、「フォームのactionは/taskというURLへのPOSTである」と自動的に判定しますが、セッションの場合はリソースの名前とそれに対応するURLを具体的に指定する必要があります。

```erb:/app/views/sessions.new.html.erb
<h1>ログイン</h1>
<%= form_for(:session, url: login_path) do |f| %>
  <%= f.label :email %>
  <%= f.email_field :email %>
  <%= f.label :password %>
  <%= f.password_field :password %>
  <%= f.submit "Log in" %>
<% end %>
```

###ログイン状態に応じてトップページの表示を切り分け
ログイン前はログイン画面へのリンク、ログイン後はタスク一覧とログアウトボタンが表示されるようにします。(`logged_in?`メソッドについては後述)


```erb:/app/views/tasks/index.html.erb
<h1>TODOアプリ</h1>
<% if logged_in? %>
  <%= render 'tasks/logged_in' %>
<% else %>
  <%= render 'tasks/not_logged_in' %>
<% end %>
```
```erb:/app/views/tasks/_logged_in.html.erb
<h2>タスク一覧</h2>
<table>
    <thead>
      <tr>
        <th>タスク名</th>
      </tr>
    </thead>
    <tbody>
      <% @tasks.each do |task| %>
        <tr>
          <td><%= task.title %></td>
          <td><%= link_to "編集", edit_task_path(task) %></td>
          <td><%= link_to "削除", task_path(task), method: :delete %></td>
        </tr>
      <% end %>
    </tbody>
</table>
<%= link_to "タスク追加", new_task_path %>
<%= link_to "ログアウト", logout_path, method: :delete %>
```
```erb:/app/views/tasks/_not_logged_in.html.erb
<p>タスクの管理ができるアプリです。まずはログインをしてください。</p>
<%= link_to "ログイン", login_path%>
```

##セッション用ヘルパーメソッドを作成
###`log_in`メソッド
ブラウザのcookieに、ハッシュ化したユーザーidを保存するメソッドです。

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

###`current_user`メソッド
cookieに保存されたユーザーidを元に、ユーザーの情報を取得するメソッドです。

```ruby:app/helpers/sessions_helper.rb
module SessionsHelper
.
.
 # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    if session[:user_id]
     #@current_user = @current_user || User.find_by(id: session[:user_id])と同じ意味
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
end
```
`find`ではなく`find_by`で検索をしているのは、ユーザーidが存在しなかった場合`find`を使うと例外が返されてしまうためです。`find_by`を使うと`nil`が返されます。

###`current_user?`メソッド
マイページなど、ログイン中のユーザーにしか表示させたく無いページの`before_action`でよく使われます。

```ruby:app/helpers/sessions_helper.rb
module SessionsHelper
.
.
#受け取ったユーザーがログイン中のユーザーと一致すればtrueを返す
def current_user?(user)
  user == current_user
end
```

###`logged_in?`メソッド
現在のユーザーがログインしているかどうかを判別するメソッドです。ログイン状況に応じて表示する画面を切り替えたりする処理が簡単に実装できるようになります。

```ruby:app/helpers/sessions_helper.rb
module SessionsHelper
.
.
 # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end
end
```

###`log_out`メソッド
ブラウザのcookieに保存されているユーザーidを削除するメソッドです。

```ruby:app/helpers/sessions_helper.rb
module SessionsHelper
.
.
 # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end
```

##ログイン、ログアウト処理を実装
###Sessionヘルパーモジュールを読み込む
作成したヘルパーメソッドを全てのページで使えるようにするために、`Applicationコントローラ`から読み込みを行います。（heplerメソッドはデフォルトで全てのviewファイルから呼び出すことができますが、controllerから呼び出す場合には以下のように明示的に読み込む必要があります。）

```ruby:/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include SessionsHelper
end
```
###`create`アクションを作成
ログインページから送信された情報を受け取り、ログイン処理を行うアクションです。

```ruby:app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to root_url
    else
      render 'new'
    end
  end
.
.
```

###`destroy`アクションを作成
cookieに保存されたユーザーidを削除し、ログアウトを行うアクションです。

```ruby:app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end
```

##タスクをいじる前にログイン状況をチェック
タスクの表示、作成、編集、削除の前にログイン状況をチェックするように設定します。
###`logged_in_user`アクションを作成
ログインしていないユーザーをログインページにリダイレクトするアクションを作成します。`application_controller`に追加することで、**全てのコントローラーで利用できる**ようになります。

```ruby:/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
.
.
  private
   # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        redirect_to login_url
      end
    end
end
```

###`before_action`を設定
コントローラーの冒頭に`before_action :<privateアクション名>, only:[:アクション名]`と書き込むことで、アクションが実行される前に、指定したprivateアクションを実行することができます。

```ruby:/app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  before_action :logged_in_user, only:[:edit, :update, :destroy]
.
.
.
```
##終わりに
予想以上に分量が多くなりました。今回実装した内容だと、ブラウザを閉じる度にセッションが切れてログアウトします。ブラウザを閉じてもセッションを維持する方法もあるので、またいつか他の記事でまとめたいと思います。