Rails 総復習1ヶ月チャレンジ 5日目(Railsチュートリアル編)

チャレンジ5日目です!そしてワクチン接種2回目に熱が出てしまい1日動けず休んでしまいました・・・

気を取り直して今日から頑張ります!!

5日目(Ruby on Rails チュートリアル 11章アカウントの有効化〜12章パスワードの再設定)

この二つの章、個人的に非常に難しく、テキストでは理解が出来ないと判断して動画学習に切り替えました。動画学習の方が頭に入ってきやすい感じでいい感じでした!Ruby on Rails チュートリアルは動画教材もしっかりしていて学習しやすいですね!

今回は視覚的に分かりやすくしたい為、 流れを動画で使われていたスライドを引用させていただき理解していきます!

https://i.gyazo.com/52bfb28a91a8dbfde2892271bbf8c700.png

(Ruby on Rails 第11章 アカウントの有効化より引用)

11章大まかな流れ

  • ユーザー登録申請
  • トークン作成(以前作成のnew_tokenを使用)、その後ハッシュ化
  • usersテーブルにactivation_digest(ハッシュ化したトークンを保存)、activated (ユーザー認証待ちか認証済みか判別、boolean型)カラムをそれぞれ追加
  • ユーザーより申請のあったメールアドレスへ認証に必要なトークン、ユーザーメールアドレスを含めたリンクを載せたメールを送信
  • ユーザーがリンクをクリックすると、管理者側でトークンとユーザーアドレスが正しいものかを判別し、正しければ認証する。

下記が具体的な流れです。

https://i.gyazo.com/5c37db5989e74d4ae057ea90891199c9.png 今回本番環境は作っていない為、4.については省略します。

(Ruby on Rails 第11章 アカウントの有効化スライドより引用)

下記がアカウント申請から承認までの流れです。 https://i.gyazo.com/08d84bfda9bf2856ab850b5ccb753fc1.png

(Ruby on Rails 第11章 アカウントの有効化スライドより引用)

アカウント申請から承認までの流れ

  • ユーザーから申請が来た際、DBにデータ保存はするが状態はunactivated(承認待ち)
  • 有効化トークンからハッシュ値を生成
  • ハッシュ値をDBに保存
  • 有効化トークンとメールアドレスを含めたリンク記載のメールをユーザーへ送信
  • ユーザーがクリックしたら、クリックされたURLを分解、emailをfind_byで検索、トークンを検証し、それぞれ問題なければ認証し、DBデータを有効化

では早速やっていきます

まずAccountActivationsコントローラーを作成します。

$ bundle exec rails generate controller AccountActivations

続いてルーティング作成です。

#config/routes.rb
Rails.application.routes.draw do
 :
  resources :account_activations, only: %i[edit]
end

editアクションにしている点に注目してください。
通常元々登録しているDB情報の変更であれば(今回の場合、ユーザー情報は既に登録してあり、activatedカラムのステータスをfalseからtrueに変える)のであればupdateアクションかな?と思うのが普通だと思います。しかし、ユーザーが送られたメールをクリックする際にブラウザではGETリクエストが発行される為、それに対応するようにeditアクションにしています。

続いてデータモデルを作成します。
https://i.gyazo.com/417b9648a620e5ba1225ee57a61bfa3e.png

(Ruby on Rails 第11章 アカウントの有効化スライドより引用)

$ bundle exec rails generate migration add_activation_to_users \
> activation_digest:string activated:boolean activated_at:datetime

下記それぞれカラムの役割を記載します。

activation_digest→ハッシュ化したトークンを保存しておく
activated→ユーザー認証済みか否かを判別する
activated_at→ユーザー認証された日付を記録(今回実装ではなくても良い)

activatedカラムはboolean型の為、default値はfalseとしておきます。

#db/migrate/[timestamp]_add_activation_to_users.rb
class AddActivationToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :activation_digest, :string
    add_column :users, :activated, :boolean, default: false
    add_column :users, :activated_at, :datetime
  end
end
$ bundle exec rails db:migrate

続いてUserモデルにアカウント有効化のコードを追加します。

#app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save   :downcase_email
  before_create :create_activation_digest
  .
  .
  .
  private
  .
  .
  .
    # 有効化トークンとダイジェストを作成および代入する
    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end

9章のrememberメソッドを作った時と似ており、attr_accessorで仮想のactivation_token属性を作成し、有効化トークンを作成・ハッシュ化するメソッドcreate_activation_digestを定義しています。違いは2点です。

  • update_attributeを使っていない →rememberの時は既にデータベースに存在するユーザーのために作成するのに対し、before_create(この後記述)コールバックの方はユーザーが作成される前に呼び出される為。
  • before_createコールバックの使用
    →User.newで新しいユーザーが定義されるとactivation_token属性やactivation_digest属性が得られるようになる。

続いてアカウント有効化のメール送信についてです。 https://i.gyazo.com/199ad8b32adbbcf3b9f6ba3621ce10bf.png

(Ruby on Rails 第11章 アカウントの有効化スライドより引用)

Action Mailerライブラリを使ってUserのメイラーを追加していきます。Usersコントローラーのcreateアクションで有効化リンクをメール送信する為のものです。なおメイラーの構成はコントローラーのアクションやビューと似ています。

まずUserメイラーを作成します。

$ bundle exec rails generate mailer UserMailer account_activation password_reset

これでaccount_activationメソッドとpassword_resetメソッド(12章で使用)が作成されます。

user_mailer.brを編集します。

#app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer

  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end

  def password_reset
    @greeting = "Hi"

    mail to: "to@example.org"
  end
end

次にメールのテキストビュー、HTMLビューを編集します。

#app/views/user_mailer/account_activation.text.erb
Hi <%= @user.name %>,

Welcome to the Sample App! Click on the link below to activate your account:

<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
#app/views/user_mailer/account_activation.html.erb
<h1>Sample App</h1>

<p>Hi <%= @user.name %>,</p>

<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>

<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
                                                    email: @user.email) %>

次にメールのテンプレートをメールプレビューで出来る様にします。
まずdevelopment環境のメール設定が必要です。(今回私はlocal環境だったのでこの設定にしました。お使いの環境によって適宜変更が必要です)

#config/environments/development.rb
Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = false

  host = 'localhost:3000'
  config.action_mailer.default_url_options = { host: host, protocol: 'http' }
  .
  .
end

次にUserメイラープレビュー(自動生成)を編集します。

#test/mailers/previews/user_mailer_preview.rb

class UserMailerPreview < ActionMailer::Preview

  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/account_activation
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    UserMailer.account_activation(user)
  end

  :
end

app/mailers/user_mailer.rbで先程定義したaccount_activationの引数には有効なUserオブジェクトを渡す必要があるため、user変数が開発用データベースの最初のユーザーになるように定義して、それをUserMailer.account_activationの引数として渡します。

そしてコード内注釈記載の
http://localhost:3000/rails/mailers/user_mailer/account_activation
にアクセスするとメールのプレビューが閲覧できます!!

次はUsersコントローラーのcreateアクションを更新します。
変更前はユーザー登録完了時にはユーザーのプロフィールページにリダイレクトしていましたが、これをユーザーに承認メールを送り、root_pathにリダイレクトするというものに変更します。

#app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      UserMailer.account_activation(@user).deliver_now
      redirect_to root_url,  info: "Please check your email to activate your account."
    else
      render 'new'
    end
  end
  .
  .
  .
end

最後にアカウント有効化の流れに移ります。
まず以前Userモデルに定義済みのauthenticated?メソッドを改良します。
元々記憶トークン用に作られたauthenticated?メソッドは下記のような形です。

# トークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
  return false if remember_digest.nil?
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

これを記憶トークンだけでなく、アカウント有効化とこの後のパスワードリセット機能にも使えるよう汎用性を持たせるよう下記に変更します。

#app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

注目はこの部分!

digest = send("#{attribute}_digest")

Sendメソッドはレシーバに対して指定した文字列もしくはシンボルのメソッドを実行します。
例えば

@user.send :password_digest
とか
a = "password"
@user.send("#{a}_digest")

という形で使用するとsendで指定したメソッドを実行してくれます!sendメソッドを用いて汎用性のあるauthenticated?メソッドを定義できました。

次にSessonsヘルパーを変更します。

#app/helpers/sessions_helper.rb

module SessionsHelper
  .
  .
  .
  # 現在ログイン中のユーザーを返す(いる場合)
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(:remember, cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
  .
  .
  .
end

current_userメソッド内のauthenticated?の引数を1つから2つに変更しています。

これでeditアクションを書く準備が整いました。
このアクションはparamsハッシュで渡されたメールアドレスに対応するユーザーを認証するものです。

#app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController

  def edit
    user = User.find_by(email: params[:email])
    #クリックしたURLに含まれたemailからDBに登録されたユーザーを検索する
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.update_attribute(:activated,    true)
      user.update_attribute(:activated_at, Time.zone.now)
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
  end
end

ユーザー認証の中核はここです。

if user && !user.activated? && user.authenticated?(:activation, params[:id])

下記それぞれコードの説明です。

  • user&&→nilガード
  • !user.activated?→ユーザー認証は有効じゃないですよね?ユーザーが何回もメールのリンクをクリックした場合などを防ぐ
  • user.authenticated?(:activation, params[:id])→トークンが正しいか認証する

そして上記が全て通り認証された際に下記で有効化、有効化の日時が更新されます。

user.update_attribute(:activated,    true)
user.update_attribute(:activated_at, Time.zone.now)

ユーザーの有効化の仕組みは出来ましたが、ユーザー認証が有効でないユーザーがログインできない仕組みがまだ実装出来ていないのでそれを実装します。

#app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user &.authenticate(params[:session][:password])
      if user.activated?
        log_in user
        params[:session][:remember_me] == '1' ? remember(user) : forget(user)
        redirect_back_or user
      else
        message  = "Account not activated. "
        message += "Check your email for the activation link."
        flash[:warning] = message
        redirect_to root_url
      end
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

これでアカウント有効化の実装が出来ました!

第12章大まかな流れ

続いて12章です。流れは基本的にはアカウント有効化と一緒です。
只12章を細かく書くとこれだけで1日が過ぎてしまいそうなのでここでは気になったポイントだけ抑えておきたいなと思います!(後日追加していきたいと思います。。。)

https://i.gyazo.com/69c2a66e8bf3841ed58f4f14fa9d79bd.png (Ruby on Rails 第12章 パスワードの再設定スライドより引用)

https://i.gyazo.com/37ec9511b61f7e4ffe2311dbb9b47bf5.png (Ruby on Rails 第12章 パスワードの再設定スライドより引用)

hiddenフィールドの設定

12章ではユーザーからパスワードリセット申請を受け付けるとパスワード再設定用のhtmlフォームに移行させるようにしています。パスワード再設定の際にもメールアドレスをキーとしてユーザー検索を行うのですが、ユーザーとしてはパスワード再設定申請の際にも入力し、再設定の際にもまた入力と、かなりストレスフルな形となってしまうので今回はユーザーに入力させずにこちらで用意するようにします。

#app/views/password_resets/edit.html.erb

<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user, url: password_reset_path(params[:id]),
                  local: true) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= hidden_field_tag :email, @user.email %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Update password", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

注目はこの部分ですね

 <%= hidden_field_tag :email, @user.email %>

ユーザー画面からは見えないよう隠しフィールドとしてページ内に保存する手法をとっています。なお

<%= f.hidden_field :email, @user.email %>

としていないのはparams[:user][:email]ではなく、params[:email]に保存させたい為です。

パスワード再設定用の有効期限設定

第12章ではパスワード再設定の有効期限を設定しています。

#app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # パスワード再設定の期限が切れている場合はtrueを返す
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

  private
    .
    .
    .
end
reset_sent_at < 2.hours.ago

上記の部分、何となく考えるとreset_sent_atが2.hours.agoより少ない?と解釈しがちで何となく逆の意味に捉えがちなのですが、これはreset_sent_atが2.hours.agoより早いと捉えるとスッキリ解釈できます!

12章駆け足になってしまいましたが・・・今回は以上となります!!