Rails 総復習1ヶ月チャレンジ 5日目(Railsチュートリアル編)
チャレンジ5日目です!そしてワクチン接種2回目に熱が出てしまい1日動けず休んでしまいました・・・
気を取り直して今日から頑張ります!!
5日目(Ruby on Rails チュートリアル 11章アカウントの有効化〜12章パスワードの再設定)
この二つの章、個人的に非常に難しく、テキストでは理解が出来ないと判断して動画学習に切り替えました。動画学習の方が頭に入ってきやすい感じでいい感じでした!Ruby on Rails チュートリアルは動画教材もしっかりしていて学習しやすいですね!
今回は視覚的に分かりやすくしたい為、 流れを動画で使われていたスライドを引用させていただき理解していきます!
(Ruby on Rails 第11章 アカウントの有効化より引用)
11章大まかな流れ
- ユーザー登録申請
↓ - トークン作成(以前作成のnew_tokenを使用)、その後ハッシュ化
↓ - usersテーブルにactivation_digest(ハッシュ化したトークンを保存)、activated
(ユーザー認証待ちか認証済みか判別、boolean型)カラムをそれぞれ追加
↓ - ユーザーより申請のあったメールアドレスへ認証に必要なトークン、ユーザーメールアドレスを含めたリンクを載せたメールを送信
↓ - ユーザーがリンクをクリックすると、管理者側でトークンとユーザーアドレスが正しいものかを判別し、正しければ認証する。
下記が具体的な流れです。
(Ruby on Rails 第11章 アカウントの有効化スライドより引用)
(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アクションにしています。
(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属性が得られるようになる。
(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日が過ぎてしまいそうなのでここでは気になったポイントだけ抑えておきたいなと思います!(後日追加していきたいと思います。。。)
(Ruby on Rails 第12章 パスワードの再設定スライドより引用)
(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章駆け足になってしまいましたが・・・今回は以上となります!!