Rails6.1ハンズオン(3)～ユーザー機能編

# はじめに
[Rails6.1ハンズオン(2)](https://qiita.com/Hirororocky/items/910464c0ae74c8f1ea12)の続きです。
環境は[前回](https://qiita.com/Hirororocky/items/910464c0ae74c8f1ea12)を参考にしてください。

いつRails6.1固有の機能にたどり着くのか...

今回はメールアドレスとパスワードを使った、
ユーザーログイン・ログアウトの機能を追加します。
コードは[Github](https://github.com/hirorocky/Rails6.1_hands_on)にあげています。章ごとにコミットしてますので、参考にしていただければ幸いです。

# やること

Railsでユーザー関連の機能をつけるときは、Railsチュートリアルのように自前で全部用意することも可能ですが、今回は楽をするためにGemを使います。

定番は「devise」でしょうか？仕事でもよく使いますが、せっかくなので、「[sorcery](https://github.com/Sorcery/sorcery)」を使ってみようと思います。

最近(21/2/17くらい)v0.16.0になり、Rails6に対応したようです。ぴったりですね！

# 実装

参考にさせていただくもの：
[Sorcery/sorcery](https://github.com/Sorcery/sorcery/wiki/Simple-Password-Authentication)
[docker-compose下でrails newして Rails6.1 + Sorcery を試す（ Sorcery の仕組み少し解説）](https://zenn.dev/junki555/articles/e4172b3b79c29e)
[シンプル認証gem sorceryを完全入門するで！！ - Qiita](https://qiita.com/babashunsu/items/9937b0a2e08d318edece)

## 4-1. sorcery gem導入、Userクラスをつくる

Gemfileにて以下を追加：

```ruby
gem 'sorcery'
gem 'bcrypt'
```

↓これに準拠して進めます
[Sorcery/sorcery](https://github.com/Sorcery/sorcery/wiki/Simple-Password-Authentication)

コマンドを実行：

```bash
bundle install
bundle exec rails g sorcery:install
```

> File unchanged! The supplied flag value not found! app/models/user.rb

と赤字で出た。なんだろう... コマンドオプションを設定するとでないのかな？
とりあえずヨシ！

gコマンドで生成されたmigrationファイルでテーブル名がUserになっているのをusersに直す。（Userのまま進めたらエラーになりました）

db/migrate/（数字）_sorcery_core.rb

```ruby
class SorceryCore < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :email,            null: false
      t.string :crypted_password
      t.string :salt

      t.timestamps                null: false
    end

    add_index :users, :email, unique: true
  end
end
```

Userモデルのバリデーション設定を追加する。

app/models/user.rb

```ruby
class User < ApplicationRecord
  authenticates_with_sorcery!

  validates :email, uniqueness: true, presence: true

  validates :password, length: { minimum: 3 }, 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] }
end
```
チュートリアルではemailのバリデーションに`presence: true`が入っていなかったので追加。

## 4-2. ユーザー作成・編集機能

```bash
rails g controller Users new create edit update
```

app/views/layouts/_header.html.haml（セッション機能は後で作る）

```haml
%nav.navbar.navbar-expand-lg.navbar-dark.bg-dark
  .container-fluid
    = link_to root_path, class: 'navbar-brand' do
      Rails6.1掲示板
    %button.navbar-toggler{"aria-controls" => "navbarSupportedContent", "aria-expanded" => "false", "aria-label" => "Toggle navigation", "data-bs-target" => "#navbarSupportedContent", "data-bs-toggle" => "collapse", :type => "button"}
      %span.navbar-toggler-icon
    #navbarSupportedContent.collapse.navbar-collapse
      %ul.navbar-nav.me-auto.mb-2.mb-lg-0
        %li.nav-item
          = link_to root_path, class: 'nav-link' do
            トップ
        %li.nav-item
          = link_to new_community_path, class: 'nav-link' do
            コミュニティを作る
      .d-flex
        -if current_user
          .nav-item.dropdown
            %a.nav-link.text-white.dropdown-toggle#navbarDropdownMenuLink{href: "#", role: "button", "data-bs-toggle": "dropdown", "aria-expanded": "false"}
              = current_user.email
            %ul.dropdown-menu{"aria-labelledby": "navbarDropdownMenuLink"}
              %li
                = link_to t('header.to_user_edit'), edit_user_path(current_user.id), class: 'dropdown-item'
              %li
                = link_to t('header.to_logout'), :logout, method: :post, class: 'dropdown-item'
        - else
          = link_to t('header.to_register'), new_user_path, class: 'btn btn-primary me-2'
          = link_to t('header.to_login'), :login, class: 'btn btn-secondary'
```

app/controllers/users_controller.rb（require_loginメソッドは後で作ります）

```ruby
class UsersController < ApplicationController
  skip_before_action :require_login, only: %i[new create]
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to root_path, notice: 'ユーザーを新規作成しました。'
    else
      render :new
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      redirect_to root_path, notice: 'ユーザー情報を変更しました。'
    else
      render :edit
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation)
  end
end
```

app/views/users/new.html.haml

```haml
%h1= t '.title'

= form_with model: @user do |f|
  = render 'shared/error_messages', object: f.object
  
  = f.label :email, class: 'form-label'
  = f.text_field :email, class: 'form-control mb-2'

  = render 'form', form: f
  = f.submit t('.submit'), class: 'btn btn-primary'
```

app/views/users/edit.html.haml

```haml
%h1= t '.title'

= form_with model: @user do |f|
  = render 'shared/error_messages', object: f.object
  = render 'form', form: f
  = f.submit t('.submit'), class: 'btn btn-primary'
```

app/views/users/_form.html.haml

```haml
= form.label :password, class: 'form-label'
= form.password_field :password, class: 'form-control mb-2'

= form.label :password_confirmation, class: 'form-label'
= form.password_field :password_confirmation, class: 'form-control mb-2'
```

app/views/shared/_error_messages.html.haml

```haml
- if object.errors.present?
  .card.mb-2.border-danger
    .card-header.bg-danger.text-white
      エラーがあります
    .card-body
      %ul
        - object.errors.each do |e|
          %li= e.full_message
```

config/locales/model.ja.yml
config/locales/views/users/ja.yml
config/routes.rb
をいじる（コードは省略、リポジトリをみてください）

## 4-3. セッション機能

```bash
rails g controller UserSessions new create destroy
```

app/controllers/application_controller.rb

```ruby
class ApplicationController < ActionController::Base
  before_action :require_login

  private

  def not_authenticated
    redirect_to login_path, alert: 'ログインしてください。'
  end
end
```
require_loginはsorceryにもともと入っているメソッドらしい。
ログインしていなかったらnot_authenticatedメソッドを呼ぶという挙動。
not_authenticatedも忘れずに定義しておく。

app/controllers/user_sessions_controller.rb

```ruby
class UserSessionsController < ApplicationController
  skip_before_action :require_login, only: %i[new create]
  def new; end

  def create
    @user = login(params[:email], params[:password])

    if @user
      redirect_back_or_to(root_path, notice: 'ログインしました。')
    else
      flash.now[:alert] = 'ログインできませんでした。'
      render :new
    end
  end

  def destroy
    logout
    redirect_to(login_path, notice: 'ログアウトしました。')
  end
end
```

login(メアド, パスワード)でログインできる。
redirect_back_or_toで#not_authenticatedでリダイレクトする前のページに戻せる。
logout()でログアウト。

app/views/user_sessions/new.html.haml

```haml
%h1= t '.title'
= form_with url: login_path, method: :post do |f|
  = f.label t('.email'), class: 'form-label'
  = f.text_field :email, class: 'form-control mb-2'

  = f.label t('.password'), class: 'form-label'
  = f.password_field :password, class: 'form-control mb-4'

  = f.submit t('.submit'), class: 'btn btn-primary'
```

config/routes.rb

```ruby
Rails.application.routes.draw do
  root to: 'communities#index'

  get 'login' => 'user_sessions#new', as: :login
  post 'login' => 'user_sessions#create'
  post 'logout' => 'user_sessions#destroy', as: :logout
  resources :users

  resources :communities, only: %i[index new create show] do
    resources :comments, only: %i[new create]
  end
end
```

## 4-4. おまけ

flashメッセージを表示する。

app/views/layouts/application.html.haml

```haml
!!!
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %title Rails61HandsOn
    %meta{:content => "width=device-width,initial-scale=1", :name => "viewport"}/
    = csrf_meta_tags
    = csp_meta_tag
    = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
    = stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
  %body
    = render '/layouts/header'
    - if flash[:notice]
      .alert.alert-info.m-2= flash[:notice]
    - if flash[:alert]
      .alert.alert-warning.m-2= flash[:alert]
    .container-fluid.pt-4.px-4
      = yield
```