プログラミング年少組のど素人が、アウトプット用に素人目線の解説を書いていきます。
自分が自分に教えていることを想像しながら進めていきます。
下記と同じような境遇の方であれば参考になるかも
- まっさらなプログラミング初学者目線
- オール独学
- 勉強は得意じゃない
- 社会人のため、1日に勉強できる量は限られる。
電子書籍のwebテキスト(第6版)を購入して使用
Progateの「Web開発パス」完走済
ドットインストールのプレミアム会員卒業
Railsチュートリアル解説動画を見ながら学習中
#11章 アカウントの有効化#
これから取り組む11章・12章はチュートリアルで最も難しい部分になるそうです・・・と、動画で言ってます。これまでの章でもうすでに難しのですが・・・
これまでの章で、登録・ログイン・ログアウト・ユーザー編集・更新・削除など、サイトに必要な機能は大体盛り込むことができました。
これからの章ではサイトを使うにあたって足りない部分や、追加の機能を見ていきます。
今回の章では登録時にアカウント有効化機能を使って本人確認する機能を追加します。
現在、新規登録には名前、メールアドレス、パスワードを入力しますが、ここに入れる値をバリデーションが通るものにするだけで、同じ人が無限にアカウントを作成することができるようになってます。
実在しないデタラメなメールアドレスを入力してもアカウントを作成できちゃいます。
何なら他人に成り済ましてアカウント作成など迷惑なこともできちゃうワケです。
これを実在するメールアカウントに紐づけてユーザー登録するようにして、登録途中の段階でそのアドレスにサイト登録のためにサーバーから確認のメールを送り、ユーザーはそのメールのリンクから登録を完了させる流れにするとどうでしょう
アカウント無限作成も成り済ましも防げそうです。
私が利用しているサービスも、確かにサイトに新規登録する時にメールからリンクに飛んで登録する方法はありましたが、こういう理由からだったんですね
機能を盛り込むのは、辛い道のりになりそうですが・・・
簡単に言えばこんな感じで進めていきます
【ユーザー】
サイト登録手続き開始!(アカウントは有効化していない状態)
【ユーザー → サーバー】
登録フォームに入力した情報をpost送信する
【サーバー】
before_createが発動しアカウント有効化するためのトークンを発行し、ハッシュ化してデータベースの「account_digest」に保存
「その後に」バリデーションをクリアすれば送信された情報(「name」「email」「password」)も保存する
【サーバー → ユーザー】
登録情報にあったemailアドレスに、先程発行したハッシュ化する前のトークン及びemailを送信
【ユーザー → サーバー】
email記載の登録ページに進むリンクをクリックすると、サーバーにユーザーの「email」情報と「ハッシュ化する前のトークン」の情報が送られる
【サーバー】
emailで送られてきた情報「email」と「有効化する前のトークン(をハッシュ化したもの)」
データベースに保存していた情報「email」と「ハッシュ化された有効化トークン」
この2つを比較して認証に使い、情報が合致したら晴れて「アカウントが有効化」される
#11.1 AccountActivationsリソース#
この章では新しいコントローラーである**「AccountActivation」コントローラー**を作成することから始めます。ここではアカウントの有効化・無効化を判断したり切り替えたりするアクションが入ってくると勝手に思ってます
アカウントが無効になっている状態を有効に変更させる場合は、「変更する」という観点からPATCHリクエストでupdateアクションを使って切り替えることが想定されるものの、そう簡単にはいきません。
テキストでは後々説明するのかこれ以上説明されてません
とりあえずトピックブランチを作成しています
$ git checkout -b account-activation
##11.1.1 AccountActivationsコントローラー##
先程言ってた**「AccountActivation」コントローラー**をさっそく作成します
$ rails generate controller AccountActivations
が、モデルは作成しません。その代わり「activation_digest(string)」「activated(boolean)」「activated_at(datetime)」などのカラムをUserモデルの中に追加します。sessionsコントローラーで「remember_digest(string)」カラムを追加したのと同じようなことです。
続きましてはルーティングです
こちらを追加
# 7つのアクションの中からeditのみのルートを作成する
resources: account_activations, only: [:edit]
7つのアクションに一括でルーティング作成できる「resources」を使いますが、今回は「edit」アクションしか関係ないらしいので「only: [:edit]」がついてます。
と、いうことは、AccountActivationsコントローラーの中身は「edit」アクションだけになるのかな・・・
ルートはできているので、とりあえずeditアクションで動く名前付きルート名は**「edit_account_activation_url(token)」**と、なるみたいです。
##11.1.2 AccountActivationのデータモデル##
ここでやること自体は簡単です。Userデータベースにカラム追加するだけですから
カラムを追加するだけなら「rails generate migration 〜」でした
$ rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
初めは有効化していない設定でしたので「activated」については「default: faulse」を加えます
add_column :users, :activated, :boolean, default: false
新たにデータベースの設定を加えましたのでこれを忘れずに
$ rails db:migrate
##Activationトークンのコールバック##
ここで11章で登場した概念「アカウント有効化」について考えてみましょう
ユーザーが登録を開始する前はもちろん「アカウント無効」状態です
アカウントが有効化された後、ユーザーは登録を完了させることができます。
と、いうことはデータベースにユーザーの情報が保存される前の段階でアカウントを有効化させなくてはいけません。
そこで利用するのがコールバックです
コールバックとは6章でバリデーションをやってた時に登場しました。
ユーザー登録の「前段階」でやることとしてemailアドレスを強制的に小文字に変える手順before_saveを使って加えました。
class User < ApplicationRecord
# これ
before_save { self.email = email.downcase }
今回ユーザー登録の「前段階」でやることとしてアカウントの有効化を加える必要があります。
今回も同じように「before_save」を使いたいところですが、これは「作成時」だけでなくて「更新時」も呼び出される仕様になってます。
ユーザーが登録する段階ならまだしも、登録情報を編集する際にはアカウントを有効化する必要はありません。
ですので今回は**「作成時」にだけ呼び出される「before_create」**を使用します。
前回は「before_save」に「{ self.email = email.downcase }」を渡していましたが、今回「before_create」にはメソッドを渡して見た目もスッキリさせます
before_create :メソッド名
private
# 使用するメソッドはここで定義する
def メソッド名
~
end
今回作成するメソッドは「create_activation_digest」メソッドです
ユーザーのアカウントを有効化するために使うトークンを発行して、ハッシュ化したものを「activation_digest」カラムに突っ込みます。役割はちょうどmodels/user.rbで定義した「remember」メソッドと同じようなもんです。
おさらいですが「remember」メソッドはこんな感じで定義していました
# 永続セッションのためにユーザーをデータベースに記憶する
def remember
# トークン発行
self.remember_token = User.new_token
# ハッシュ化させて「remember_digest」カラムに突っ込む
self.update_attribute(:remember_digest, User.digest(remember_token))
end
今回は
# プライベート空間で
private
def create_activation_digest
# トークン発行
self.activation_token = User.new_token
# ハッシュ化させて「activation_digest」に突っ込む
self.activation_digest = User.digest(activation_token)
end
やってることは同じですが、書き方が全然違います
それは「remember」メソッドが既にデータベースにいるユーザーに対して動作するメソッドであることに対し、「create_activation_digest」メソッドはデータベースに登録する前の段階で呼び出されるメソッドだからです。
そもそも「update_attribute(A,B)」で、「AをBに更新する」という意味ですからね。
登録前の状態では更新しようとするものがありません。
また、
しれっと登場していた**「activation_token」も役割としては「remember_token」同様に「仮のトークン置き場」**です。
ですので、その設定にするためには「remember_token」同様に「attr_accessor」に入れる必要があります
attr_accessor :remember_token, :activation_token
これで「create_activation_digest」メソッドが完成しました
before_create :create_activation_digest
private
def create_activation_digest
# トークン発行
self.activation_token = User.new_token
# ハッシュ化させて「activation_digest」に突っ込む
self.activation_digest = User.digest(activation_token)
end
「before_save」もしれっと同じ形に変更させてます
before_save :downcase_email
private
# メールアドレスをすべて小文字にする
def downcase_email
self.email = email.downcase
end
ここでseedsやfixtureに作ったサンプルユーザー達をアカウント有効化状態にします
(テストの度にアカウント有効化するのは面倒なので予め有効化されているようにします)
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true,
# これで有効化完了
activated: true,
activated_at: Time.zone.now)
これと同じものを他のユーザー(fixtureも含む)にもはめこんでいくだけです。
最後に
これまでのseedsのデータを一度消してから
$ rails db:migrate:reset
変更した部分(有効化した)を反映させることにします
$ rails db:seed
#11.2 アカウント有効化のメール送信#
ここまでが長い道のりすぎて何をしているか忘れかけてましたが、まだまだ続きます
むしろここから新たな概念が出てきますので。ここからが本番です
11章で実装する機能をもう一度見てみますと・・・
【ユーザー】
サイト登録手続き開始!(アカウントは有効化していない状態)
【ユーザー → サーバー】
登録フォームに入力した情報をpost送信する
【サーバー】👈現在ここまで終わってる
before_createが発動してアカウント有効化するためのトークンを発行し、ハッシュ化してデータベースの「account_digest」に保存
※ここで保存されるユーザーの情報は「email」と「ハッシュ化された有効化トークン」
【サーバー → ユーザー】👈これからこちらを実装する
登録情報にあったemailアドレスに、先程発行したハッシュ化する前のトークンをメール送信
ここからは、サーバーからユーザーにメールを送る機能を追加します
ここで新しく登場するのが**メイラー(mailer)**です。いかにもメールを送る役割をしている感があります。
Usersコントローラーのcreateアクションで有効化リンクを送信するために使われます。
Actionメイラーという初めて聞くワードですが、アクションとビューの関係のようなものです。
アクションで呼び出すのが、「ビュー」
メイラーで呼び出すのが「メールの文面」
という感じでみるとイメージがつきやすいでしょう。
「メールの文面」に含まれているもの
・メールアドレス情報:URLに含める(@user.email
)
・トークン:IDに含める(@user_activation_token
)
👉 GET /account_activations/IDに含められたトークン/edit?email=メールアドレス
##11.2.1 送信メールのテンプレート##
メイラーは「rails generate」で生成します
と、言いましても、ファイルツリーよく見てみますと「models」の上に「mailers」なんてディレクトリが初めからありましたけども・・・
先程も言いましたが、コントローラーと同じような感じだと思えば簡単です。コントローラーなら「rails generate controller Sessions アクション名」みたいな感じで作成しましたから
$ rails generate mailer UserMailer account_activation(アクション1) password_reset(アクション2)
とりあえずこのコマンドを実行してみると
先程見ていた「mailers」ディレクトリの中に新しく「user_mailer.rb」ができました
開いてみるとコマンドで指定した「accoutn_activation」アクション(みたいなもの)と「password_reset」アクション(みたいなもの)ができています。
※内容は似たようなもんだけど実際はアクションではないので(みたいなもの)をつけてます。どちらかと言うとメソッドなので、これからは「メソッド」とします
def account_activation
# インスタンス変数
@greeting = "Hi"
mail to: "to@example.org"
end
「accoutn_activation」は見た感じ「アカウント有効化」:11章で使用
「password_reset」は見た感じ「パスワードをリセット」:12章で使用
さらに、ビューファイルも各メソッドに対して2つずつ自動作成されています
「accoutn_activation」メソッドに対して「accoutn_activation.html.erb」「accoutn_activation.text.erb」の2つのファイル
「password_reset」メソッドに対してもHTMLファイルとテキストファイルができています。
今までの流れですとアクションを作成すると、自動で作成されるビューは1つ(html.erbファイル)だけした。
今回はメールを描画する側に配慮するため、HTMLファイルは表示できないが、textファイルだったら表示できるケースが考えられることから、2つ作られているみたいです。
メイラーについてはまだまだあります
**「mailers/application_mailer.rb」**です
コントローラーにおいても同じようなものがありますよね
「controllers/application_controller.rb」
こちらはコントローラーの親玉みたいなものでしたから、「mailers/application_mailer.rb」もメイラーの親玉みたいなものでしょう
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout 'mailer'
end
ファイル名が「application」とあるので、全体に影響する事項が設定してあるのでしょう
「default from」についてるアドレスはサーバー側からメールを送る際のアドレス
「layout」はメールのレイアウトのことでしょう
まあ、自動的にできるものの紹介はこんな感じでしょうか。
さてここからテンプレートをカスタマイズさせていきます。
まずは親玉メイラーの修正から
class ApplicationMailer < ActionMailer::Base
default from: "noreply@example.com"
layout 'mailer'
end
返信できません(noreply)という文言にアドレスを変更する
コンピューターが機械的に送っていることを分かりやすくするための変更です。
続いては「accoutn_activation」メソッドです
# GETリクエストがaccount_activation(user)に来たら・・・
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
account_activation(user)
引数取ってますが、これが明確なアクションとの違いです。
メソッドは引数を取ることができます。というわけでアクション(みたいなもの)とせずに「メソッド」としたわけです。
ですので、引数を渡した結果メールオブジェクト(textとかhtmlとか)が返され(returnされ)ます。
def account_activation(user)
# userをインスタンス化する
@user = user
mail to: user.email,
subject: "Account activation"
# => return: mail object (text/html)
end
少し分かりやすく変えるとこんな感じみたいです
インスタンス化した後に
「mailメソッド」を呼び出して「toオプション(送信先)」や「subjectオプション(件名)」をつけるような感じです
「ユーザー受け取ったら(送信先)や(件名)を指定してメール送る」
これだけ抑えときましょう
お次はメールの中身です。text形態とHTML形態の2種類のビューファイルがありました。
上の手順ですとこう書いていました。
【ユーザー → サーバー】
email記載の登録ページに進むリンクをクリックすると、サーバーにユーザーの「email」情報と「ハッシュ化する前のトークン」の情報が送られる
よって、サーバーに「email」や「トークン」の情報を送って有効化のための認証ができるようにするためのリンクをメールの文面の中に貼らなくてはなりません。
そのために、「メールアドレス」と「トークン」情報を送信できるようなリンクを作成する必要があります。
どのように作成するのかと言いますと
サーバーへのリンクの引数に「メールアドレス」と「トークン」を渡すようにします
こんな感じ👉GETリクエストを送る先のURL(「トークン」, 「メールアドレス」)
「GETリクエストを送る先のURL」については先程ルーティングで名前付きルートを作成しました。(resources :account_activations, only: [:edit])
名前付きルート:edit_account_activation_url(token)
URL:/account_activation/トークン/edit
よってまずはこんな感じです
edit_account_activation_url(「トークン」, 「メールアドレス」)
次は「トークン」です。
記憶の糸を辿りましょう。
このメールが送付されるのはユーザーを作成する前段階です。作成の前段階・・・作成の前段階・・・作成の前段階・・・
そう。**「before_create」**です
# 仮保存
attr_accessor :remember_token, :activation_token
:
before_create :create_activation_digest
:
# 有効化トークンとダイジェストを作成および代入する
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
今回使用するのは仮保存していた「activation_token」です。ハッシュ化なんてあとでやりゃあええんです。
@user
さんのトークン情報なので「@user.activation_token」とします
これを当てはめて
edit_account_activation_url(@user.activation_token, 「メールアドレス」)
最後の「メールアドレス」は@user
さんのemailなので。「@user.email」でOKでしょう。
これを当てはめて
edit_account_activation_url(@user.activation_token, @user.email)
これでリンクが完成です。
リンクをクリックするとaccount_activationコントローラーのeditアクションに「トークン」と「email」の情報が入ったGETリクエストが送られます。
そしてeditアクション内のauthenticateメソッドによって、「activation_digest」のトークン情報と比較されて有効化・非有効化が判断されるという流れになります。
あとは2つのビューファイルにできたリンクを入れておけば、表示内容はRailsがうまいことやってくれます。
テキストだけ紹介しときましょう
Hi <%= @user.name %>,
Welcome to the Sample App! Click on the link below to activate your account:
# ここが「トークン」と「email」情報が入ったリンク
<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
###CGI.escapeについて###
ここで「CGI.escape」が紹介されています。
「CGI」モジュールの「escape」メソッドということです。
URLには表現することのできない「文字列」があります。「@」や「空白」、「日本語」などはURLにそのまま入れることができません。
例えば「foo@example.com
」という文字列(というかemailでしょうけど)をURLの中に入れたかったとします。
「@」が含まれており、このままではURLで表示できません
そこで「CGI.escape」を使います。
CGI.escape('foo@example.com')
とすることで、URLで表示できる形にして返してくれます
この場合ですと
「"foo%40example.com"」になるみたいです。
##11.2.2 送信メールのプレビュー##
リンクのついたメールを作成することができました。
このメールについて、どんな風にできているかを確認する必要があります。
しかし「rails server」では確認できませんし、どうしましょう・・・
そこで使用するのがメールプレビューです
これを使用するとメールを実際に送信することなく、指定したURLを表示すると、どのようなメールが送信されるかを確認することができます。
が、これを使用するにあたっては色々と設定が必要になってきます。
1.development環境の設定(クラウドIDEの場合)
host = '「ここに入れるやつ」' # クラウドIDE
config.action_mailer.default_url_options = { host: host, protocol: 'https' }
「ここに入れるやつ」について
まずは「rails server」でHOME画面を開きます
URLの「https://」の後に続く部分を最後までコピーします
それを「ここに入れるやつ」に貼り付けます
2.プレビューファイルを更新する
メールを送ることなく、こんな感じのメール内容になるよ〜を確認できるようにする設定
プレビューファイルはここにあります👉test/mailers/previews/user_mailer_preview.rb
「acount_activation」部分を編集します
→今回はリンクとして貼り付けたURLがちゃんとした形になっているかをチェック
とりあえず出来上がり見ながら考えます
def account_activation
# とりあえず最初のユーザー引っ張ってくる
user = User.first
# テンプレートで使うトークンを代入
user.activation_token = User.new_token
# メールオブジェクトを生成する
UserMailer.account_activation(user)
end
ここがポイントですね
# メールオブジェクトを生成する
UserMailer.account_activation(user)
これで、メールの内容みたいなプレビューが作成されます。
ちなみにこれをどこで定義したかを覚えていますか
ここです👉meilers/user_meiler.rbです
def account_activation(user)
# userをインスタンス化する
@user = user
mail to: user.email,
subject: "Account activation"
# => return: mail object (text/html)
end
メールオブジェクトを作成するメソッドでしたね。
そのメールオブジェクトを、プレビューが簡単な表示内容で表示してくれるわけです。
もう一つこちら。
# テンプレートで使うトークンを代入
user.activation_token = User.new_token
これを入れているのは、URLにはトークン情報が含まれているので、これがないとURLが完成しません。
やっとこさ準備が完了しましたのでプレビューを見てみます
プレビューのコメントアウトしているところに親切にもここにプレビュー見たければここにアクセスしましょう的なことが書いてあります
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/account_activation
が、これは罠です。クラウドIDEでやってたらここのままでは表示されません。。
「localhost:3000」の部分をクラウドIDE用に書き換える必要があります
それがこれです
host = '「ここに入れるやつ」'
「localhost:3000」を「ここに入れるやつ」に書き換えましょう。そうするとプレビューが表示されるはずです。
##11.2.3 送信メールのテスト##
プレビューを表示させて「へえ〜」で終わってはいけません
メールで表示される内容をテストまでやりましょう
と言っても実は「rails generate mailers ~」をやった時に自動でテストが作られています。
test "account_activation" do
# メールオブジェクト作成します
mail = UserMailer.account_activation
# 件名 => OK
assert_equal "Account activation", mail.subject
# 宛先 => OK
assert_equal ["to@example.org"], mail.to
# 送り主 => 変更したのでNG
assert_equal ["from@example.com"], mail.from
# メール本文に"Hi"が含まれているかどうか => OK
assert_match "Hi", mail.body.encoded
end
このまま作っても不十分というかむしろ、NGあるしそもそも通りません
ですので修正です
test "account_activation" do
# 具体的にfixtureにいるまいけるさんでテストします
user = users(:michael)
# リンクに含めるのでトークンを作成してattr_accessorに入れる
user.activation_token = User.new_token
# メールオブジェクトをまいけるさんで作成して、以下で内容を確認していきます
mail = UserMailer.account_activation(user)
# 件名
assert_equal "Account activation", mail.subject
# 宛先 => まいけるさんのemailに変更
assert_equal [user.email], mail.to
# 送り主 => noreplyに変更したので修正
assert_equal ["noreply@example.com"], mail.from
# メール本文に名前が入っているか(<%= @user.name %>があるので)
assert_match user.name, mail.body.encoded
# メール本文にトークンは入っているか
assert_match user.activation_token, mail.body.encoded
# メール本文にemailは入っているか・・・ん?
assert_match CGI.escape(user.email), mail.body.encoded
end
一つ一つ見ていけばなんてこと無いテストですが、
最後の最後で少々引っ掛かりそうでしたが、先程紹介したCGI.escapeメソッドがさっそく登場しています。
テキストファイル上ではこう記しました
<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
が、実際にテキストを開いて見ると分かりますが、そのままで表示されていません。
リンクがURLの形で表示されています。
URLでは表示できない文字列がありそれに対応したものを表示してくれるのが「CGI.escapeメソッド」でした。
今回の使い方としては「CGI.escape(example@railstutorial.org)」
こうすることで、これ「example%40railstutorial.org」
があることを確認できるわけです。
これで晴れてテストが通りま・・・・せん!
テストファイル内のドメイン名を正しく設定する必要があります
先程もドメイン名の設定をしましたが、「開発環境(development.rb)」での設定でした
ですので、「テスト環境(test.rb)」についても設定する必要があります
config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: '「ここに入れるやつ」' }
「ここに入れるやつ」は「config/environments/development.rb」で使用したものと同じものです
これでテストはOKになるはずです
##11.2.4 ユーザーのcreateアクションを更新##
ここでやる作業を確認します
【ユーザー】
サイト登録手続き開始!(アカウントは有効化していない状態)
【ユーザー → サーバー】
登録フォームに入力した情報をpost送信する
【サーバー】👈ここから
before_createが発動しアカウント有効化するためのトークンを発行し、ハッシュ化してデータベースの「account_digest」に保存
「その後に」バリデーションをクリアすれば送信された情報(「name」「email」「password」)も保存する
【サーバー → ユーザー】👈ここまで
登録情報にあったemailアドレスに、先程発行したハッシュ化する前のトークン及びemailを送信
createアクションが正しい登録情報を受け取って、データベースに保存できたら有効化の認証のためメールを送る働きをします。
def create
@user = User.new(user_params)
# 保存に成功したら
if @user.save
# mailerで定義したaccount_activationメソッドを使ってメールを送信する
UserMailer.account_activation(@user).deliver_now
# フラッシュ表示
flash[:info] = "Please check your email to activate your account."
# メール送信後はHOME画面に戻される
redirect_to root_url
else
# バリデーション通らなかったら登録ページを再表示
render 'new'
end
end
毎回「account_activation(@user)」
を忘れそうになりますが、「meilers/user_mailer.rb」で定義した「件名」と「宛先」指定してメールすることを定義したメソッドです
しれっと**「deliver.now」**がメソッドチェーンでくっついてますが、これはその通り「その場で送信する」という意味合いで捉えときましょう
ここでは保存に成功した後、メールを送ったらHOMEページに飛ぶように指定してあります
前は自動でログインして個人のページに飛ぶようになってました・・・
なぜならまだ有効化されてないですからね
有効化してないのにログインできちゃったら、これまでの苦労は一体なんだったのか・・・となります
ですので統合テストの手順も少し変更が必要です
新規作成後、自動でログインし、個人の詳細ページに飛ぶ手順をコメントにしちゃいましょう
follow_redirect!
# assert_template 'users/show'
# assert is_logged_in?
一応これで動きそうですが、実際登録してみてもメールは届かないし、動いている様子はありません。
開発環境だからでしょうか・・・
動いていないわけではないみたいです。ログは残ってます。
#11.3 アカウントを有効化する#
やっとアカウントを有効化するところまできました
【ユーザー】
サイト登録手続き開始!(アカウントは有効化していない状態)
【ユーザー → サーバー】
登録フォームに入力した情報をpost送信する
【サーバー】
before_createが発動しアカウント有効化するためのトークンを発行し、ハッシュ化してデータベースの「account_digest」に保存
「その後に」バリデーションをクリアすれば送信された情報(「name」「email」「password」)も保存する
【サーバー → ユーザー】
登録情報にあったemailアドレスに、先程発行したハッシュ化する前のトークン及びemailを送信
【ユーザー → サーバー】
email記載の登録ページに進むリンクをクリックすると、サーバーにユーザーの「email」情報と「ハッシュ化する前のトークン」の情報が**「GETリクエストでeditアクションに」**送られる
【サーバー】👈これからここをやる
emailで送られてきた情報「email」と「有効化する前のトークン(をハッシュ化したもの)」
データベースに保存していた情報「email」と「ハッシュ化された有効化トークン」
この2つを比較して認証に使い、情報が合致したら晴れて「アカウントが有効化」される
これまでのために一生懸命やってきたアカウント有効化までの道のりですが、最後、ユーザーからのメールを受信し、認証する作業を行うのが「edit」アクションです。
そういえばルートも「resources :account_activations, only: [:edit]」てな感じでeditアクションのものだけ作成していました。
認証には「email」と「トークン」を使用してauthenticateで行うので、9章の知識があればできるそうです・・・が、それをそのまま使うのであったら上達しません。ので今回新しい概念を加えます!
まあ、9章のをそのままやれと言われてもだいぶ怪しいですけど・・・
##11.3.1 authenticated?メソッドの抽象化##
先程書いた新しい概念!がタイトルにもあるauthenticated?の抽象化です
唐突ですが、authenticated?メソッドで認証を行うケースはたくさん登場します
ログイン認証での
「remember_token」と「remember_digest」比較
アカウント有効化認証での
「activation_token」と「activation_digest」比較
12章で登場するパスワード再設定認証での
「reset_token」と「reset_digest」比較
これらに出てくるワードに共通しているパターン
「ホニャララ_token」「ホニャララ_digest」
・・・ちゃんとした形でいうと
「#{認証名}_digest」と「#{認証名}_token」という分類の仕方にたどり着きます
これを見ると、共通の部分を変数にすれば使い回しできるような感じが出てきます
ちなみに今のところauthenticated?メソッドはユーザーモデルで定義されています
# トークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
これだと「remember_token」(ログイン認証)専用ですので、もちろんこのままではアカウント有効化認証では使えないですけどね。
さて、これからこのauthenticated?の定義を変更することで、「remember_token」以外でも使えるような形に変更していきます
①Rubyのsendメソッドを使う
初めて出てきましたsendメソッド
それでは、このメソッドを使うに至るまでをみていきましょう
先程見ていた通り「ホニャララ_token」「ホニャララ_digest」が共通してありますので、これをなんとかして使いたいです
この「ホニャララ」部分を仮に「a」とした場合(a = "ホニャララ")、erbの中では
「#{a}_token」だったり「#{a}_digest」みたいな形にすれば使えそうです
が、そうはいきません。というのを「rails console」で見ていきましょう
# とりあえず最初のユーザーで見ていくとして
>> @user = User.first
# これで名前が見れたり
>> @user.name
# これでメールアドレスが見れたりしますが
>> @user.email
# これでハッシュ化された記憶トークンが見れます(ログイン状態保存してれば)
>> @user.remember_digest
出てきました!「remember_digest」。これを先程の形にしようとすると・・・
# とりあえず「a」に入れてみる
>> a = "remember"
# さっそく入れてみる・・・
>> @user.#{a}_digest
# 失敗します
?>
そう簡単にはできる話ではなさそうですね
文字列にしてみると
>> "#{a}_digest"
# ちゃんとなっているみたいです。が、あくまで文字列です
=> "remember_digest"
# もちろんこれも失敗します
>> @user."#{a}_digest"
ここで、sendメソッドの動きについて見ていきましょう
オブジェクトに「@user.send」みたいな形で渡します
# nameカラムをハッシュで持ってこれる
>> @user.send :name
=> "Example User"
# なんなら文字列でも持ってこれる
>> @user.send "name"
=> "Example User"
何となく見えてきましたが、引数に欲しいデータをハッシュや文字列にして入れることができるみたいです
文字列を引数に取れるなら・・・
# これは先程失敗したやつですが
>> @user."#{a}_digest"
# こうやって式展開することでうまくいきます
>> @user.send "#{a}_digest"
# もちろんこれらもうまくいきます
>> @user.send :remember_digest
>> @user.send "remember_digest"
ちなみにメソッドだって同様に扱うことができます
# 長さを返すメソッドlength
>> @user.name.length
=> 12
# sendメソッドをつけると、メソッドがハッシュや文字列にできちゃう
>> @user.name.send :length
=> 12
>> @user.name.send "length"
=> 12
こんな使い方もできちゃいます
# 文字列"a"に代入すれば
>> a = "length"
# 式展開でしっかり使えます
>> @user.name.send "#{a}"
テキストの内容に近づけていきましょう
# activation_digestカラムの値を引っ張ってきます
>> @user.activation_digest
# sendメソッドを使うと文字列にできる
>> @user.send "activation_digest"
# attributeにactivationを代入すると
>> attribute = "activation"
# 式展開でしっかり使えます
>> @user.send "#{attribute}_digest"
この「attribute」に時にはremember、時にはactivation、時にはpassword。みたいな感じにしてそれぞれの状況に合わせて入れていけば「authenticate?メソッド」を使い回すことができるというわけです
このメタプログラミング、すごい便利に扱えそうな機能ですが・・・
これを実務で実装するのはそう簡単ではありません。
将来的に扱うコードを予測するということができないといけないからです。
共通しているパターンに対して行うという点についてですが、今回みたいに後から見れば当然共通しているパターンだなと分かります。むしろ、そうなるように作者が仕組んでました。
ですが、本来であれば「共通したパターンになりそうなので抽象化しよう」とあらかじめやっておくべきです。じゃないと過去に戻る必要性が出てきて一度書いたものを修正して・・・ということが生じます。(今回も、抽象化することによって修正しなければいけない箇所がいくつか出てきます・・・)
よって、「remember_digest」が出てきた時点で将来的に「activation_digest」や「password_digest」を使うことになることを予測し、抽象化する。ということができないといけません。
到底初学者ペーペーの為せる技ではありません。
そう聞くとかなり上級者向けの機能ですよね。
話を戻します。ここでauthenticated?メソッドの定義を修正します
修正前はremember_tokenでしか使えません
# トークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
修正後はattribute(認証するもの)の値を変更すれば使い回しできる
# ここでのattributeの使い方は状況に合わせて「remember」や「activation」などを入れる
def authenticated?(attribute, token)
digest = self.send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
ちょっとついていけません。。
まずいきなりauthenticated?引数が2個ついています。元々は「remember_token」だけだったのが、分裂でもしたのでしょうか。。。
ここでのattributeの使い方は状況に合わせて「remember」や「activation」を入れるということになってます。
そして「token」です。簡単に「一般化」してます。とだけ書いてますが、よく分かりません。。
説明も特にないので、「第一引数」の「第二引数」ということことかなと無理やりに解釈しときます。
第一引数にrememberが入れば「rememberトークン」。activationが入れば「activationトークン」てとこでしょう。
しかもsendメソッドごと変数「digest」に代入してます。。。。
まあ、これ自体(self.send("#{attribute}_digest"))が「2a$12$H83n4Rv9.9NWzc9Oz06u/uMGHbMKWWdPJqarwXhfjlMPFX4h/0Qay」みたいな文字列なので、入れてるのは変数sendじゃなくてあくまで「文字列」です。
(コンソールでは「@user.send("#{attribute}_digest")」と入れると上の文字列が出てきます)
これだけゆっくり見ていけば少しは理解ができそうです。
そしてまだまだ終わりではないのです
このコード自体がユーザーモデル内(models/user.rb)にあることから「self」を省略できるそうです
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
まあ、コンソール上で
$ send("#{attribute}_digest")
なんてやっても当然エラーで何も出力できませんので、初学者ペーペーには省略してもらわないほうがありがたいですがね。。。
何はともあれ、authenticated?メソッドはこれで完成です
しかしここでテストを実施すると失敗します。
もちろん「authenticated?」メソッドを抽象化したせいです。
抽象化で影響が出た範囲を修正していきます
主な修正点は**「引数を2つ取るようになった」**です。
①current_user内の抽象化したauthenticated?メソッド
修正前
def current_user
:
# 引数は一つです
if user && user.authenticated?(cookies[:remember_token])
:
end
修正後
def current_user
:
# 第一引数に「:remember」を追加します
if user && user.authenticated?(:remember, cookies[:remember_token])
:
end
②Userテスト内の抽象化したauthenticated?メソッド
修正前
test "authenticated? should return false for a user with nil digest" do
assert_not @user.authenticated?('')
end
修正後
test "authenticated? should return false for a user with nil digest" do
# 第一引数に「:remember」を追加します
assert_not @user.authenticated?(:remember, '')
end
修正は2点ですが、共に**「attribute」に入る「:remember」が追加されている**のが分かります。
sendメソッドについて思い出すと、引数には「文字列」か「シンボル」を取ることができましたね。
今回の例では「シンボル」を選択しています。こちらの方がよりメソッドに関連していることを強調できるみたいです。私には全く伝わりませんが・・・
これでテストは成功するはずです。
後から修正した場合、どこに影響が及ぶかを掴むことができるのでテストは大事です。
今後はこの抽象化して「activation_digest」にも対応できるようになった「authenticated?メソッド」を使っていきます
##11.3.2 editアクションで有効化##
先程定義し直した「authenticated?」メソッドをさっそくeditアクションで使います
まずは全体像からです
def edit
# メールのリンクからGETリクエストで送られてきたemail
user = User.find_by(email: params[:email])
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
で、今回の抽象化に関係している部分がこちら
if user && !user.activated? && user.authenticated?(:activation, params[:id])
有効化の作業は現時点で有効化されていないユーザーに対してのみ行えば良いので「!user.activated?」(有効化されていない)がtrueを返した場合にのみ、右に進めるようにしています。
すでに有効化されているユーザーを何度も有効化する必要はありませんからね
これは理解できるのですが、次の「authenticated?メソッド」の第二引数が「params[:id]」となっている点がどうも分かりませぬ
新・authenticated?メソッドは第一引数に「何の認証か」第二引数に「トークン」を取ってました。
今回は「params[:id]」となっています。どういうことでしょうか・・・メールのリンクからGETリクエストで送られたのは確か「email」と「トークン情報」だったはず
この「params[:id]」中身はトークンということみたいです・・・それ以上はどこにも説明がありません。モヤっとしますが、調べようがないので次に進むしかありません。無念。
有効化の認証が通ったあとは
update_attributeで「:activated」を「true」に更新し、「:activated_at」を「Time.zone.now(今の時刻)」に更新しています
そんなこんなで「edit」アクション実装完了です
これで有効化の実装が完了しました。長い道のりでした。
いやいやまだ終わりません。。
実は今、有効化しなくてもログインできちゃう状態なんです
「Sign up」から登録情報送ったら「Please check your email to activate your account」とフラッシュが表示されて、メールでリンクが飛ばされ、アカウントの有効化に進むようにはなってます。
そこで有効化の手順に進むのですが、有効化をしないまま、送った登録情報でログインしようとすると普通にログインできます。
マジか。です。
有効化してないユーザーが普通にログインできるなら、有効化の意味全くないジャン。となります。
ですので、ログインする仕組みを見直しましょう。
ログインといえば「sessionsコントローラー」です。
修正前はこうなっていました。当然有効化してるかどうかなんて関係なくemailとpasswordで判断してログインしてます。
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in(user) # 引数で渡されたユーザーをログインさせる
:
これを、「有効化してるなら、ログイン可」という流れに変更しましょう
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && 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
これで晴れて有効化していなかったらログインできないようにできました。
##11.3.3 有効化のテストとリファクタリング##
当然!テストします。
ユーザー登録の統合テストに作成していた登録の流れを整理していきます
出来上がりを見てみます
def setup
ActionMailer::Base.deliveries.clear
end
:
test "valid signup information with account activation" do
get signup_path
# 有効な登録内容で送信するとユーザー数が1増えるよね
assert_difference 'User.count', 1 do
post users_path, params: {user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password"} }
end
assert_equal 1, ActionMailer::Base.deliveries.size
# アクション内のインスタンス変数にアクセス
user = assigns(:user)
assert_not user.activated?
# 有効化していない状態でログインしてみる
log_in_as(user)
assert_not is_logged_in?
# 有効化トークンが不正な場合
get edit_account_activation_path("invalid token", email: user.email)
# ログインできないですよね?
assert_not is_logged_in?
# トークンは正しいがメールアドレスが無効な場合
get edit_account_activation_path(user.activation_token, email: 'wrong')
# ログインできないですよね?
assert_not is_logged_in?
# 有効化トークンが正しい場合
get edit_account_activation_path(user.activation_token, email: user.email)
assert user.reload.activated?
follow_redirect!
assert_template 'users/show'
assert is_logged_in?
end
よくわかんない感じのところから考えていきますか
def setup
ActionMailer::Base.deliveries.clear
end
「ActionMailer::Base.deliveries」って何やねん問題です。
これについては特に何も書いてありません。が、同じものが書かれている部分がもう一つあります
assert_equal 1, ActionMailer::Base.deliveries.size
このコードについては、配信されたメッセージがきっかり1つであるかどうか確認していると説明があります。要は、メールが1通送られてますよね?を確認しているということです
「1」=「1つ」
「ActionMailer::Base.deliveries.size」=「配信されたメッセージ」
と、いうことか!
で、あれば
**「ActionMailer::Base.deliveries.clear」は「配信されたメッセージがない状態」**みたいな感じで捉えておけば、流れとしては問題なさそうです。
まあ、ユーザー新規登録のテストなので、テストの中でこれから登録しようとしてるユーザーが、まずは有効化されていないことを前提としているのは納得いきます。
前提条件で配信を消して有効化してない状態にすることで、テストが開始する時に実は他のテストで有効化されちゃってましたーみたいなケースに対応できるということみたいです。
今回作成したテスト単体ですと影響なさそうですけどね。
setupの部分を消して何回テストしても成功しますし。。。
お次はこちら
# アクション内のインスタンス変数にアクセス
user = assigns(:user)
「assignsメソッド」?
またまた初見さんです。
これには説明があります。**「アクション内のインスタンス変数にアクセスできるようになる」**ということみたいです。。。
今回はUsersコントローラーのcreateアクションでPOSTリクエストから送られてきたデータを受け取ったインスタンス変数「@user」
にアクセスしているみたいです。アクセスできる変数にアクセスするためのメソッド??アプリケーションコードのインスタンス変数@user
にアクセスできる
うう〜ん。モヤっとした感じがあります。
ここは「user = assigns(:user)」を消してみましょう。
すると下の行の
「assert_not user.activated?」がエラーになっちゃいます
そりゃそうです。「user」がいなくなっちゃったから。確かに実体のないものに対して、有効化されたのか判断はつきません。
なるほど!「実体」作るために「assignsメソッド」が登場したんですね。
最後に「follow_redirect!」以下を元に戻してテストは完了です。。。
リファクタリングをしてコードをスッキリさせよう
今回2つの機能をメソッド化します
activateメソッド・・・ユーザーの有効化属性を更新する機能
def activate
update_attribute(:activated, true)
update_attribute(:activated_at, Time.zone.now)
end
→「account_activations_controller.rb」へ
send_activation_emailメソッド・・・有効化メールを送信する機能
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
→「users_controller.rb」へ
どこに何があるかがもうごちゃごちゃになってきてますね。
#11.4 本番環境でのメール送信#
実際にメールが送れているかまで確認しようとすると、Herokuアカウントにクレジットカード設定が必要になります。
ページを開いてみると全部英語!全部英語のページにクレジットの設定をするのは勇気がいりましたが、料金は発生しませんというテキストの言葉を信じて登録しました。
あとは本番環境で設定を加えてやればOKです
「config/environments/production.rb」にテキスト通りの設定を入れます
とりあえずこれで11章で入力するコードは全て入れましたのでgitにプッシュしちゃいます
いつもの流れからやっていきます
$ rails test
$ git add -A
$ git commit -m "Add account activation"
$ git checkout master
$ git merge account-activation
ここまで済んだらgitにプッシュできます
$ rails test
$ git push
$ git push heroku
$ heroku run rails db:migrate
ここからが有効化メールの設定です
$ heroku addons:create mailgun:starter
受信メールの認証を行うそうです
$ heroku addons:open mailgun
MailGun公式ドキュメントの設定を書いてある通りにやります
一応確認がてら、設定方法を残しときます
1.herokuを開いてログインする
2.該当のサブドメインに入る
3.「MailGun Starter」のリンクがあるのでクリックする
4.「Sending」 => 「Domains」
5.「Sandbox~」で始まるリンクを探してクリック
6.「Email address」のところに「ApplicationMailer」で指定した、送り主のアドレスを入れる
あとは本番環境でサイトを開いていざ登録!!
するとメールが無事に届くことを確認できるでしょう。私はうまくいきませんでしたが・・・
#11章の要約をようやく作り終えての感想#
最後の最後でうまくいかなくてかなりもやっとする結果になりました。本番環境でサイトを開き、adminユーザー(example@railstutorial.org)としてログインしようとするも、有効化されていないためログインできないと表示されます。URLで直接(users/1)検索すると、確かに「Example User」は存在します。どのユーザーも「activated: true」としてるためできそうな気もするんですが。
開発環境ですと、なぜか有効化せずともログインできます。
データベースが、sqlite3とpgで違うことも何か影響しているのでしょうか・・・
どうにもこうにも解決できそうにないので諦めます。
どこかで何かの設定を間違えているのでしょう。誰か教えて下さい。助けて下さい。
※この問題は13章終えたところで解決しました。問題はやはりデータベースでした。
$ heroku pg:reset DATABASE
これをやることで、pgのデータベースをいったんスッキリさせることができます。
サービスのリリース後は決してやるべきではないですが・・・これで、本番環境でも晴れてログインすることができるようになりました
この章は本当に難しです。一つ一つこれはこれで・・とやっていると、時間かかりまくりました。