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

[sorcery公式](https://github.com/Sorcery/sorcery)
[sorceryメソッド](https://github.com/Sorcery/sorcery/blob/7c5fee441e03779ee2888ecef317440cd10a17e7/lib/sorcery/controller.rb#L76-L78)

## sorceryのインストール

```ruby:Gemfile
gem 'sorcery'
```

## Userモデルを作成する
- `bundle exec rails g sorcery:install`を実行すると、以下のファイルが作成される。 
 - migrationファイル(db/migrate/〇〇〇〇_sorcery_core.rb)
 - initializerファイル(config/initializers/sorcery.rb）
 - Userモデル

- 暗号化されたパスワード(`crypted_password`)やソルト(`salt`)というカラムをユーザーに編集/閲覧させたくない。つまりビューで使用したくない。
 - 以下のように、「仮想的な」passwordフィールドを使用し、データベースに暗号化される前のパスワードをビューで扱う。  
 →このpassword属性はカラムに対応していないため、平文のパスワード情報がDBに保存されることは無い。  
 →パスワードは暗号化され、DBに保存される。
  
```erb:app/views/user_sessions/new.html.erb
  <%= f.label :password %>
  <%= f.password_field :password%>

  <%= f.label :password_confirmation %>
  <%= f.password_field :password_confirmation%>
```

- Userモデルに以下のように記述することで、Userクラスがsorceryを使えるようになる。
  - `validates :password, confirmation: true`で、passwordというDBに存在しない仮想的な属性(virtual attributes)が追加される。
  - `if: -> { new_record? || changes[:crypted_password] }`で、ユーザーがパスワード以外のプロフィール項目を更新したい場合に、パスワードの入力を省略できるようになる。

```ruby:app/models/user.rb
class User < ActiveRecord::Base
  authenticates_with_sorcery!

  validates :password, length: { minimum: 8 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: true
end
```


## 認証機能

- ルーティング

```ruby:config/routes.rb
  get '/login', to: 'user_sessions#new', as: :login
  post '/login', to: 'user_sessions#create'
  delete 'logout', to: 'user_sessions#destroy', as: :logout
```

- require_loginメソッド
  - application_controller.rbに記述し共通化。
    - 各コントローラで`only`を使用し、アクションごとにログインを要求する。
- not_authenticatedメソッド
  - ログインできなかった(認証されなかった)場合に、このメソッドに記述した処理が行われる。

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :require_login

  private

  def not_authenticated
    redirect_to login_path, danger: "ログインしてください"
  end
end
```


- `login`メソッドで、認証処理が行われる。
  - `@user = login(params[:email], params[:password])`でemailによるUser検索、パスワードの検証を行い、正常に処理できるとセッションデータにUserレコードのid値を格納する、という処理が行われている。
- `logout`メソッドで、セッションをリセットする。
- `redirect_back_or_to`メソッド・・・例えば、掲示板ページにアクセスしようとしたユーザにログインを要求する場合、`require_login`メソッドでユーザをログインページに誘導し、ログインが成功したら、最初に訪れようとしていた掲示板ページにリダイレクトさせるということが可能になる。

```ruby:app/controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
  skip_before_action :require_login, only: %i[create new]

  def new; end

  def create
    @user = login(params[:mail], params[:password])
    if @user
      redirect_back_or_to root_path, success: "ログインに成功しました"
    else
      flash.now[:danger] = "ログインに失敗しました"
      render :new
    end
  end

  def destroy
    logout
    redirect_back_or_to root_path, success: "ログアウトしました"
  end
end
```

```erb:app/views/user_sessions/new.html.erb
<%= form_with url: login_path, local:true do |f| %>

    <%= f.label :email, "メールアドレス" %>
    <%= f.text_field :email%>

    <%= f.label :password, "パスワード" %>
    <%= f.password_field :password%>

    <%= f.submit "ログイン" %>
<% end %>
```

## ビューでログイン判定を使用し、表示するテンプレートを変える。

- sorceryの`logged_in?`メソッドを使用。

```erb:app/views/layouts/application.html.erb
<% if logged_in? %>
  // ログイン後のテンプレートを表示
  <% else %>
  // ログイン前のテンプレートを表示
<% end %>
```

## [参考]上記で使用したsorceryが用意してくれているメソッドの一例

```ruby
module InstanceMethods
  # To be used as before_action.
  # Will trigger auto-login attempts via the call to logged_in?
  # If all attempts to auto-login fail, the failure callback will be called.
  def require_login
    unless logged_in?
      session[:return_to_url] = request.url if Config.save_return_to_url && request.get? && !request.xhr?
      send(Config.not_authenticated_action)
    end
  end

  # Takes credentials and returns a user on successful authentication.
  # Runs hooks after login or failed login.
  def login(*credentials)
    @current_user = nil

    user_class.authenticate(*credentials) do |user, failure_reason|
      if failure_reason
        after_failed_login!(credentials)

        yield(user, failure_reason) if block_given?

        return
      end

      old_session = session.dup.to_hash
      reset_sorcery_session
      old_session.each_pair do |k, v|
        session[k.to_sym] = v
      end
      form_authenticity_token

      auto_login(user)
      after_login!(user, credentials)

      block_given? ? yield(current_user, nil) : current_user
    end
  end

def logout
  if logged_in?
    user = current_user
    before_logout!
    @current_user = nil
    reset_sorcery_session
    after_logout!(user)
  end
end

def logged_in?
  !!current_user
end

def current_user
  unless defined?(@current_user)
    @current_user = login_from_session || login_from_other_sources || nil
  end
  @current_user
end

# used when a user tries to access a page while logged out, is asked to login,
# and we want to return him back to the page he originally wanted.
def redirect_back_or_to(url, flash_hash = {})
  redirect_to(session[:return_to_url] || url, flash: flash_hash)
  session[:return_to_url] = nil
end

# The default action for denying non-authenticated users.
# You can override this method in your controllers,
# or provide a different method in the configuration.
def not_authenticated
  redirect_to root_path
end

# login a user instance
#
# @param [<User-Model>] user the user instance.
# @return - do not depend on the return value.
def auto_login(user, _should_remember = false)
  session[:user_id] = user.id.to_s
  @current_user = user
end
```

## おわり

以上です！
READMEを読んでおけば、なんとなくでsorceryのログイン機能を使えちゃったりもしますが、

- ステートレスなHTTP通信上における一連のリクエスト/レスポンスの中で、どのようにユーザーの状態を維持させているのか？
- なぜpasswordという仮想的な属性をフォームで使用するのか？

などログイン機能の仕組みを理解した上で実装することが重要だと思います！