Rails ~会員登録~

## これまでのあらすじ

1. 仮登録でメールアドレスを登録
2. 登録したメールアドレスへ本登録画面に飛ぶURLを記載したメールを送信
3. メールでURLへ遷移
4. 登録に必要な情報の入力
5. 登録完了
6. ログイン
7. ログアウト

までやっていきたいと思います。

-----

## メール送信

[前の記事](https://qiita.com/tsunemiso/items/a032ddd2a8ca144742ac)でユーザ仮登録のモーダルウィンドウ表示まで実装しました。
で、登録ボタンで以下のアクションを呼びます。

```ruby:top_controller.rb
  def create
    @temp_user = TempUser.create_temp_user(temp_users_params)
    respond_to do |format|
      if @temp_user.save
        UserMailer.with(temp_user: @temp_user, locale: params[:locale]).request_registration.deliver_later
        format.js { @status = "success" }
      else
        format.js { @status = "fail" }
      end
    end
  end

```

TempUserクラスの#create_temp_userメソッドで初期化します。

```ruby:temp_user.rb
    # 仮ユーザの作成
    def create_temp_user(params)
      # temp_usersにmail_addressで検索 初期化する
      temp_user = find_or_initialize_by(mail_address: params[:mail_address])
      temp_user.last_name = params[:last_name]
      temp_user.first_name = params[:first_name]
      temp_user.token = create_token
      temp_user.expired_at = DateTime.now + 1
      return temp_user
    end
```

find_or_initialize_byでメールアドレスを検索します。
もし、仮登録済みで本登録していないメールアドレスの場合、後勝ちにして最後に入力した情報でUPDATEします。
もし、temp_userにない場合は、入力情報でINSERTします。
find_or_initialize_byを使用し、saveをするタイミングでINSERTかUPDATEか判定、UPSERTが実現できます。

expired_at（有効期限）は、とりあえず1日にしていますが、設定を外出ししたいですね。

saveメソッドの戻り値でSQLの成功・失敗を判定して処理を判定しています。

成功時は、登録情報を元にUserMailerでメールを作成しています。

```ruby:user_mailer.rb
class UserMailer < ApplicationMailer
  default from: "hogehoge@gmail.com"

  def request_registration
    @temp_user = params[:temp_user]
    @locale = params[:locale]
    mail(to: @temp_user.mail_address, subject: I18n.t("mailers.user_mailer.request_registration.subject"))
  end
end
```

ApplicationMailerを継承したUserMailerです。
from:は送信元のメールアドレスを設定します。
request_registrationがメール送信の本体です。
paramsで引数を受け取ってメールを生成します。

メール本体は、viewsの下に作ります。

```erb:request_registration.html.erb
<%= stylesheet_link_tag "mailers/request_registration.css", media: "all" %>
<p class="message"><%= t('mailers.user_mailer.request_registration.dear', last_name: @temp_user.last_name, first_name: @temp_user.first_name) %></p>
<pre class="message"><%= t('mailers.user_mailer.request_registration.message_text', expired_at: l(@temp_user.expired_at, format: :default)) %></pre>
<div class="btn">
  <%= link_to t('mailers.user_mailer.request_registration.button'), {controller: 'account', action: 'regist', locale: @locale, token: @temp_user.token } %>
</div>
```

メソッド名のhtml.erb(HTMLメール）またはtext.erb（テキストメール）を雛形として作ります。
上記は、HTMLメールの雛形です。link_toでaccount_controller.rbのregistメソッドを指定しています。temp_user登録の際、生成したtokenをGETリクエストをパラメタとしてURLに付与し、link_toで生成しています。

-----

## メールを受信する

開発中に動作確認したいですが、実際に送ると、誤送信する恐れがあるので、以下のgemを入れます。

```ruby:Gemfile
  # letter_opener_web
  gem 'letter_opener_web', '~> 1.0'
```

このgemはメールを送信・受信してくれます。
config/environments/development.rbに設定を追加します。

```ruby:development.rb
config.action_mailer.delivery_method = :letter_opener_web
```

また、routes.rbに以下のパスを設定し、送信メールを確認できるようにします。

```ruby:routes.rb
    # letter_opener_web
    mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
```

これで(http://localhost:3000/letter_opener) でアプリが送信したメールをブラウザで確認できます。

-----

## 登録

メールに記載されたURLを押下すると、登録に必要な情報を入力する画面へ遷移できます。

### パスワードについて

ライブラリを使わないでログイン機能を実装するために以下のgemを使います。

```ruby:Gemfile
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
```

Userモデルにhas_secure_passwordを追加します。

```ruby:user.rb
class User < ApplicationRecord
  include ActiveModel::Validations
  has_secure_password validations: true

  validates :last_name, presence: true
  validates :first_name, presence: true
  validates :last_name_roman, upper_case_format: true, unless: Proc.new { |p| p.last_name_roman.blank? }
  validates :first_name_roman, upper_case_format: true, unless: Proc.new { |p| p.first_name_roman.blank? }
  validates :sex, inclusion: { in: [0, 1] }
  validates :birthed_on, presence: true
  validates :mail_address, uniqueness: true
  validates :password, password_format: true
  validate :already_used_mail_address

  # already used mail address
  def already_used_mail_address
    unless User.find_by(mail_address: mail_address).nil?
      errors.add(:mail_address, I18n.t("validate.already_use"))
    end
  end

  # create remember token
  def self.create_remember_token
    SecureRandom.urlsafe_base64
  end

  # encrypt
  def self.encrypt(token)
    Digest::SHA256.hexdigest(token.to_s)
  end
end
```

passwordとpassword_confirmation属性、さらにauthenticateメソッドが使用できるようになります。
さらにDB内ではpassword_digestというカラムで暗号化されたパスワードは保存されます。
（password_digestをmigrationで対象のテーブルに追加します。)

アプリケーションログには、入力値は当然出力されず
DBには、暗号化されたパスワード文字列が登録されます。

登録ボタンでcreateメソッドが呼び出され、登録成功したら完了画面、失敗したら再度登録画面をレンダリングします。

```ruby:account_controller.rb
  def create
    @user = User.new(users_params)
    if @user.save
      render action: :complete
    else
      render action: :regist
    end
  end
```

-----

## ログイン・ログアウト

application_controller.rbに以下を定義します。

```ruby:application_controller.rb
class ApplicationController < ActionController::Base

  # filter
  # actionの直前に実行されるfilter
  before_action :set_locale
  before_action :current_user
  before_action :require_sign_in!
  # helper methodとして使用できる
  helper_method :signed_in?

  def set_locale
    I18n.locale = locale
  end

  def locale
    @locale ||= params[:locale] ||= I18n.default_locale
  end

  def default_url_options(options = {})
    options.merge(locale: locale)
  end

  def current_user
    remember_token = User.encrypt(cookies[:remember_token])
    @current_user ||= User.find_by(remember_token: remember_token)
  end

  def sign_in(user)
    remember_token = User.create_remember_token
    # cookieにremember_tokenをsetする
    cookies.permanent[:remember_token] = remember_token
    # remember_tokenを更新する
    user.update_column(:remember_token, User.encrypt(remember_token))

    @current_user = user
  end

  def sign_out
    @current_user = nil
    # cookieのremember_tokenを削除する
    cookies.delete(:remember_token)
    redirect_to login_path
  end

  def signed_in?
    @current_user.present?
  end

  private

  def require_sign_in!
    redirect_to login_path unless signed_in?
  end
end
```

filter処理として
current_userメソッド
ログイン時にCookieとDBに登録したトークン情報を突き合わせて、ユーザ情報を取得する
require_sign_in!メソッド
ユーザ情報が取得できない場合(=ログインしていない場合)ログイン画面へリダイレクトする

これをapplication_controller.rbに定義することによりapplication_controllerを継承する全てのcontrollerにこのfilter処理が走ります。

これにより画面上でログイン状態・非ログイン状態を判断するわけです。

ログイン処理はsession_controller.rbに定義しています。

```ruby:session_controller.rb
class SessionsController < ApplicationController
  # filter
  # actionの直前に実行されるfilterをskipする
  skip_before_action :require_sign_in!, only: [:new, :create]
  # actionの直前に実行されるfilter
  before_action :set_user, only: [:create]

  # GET /login
  def new
    redirect_to root_path
  end

  # PUT /login
  def create
    if @user.authenticate(@session.password)
      sign_in(@user)
    else
      @session.sign_in_failure
    end
    render "top/index"
  end

  # DELETE /logout
  def destroy
    sign_out
    redirect_to login_path
  end

  private

  def set_user
    @session = Session.new(session_params)
    if !@session.valid?
      render "top/index" and return
    end
    @user = User.find_by!(mail_address: @session.mail_address)
  rescue
    @session.sign_in_failure
    render "top/index"
  end

  def session_params
    params.require(:session).permit(:mail_address, :password)
  end
end
```

Sessionモデルはmail_addressとpasswordのログイン認証に必要な情報をもっているクラスです。

skip_before_actionは、actionの直前に実行されるfilterをskipするための宣言です。
当然ログイン画面表示とログイン処理にログイン認証のfilterが入っているとログインできないため、skipしています。

createメソッドの前のみset_userのfilterが適用されます。

set_userメソッドは画面から入力されたフォーム情報を元にmail_addressでユーザ情報を取得します。
取得できない場合、例外を発生させます。例外発生時はログイン画面をレンダリングします。

createメソッドは、set_userメソッドで取得したユーザ情報とパスワードを確認します。
確認できた場合、ログイン処理(application_controller.rbに定義)をします。

ログイン処理は、ログイン認証していることを示す、remember_tokenを画面に保持します。
remember_tokenはUserクラスで定義したメソッドでランダム文字列で生成されます。
それをCookieに保存し、その後DB（userテーブル)UPDATEします。

前述のログイン確認のfilterはこのremember_tokenを元にログインしているか、していないかを判断するというわけです。

-----

## 終わりに

* 勉強会の資料なので、急いで作ったので、もうちょっと修正します。
* 参考リンクとかも記載せねば…

-----

## 参考

工事中…