Rails5でdeviseをAmazon SES SMTPサーバーを使って動かす

#はじめに
Rails5でdeviseを使う記事はほとんどがGmailをSMTPサーバーにしているが、SESで動くようにしたので、後で忘れないようにその時の作業記録を残す。

環境

```
Rails 5.2.2
Ruby 2.5.3
MySQL 5.6
```

#deviseとは
参考記事より引用
>ユーザー登録して、送られてきたメールのリンクをクリックして本登録して、ログインして、パスワード忘れたら再設定して、何回もログインミスったらアカウントロックして…などといった認証系アプリに必要な機能を簡単に追加できる便利なgemです。

#Gemのインストール
###1. Rails Projectの作成
私はRubyMineを使っているので新規Applicationプロジェクトを作成。これは説明不要と思うが、要するに以下のコマンド。

```
$ rails new devise_test
$ cd devise_test
```

###2. gemファイルの編集とインストール
Gemfileに以下を追記してgemをインストールする。SESの認証情報をコードに書きたく無いのでdotenvを使う。

```ruby:Gemfile
# devise
gem 'devise'
gem 'dotenv'
```

```ruby:インストール
$ bundle install
```

```ruby:devise関連ファイルを追加
$ rails g devise:install
```

`rails g devise:install`　が動かない時はこの記事を参照。私の時は`spring`の停止で解決した。
http://ohara.geniusroots.com/entry/2017/03/26/145253


```ruby:出力されるメッセージ
Running via Spring preloader in process 43961
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================
```

###3. deviseの設定
上記のメッセージにいろいろ書いてあるが、今回は自分のMacでRailsサーバーを動かすので英文の例に書いてあった`config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }`を以下のファイルに追加。

```ruby:config/environments/development.rb
以下を一番下の end の手前に追記
# mailer setting
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
```

###4. ページの作成
現状ページは1つも作っていないため、　Topページにアクセスした際に表示されるページを作成。rootだけは特別で / ではなく # を使うことに注意。

```
$ rails g controller Pages index show
```

```ruby:config/routes.rb
Rails.application.routes.draw do
  root 'pages#index'　　ここのgetをrootにして/を#に変更。
  get 'pages/show'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
```

###5. flashメッセージの設定
参考記事そのまま
>ログインした時などに上の方に「ログインしました」みたいなメッセージが出るようにします。以下のファイルの`<body>`タグのすぐ下に指定されたタグを挿入します。

```ruby:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>DeviseTest2</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <p class="notice"><%= notice %></p>
    <p class="alert"><%= alert %></p>

    <%= yield %>
  </body>
</html>
```

###6. DeviseのViewを生成
これも参考記事そのまま。
>`Deviseの導入で追加されるViewは、以下のコマンドを実行しなければデザインを変更できないので、デザインをカスタマイズするためにも実行します。`

```
$ rails g devise:views
```

いろいろファイルが作成されます。

###7. モデルの作成
以下を実行してmigrationファイルを作る。

```
$ rails g devise User
```

###8. Userモデル
deviseの機能を全部使いたいので、`trackable`, `confirmable`, `lockable`, `timeoutable` をモデルに追加。

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

###9. マイグレーションファイルの編集
deviseの機能を全部使いたいので、上記7.でできたマイグレーションファイルのコメントを8.のモデルに合わせて該当部分を外す。

```ruby:db/migrate/20181220121754_devise_create_users.rb
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      t.string   :unlock_token # Only if unlock strategy is :email or :both
      t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    add_index :users, :unlock_token,         unique: true
  end
end
```

アカウント名を保存しておきたいのでusernameもUserテーブルに追加します。参考記事ではTwitter認証もやっているのでprovider, uidもそのまま入れてますが、ここでは使いません。

```
$ rails g migration add_columns_to_users provider uid username
```

以下のようなマイグレーションファイルができます。

```
class AddColumnsToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :provider, :string
    add_column :users, :uid, :string
    add_column :users, :username, :string
  end
end
```


###10. DBの作成とマイグレーション
ここまで来たら、`config/database.yml`を環境に合わせて設定してDBを作成してからマイグレーションしてテーブルを作成します。

```
$ rake db:create
$ rake db:migrate
```

###11. viewの編集
参考記事そのままです。
>以下のファイルを編集して、ページ上部にメニューが出るようにします。
`user_signed_in?`はdeviseのHelperメソッドです。
ログインしているかしてないかで上部のメニューの表示が変わるようになります。
`current_user`で現在サインインしているユーザーの情報を取得できます。

```ruby:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>DeviseTest2</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <header>
     <nav>
        <% if user_signed_in? %>
          <strong><%= link_to current_user.username, pages_show_path %></strong>
          <%= link_to 'プロフィール変更', edit_user_registration_path %>
         <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
        <% else %>
          <%= link_to 'サインアップ', new_user_registration_path %>
          <%= link_to 'ログイン', new_user_session_path %>
        <% end %>
      </nav>
     </header>

    <p class="notice"><%= notice %></p>
    <p class="alert"><%= alert %></p>

    <%= yield %>
  </body>
</html>
```

>以下の2ページも修正。
indexの方がトップページ、showの方がログインしているユーザー用のページにします。

```ruby:app/views/pages/index.html.erb
<h1>ようこそ</h1>
<p>トップページです。</p>
```

```ruby:app/views/pages/show.html.erb
<h1>こんにちは、<%= current_user.username %>さん</h1>
<p>ユーザー用ページです。</p>
```

###12. 動作確認
ここでRailsサーバーを起動して動作確認します。

```
$ rails server
```

http://localhost:3000 をブラウザで開いてTopページを確認。


#DeviceでSESを使ったメール送信の設定
次にSESのSMTPサーバーを使ってメール送信する設定をします。
###1. development.rbの下記をコメントアウト。
33行目あたり。

```ruby:config/environments/development.rb
# Don't care if the mailer can't send.
#config.action_mailer.raise_delivery_errors = false
```

###2. メール送信者の設定
config/initializers/devise.rbのconfig.mailer_senderを、
SES に登録されているメールアドレスに書き換えます。例えば、`customer_support<noreply@hogehoge.com>`という感じ。私の場合は hogehoge.com は自分が管理しているドメイン名で、SESに登録しているものを使いました。`noreply`の所は何を入れても動くようです。

21行目あたり

```ruby:config/initializers/devise.rbのconfig.mailer_sender
#config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
  config.mailer_sender = 'customer_support<reply@hogehoge.com>'
```

development.rbに下記を記述。

```ruby:config/environment/development.rb
Rails.application.configure do
  require 'dotenv'　⑧

# 途中省略

# derault url
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } 　①

# Amazon SES smtp setting
  Dotenv.load ⑧
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
      :address => 'email-smtp.us-east-1.amazonaws.com',　②
      :port => 587,　⑦
      :authetication => :login,
      :user_name => ENV['SES_SMTP_USER'],　③
      :domain => 'hogehoge.com', 　④
      :password => ENV['SES_SMTP_PASSWORD'],　⑤
      :enable_starttls_auto => true　⑥
  }
```

####設定項目の説明
①自分のMacでRailsサーバーを動かすので`localhost:3000`に合わせて設定。
②自分が使っているAmazonのRegionに合わせて、以下を参照してAmazon SES SMTP エンドポイントを記述。

>SES参考リンク: [Amazon SES SMTP エンドポイントへの接続](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/smtp-connect.html "Amazon SES SMTP エンドポイントへの接続")

③下記のSESの説明`Amazon SES コンソールを使用した Amazon SES SMTP 認証情報の取得`を参考にして、SMTP認証情報を作り、そこで作られたSMTPユーザー名を入れる。例えば`AKIXXXX9999XXXXEA`のような文字列になる。コードに認証情報を書きたくないので　dotenvを使った。

>SES参考リンク: [Amazon SES SMTP 認証情報の取得](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/smtp-credentials.html "Amazon SES SMTP 認証情報の取得")

④ドメイン名は自分が使いたいドメインでSESに登録しているものを指定。
⑤同様にSMTPパスワードを入れる。例えば`AxHYujHOGEhoge999hYuHoge`のような文字列になる。
⑥STARTTLSを使うので`true`
>STARTTLS とは、暗号化されていない接続を暗号化された接続にアップグレードする方法です。STARTTLS には、様々なプロトコルに対応したバージョンがあります。SMTP バージョンは、「RFC 3207」に定義されています。

>STARTTLS 接続を設定する場合、SMTP クライアントは、ポート 25、587、または 2587 で Amazon SES SMTP エンドポイントに接続し、EHLO コマンドを発行します。次に、サーバーから STARTTLS SMTP 拡張機能をサポートしているという通知が来るのを待ちます。通知を受けたクライアントは、STARTTLS コマンドを発行し、TLS ネゴシエーションを開始します。ネゴシエーションが完了すると、クライアントが暗号化された新しい接続で EHLO コマンドを発行し、SMTP セッションが正常に進行します。

⑦STARTTLSを使うので587番ポートを指定。
⑧これを入れないとdotenv③と④を入れてくれないのでロードする。


## 動作確認
ここまで来たらメールが飛ぶはずなので、動作確認してみる。

サインアップすると次のメールが届いた！

````
Confirmation instructions

Customer_support <noreply@hogehoge.com>　<=config.mailer_senderで設定した送信元アドレス

Welcome サインアップしたメールアドレス!

You can confirm your account email through the link below:

Confirm my account
```

>メールで送られる文章は以下のファイルを編集することで可能です。
`app/views/devise/mailer/confirmation_instructions.html.erb`

#ユーザー名の追加
上記の動作確認でメールが飛ぶことは確認した。しかし、usernameメソッドは未定義なのでログイン後にNoMethodErrorが発生する。なのでユーザー名を入れられるようにする。

###1. ユーザー名を入力するエリアを追加
参考記事のままエリアを追加。
>サインアップページにユーザー名を入力するエリアを追加します。ユーザーのプロフィール変更ページ（views/devise/registrations/edit.html.erb）にも同様に追加しときます。

```ruby:views/devise/registrations/newt.html.erb
<h2>Sign up</h2>

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

  <div class="field">
    <%= f.label :username %><br />
    <%= f.text_field :username %>
  </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>
```

```ruby:views/devise/registrations/edit.html.erb
<h2>Edit <%= resource_name.to_s.humanize %></h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
  <%= devise_error_messages! %>

  <div class="field">
    <%= f.label :username %><br />
    <%= f.text_field :username %>
  </div>
  
  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
    <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
  <% end %>

  <div class="field">
    <%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
    <% if @minimum_password_length %>
      <br />
      <em><%= @minimum_password_length %> characters minimum</em>
    <% end %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
    <%= f.password_field :current_password, autocomplete: "current-password" %>
  </div>

  <div class="actions">
    <%= f.submit "Update" %>
  </div>
<% end %>

<h3>Cancel my account</h3>

<p>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %></p>

<%= link_to "Back", :back %>
```

###2. ApplicationControllerに以下を追加
参考記事そのまま
>次にApplicationControllerに以下を追加します。
テンプレートを変えて、ユーザー名を入力するようにしただけでは実際に登録されないからです。
詳しくはstrong_parametersについて調べてください。
簡単に言えばよく分からんパラメーターは渡せないようになってるので渡せるようにします。

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

  def after_sign_in_path_for(resource)
    pages_show_path
  end

  private
  def sign_in_required
    redirect_to new_user_session_url unless user_signed_in?
  end

  protected
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
    devise_parameter_sanitizer.permit(:account_update, keys: [:username])
  end
end
```

#メールアドレスの確認欄の追加
メールアドレスを2回入力させて、一致するかどうかのValidationを追加したい要件はよくある話だと思うので実装した。

###1. email_confirmationの追加

```ruby:app/views/devise/registrations/new.html.erb
<h2>Sign up</h2>

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

  <div class="field">
    <%= f.label :email, "Email" %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :email, "Email (Confirmation)" %><br />
    <%= f.email_field :email_confirmation, autofocus: true, autocomplete: "email" %>
  </div>
以下省略
```

###2. application_controller.rbにemail_confirmationを追加

```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
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
    devise_parameter_sanitizer.permit(:sign_up, keys: [:email_confirmation])
    devise_parameter_sanitizer.permit(:account_update, keys: [:username])
  end
end
```

###3. user.rbモデルにvalidationを追加

```ruby:app/models/user.rb
class User < ApplicationRecord
　　　　途中省略
  validates :email, confirmation: true
end
```

これで登録画面でメールアドレスの確認欄が追加されます。

#FlashメッセージをかっこよくPopupにする。
defaultのDeviseだとnoticeがそのまま表示されて、ちょっとかっこ悪いのでJavaScriptでPopupさせるようにした。参考記事そのままで使えるようになった。

###1. Gemを追加

```ruby:/Gemfile
gem 'toastr-rails'
```

```
$ bundle install
```

###2. ファイルに読み込ませるように追加

```ruby:app/stylesheets/application.css
 *= require_tree .
 *= require_self
 *= require toastr
```
```ruby:app/javascripts/application.js
//= require rails-ujs
//= require activestorage
//= require jquery
//= require bootstrap-sprockets
//= require toastr
//= require jquery_ujs
//= require turbolinks
//= require_tree .
```

###3. appの全体に表示させるために下記ファイルに定義

```ruby:app/views/layouts/application.html.erb
    <p class="notice"><%= notice %></p>  この行を削除して以下を追加
   　　<p class="alert"><%= alert %></p>　　　　　　　　この行も削除

    <!– flashメッセージを表示させる –>　　　
    <% unless flash.empty? %>
    <script type="text/javascript">
        <% flash.each do |f| %>
        <% type = f[0].to_s.gsub('alert','error').gsub('notice','info') %>
        toastr['<%= type %>']('<%= f[1] %>');
        <% end %>
    </script>
    <% end %>
```

###4. gem-deviseの警告表示にも反映させる。
>部分テンプレートとして使うために別ファイルを作成

参考記事の通りだとエラーになるのでdeviseの共有erbディレクトリ内にファイルを置いた。コードの中身は参考記事と同じです。

```ruby:app/views/devise/shared/errormessages.html.erb
<% unless resource.errors.empty? %>
　<script type= "text/javascript">
　　<% resource.errors.full_messages.each do |msg| %>
　　　toastr.error('<%= msg %>');
　　<% end %>
　</script>
<% end %>
```

>devise/confirmaitions/new.html.erb
devise/passwords/edit.html.erb
devise/passwords/new.html.erb
devise/registrations/edit.html.erb
devise/registrations/new.html.erb
devise/unlocks/new.html.erb
上記ファイルにある、
<%= devise_error_messages! %> を

<%= render "devise/shared/errormessages" %> に置換。ここは参考記事の通りだとNoMethodエラーになるので修正した。



###参考記事
https://qiita.com/cigalecigales/items/f4274088f20832252374
http://lina-marble.hatenablog.com/entry/2016/03/30/180622
https://qiita.com/MZ1TB0ZSoVfeX9L/items/23d0214852d40902323b


#関連記事
DeviseのSign upとLog inをBootstrapでModalポップアップにカスタマイズする
https://qiita.com/NYC-Blue/items/d104431b90e717e62051