Rails5とDeviseで複数メールアドレスを持つユーザー認証機能を実装した話

複数のメールアドレスを持つユーザーモデルをDeviseでさくさくっと作ろうと思ってたら思ったより手間取ったのでメモ

##DeviseのインストールとUserモデルの作成

```bash:コマンド
$ rails new demo --skip-bundle
$ cd demo
$ echo "gem 'devise'" >> Gemfile
$ rails g devise:install
```

```ruby:config/environments/development.rb
Rails.application.configure do

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
  #↓追加
  
  config.action_mailer.default_url_options = { host: 'localhost:3000' }
end
```

## Userモデルの作成
```bash:コマンド
$ rails g devise User
```

##Emailモデルの作成
```bash:コマンド
$ rails g model Email user:belong_to address:strings
```

##Usersテーブルの編集
```ruby:db/migrate/20xxxxxxxxxxxx_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      #↓コメントアウトor削除
      #t.string :email,              null: false, default: ""
      
      #↓追加
      t.belongs_to :main_email, null: true
      #↑ここでハマった、emailカラムはDeviseがメソッドオーバーライドしてるらしく
      #'t.belong_to :email' としてしまうと保存ができない
      #後、'null: false'にしてしまうとデッドロックが発生し保存できない
      #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
    end
    #↓コメントアウトor削除
    #add_index :users, :email,                unique: true
  end
end
```

##Emailsテーブルの編集
```ruby:db/migrate/20xxxxxxxxxxxx_create_emails.rb
class CreateEmails < ActiveRecord::Migration[5.0]
  def change
    create_table :emails do |t|
      #↓行末に', null: false'を追加
      t.belongs_to :user, null: false
      t.string :address

      t.timestamps
    end
    #↓追加
    add_index :emails, :address, unique: true
  end
end
```

##Userモデルの編集
```ruby:app/model/user.rb
class User < ApplicationRecord
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
  #↓以下追加

  belongs_to :main_email, class_name: Email, optional: true
  accepts_nested_attributes_for :main_email

  has_many :emails, dependent: :destroy, index_errors: true
  accepts_nested_attributes_for :emails
  
  attr_accessor :email
  
  after_save :set_main_email
  after_save :add_main_email_to_emails

  #↓deviseにemailのチェックをさせないようにする
  def email_required?
    false
  end

  def email_changed?
    false
  end
  

  protected
  #↓deviseのユーザー取得メソッドをオーバーライド
  def self.find_first_by_auth_conditions(warden_conditions)
    conditions = warden_conditions.dup
    if email = conditions.delete(:email)
      #↓emailをemailsに持つユーザーを返す
      where(conditions).joins(:emails).find_by('emails.address': email)
      #↓main_emailでしかログインさせたくない場合はこれ
      #where(conditions).joins(:main_email).find_by('main_email.address': email)
    else
      where(conditions).first
    end
  end
  
  #↓main_emailが設定されていない時にemailsの最初の値を設定する
  def set_main_email
    return false if self.emails.empty?
    if self.main_email.nil?
      self.main_email = self.emails.first
      self.save
    end
  end

  #↓main_emailが変更された時にemailsに追加
  #今回の実装では必要ないが一応
  def add_main_email_to_emails
    return false if self.main_email.nil?
    if self.main_email.changed? && !self.emails.include?(self.main_email)
      self.emails << self.main_email
      self.save
    end
  end
end

```

##Emailモデルの編集
```ruby:app/model/email.rb
class Email < ApplicationRecord
  has_one :user

  belongs_to :user, optional: true
  #↑Rails5からbelong_toカラムは必須になったが保存してからでないと
  #IDが定まらず保存できないので'optional: true'を指定
end

```

##ApplicationControllerの編集
```ruby: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
    #Devise4での書き方 古いバージョンでは
    #'devise_parameter_sanitizer.for'を使う
    devise_parameter_sanitizer.permit(:sign_up) do |u|
      u.permit([emails_attributes: [:address]], :password, :password_confirmation, :remember_me)
    end
    devise_parameter_sanitizer.permit(:sign_in) do
      |u| u.permit([emails_attributes: [:address]], :password, :remember_me)
    end
    devise_parameter_sanitizer.permit(:account_update) do
      |u| u.permit([emails_attributes: [:address]], :password, :password_confirmation, :current_password)
    end
  end
end

```

##Viewsの編集

```Bash:コマンド
$ rails g devise:views user
```

```ruby:config/initializers/devise.rb
  #↓223行目付近 コメントアウトのまま放置or削除
  # config.scoped_views = false
  config.scoped_views = true
  #↑追加
```

```ruby:app/views/users/registrations/*.html.erb
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>

  #↓追加
  <% @user.emails.new if @user.emails.empty? %>
  <%= f.fields_for :emails, @user.emails || @user.emails.new do |e| %>
  <div class="field">
    <%= e.label :email %><br />
    <%= e.email_field :address %>
  </div>
  <% end %>
  
  #↓削除
  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true %>
  </div>


  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
  
```
これでおしまい。