【Rails】deviseを用いたユーザー新規登録機能の実装（基礎〜少し応用）

#はじめに
Ruby on Rails を用いてWEBアプリを作成中です。
ユーザー新規登録の機能を実装するために、「devise」というgemを用いることにしました。
備忘もかねて、本文に実装した内容を残しておきます。
errorで困ったポイントも残しておきますので、参考にしていただけると幸いです^^

専門用語や参考にさせていただいた記事は最後に記載させていただいております。
「これ、なんで記述してるんだろう？」と、私自身が初めて作成した時に感じたポイントでもありますので、
初心者の方は気になるところかとも思います。
そちらも参照していただければと思います。

今回の方法はあくまで一つの実装方法でしかなく、やり方は色々あるのと、
私もまだ未熟なため、わかりにくい記述や余分な記述もございますし、調べきれてない、試しきれてないところもございます。
共にブラッシュアップしていければとも思ってますので、ご指摘もいただければと思います。

記載しきれなかった部分は、随時更新か、別途記事を用意いたします。

#実装の目標
今回は以下の画像のような表示を目標として実装しました。
全てを解説するのは大変だったので、「ニックネーム」「メールアドレス」「パスワード」「生年月日」のみに絞っておりますのでご了承ください。
カラムのカスタマイズ、エラー表示、パスワードの表示/非表示、生年月日の選択など、「devise」の基本だけでなく、少し工夫が必要な内容も網羅してるかと思いますので、参考になれば幸いです。
[![Screenshot from Gyazo](https://gyazo.com/6bbf2e175a51422afb4bd706e8c4e681/raw)](https://gyazo.com/6bbf2e175a51422afb4bd706e8c4e681)
[![Screenshot from Gyazo](https://gyazo.com/7f91611c807722d19d5103bcd97ea9df/raw)](https://gyazo.com/7f91611c807722d19d5103bcd97ea9df)


#1. deviseのインストール手順
##　1-1. Gemfileに追記

```Gemfile
（前略）
gem 'devise'
（後略）
```
※注意
「development、test、production」のgroup内に記述すると特定の環境でのみ使用する設定となりますので、group外に記述するようにしてください。

##　1-2. コマンドの実行(インストール)

```
$ bundle install
$ rails g devise:install
```
#2. Userモデルを作成
インストール後、モデルとテーブルを作成するために、以下をターミナルで実行してください。
通常のモデル作成とはコマンドが違うので注意してください。

```
$ rails g devise user 
```
マイグレーションファイルとモデルファイルが出来るので、以下のように記述ください。

```2020*****_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.string :nickname, null: false
      t.string :email, null: false, default: ""
      t.string :password, null: false, default: ""
      t.string :encrypted_password, null: false, default: ""
      t.date :birthday, null: false

      # 〜省略〜

    end

    add_index :users, :email, unique: true

    # 〜省略〜

  end
end
```

```app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable,  :validatable

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  VALID_PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i

  # 〜省略〜

  validates :nickname, presence: true, length: { maximum: 20 }
  validates :email, presence: true, uniqueness: true, format: { with: VALID_EMAIL_REGEX }
  validates :password, presence: true, length: { minimum: 7 }, format: { with: VALID_PASSWORD_REGEX }
  validates :birthday, presence: true

  # 〜省略〜

end
```
モデル「user.rb」について
「VALID_EMAIL_REGEX」「VALID_PASSWORD_REGEX」は正規表現によって、特定の文字を弾くようにしております。
「presence: true」は記述することで、空で登録することを弾くようにしてます。
「length: { maximum: 20 }」と「length: { minimum: 7 }」は文字数制限です。

ここで、以下の「devise.rb」に記述されてる「config.password_length = ６..128」についても、次のように編集しておきます。
この数字は文字制限を表してます（つまり、minimumが「6」です）。
理由はモデル「user.rb」より、今回passwordは7文字以上としているため、minimumが「7」である必要があります。
また、こちらを編集後はサーバーの再起動をしないと、反映されませんのでご注意を。

「devise.rb」の「config.password_length = ６..128」を編集した理由は、そのままにするとモデル「user.rb」側のバリデーション「length: { minimum: 7 }」と「devise.rb」側のバリデーション「config.password_length = ６..128」が共存することになるため、2つのバリデーションに引っかかって、2つのエラー表示されてしまうからです。
どちらか片方を削除する方法もありかと思いますが、経験もかねて、どちらも残す方法しか試せておりません。

```config/initializers/devise.rb
# frozen_string_literal: true

# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
  Rails.application.credentials[:secret_key_base]

  # 〜省略〜

  config.password_length = 7..128

  # 〜省略〜

end
```
最後にmigrationファイルをデータベースに反映するために、以下のコマンドを実行。

```
$ rails db:migrate
```

#3. コントローラーの編集
Viewなどを先に提示してもよかったのですが、エラー表示などについての説明を一緒にした方が良いと判断し、ルーティングやコントローラーの設定を先に説明いたします。

まずはコントローラーから。
今回はユーザー新規登録（サインアップ/sign_up）の実装についてですので、コントローラーは「application_controller.rb」のみで大丈夫です。
以下のように記載します。

「configure_permitted_parameters」メソッドの定義をしてますが、deviseをインストールすることでdevise_parameter_sanitizerのpermitメソッドが使えるようになります。これがストロングパラメータに該当する機能です。サインアップ時に入力された「nickname、email、password、birthday」のキーの内容の保存を許可しています。

```app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    added_attrs = [ :nickname, :email, :password, :birthday ]
    devise_parameter_sanitizer.permit :sign_up, keys: added_attrs
  end
end
```

#4. ルーティングの編集
以下のように記載してください。

注意点としては、「routes.rb」の順番です。
「users/:id」が「users/sign_in」を包括してエラーが発生しないように、「devise_for :users」を「resources :users」より先に書く必要があります。

また、deviseでサインアップする際に、例えばパスワードを忘れて保存しようとしてエラーが発生すると、親ページにリダイレクトされます。
つまり、サインアップページ「/users/sign_up」でエラーが発生した場合、「/users」にリダイレクトされてしまいます。
そのまま登録するとルーティングエラーが表示されます。

これを回避するために、「devise_scope :users」以下の記述を追記して、任意のルーティングをさせています。
方法はいくつかありますが、今回は手っ取り早そうです。

```config/routes.rb
Rails.application.routes.draw do

  devise_for :users

  devise_scope :users do
    get '/users', to: redirect("/users/sign_up")
  end

  # 〜省略〜

end
```

#5. Veiwの編集
まずは、以下のコマンドを実行してください。Modelに対応するViewが生成されます。
Modelには「users」などを入れる文献が多ので、以下は「$ rails g devise:views users」で実行してます。
私が学んだ某スクールで最初に学んだ時は「devise」でしたので、ユーザーマイページと分けるために、私の場合は「devise」で生成してますが。

```
$ rails g devise:views Model
```
>※生成されるViewファイル
　app/views/users/confirmations/new.html.erb
　app/views/users/mailer/confirmation_instructions.html.erb
　app/views/users/mailer/password_change.html.erb
　app/views/users/mailer/reset_password_instructions.html.erb
　app/views/users/mailer/unlock_instructions.html.erb
　app/views/users/passwords/edit.html.erb
　app/views/users/passwords/new.html.erb
　app/views/users/registrations/edit.html.erb
　app/views/users/registrations/new.html.erb
　app/views/users/sessions/new.html.erb
　app/views/users/shared/_links.html.erb
　app/views/users/unlocks/new.html.erb

　
##　５-1. HTML/HAMLの記述
「registrations/new.html.erb」をユーザー新規登録ページとして編集していきます。
また、以下の通り、HTMLからHAMLに変換して記述してます。

```app/views/users/registrations/new.haml.erb
.signup__main
    .signup__main__content
      %h2.signup__main__content__head
        会員情報入力
      
      = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|

 　　　　# 〜省略〜

              .field
                .field-label 
                  = f.label 'ニックネーム'
                %span.form-require 必須
                .field-input
                  = f.text_field :nickname, class: "field-input-full", autofocus: true, autocomplete: "nickname", placeholder: "例）メルカリ太郎"
                .input-error
                  = resource.errors.full_messages_for(:nickname)[0]

              .field
                .field-label
                  = f.label 'メールアドレス'
                %span.form-require 必須
                .field-input
                  = f.email_field :email, class: "field-input-full", autofocus: true, autocomplete: "email", placeholder: "PC・携帯どちらでも可"
                .input-error
                  = resource.errors.full_messages_for(:email)[0]

              .field
                .field-label 
                  = f.label :password, 'パスワード'
                  %span.form-require 必須
                .field-input.toggle
                  = f.password_field :password, class: "field-input-full", autocomplete: "off", autocomplete: "password", placeholder: "7文字以上の半角英数字", id:'password'
                  .checkbox-field
                    %input#js-passcheck.checkbox.js-password-toggle{type: "checkbox"}
                    %label.btn-label.js-password-label{for: "js-passcheck"}
                      %i.fas.fa-eye-slash{style: "font-size:20px;color:#808080"}
                .input-error
                  = resource.errors.full_messages_for(:password)[0]
                .field-info
                  ※ 英字と数字の両方を含めて設定してください

              .field
                .field-label 
                  = f.label '生年月日'
                %span.form-require 必須
                .birthday-select
                  = raw sprintf(f.date_select( :birthday, use_two_digit_numbers: true, prompt: "--", start_year: Time.now.year, end_year: 1900, date_separator: '%s'), '年 ', '月 ') + "日"
                .input-error
                  = resource.errors.full_messages_for(:birthday)[0]

 　　　　# 〜省略〜
  
              .actions
                = f.submit "登録", class: 'btn'
```

　
##　５-2. エラー表示
今回のエラー表示は、ActiveRecordのvalidatesでエラーになった時に表示されるメッセージを利用します。
上記のように、バリデーションエラーは「errors.full_messages_for(:attribute_name)」で各attributeのエラーメッセージは取得してます。

なるほど！
ん？「各attribute」って何？

まずは何も考えずに、ネット上から「devise.ja.yml」をダウンロードし、「/config/locales」に保存してください。
次に、「devise.ja.yml」に以下を追記。
ここに追記した、「birthday、nickname、email、password」が「attribute」です。

ついでに、[日本語化](https://qiita.com/kusu_tweet/items/b534c808ac1ee0382f05)も実施。
「ja.yml」をネットからダウンロードし、先ほどと同じく「/config/locale/」以下に入れる。
「"%{attribute}%{message}"」とあるように、バリデーションエラーの内容に合わせて、「attribute」と「message」を選択してくれる。

```config/locales/devise.ja.yml
ja:
  activerecord:
    attributes:
      user:
        birthday: "生年月日"
        nickname: "ニックネーム"
        email: Eメール
        password: パスワード

 　　# 〜省略〜
```


```config/locales/ja.yml
 # 〜省略〜

   errors:
     format: "%{attribute}%{message}"
     messages:
       accepted: を受諾してください
       blank: を入力してください

 # 〜省略〜
```

　
##　５-3. パスワードの表示/非表示
「/registrations/new.haml.erb」のパスワード入力は、何もしなければ非表示です。
何かしらのアクションを起こしたら、表示出来るようにしないと、利用者にとってわかりにくかったりします。
今回は、最近よく見る「目のマーク」を押したら表示/非表示を切り替えられるようにjavascriptとSCSSを駆使して表現しました。
参考までに一部コードを提示します。
ポイントとしては、「(password).attr('type','text');」「(password).attr('type','password');」を切り替えることで、「f.password_field」が持ってる「type」を切り替えることができ、表示/非表示を切り替えることができます。

```app/views/users/registrations/new.haml.erb
 　　　　# 〜省略〜

  .field-input.toggle
    = f.password_field :password, class: "field-input-full", autocomplete: "off", autocomplete: "password", placeholder: "7文字以上の半角英数字", id:'password'
    .checkbox-field
      %input#js-passcheck.checkbox.js-password-toggle{type: "checkbox"}
      %label.btn-label.js-password-label{for: "js-passcheck"}
        %i.fas.fa-eye-slash{style: "font-size:20px;color:#808080"}

 　　　　# 〜省略〜
```
```stylesheets/modules/users.scss
    .toggle{ 
      position: relative;
      .checkbox-field{
        position: absolute;
        right: 15px;
        top: 45px;
        .checkbox {
          display: none;
        }
      }
    }
```
```app/assets/javascripts/registrations_new.js
$(function(){
  var password = '#password';
  var passcheck = '#js-passcheck';
  $(passcheck).change(function(){
    const passwordLabel = document.querySelector('.js-password-label');
    if ($(this).prop('checked')){
      $(password).attr('type','text');
      passwordLabel.innerHTML = '<i class="fas fa-eye" style="font-size:20px;color:#808080"></i>';
    } else {
      $(password).attr('type','password');
      passwordLabel.innerHTML = '<i class="fas fa-eye-slash" style="font-size:20px;color:#808080"></i>';
    }
  })
})
```

##　５-4. 生年月日の選択
以下の「new.haml.erb」の通り、「date_selectタグ」という方法を使って実装しました。
超簡単です。

```app/views/users/registrations/new.haml.erb
 　　　　# 〜省略〜

  .birthday-select
    = raw sprintf(f.date_select( :birthday, use_two_digit_numbers: true, prompt: "--", start_year: Time.now.year, end_year: 1900, date_separator: '%s'), '年 ', '月 ') + "日"

 　　　　# 〜省略〜
```

ですが１点、注意点があります。
バリデーションエラーが発生すると、「field_with_errors」クラスのdivタグが挿入され、「+ "日"」が切り離されてしまい、以下の画像のようにズレてしまいます。
[![Screenshot from Gyazo](https://gyazo.com/f88498aaa27fbf22ffb7ca1834969cf5/raw)](https://gyazo.com/f88498aaa27fbf22ffb7ca1834969cf5)

```【バリデーションエラー発生時】app/views/users/registrations/new.haml.erb
 　　　　# 〜省略〜

  .birthday-select
    = raw sprintf(f.date_select( :birthday, use_two_digit_numbers: true, prompt: "--", start_year: Time.now.year, end_year: 1900, date_separator: '%s'), '年 ', '月 ')
　.field_with_errors
　  + "日"

 　　　　# 〜省略〜
```
これを回避する方法はいくつかあるようですが、今回は以下のように、「config/application.rb」に「config.action_view.field_error_proc = Proc.new { |html_tag, instance| html_tag }」を追記しました。
追記後、サーバーを再起動すると、追記が反映され、Railsによる自動挿入が回避できます。

```config/application.rb
   # 〜省略〜

module FleamarketAppTeamA
  class Application < Rails::Application
   
   # 〜省略〜

    config.action_view.field_error_proc = Proc.new { |html_tag, instance| html_tag }
  end
end


```

#最後に
最後まで読んでいただきありがとうございます。
以上で、ユーザー新規登録についての実装は終わりです。

本記事はカスタムの部分で、大枠の作業フローと、小技的なところで参考にはなるかと思いますので、活用いただければ幸いです。
もっとわかりやすい記事、個々の機能で詳しく記載された記事はたくさんございますので、そこに至るまでのキッカケにもなればとも思っております。

今回私も、以下の記事を主に参考にさせていただきました。
作者の方々には感謝申し上げます。

量が多いため割愛させていただいておりますが、他にも参考にした記事はたくさんありますので、それら記事にも感謝です。
本記事を参考にされる方は、それらの記事も参考にしていただければと思います。


#参考記事
・deviseについて
-> [rails devise完全入門！結局deviseって何ができるの？](https://www.sejuku.net/blog/13378)
-> [【Rails】deviseの使い方を徹底解説！](https://pikawaka.com/rails/devise)
-> [Deviseでログイン機能をつくる [Ruby on Rails]](https://www.petitmonte.com/ruby/devise.html)
-> [Deviseの設定手順をまとめてみた。 その1　導入編](https://qiita.com/aplifyjp/items/ea0d9236d89fa24a7a1c)

・encrypted_password
->[Gem Deviseによるパスワードの保存及び保安方法](https://freelance.techcareer.jp/skills/98/articles/9114/)

・add_index
->[データベースにindexを張る方法](https://qiita.com/seiya1121/items/fb074d727c6f40a55f22)

・正規表現とは
-> https://www.megasoft.co.jp/mifes/seiki/about.html

・正規表現（ライブラリ）
-> https://github.com/kkos/oniguruma/blob/master/doc/RE.ja
-> http://k-takata.o.oo7.jp/mysoft/bregonig.html

・正規表現（確認ツール）
-> https://rubular.com/

・protect_from_forgery with: :exception（CSRF対策）
-> [RailsのCSRF保護を詳しく調べてみた（翻訳）](https://techracho.bpsinc.jp/hachi8833/2017_10_23/46891)

・日本語化
-> https://qiita.com/kusu_tweet/items/b534c808ac1ee0382f05)

・エラー表示
-> [ActiveRecordのvalidatesで表示されるエラーメッセージのフォーマットを変更する](https://qiita.com/okeicalm/items/9638250ddfb361d80a16)
-> [【Rails】バリデーションのエラーメッセージを取得・表示・日本語化する方法を完全解説！](https://techtechmedia.com/error-messages-validation-rails/)

・生年月日の選択
-> [【Rails】date_selectタグの使い方メモ](https://qiita.com/ozackiee/items/3c027d07cdeb61df6029)
-> [Railsのバリデーションエラーで、「field_with_errors」によるレイアウト崩れを防ぐ](https://yukimasablog.com/rails-field-with-errors)


#辞書
・gem
　便利な機能をひとまとめにしたもの（ライブラリ）

・devise
　ユーザー新規登録/ログインといった認証機能を簡単に実装できるgemのこと

・encrypted_password
　暗号化されたパスワードを保存するカラム

・add_index
　特定のカラムからデータを取得する際に、テーブルの中の特定のカラムのデータを複製し検索が行いやすいようにする

・正規表現
　「検索」や「置換」で指定する文字列をパターン表現する方法で、プログラミング言語やテキストエディタなどで利用できる

・protect_from_forgery with: :exception
　セキュリティ対策。このコードがあると、Railsで生成されるすべてのフォームとAjaxリクエストにセキュリティトークンが自動的に含まれ、セキュリティトークンがマッチしない場合ははじかれる。ユーザー認証が完了したwebアプリのページに悪意のあるコードやリンクを仕込むCSRF（Cross-Site Request Forgery）という攻撃手法から保護出来る。

・before_action
　全てのアクションが実行される前に、この部分が実行される