- 投稿日:2019-01-26T22:58:02+09:00
Ruby on Rails チュートリアル 第11章 アカウント有効化(AcctionMailer Activation)やSendGridの使い方など
著者略歴
YUUKI
ポートフォリオサイト:Pooks
RailsTutorial2周目11 アカウントの有効化
現時点では、新規登録したユーザーは初めから全ての機能にアクセスできるようになっている。
本章では、アカウントを有効化するステップを新規登録の途中に差し込むことで、本当にそのメールアドレスの持ち主なのか、確認できるようにする。具体的には
①有効化トークンやダイジェストを関連付けておく
②有効化トークンを含めたリンクをユーザーにメールで送信
③ユーザーがそのリンクをクリックすると有効化このような仕組みで、メールアドレスの持ち主であることを証明させる。
第12章でも似たような仕組みを使って、ユーザーがパスワードを忘れた時にパスワードを再設定できる仕組みを実装する。
これらの機能ごとに新しいリソースを作成し、コントローラ/ルーティング/データベース以降/の例について、1つずつ学んでいく。
最後に、Railsの開発環境や本番環境からメールを実際に送信する方法についても学ぶ。
アカウントを有効化する段取りは、ユーザーログイン&ユーザーの記憶と似ている。
①ユーザーの初期状態を「有効化されていない(unactivated)」にする
②ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する
③有効化ダイジェストはDBに保存。有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
④ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、DB内に保存しておいた有効化ダイジェストと比較することで、トークンを認証する
⑤ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み(activated)」に変更する。都合の良いことに、今回実装するアカウント有効化やパスワード再設定の仕組みと、以前に実装したパスワードや記憶トークンの仕組みにはよく似た点が多いので、多くのアイデアを使い回すことができる。
例えば、
User.digest
やUser.new_token
、改良版のuser.authenticated?
メソッドなど検索キー、string、digest、authenticationごとの点
検索キー string digest authentication password password_digest authenticate(password) id remember_token remember_digest authenticated?(:remember, token) activation_token activation_digest authenticated?(:activation, token) reset_token reset_digest authenticated?(:reset, token) 出典:表 11.1: ログイン/記憶トークン/アカウントの有効化/パスワードの再設定で似ている点
今章でアカウント有効化に必要なリソース、データモデルを作っていく。
アカウント有効化時のメール送信部分は
メイラーを使って作っていく。また、authenticated?メソッドを使って、実際にアカウントを有効化する部分も実装していく。
11.1 AccountActivationsリソース
8章で扱ったセッション機能を使って、アカウント有効化の作業をリソースとしてモデル化する。
アカウントの有効化リソースはActive Recordのモデルとは関係ないため、関連付けはしない。
その代わり、この作業に必要なデータ(有効化トークンや有効化ステータスなど)をUserモデルに追加する。アカウント有効化もリソースとして扱いたいが、いつもとは少し使い方が異なる。
例えば、有効化用のリンクにアクセスして有効化のステータスを変更する部分では、RESTのルールに従うとPATCHリクエストとupdateアクションになるべき。
しかし、有効化リンクはメールでユーザーに送られる。
ユーザーがこのリンクをクリックすれば、それはブラウザで普通にクリックしたときと同じであり、その場合ブラウザから発行されるのは(updateアクションで使うPATCHリクエストではなく)GETリクエストになってしまう。
このため、ユーザーからのGETリクエストを受けるために、updateアクションではなくeditアクション
に変更して使っていく。$ git checkout -b account-activation11.1.1 AccountActivationsコントローラ
UsersリソースやSessionsリソースのときと同様に、AccountActivationsリソースを作るために、
まずはAccountActivationsコントローラ
を生成する。$ rails g controller AccountActivationsここで、有効化のメールには
edit_account_activation_url(activation_token, ...)これは、editアクションへの名前付きルートが必要になるということ。
そこで、まずは名前付きルートを扱えるようにするため、ルーティングにアカウント有効化用のresources行を追加する。routes.rbRails.application.routes.draw do get 'sessions/new' get 'users/new' root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' post '/signup', to: 'users#create' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users # usersリソースをRESTfullな構造にするためのコード。 resources :account_activations, only: [:edit] # editアクションのみaccount_activationsリソースを適用 end
HTTPリクエスト URL Action 名前付きルート GET /account_activation//edit edit edit_account_activation_url(token) 出典:表 11.2: アカウント有効化用のRESTfulなルーティング設定 (リスト 11.1)
これからアカウント有効化用のデータモデルとメイラーを作って行く。
演習
1:テストがパスすることを確認
確認済み
2:名前付きルートでは、
_path
ではなく_url
を使うように記してある。その理由は?
_url
は(http://~)を指定してメールから飛べるようにしている。
(_pathだと相対パスな為http://〜からの指定ができない)11.1.2 AccountActivationのデータモデル
有効化のメールは一位の有効化トークンが必要。
例えば送信メールとデータベースのそれぞれに同じ文字列を置いておく方法がある。
しかし、この方法だとデータベースの内容が漏れた時、多大な被害に繋がってしまう。攻撃者がDBへのアクセスに成功して、新しく登録されたユーザーアカウントの有効化トークンを盗み取り、本来のユーザーが使う前にそのトークンを使ってしまうケースなど、セキュリティが甘いケースに繋がってしまう。
このような事態を防ぐために、パスワードの実装や記憶トークンの実装と同じように、仮想的な属性を使ってハッシュ化した文字列をDBに保存するようにする。
具体的には
user.activation_tokenこのようなコードで仮想属性の有効化トークンにアクセスし
user.authenticated?(:activation, token)Userモデルで編集した
authenticated?
メソッドを使って、activated属性
を追加して論理値を取るようにする。これで、自動生成の論理値メソッドと同じような感じで、ユーザーが有効であるかどうかをテストできるようになる。
if user.activated?ユーザーを有効化した時の、変更後のデータモデルはこうなる
出典:図 11.1: Userモデルにユーザー有効化用の属性を追加する
次のマイグレーションをコマンドラインで実行し、データモデルを追加すると、3つの属性が新しく追加される。
rails g migration add_activation_to_users ¥次に、admin属性の時と同様に、
activated属性
のデフォルトの論理値をfalseにしておく。[timestamp]_add_admin_to_users.rbclass AddActivationToUsers < ActiveRecord::Migration[5.1] def change add_column :users, :activation_digest, :string add_column :users, :activated, :boolean, default: false add_column :users, :activated_at, :datetime end endマイグレーションを実行して変更を反映。
$ rails db:migrateActiveトークンのコールバック
ユーザーが新しい登録を完了するためには必ずアカウントの有効化が必要になる。
その為、「有効化トークン」や「有効化ダイジェスト」はユーザーオブジェクトが作成される前に、生成されるようにしておく必要がある。メールアドレスをDB保存時も、保存前に全部小文字に変換するようにしたが、その時は
before_save
コールバックにdowncase
メソッドをバインドした。オブジェクトにbefore_saveコールバックを用意しておくと、オブジェクトが保存される直前、オブジェクトの作成時や更新時にそのコールバックが呼び出される。
しかし、今回はオブジェクトが作成された時だけコールバックを呼び出したい。
(それ以外の時は呼び出したくない)そこで、
before_create
コールバックを使う。before_create :create_activation_digestこのコードは
メソッド参照
と呼ばれるもので、こうするとRailsはcreate_activation_digest
というメソッドを探し、ユーザーを作成する前に実行するようになる。6章では、
before_save
に明示的にブロックを渡していたが、メソッド参照の方がおすすめ。また、create_activation_digest(作成した有効化記憶トークンメソッド)メソッド自体はUserモデル内でしか使わない為、
private
キーワードの中に書く。private def create_activation_digest # 有効化トークンとダイジェストを作成および代入する endクラス内でprivateキーワードより下に記述したメソッドが非公開なのは、コンソールで確かめられる。
>> User.first.create_activation_digest User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] NoMethodError: private method `create_activation_digest' called for #<User:0x007f8da4098e98> Did you mean? restore_activation_digest! from (irb):1今回は
before_create
コールバックを使う目的は、トークンとそれに対応するダイジェストを割り当てるためである。self.activation_token = User.new_token self.activation_digest = User.digest(activation_token)このコードでは、記憶トークンや記憶ダイジェストのために作ったメソッドを使いまわしている。
9章で扱ったremember
メソッドと比べてみる。# 永続セッションのためにユーザーをデータベースに記憶する def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) end主な違いは、後者の
update_attribute
の使い方にある。この違いは、記憶トークンやダイジェストは既にDBにいるデータベースのために作成されるのに対し、
before_create
コールバックの方はユーザーが作成される前に呼び出される。このコールバックがあることで、
User.new
で新しいユーザーが定義されると、activation_token
属性やactivation_digest
属性が得られるようになる。後者の
activation_digest
属性は既にDBのカラムとの関連付けができあがっているので、ユーザーが保存されるときに一緒に保存される。
上記の説明をUserモデルに実装してみる。
user.rbclass User < ApplicationRecord # インスタンス変数の定義 attr_accessor :remember_token , :activation_token # 記憶トークンと有効化トークンを定義 before_save :downcase_email # DB保存前にemailの値を小文字に変換する before_create :create_activation_digest # 作成前に適用 private # メールアドレスを全て小文字にする def downcase_email self.email = email.downcase # emailを小文字化してUserオブジェクトのemail属性に代入 end # 有効化トークンとダイジェストを作成および代入する def create_activation_digest self.activation_token = User.new_token # ハッシュ化した記憶トークンを有効化トークン属性に代入 self.activation_digest = User.digest(activation_token) # 有効化トークンをBcryptで暗号化し、有効化ダイジェスト属性に代入 endサンプルユーザーの作成とテスト
seedsファイルを更新しサンプルユーザーを作成、
fixtureを更新してテスト時のサンプルユーザーを事前に作成しておく。seeds.rbUser.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true, activated: true, activated_at: Time.zone.now ) 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password, activated: true, activated_at: Time.zone.now) end
Time.zone.now
はRailsの組み込みヘルパーであり、サーバーのタイムゾーンに応じたタイムスタンプを返す。users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: true activated: true activated_at: <%= Time.zone.now %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> <% end %>DBを初期化して、サンプルデータを再度生成し直し、変更を反映
$ rails db:migrate:reset $ rails db:seed演習
1:テストが通ることを確認
確認済み
2:コンソールからUserクラスのインスタンスを生成し、そのオブジェクトから
create_activation_digest
メソッドを呼び出そうとするとNoMethodError
が発生することを確認。また、そのUserオブジェクトからダイジェストの値も確認。>> user = User.new >> user.create_activation_digest NoMethodError: private method `create_activation_digest' called for #<User:0x000000045483b0> Did you mean? restore_activation_digest! from (irb):2 >> user.digest NoMethodError: undefined method `digest' for #<User:0x000000045483b0> from (irb):3 >> user.activation_digest => nil3:6章で、
email.downcase!
を使いemail属性を変更する方法を学んだ。(破壊的メソッド)
このメソッドを使って、user.rbのdowncase_email
メソッドを改良してみる。user.rbdef downcase_email email.downcase! # emailを小文字化してUserオブジェクトのemail属性に代入 end$ rails t 11 tests, 15 assertions, 0 failures, 0 errors, 0 skipsテストがパスしたのでOK。
11.2 アカウント有効化のメール送信
データのモデル化が終わったので、今度はアカウント有効化メールの送信に必要なコードを追加する。
このメソッドではAction Mailerライブラリを使ってUserのメイラーを追加する。
このメイラーはUsersコントローラのcreateアクションで有効化リンクをメールで送信するために使う。メイラーの構成はコントローラのアクションとよく似ており、メールのテンプレートをビューと同じ要領で定義する。
このテンプレートの中に有効化トークンと、メールアドレスを有効にするアカウントのアドレスのリンクを含め、使っていく。11.2.1 送信メールのテンプレート
メイラーは、モデルやコントローラと同様に
rails generate
で生成できる。$ rails g mailer UserMailer account_activation password_resetこのコマンドを実行したことにより、今回必要となる
account_activation
メソッドと、次章で必要となるpassword_reset
メソッドが生成された。また、上記のコマンドは生成したメイラーごとに、ビューのテンプレートが2つずつ生成される
アカウント有効化メイラーのビュー2つ
①テキストメール用のテンプレート
②HTMLメール用のテンプレートアカウント有効化に使うテンプレートを確認してみる。
account_activation.text.erbUser#account_activation <%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erbaccount_activation.html.erb<h1>User#account_activation</h1> <p> <%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb </p>また、mailerディレクトリの中に生成されたメイラーファイル
application_mailer.rb
とuser_mailer.rb
の2つも確認。この2つのファイルはメールの動きを設定する(モデルで言うコントローラみたいなもの)
application_mailer.rbclass ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' enduser_mailer.rbclass UserMailer < ApplicationMailer # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # # en.user_mailer.account_activation.subject # def account_activation @greeting = "Hi" mail to: "to@example.org" end # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # # en.user_mailer.password_reset.subject # def password_reset @greeting = "Hi" mail to: "to@example.org" end endapplication_mailerでは、アプリケーション全体で共通のデフォルトのfromアドレスがある。
user_mailerではmail to:
にて宛先のメールアドレスを設定している。また、メールフォーマットに対応するメイラーレイアウトも使われている。
生成されるHTMLメイラーのレイアウトやテキストメイラーのレイアウトはapp/views/layouts
で確認できる。生成されたコードにはインスタンス変数
@greeting
も含まれている。このインスタンス変数は、丁度普通のビューでコントローラのインスタンス変数を利用できるのと同じように、メイラービューで利用できる。
最初に、生成されたテンプレートをカスタマイズして、実際に有効化メールで使えるようにする。
次に、ユーザーを含むインスタンス変数を作成してビューで使えるようにし、
user.email
にメールを送信する。
user_mailer.rb
では、mailにsubjectキーを引数として渡している。この値は、メールの件名にあたる。application_mailer.rbclass ApplicationMailer < ActionMailer::Base default from: 'noreply@example.com' layout 'mailer' enduser_mailer.rbclass 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テンプレートビューは、通常のビューと同様ERBで自由にカスタマイズできる。
ここでは挨拶文にユーザー名を含め、カスタムの有効化リンクを追加する。
この後、Railsサーバーでユーザーをメールアドレスで検索して有効化トークンを認証できるようにしたいので、リンクにはメールアドレスとトークンを両方含めておく必要がある。AccountActivationsリソースで有効化をモデル化したので、トークン自体は名前付きルートの引数で使われる。
edit_account_activation_url(@user.activation_token, ...)例えば
edit_user_url(user)上のメソッドは、絶対パスの
user_url
でurlを生成し、引数のユーザーの編集ページにアクセスする
つまりhttp://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit上記のURLの
q5lt38hQDc_959PVoo6b7A
という部分はnew_token
メソッドで生成されたもの。URLで使えるようにBase64でエンコードされている。
これは丁度/users/1/edit
の1のようなユーザーIDと同じ役割を果たす。このトークンは、特にAccountActivationsコントローラのeditアクションでは
params
ハッシュでparams[:id]
として参照できる。クエリパラメータを使って、このURLにメールアドレスをうまく組み込んでみる。
account_activations/q5lt38hQDc_959Voo6b7A/edit?email=foo%40example.comこの時、メールアドレスの@は%40と言う文字にエスケープしている。
(@は通常URLでは扱えない)Railsでクエリパラメータを設定するには、名前付きルートに対して、次のようなハッシュを追加しする。
edit_account_activation_url(@user.activation_token, email: @user.email)このようにして名前付きルートでクエリパラメータを定義すると、Railsが特殊な文字を自動的にエスケープしてくれる。
コントローラで
params[:email]
からメールアドレスを取り出す時には、自動的にエスケープを解除してくれる。ここまでできれば、user_mailerで定義した@userインスタンス変数、editへの名前付きルート、ERBを組み合わせて、必要なリンクを作成できる。
アカウント有効化のHTMLテンプレートでは、正しいリンクを組み立てるために
link_to
メソッドを使われている。account_activation.text.erbこんにちは <%= @user.name %>, YUUKIのポートフォリオへようこそ!下記リンクをクリックしたらあなたのアカウントは有効化されます♫: <%= edit_account_activation_url(@user.activation_token, email: @user.email) %>account_activation.html.erb<h1>YUUKIのポートフォリオ</h1> <p>こんにちは <%= @user.name %>,</p> <p> YUUKIを有効化する: </p> <%= link_to "Activate", edit_account_activation_url(@user.activation_token, email: @user.email ) %>演習
1:コンソールを開き、CGIモジュールのescapeメソッドでメールアドレスの文字列をエスケープできることを確認してみる。
このメソッドでDon't panic!
をエスケープすると、どんな結果になるか?> CGI.escape('foo@example.com') => "foo%40example.com" >>11.2.2 送信メールのプレビュー
テンプレートで定義した実際の表示を確認するため、メールプレビューという裏技を使ってみる。
Railsでは、特殊なURLにアクセスするとメールのメッセージをその場でプレビューすることができる。
メールを実際に送信しなくてもいいので大変便利。
これを利用するには、アプリケーションのdevelopment環境(開発環境)の設定に手を加える必要がある。development.rbRails.application.configure do # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = true config.action_mailer.delivery_method = :test host = 'us-east-2.console.aws.amazon.com' config.action_mailer.default_url_options = { host: host, protocol: 'https' }このように、host名のとこに自分の開発環境のホスト名を記入する。
developmentサーバーを再起動してdevelopmentの設定を読み込んだら、次はUserメイラーのプレビューファイルを更新する。
user_mailer_preview.rb# Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at http://localhost:3000/rails/mailers/user_mailer/account_activation def account_activation UserMailer.account_activation end # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset UserMailer.password_reset end end
user_mailer.rb
ファイルで指定したaccount_activation(user)
の引数には有効なUserオブジェクトを渡す必要があるため、このままではプレビューファイルは動かない。
これを回避するため、user変数が開発用データベースの最初のユーザーになるよう定義して、それをUserMailer.account_activation_token
の引数として渡す。user_mailer_preview.rb# Preview all emails at http://localhost:3000/rails/mailers/user_mailer 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 # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset UserMailer.password_reset end endここで、user.activation_tokenの値にも代入している点に注目。
アカウント有効化ビューのテンプレートでは、アカウント有効化のトークンが必要なので、代入は省略できない。なお、
activation_token
は仮の属性でしかないので、DBのユーザーはこの値を実際には持っていない(ダイジェストしかない)演習
1:Railsのプレビュー機能を使って、ブラウザから先ほどのメールを表示してみる。
Dateの欄にはどんな内容が表示されているか?アクセスした時刻が表示されている。
11.2.3 送信メールのテスト
最後に、このメールプレビューのテストも作成して、プレビューをダブルチェックできるようにする。
mailer作成時にテストも自動で生成されているので、これを利用すればテストの作成は簡単。user_mailer_test.rbrequire 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do mail = UserMailer.account_activation assert_equal "Account activation", mail.subject assert_equal ["to@example.org"], mail.to assert_equal ["from@example.com"], mail.from assert_match "Hi", mail.body.encoded end test "password_reset" do mail = UserMailer.password_reset assert_equal "Password reset", mail.subject assert_equal ["to@example.org"], mail.to assert_equal ["from@example.com"], mail.from assert_match "Hi", mail.body.encoded end end上記のテストでは、
assert_match
というメソッドが使われており、これを使えば正規表現で文字列をテストできるassert_match 'foo', 'foobar' #true assert_match 'baz', 'foobar' #false assert_match '/¥w+/', 'foobar' #true assert_match '/¥w+/', '$#!*+@' #falseassert_matchメソッドを使って、名前・有効化トークン・エスケープ済みメールアドレスがメール本文に含まれているかどうかをテストする。
また、
CGI.escape(user.email)こうすることで、引数に取ったemailをエスケープ処理することができる。
user_mailer_test.rbrequire 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end endなお、このテストではまだ失敗する。
上記テストコードでは、fixtureユーザーに有効化トークンを追加している点に注目。
(user.activation_token = のところ)
追加しない場合は、空白になる。なお、生成されたパスワード設定のテストも削除しているが、のちに戻す。
このテストをパスさせるには、テストファイル内のドメイン名を正しく設定する必要がある。
test.rbconfig.action_mailer.delivery_method = :test config.action_mailer.default_url_options = { host: 'example.com' }これでテストはパスする。
$ rails t 1 tests, 9 assertions, 0 failures, 0 errors, 0 skips*ここで注意だが、
account_activation.html
にて、pタグで長く名付けるとassert_match
でテストが失敗する。例
account_activation.html.rb<p> YUUKIのポートフォリオへようこそ!下記リンクをクリックしたらあなたのアカウントは有効化されます: </p>失敗
account_activation.html.rb<p> 有効化: </p>成功
2:CGI.escapeの部分を削除するとテストが失敗することを確認
確認済み
11.2.4 ユーザーのcreateアクションを更新
あとはユーザー登録を行う
create
アクションに数行追加するだけで、メイラーをアプリケーションで実際に使うことができる。users_controller.rbdef create @user = User.new(user_params) # newビューにて送ったformの中身(nameやemailの値)をuser_paramsで受け取り、ユーザーオブジェクトを生成、@userに代入 if @user.save UserMailer.account_activation(@user).deliver_now # アカウント有効化メールの送信 flash[:info] = "メールを確認してアカウントを有効化してね" # アカウント有効化メッセージの表示 redirect_to root_url # ホームへ飛ばす else render 'new' end endここで、
UserMailer
を使って入力されたメールアドレス宛にアカウント有効化のメッセージを送っている点と、
ユーザー登録時にリダイレクト先をroot_urlへ飛ばしてる点に、
ログインしないように変更した点に注目。登録時のリダイレクトの挙動が変更されたため、テストは失敗する。
そこで、該当箇所のテストはとりあえずコメントアウトしておく。
users_signup_test.rbtest "valid signup information" do # 新規登録が成功(フォーム送信)したかのテスト get signup_path # signup_path(/signup)ユーザー登録ページにアクセス assert_difference 'User.count', 1 do # User.countでユーザー数をカウント、1とし、ユーザー数が変わったらtrue、変わってなければfalse post users_path, params: { user: { name: "Example User", # signup_path(/signup)からusers_path(/users)へparamsハッシュのuserハッシュの値を送れるか検証 email: "user@example.com", password: "password", password_confirmation: "password" } } end follow_redirect! # 指定されたリダイレクト先(users/show)へ飛べるか検証 #assert_template 'users/show' # users/showが描画されているか確認 assert_not flash.blank? # flashが空ならfalse,空じゃなければtrue #assert is_logged_in? # 新規登録時にセッションが空じゃなければtrue endこの状態で実際に新規ユーザーとして登録してみる。
サーバーログ(rails sの画面)を確認
UserMailer#account_activation: processed outbound mail in 177.1ms Sent mail to itotasuku2@gmail.com (6.6ms) Date: Tue, 22 Jan 2019 06:07:32 +0000 From: noreply@example.com To: ●●@gmail.com Message-ID: <5c46b32415fe1_3ed625687987001@ip-172-31-25-8.mail> Subject: Account activation Mime-Version: 1.0 Content-Type: multipart/alternative; boundary="--==_mimepart_5c46b32414b75_3ed625687986994e"; charset=UTF-8 Content-Transfer-Encoding: 7bit ----==_mimepart_5c46b32414b75_3ed625687986994eおぉ、アカウント有効化メールアドレスが表示されている。
ただし、これは実際にメールが生成されるわけではないので注意。
のちに実際のメールを送信する方法を詳解する。演習
1:新規ユーザー登録でリダイレクト先が適切なURLに変わったことを確認。その後、Railsサーバーのログから送信メールの有効化トークンの値を確認。
Redirected to https://eac437457e484fe491559aaa135f7f93.vfs.cloud9.us-east-2.amazonaws.com/ Completed 302 Found in 470ms (ActiveRecord: 11.3ms)リダイレクト先がroo_urlの指示通り、ホームページへリダイレクトできている。
"authenticity_token"=>"GqyypcjNdXgYvimMg9lCL6tXBhcYqsMIBqIA6R9EkgU+34HzSxxH4TmLun6dMaeiM9RwFfaImLGm5KspzMKxNg=="有効化トークン(authenticity_token)がハッシュ化されている
2:コンソールを開き、DB常にユーザーが作成されたことを確認。
また、ユーザーはDB上にはいるが、有効化のステータスがfalseのままになっていることを確認。>> user = User.find(101) >> user.activated? => false11.3 アカウントを有効化する
メールが生成できたら、今度はAccountActivationsコントローラのeditアクションを書いていく。
また、アクションへのテストを書き、しっかりとテストできていることが確認できたら、AccountActivationsコントローラからUserモデルにコードを移していく。
11.3.1 authenticated?メソッドの抽象化
有効化トークンとメールはそれぞれ
params[:id]
params[:email]
で参照できる。
なので、パスワードのモデルと記憶トークンで学んだことを元に、次のようなコードでユーザーを検索して認証することにする。
user = User.find_by(email: params[:email]) if user && user.authenticated?(:activation, params[:id])上のコードで使っている
authenticated?
メソッドは、アカウント有効化のダイジェストと、渡されたトークンが一致するかどうかをチェックしている。ただし、このメソッドは記憶トークン用なので、今は正常に動作しない。
# トークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) return false if remember_digest.nil? BCrypt::Password.new(remember_digest).is_password?(remember_token) end
remember_digest
はUserモデルの属性なので、モデル内では次のように置き換えることができる。self.remember_digest今回は、上記のコードの
remember
の部分をどうにかして変数として扱いたい。
つまり、次のコードの例のように、状況に応じて呼び出すメソッドを切り替えたい。self.FOOBAR.digestこれから実装する
authenticated?
メソッドでは、受け取ったパラメータに応じて呼び出すメソッドを切り替える手法を使う。この手法をメタプログラミングと呼ぶ。
今回は
send
メソッドを用いてペアプログラミングを行う。このメソッドは、渡されたオブジェクトにメッセージを送ることによって、呼び出すメソッドを動的に決めることができる。
Railsコンソールでやってみる。
まずは、Rubyのオブジェクトに対してsendメソッドを実行し、配列の長さを得る。
$ rails c Running via Spring preloader in process 19959 Loading development environment (Rails 5.1.6) >> a = [1,2,3] => [1, 2, 3] >> a.length => 3 >> a.send(:length) => 3 >> a.send("length") => 3この時、sendを通してシンボルの
:length
や文字列の"length"
は、いずれもlength
メソッドと同じ結果になった。つまり、どちらもオブジェクトにlengthメソッドを渡しているため、等価である。もう1つの例は、DBの最初のユーザーが持つ
activation_digest
属性にアクセスする。$ rails c Running via Spring preloader in process 7511 Loading development environment (Rails 5.1.6) >> user = User.first User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2019-01-20 02:17:40", updated_at: "2019-01-20 02:17:40", password_digest: "$2a$10$ambbgHUgH.09zBb8AbfXqOPn2//.8cblJ2qQKEsPXyA...", remember_digest: nil, admin: true, activation_digest: "$2a$10$Hv0qwXYkDj7k6huuD.Kfguk4/2eyMimSbllKnJ0rIJv...", activated: true, activated_at: "2019-01-20 02:17:40"> >> user.activation_digest => "$2a$10$Hv0qwXYkDj7k6huuD.Kfguk4/2eyMimSbllKnJ0rIJvJKstLZfLba" >> user.send(:activation_digest) => "$2a$10$Hv0qwXYkDj7k6huuD.Kfguk4/2eyMimSbllKnJ0rIJvJKstLZfLba" >> user.send("activation_digest") => "$2a$10$Hv0qwXYkDj7k6huuD.Kfguk4/2eyMimSbllKnJ0rIJvJKstLZfLba" >> attribute = :activation => :activation >> user.send("#{attribute}_digest") => "$2a$10$Hv0qwXYkDj7k6huuD.Kfguk4/2eyMimSbllKnJ0rIJvJKstLZfLba" >>最後の例では、シンボル
:activation
と等しいattribute
変数を定義し、文字列の式展開(interpolation)を使って引数を正しく組み立ててから、sendに渡している。文字列
activation
でも同じことができるが、Rubyではシンボルを使うのが一般的。"#{attribute}_digest"シンボルと文字列どちらを使った場合でも、上のコードでは次のように文字列に変換される。
"activation_digest"
sendメソッドの動作原理を理解した所で、この仕組みを利用して
authenticated?
メソッドを書き換えてみる。def authenticated?(remember_token) digest = self.send("remember_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(remember_token) end上のコードの各引数を一般化し、文字列の式展開も利用すると、次のようなコードになる。
def authenticated?(attritubte, token) digest = self.send("#{attritubte}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) end他の認証でも使えるように、上では2番目の引数tokenの名前を変更して一般化している点に注意。
また、このコードはモデル内にあるのでselfは省略することもできる。
最終的にRubyらしく書かれたコードは以下def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token)ここまでできれば、次のように呼び出すことで
authenticated?
の従来の振る舞いを再現できる。user.authenticated?(:remember, remember_token)抽象化したauthenticated?メソッドをUserモデルに書いてみる。
user.rb# トークンがダイジェストと一致したらtrueを返す def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) endこの時点ではテストは失敗する。
テストが失敗する理由は
current_user
メソッドとnilダイジェストのテストの両方で、
authenticated?
が古いママとなっており、さらに引数も2つではなくまだ1つのままだから。これを解消するため、両者を更新して、新しい一般的なメソッドを使うようにする。
sessions_helper.rbdef current_user if (user_id = session[:user_id]) # 一時的なセッションユーザーがいる場合処理を行い、user_idに代入 @current_user ||= User.find_by(id: user_id) # 現在のユーザーがいればそのまま、いなければsessionユーザーidと同じidを持つユーザーをDBから探して@current_user(現在のログインユーザー)に代入 elsif (user_id = cookies.signed[:user_id]) # user_idを暗号化した永続的なユーザーがいる(cookiesがある)場合処理を行い、user_idに代入 user = User.find_by(id: user_id) # 暗号化したユーザーidと同じユーザーidをもつユーザーをDBから探し、userに代入 if user && user.authenticated?(:remember, cookies[:remember_token]) # DBのユーザーがいるかつ、受け取った記憶トークンを暗号化した記憶ダイジェストと、remember値が入ってるユーザーがいる場合処理を行う log_in user # 一致したユーザーでログインする @current_user = user # 現在のユーザーに一致したユーザーを設定 end end enduser_test.rbtest "authenticated? should return false for a user with nil digest" do # authenticatedメソッドで記憶ダイジェストを暗号化できるか検証 assert_not @user.authenticated?(:remember, '') # @userのユーザーの記憶ダイジェストと、引数で受け取った値が同一ならfalse、異なるならtrueを返す end上記の変更を加えたらテストは成功する。
$ rails t 11 tests, 15 assertions, 0 failures, 0 errors, 0 skipsこのようなリファクタリングを施すとエラーが発生しやすくなるので、しっかりしたテストスイートが不可欠。
演習
1:コンソール内でユーザーを作成してみる。
新しいユーザーの記憶トークンと有効化トークンはどのような値になっているか?
また、各トークンに対応するダイジェストの値はどうなっているか?>> user = User.create(name: "tesuto4", email: "tesuto@test.co.jp", password: "tesuto4", password_confirmation: "tesuto4") (0.1ms) SAVEPOINT active_record_1 User Exists (0.3ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "tesuto@test.co.jp"], ["LIMIT", 1]] SQL (2.8ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?) [["name", "tesuto4"], ["email", "tesuto@test.co.jp"], ["created_at", "2019-01-23 04:17:30.872700"], ["updated_at", "2019-01-23 04:17:30.872700"], ["password_digest", "$2a$10$Edcro0emsnf9BuYd0WXzMuHYGxzKI1AFiBh.kPtLz6qc81Okuf0by"], ["activation_digest", "$2a$10$d9vrDK5aEZCfFgaQIE/CUuLjRjrjAWHLomBJ/7u.4PdumyRNGqJQ2"]] (0.1ms) RELEASE SAVEPOINT active_record_1 => #<User id: 102, name: "tesuto4", email: "tesuto@test.co.jp", created_at: "2019-01-23 04:17:30", updated_at: "2019-01-23 04:17:30", password_digest: "$2a$10$Edcro0emsnf9BuYd0WXzMuHYGxzKI1AFiBh.kPtLz6q...", remember_digest: nil, admin: false, activation_digest: "$2a$10$d9vrDK5aEZCfFgaQIE/CUuLjRjrjAWHLomBJ/7u.4Pd...", activated: false, activated_at: nil> >> user.remember_token => nil >> user.activation_token => "l39qT2c2s5LXb1r5C3OmjQ" >> user.remember_digest => nil >> user.activation_digest => "$2a$10$d9vrDK5aEZCfFgaQIE/CUuLjRjrjAWHLomBJ/7u.4PdumyRNGqJQ2"アカウント有効化用のトークン・ダイジェストは生成されたが、ユーザーが有効化していないので、記憶トークン/記憶ダイジェスト値はnil。
2:authenticated?メソッドを使って、先ほどの各トークン/ダイジェストの組み合わせで認証が成功することを確認。
>> user.remember_token = User.new_token => "JfMcsK1wbYkz8L_uG2kzhA" >> user.update_attribute(:remember_digest, User.digest(user.remember_token)) (0.2ms) SAVEPOINT active_record_1 SQL (0.2ms) UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ? [["updated_at", "2019-01-23 04:37:39.302291"], ["remember_digest", "$2a$10$WiFAB0sIsY9pXkoioQ9nDeb3Z0VbdJbqZTe3kUWN7fAavjo5YOY.a"], ["id", 102]] (0.1ms) RELEASE SAVEPOINT active_record_1 => trueまず記憶トークンと記憶ダイジェストを生成。
>> user.authenticated?(:remember,user.remember_token) => true引数に渡したダイジェスト値と、トークン値が一致したらtrue
trueが返ってきたので、認証が成功した。
(ログインが成功した)11.3.2 editアクションで有効化
authenticated?
メソッドを完成させたので、editアクションを書いてみる。このアクションでは、paramsハッシュで渡されたメールアドレスに対応するユーザーを認証する。
ユーザーが有効であることを確認する中核派、次の部分になる。
if user && !user.activated? && user.authenticated?(:activation, params[:id])
!user.activated?
という記述に注目。このコードは既に有効になっているユーザーを誤って再度有効化しないために必要。
正当であろうとなかろうと、有効化が行われるとユーザーはログイン状態になる。もしこのコードがなければ、攻撃者がユーザーの有効化リンクを後から盗みだしてクリックするだけで、本当のユーザーとしてログインできてしまう。
そうした攻撃を防ぐためにこのコードは非常に重要。
上記の論理値に基づいてユーザーを認証するには、ユーザーを認証してから
activated_at
タイムスタンプを更新する必要がある。user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone_now)上記のコードを
edit
アクションで使う。
これで日付更新ができる。account_activations_controller.rbclass AccountActivationsController < ApplicationController def edit 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] = "アカウントを有効にしたンゴよ〜" redirect_to user else flash[:danger] = "きみはまだまだだね^^" redirect_to root_url end end end有効化のアクションを書いたら、実際にアカウントを有効化させてみる。
ブラウザでユーザーを新規登録後、rails sに載っているURLを貼り付けて有効化ページにアクセスする。
この状態ではユーザーのログイン方法を変更していないのでこれでは何の意味もない。
ということで、ユーザーの有効化を行う為に、
ユーザーが有効である場合のみログインできるようにログイン方法を変更する必要がある。これを行うには、
user.activated?
がtrueの場合にのみログインを許可し、
そうでない場合はルートURLにリダイレクトしてwarning
で警告を表示する。sessions_controller.rb# ユーザーを新規作成する def create @user = User.find_by(email: params[:session][:email].downcase) # paramsハッシュで受け取ったemail値を小文字化し、email属性に渡してUserモデルから同じemailの値のUserを探して、user変数に代入 if @user && @user.authenticate(params[:session][:password]) # user変数がデータベースに存在し、なおかつparamsハッシュで受け取ったpassword値と、userのemail値が同じ(パスワードとメールアドレスが同じ値であれば)true if @user.activated? # userが有効の処理 log_in @user # sessions_helperのlog_inメソッドを実行し、sessionメソッドのuser_id(ブラウザに一時cookiesとして保存)にidを送る params[:session][:remember_me] == '1' ? remember(@user) : forget(@user) # ログイン時、sessionのremember_me属性が1(チェックボックスがオン)ならセッションを永続的に、それ以外なら永続的セッションを破棄する redirect_back_or @user # userの前のページもしくはdefaultにリダイレクト else # userが有効でない処理 message = "アカウントは有効ではありません" message += "メールで送られたURLから有効化してね" flash[:warning] = message redirect_to root_url end else flash.now[:danger] = 'Invalid email/password combination' # flashメッセージを表示し、新しいリクエストが発生した時に消す render 'new' # newビューの出力 end end演習
1:コンソールからメールのURLを調べて、有効化トークンはどれか確認する。
<a href="https://eac437457e484fe491559aaa135f7f93.vfs.cloud9.us-east-2.amazonaws.com/account_activations/cDACvOyopUBbsOnFsIr2sg/edit?email=tyaou%40example.com">Activate</a>これの
cDACv〜
の部分。2:URLから有効化リンクに飛んでユーザーの認証に成功し、有効化できることを確認する。
また、有効化ステータスがtrueになるかも確認。$ rails c Running via Spring preloader in process 29877 Loading development environment (Rails 5.1.6) >> user = User.find(110) User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 110], ["LIMIT", 1]] => #<User id: 110, name: "tyaou", email: "tyaou@example.com", created_at: "2019-01-24 10:06:17", updated_at: "2019-01-24 10:06:27", password_digest: "$2a$10$7/vxN5esSjb1gxWrtZb9K.vkYnnbL/GsI5JwMx9/Kqh...", remember_digest: nil, admin: false, activation_digest: "$2a$10$Ty2/gM.jACJf3ll87jO9gO/hrVRkdgRHZdx57lakxbm...", activated: true, activated_at: "2019-01-24 10:06:27"> >> user.activated => trueOK。
有効化に半日格闘した
何回やってもeditアクションの
if user && !user.activated? && user.authenticated?(:activation, params[:id])
の
autenticated?
の部分がtrueにならずに色々試行錯誤したのだが、
結果、account_activation.html.erb
を全部日本語から英語にしたら直った。<h1>YUUKI</h1> <p>hello <%= @user.name %>,</p> <p> activate: </p> <%= link_to "Activate", edit_account_activation_url(@user.activation_token, email: @user.email ) %>こうしたってこと。
日本語だとメイラーのHTMLが文字化けしてURLが正しく発行されないので注意。
まじで疲れたよ・・・11.3.3 有効化のテストとリファクタリング
アカウント有効化の統合テストを追加する。
正しい情報でユーザー登録を行った場合のテストは既にあるので、7章で書いたテストに若干手を加える。
users_signup_test.rbrequire 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest # test "the truth" do # assert true # end def setup ActionMailer::Base.deliveries.clear # Mailerファイルを初期化しユーザーをセットアップ end test "invalid signup information" do # 新規登録が失敗(フォーム送信が)した時用のテスト get signup_path # ユーザー登録ページにアクセス assert_no_difference 'User.count' do # User.countでユーザー数が変わっていなければ(ユーザー生成失敗)true,変わっていればfalse post signup_path, params: { user: { name: "", # signup_pathからusers_pathに対してpostリクエスト送信(/usersへ)、paramsでuserハッシュとその下のハッシュで値を受け取れるか確認 email: "user@invalid", password: "foo", password_confirmation: "bar" } } end assert_template 'users/new' # newアクションが描画(つまり@user.save失敗)されていればtrue、なければfalse assert_select 'div#error_explanation' # divタグの中のid error_explanationが描画されていれば成功 assert_select 'div.field_with_errors' # divタグの中のclass field_with_errorsが描画されていれば成功 assert_select 'form[action="/signup"]' # formタグの中に`/signup`があれば成功 end test "valid signup information with account activation" do # 新規登録が成功(フォーム送信)したかのテスト get signup_path # signup_path(/signup)ユーザー登録ページにアクセス assert_difference 'User.count', 1 do # User.countでユーザー数をカウント、1とし、ユーザー数が変わったらtrue、変わってなければfalse post users_path, params: { user: { name: "Example User", # signup_path(/signup)からusers_path(/users)へparamsハッシュのuserハッシュの値を送れるか検証 email: "user@example.com", password: "password", password_confirmation: "password" } } end assert_equal 1, ActionMailer::Base.deliveries.size # Actionメイラーが1かどうか検証 user = assigns(:user) # usersコントローラの@userにアクセスし、userに代入 assert_not user.activated? # userが有効化されていればfalse、されていなければtrue # 有効化していない状態でログインしてみる log_in_as(user) # 有効化されていないuserでログイン assert_not is_logged_in? # 有効化されていなければtrue # 有効化トークンが不正な場合 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! # 指定されたリダイレクト先(users/show)へ飛べるか検証 assert_template 'users/show' # users/showが描画されているか確認 assert is_logged_in? # 新規登録時にセッションが空じゃなければtrue end endこのテストで重要な行はこれ
assert_equal 1, ActionMailer::Base.deliveries.sizeこのコードは、配信されたメッセージがきっかり1つであるかどうかを確認している。
配列
deliveries
は変数なので、setupメソッドでこれを初期化しておかないとdeliveries.clear
、並行して行われる他のテストでメールが配信されたときにエラーが発生してしまう。また、
assigns
メソッドを使うと対応するアクション内のインスタンス変数にアクセスできるようになる。例えば、Usersコントローラの
create
アクションでは@user
というインスタンス変数が定義されているが、テストでassings(:user)
と書くとこのインスタンス変数にアクセスできるようになる。これでテストはパスする。
$ rails t 2 tests, 14 assertions, 0 failures, 0 errors, 0 skipsテストができたので、ユーザー操作の一部をコントローラからモデルに移動するというリファクタリング行う準備ができた。
Userモデルでは、
activate
メソッドを作成してユーザーの有効化属性を更新し、send_activation_email
メソッドを作成して有効化メールを送信する。user.rb# アカウントを有効にする def activate update_attribute(:activated, true) update_attribute(:activated_at, Time.zone.now) end # 有効化用のメールを送信する def send_activation_email UserMailer.account_activation(self).deliver_now end private # メールアドレスを全て小文字にする def downcase_email self.email = email.downcase # emailを小文字化してUserオブジェクトのemail属性に代入 end # 有効化トークンとダイジェストを作成および代入する def create_activation_digest self.activation_token = User.new_token # ハッシュ化した記憶トークンを有効化トークン属性に代入 self.activation_digest = User.digest(activation_token) # 有効化トークンをBcryptで暗号化し、有効化ダイジェスト属性に代入 endusers_controller.rbdef create @user = User.new(user_params) # newビューにて送ったformの中身(nameやemailの値)をuser_paramsで受け取り、ユーザーオブジェクトを生成、@userに代入 if @user.save @user.send_activation_email # アカウント有効化メールの送信 flash[:info] = "メールを確認してアカウントを有効化してね" # アカウント有効化メッセージの表示 redirect_to root_url # ホームへ飛ばす else render 'new' end endaccount_activations_controller.rbclass AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.activate log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end end
user.rb
ではuser.
という記法を使っていない点に注目。
Userモデルにはそのような変数はないので、これがあるとエラーになる。-user.update_attribute(:activated, true) -user.update_attribute(:activated_at, Time.zone.now) +update_attribute(:activated, true) +update_attribute(:activated_at, Time.zone.now)userをselfに切り替える方法もあるが、selfは必ずしも必須ではない。
これで、リファクタリングを施したテストがパスすればOK。
2 tests, 14 assertions, 0 failures, 0 errors, 0 skips演習
1:activateメソッドはupdate_attributeを二回呼び出しているが、
これは各行で1回ずつDBへ問い合わせしていることになる。
update_attribute
の呼び出しを1回のupdate_columns
というメソッドでまとめてみる。また、変更後テストがパスするかも確認。
user.rb# アカウントを有効にする def activate update_columns(activated: true, activated_at: Time.zone.now) end11 tests, 15 assertions, 0 failures, 0 errors, 0 skips2:現在は
/users
のユーザーindexページを開くと全てのユーザーが表示され、/users/:id
のようにidと指定すると個別のユーザーを表示できる。しかし、非有効ユーザーは表示する意味がないので、その動作を
users_controller.rb
で変更する。users_controller.rbdef index @users = User.where(activated: true).paginate(page: params[:page]) # Userを取り出して分割した値を@usersに代入 end def show @user = User.find(params[:id]) # paramsで:idパラメータを受け取る(/users/1にアクセスしたら1を受け取る) redirect_to root_url and return unless @user.activated? # activatedがfalseならルートURLヘリダイレクト end3:ここまでの演習問題で変更したコードをテストするために、/usersと/users/:idの両方に対する統合テストを作成する。
fixtureに非有効化ユーザーを作成
users.ymlnon_activated: name: Non Activated email: non_activated@example.gov password_digest: <%= User.digest('password') %> activated: false activated_at: <%= Time.zone.now %>setupでテストユーザーを読み込む
users_controller_test.rbtest "should not allow the not activated attribute" do log_in_as (@non_activated_user) # 非有効化ユーザーでログイン assert_not @non_activated_user.activated? # 有効化でないことを検証 get users_path # /usersを取得 assert_select "a[href=?]", user_path(@non_activated_user), count: 0 # 非有効化ユーザーが表示されていないことを確認 get user_path(@non_activated_user) # 非有効化ユーザーidのページを取得 assert_redirected_to root_url # ルートurlにリダイレクトされればtrue end11.4 本番環境でのメール送信
ここまでの実装で、
development
環境に置けるアカウント有効化の流れは完成した。次はサンプルアプリケーションの設定を変更し、production(実行)環境で実際にメールを送信してみる。
具体的には、
①無料のサービス(SendGrid)を利用してメール送信の設定をする
②アプリケーションの設定
③デプロイという順に行う。
SendGridの使い方
本番環境からメールを送信するために、SendGridというHerokuアドオンを利用してアカウントを検証する。
チュートリアルでは、
starter tier
というサービスを使う。
(1日のメール数が最大400通という制限があるが、無料で利用できる)SendGridをHerokuで利用するためには、クレジットカードの登録が必要。
https://heroku.com/verify
にアクセスし、クレジットカードを登録する。アドオンをアプリケーションに追加するには
$ heroku addons:create sendgrid:starter Creating sendgrid:starter on ⬢ yuuki-heroku-sample... free Created sendgrid-rectangular-51178 as SENDGRID_PASSWORD, SENDGRID_USERNAME Use heroku addons:docs sendgrid to view documentation次に、SendGridアドオンの設定を行う。
production環境のSMTPに情報を記入する
本番Webサイトのアドレスをhost変数に定義する必要もある。production.rb# Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. config.action_mailer.raise_delivery_errors = false config.action_mailer.delivery_method = :smtp host = 'https://yuuki-heroku-sample.herokuapp.com' config.action.mailer.default_url_options = { host: host } ActionMailer::Base.smtp_settings = { :address => 'smtp.sendgrid.net', :port => '587', :authentication => :plain, :user_name => ENV['SENDGRID_USERNAME'], :password => ENV['SENDGRID_PASSWORD'], :domain => 'heroku.com', :enable_starttle_auto => true }ここで重要なのが、
user_name
とpassword
のハッシュに実際の値を記入しないことソースコードに直接機密情報を書き込むのは危険。
そのような情報は環境変数に記述し、そこからアプリケーションに読み込む必要がある。今回の場合、そうした変数はSendGridアドオンが自動的に設定してくれるが、のちに環境変数を自分で設定しなければならない。
のちに扱うHerokuの環境変数を表示したい場合は、次のコマンドを実行する。
$ heroku config:get SENDGRID_USERNAME 〇〇@heroku.com $ heroku config:get SENDGRID_PASSWORD 〇〇この時点で、トピックブランチをmasterにマージしておく
$ git add -A $ git commit -m "Add account activation" $ git checkout master $ git merge account-activation続いて、リモートリポジトリにプッシュし、herokuにデプロイ。
$ rails t $ git push $ git push heroku $ heroku run rails db:migrateここで、デプロイ後のページで有効かメールを送信してみる。
しかし、なかなかメールが届かない・・・。
*SendGridを確認したところ、スパムアカウント扱いされてアカウントが停止されてしまいました。
処理済みとなっていますが、本来なら送信済みになる筈です。
仕方なく、heroku logsからアカウント有効化URLをクリック。
URLをクリックする。
有効化完了。
SendGridはすぐにスパム判定をしてしまうようで、自分と同じTutorialを読んでいる方もこの部分で苦戦している様子。
演習
1:本番環境でメールを送る。
確認済み
2:実査にメールをクリックして有効化したあと、heorku logsから有効化に関するログがどうなっているかを調べる
2019-01-26T13:43:36.501607+00:00 heroku[router]: at=info method=GET path="/account_activations/BLndwjAJEDxPtb90w3aNpQ/edit?email,yuukitetsuyanet%40gmail.com" host=yuuki-heroku-sample.herokuapp.com request_id=1129f9d8-c64a-476e-81b2-2d3d9a1fceff fwd="122.50.45.13" dyno=web.1 connect=1ms service=113ms status=302 bytes=1031 protocol=https 2019-01-26T13:43:36.712279+00:00 heroku[router]: at=info method=GET path="/users/2" host=yuuki-heroku-sample.herokuapp.com request_id=4f7857c1-a5ba-485f-abe6-feccfa902f9e fwd="122.50.45.13" dyno=web.1 connect=1ms service=19ms status=200 bytes=4444 protocol=https一応、
heroku run rails c
でユーザーYUUKIのactivationがtrueか確認してみる。$ heroku run rails c $ user = User.find(2) $ user.activated => trueOK。
単語集
- バインド
関連付けるというIT用語。
- before_create
オブジェクト生成時のみ適用させるコールバック。
- Action Mailer
ActionMailerを使用することで、アプリケーションのメイラークラスやビューで、メールを送信することができる。
ActionMailerはActionMailer::Base
を継承し、app/mailer
に配置され、app/views にあるビューと関連付けられる。具体的な使い方は、まずメールを送信する為のメイラーとビューを生成し、メイラー内のアクションでメール送信の動きを付け、メイラービューでメールとして送られる中身のテンプレートを作成する。
- クエリパラメータ
URLの末尾で指定される?の部分。?の部分の後にパラメータを埋め込む。
実際のパラメータ値を「クエリ文字列」と呼び、その種類にはアクセス解析や広告からの流入を調べるパシッブパラエータと、実際のページのコンテンツの内容を変更させるアクティブパラメータがある
- assert_match
第二引数が第一引数の正規表現にマッチする場合はtrue
使い方
assert_match (正規表現, string,[msg] )
- メタプログラミング
プログラムでプログラムを作成するというもの。Rubyでは様々なメソッドで利用可能だが、この章では
send
メソッドを用いてメタプログラミングを行う
- send
動的にメソッドを呼べるメソッド。
- update_columns
update_attribute
メソッドの呼び出しを1行でまとめ、DBへの問い合わせを一回にまとめることができるメソッド。
- 投稿日:2019-01-26T22:27:08+09:00
macのプロンプトにブランチを表示させる
背景
gitの操作をターミナルで行なっているのに、作業しているブランチが見えないのは不便ですよね。
ということで今回はプロンプトにブランチをいい感じに表示させる方法を紹介します。対象者
- UNIX系のコマンドに慣れている、もしくは調べれば分かる人
- Gitの基礎ワードが分かる、もしくは調べれば分かる人
作業環境
MacOS Sierra Ver 10.12.6
git version 2.19.0事前準備
ブランチを表示させるには
git-prompt.sh
(プロンプトに各種追加情報を表示可能にするスクリプト)が必要です。まずは
mdfind
コマンドで上記ファイルが存在することを確認しましょう。
既にインストールされていれば、以下のようにファイルパスが表示されます。$ mdfind git-prompt.sh /usr/local/git/contrib/completion/git-prompt.sh /Library/Developer/CommandLineTools/usr/share/git-core/git-prompt.sh
インストールされていなければ、リンクを参考にするとインストールできると思います。
設定方法
ブランチ名を表示させるために
.bashrc
を編集します。
.bashrc
は通常ログインシェルがbashを起動したときに読み込むファイルでホームディレクトリに配置されています。.bashrc
が存在しない場合はリンクを参考にしてファイルを作成してください。それではここから設定していきましょう。
.bashrc
にプロンプト表示設定を入れる。
ターミナルで以下のコマンドを実行して.bashrc
に設定を追記します。$ cat << EOF >> .bashrc source /usr/local/git/contrib/completion/git-prompt.sh #git-prompt.sを読み込む export PS1='\[\[\033[32m\]\u:\[\033[34m\]\W\[\033[31m\]$(__git_ps1)\[\033[00m\]\$ '※ なお
PS1
はプロンプトの表示フォーマットを指定する環境変数ですので、上記のコマンドを実行後プロンプトが以下の形式に変わります。
[ログインユーザ]:[カレントディレクトリのベース名] [ブランチ名]
もし現在の設定を引き継ぎたい場合は
$(__git_ps1)\[\033[00m\]\$
のみをexport PS1=~
に追記してください。
.bashrc
の設定を反映させる
ターミナルで以下のコマンドを実行し手順1で設定した内容をプロンプトに反映させます。$ source .bashrcコマンドを実行したら、ローカルリポジトリへ移動してみてください。
プロンプトにブランチ名が表示されていると思います。User_Name:current_dir $ cd [ローカルリポジトリ名] User_Name:current_dir (branch_name #) $さいごに
設定作業おつかれさまでした。
今回はブランチ名をプロンプトに表示できるよう設定方法を書きましたが、人によってはブランチ名以外にも表示させたいことがあると思います。
環境変数PS1
の設定次第で表示内容だけでなく、色なども自由にカスタマイズできるので興味のある方は調べてみてください。
- 投稿日:2019-01-26T21:16:46+09:00
gitで僕がよく使うコマンド
3歩歩くと忘れてしまうのでまとめた。
sourcetreeを使っているけど、強いエンジニアになるためコマンドでもたたきたいと思ったので触っています。// init git init // 変更点をステージにあげる git add . // commit git commit -m 'commit message' // 現在の状況をみる git status // logを見る git log // push git push -u origin branch_name // 二回目からこれでいい git push // 現在のブランチを確認する git branch --contains // 新しくブランチを作成して、そのブランチに移動する git checkout -b new_branch // リモートリポジトリにPUSHする git push -u origin new_branch参考
git
Reference
https://git-scm.com/docs
- 投稿日:2019-01-26T16:21:24+09:00
エラー2 github リポジトリ登録
・エラー文
fatal: not a git repository (or any of the parent directories): .git・翻訳
fatal:gitリポジトリ(または他の親ディレクトリ)ではありません:.git・原因
git init でリポジトリ作成をしていなかった。
- 投稿日:2019-01-26T15:11:19+09:00
エラー1 git ssh接続 ssh-agent
・エラー文
ssh: Could not resolve hostname git: Name or service not known・翻訳
ssh:ホスト名を解決できませんでした
git:名前またはサービスが不明・原因
shh-agentを実行していないこと?
ssh-agentに鍵の登録をしていない?・解決策
ssh-agentの実行
$ eval $(ssh-agent -s)鍵の登録
ssh-add 秘密鍵の名前