railsチュートリアル挑戦記 第12章 パスワードの再設定
railsチュートリアルをやりながらメモしたことをそのまま記述しています。
第12章パスワードの再設定
アカウント有効化のときと異なり、パスワードを再設定する場合はビューを1つ変更する必要があるまた、新しいフォームが新たに2つ(メールレイアウト用と新しいパスワードの送信用)必要になる
ログイン時に表示されるforgotリンクを追加する
更に、forgot passwordリンクをクリックするとフォームが表示され、そこにメールアドレスを入力してメールを送信すると、そのメールにパスワード再設定用のリンクが記載されている。
この再設定用のリンクをクリックすると、ユーザーのパスワードを再設定してよいか確認を求めるフォームが表示される
11章で生成したメイラーにリソースとデータモデルを追加して、パスワードの再設定を実現する
実際の実装は12 3から
アカウント有効化と似ており、PasswordResetsリソースを作成して、再設定用のトークンとそれに対応するダイジェストを保存するのが今回の目的
順番は下記
①ユーザーがパスワードの再設定をリクエストすると、ユーザーが送信したメールアドレスをキーにしてデータベースからユーザーを見つける
②該当のメールアドレスがデータベースにある場合は、再設定用トークンとそれに対応するリセットダイジェストを生成する
③再設定用ダイジェストはデータベースに保存しておき、再設定用トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
④ユーザーがメールのリンクをクリックしたら、メールアドレスをキーとしてユーザーを探し、データベース内に保存しておいた再設定用ダイジェストと比較する(トークンを認証する)
⑤認証に成功したら、パスワード変更用のフォームをユーザーに表示する
・12 1 PasswordResetsリソース
必要なデータをUserモデルに追加していく
11章ではeditアクションのみだったが、今回はフォームが必要なのでnewアクションとedhitアクションが必要
いつものようにトピックブランチを作成する
git checkout -b password-reset
トピックブランチ作りすぎてどれが最新のものか分かりづらくなったので下記コマンドをCliborに登録
git branch --sort=authordate
git branch --sort=committerdate
上が自身の持つトピックブランチタイムスタンプ昇順で、
下がコミットしたトピックブランチのタイムスタンプ降順?
とりあえずどちらも最新のトピックブランチが一番下に表示されることを確認
違いはおいおい覚えていく
・12 1 1 PasswordResetsコントローラ
パスワード設定用のコントローラを作成する
今回はビューも扱うため、newアクションとedhitアクションも一緒に生成する
rails generate controller PasswordResets new edit --no-test-framework
ログ
----------------------------
ec2-user:~/environment/sample_app (password-reset) $ rails generate controller PasswordResets new edit --no-test-framework
k
Running via Spring preloader in process 4311
create app/controllers/password_resets_controller.rb
route get 'password_resets/edit'
route get 'password_resets/new'
invoke erb
create app/views/password_resets
create app/views/password_resets/new.html.erb
create app/views/password_resets/edit.html.erb
invoke helper
create app/helpers/password_resets_helper.rb
invoke assets
invoke coffee
create app/assets/javascripts/password_resets.coffee
invoke scss
create app/assets/stylesheets/password_resets.scss
ec2-user:~/environment/sample_app (password-reset) $
----------------------------
--no-test-frameworkってなに?
これは単体テストを生成しないということ。
統合テストでカバーするため今回は生成しない。
新しいパスワードを再設定するためのフォームと、Userモデル内のパスワードを変更するためのフォームが必要になるため、new,create,edit,updateのルーティングも用意する
パスワード再設定用リソースを追加する
config/routes.rb
----------------------------
Rails.application.routes.draw do
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'
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
resources :password_resets, only: [:new, :create, :edit, :update]
end
----------------------------
元々のpasswordのルーティングが自動生成で追加されたけどそれを削除
password関連のルーティングをちょこっと手直しして、
resources :password~に変更した
これで、以下の通りとなる
----------------------------
HTTPリクエスト URL Action 名前付きルート
GET /password_resets/new new new_password_reset_path
POST /password_resets create password_resets_path
GET /password_resets/<token>/edit edit edit_password_reset_url(token)
PATCH /password_resets/<token> update password_reset_url(token)
----------------------------
new_password_reset_pathは、new(GET)に飛ぶ
パスワード再設定画面へのリンクを追加する
app/views/sessions/new.html.erb
----------------------------
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= link_to "(forgot password)", new_password_reset_path %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
----------------------------
この時点でgreenになる
以下のエラー出たけど初見
----------------------------
ec2-user:~/environment/sample_app (password-reset) $ rails test
FATAL: Listen error: unable to monitor directories for changes.
Visit https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers for info on how to fix this.
----------------------------
Google翻訳
----------------------------
ec2-user:〜/ environment / sample_app(password-reset)$ rails test
致命的:リッスンエラー:ディレクトリの変更を監視できません。
これを修正する方法については、https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchersにアクセスしてください。
----------------------------
https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers
を参照して日本語訳して読んでみると、技術的な詳細に興味がなければ
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
を実行してくださいとあったので、実行
ログ
----------------------------
ec2-user:~/environment/sample_app (password-reset) $ echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
fs.inotify.max_user_watches=524288
net.ipv4.ip_forward = 0
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.default.accept_source_route = 0
kernel.sysrq = 0
kernel.core_uses_pid = 1
net.ipv4.tcp_syncookies = 1
kernel.msgmnb = 65536
kernel.msgmax = 65536
kernel.shmmax = 68719476736
kernel.shmall = 4294967296
fs.inotify.max_user_watches = 524288
ec2-user:~/environment/sample_app (password-reset) $
----------------------------
再度rails test
ログ
----------------------------
ec2-user:~/environment/sample_app (password-reset) $ rails test
Running via Spring preloader in process 4679
Started with run options --seed 56563
44/44: [==============] 100% Time: 00:00:05, Time: 00:00:05
Finished in 5.33255s
44 tests, 184 assertions, 0 failures, 0 errors, 0 skips
ec2-user:~/environment/sample_app (password-reset) $
----------------------------
いつものrails testのログがでたのでひと安心
これつまづくやろ・・・。
・12 1 2 新しいパスワードの設定
トークン用の仮想的な属性とそれに対応するダイジェストを用意する
トークンを平文でデータベースに保存してしまうとセキュリティ上の問題があるため、必ずダイジェストを利用する
再設定用のリンクはなるべく短時間で期限切れになるようにしなければならない
reset_digest属性(ハッシュ)とreset_sent_at属性(時間)をUserモデルに追加する
usersモデルは下記の通り
id(数字:自動生成)
name(文字列)
email(文字列)
create_at(時間:自動生成)
updated_at(時間:自動生成)
password_digest(ハッシュ)
remember_digest(ハッシュ)
activation_digest(ハッシュ)
activated(有効化:無効化)
activated_at(時間)
reset_digest(ハッシュ)
reset_sent_at(時間)
上記に従って、マイグレーションを作成する
rails generate migration add_reset_to_users reset_digest:string \
reset_sent_at:datetime
そして適用
rails db:migrate
ログインフォームのコードは下記
ログインフォームのコード (再掲)
app/views/sessions/new.html.erb
----------------------------
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
----------------------------
それを改変して下記を作成
form_forで扱うリソースとURLが異なっている点と、パスワード属性を省略した点が違う
新しいパスワード再設定画面ビュー
app/views/password_resets/new.html.erb
----------------------------
<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:password_reset, url: password_resets_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>
----------------------------
これで、Forgot password画面が作成された
・12 1 3 createアクションでパスワード再設定
フォームから送信を行ったあと、メールアドレスをキーとしてユーザーをデータベースから見つけ、パスワード再設定用トークンと送信時のタイムスタンプでデータベースの属性を更新する必要がある
それに続いてルートURLにリダイレクトし、フラッシュメッセージをユーザーに表示する
送信が無効の場合、ログインと同様にnewページを出力してflash.nowメッセージを表示
パスワード再設定用のcreateアクション
app/controllers/password_resets_controller.rb
----------------------------
class PasswordResetsController < ApplicationController
def new
end
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
@user.send_password_reset_email
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
end
----------------------------
パスワード再設定の属性を設定する
トークンを生成し、それをダイジェストに変換して設定
また、時間も設定
Userモデルにパスワード再設定用メソッドを追加する
app/models/user.rb
----------------------------
class User < ApplicationRecord
attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
.
.
.
# アカウントを有効にする
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
# パスワード再設定の属性を設定する
def create_reset_digest
self.reset_token = User.new_token
update_attribute(:reset_digest, User.digest(reset_token))
update_attribute(:reset_sent_at, Time.zone.now)
end
# パスワード再設定のメールを送信する
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
private
# メールアドレスをすべて小文字にする
def downcase_email
self.email = email.downcase
end
# 有効化トークンとダイジェストを作成および代入する
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
----------------------------
ここまでで、無効なメールアドレスを入力したときの動作が完成
有効なメールアドレスを入力をした場合の動作を次から説明
・12 2 パスワード再設定のメール送信
パスワード再設定に関するメールを送信する部分を作っていく
Userメイラーを生成したときに、デフォルトのpassword_resetメソッドもまとめて生成されているはず
・12 2 1 パスワード再設定のメールとテンプレート
メイラーにパスワードリセット時のメールアドレスと件名を設定する
パスワード再設定のリンクをメール送信する
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(user)
@user = user
mail to: user.email, subject: "Password reset"
end
end
----------------------------
パスワードリセット時の文章(テキスト版)
パスワード再設定のテンプレート (テキスト)
app/views/user_mailer/password_reset.text.erb
----------------------------
To reset your password click the link below:
<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>
This link will expire in two hours.
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
----------------------------
パスワードリセット時の文章(HTML版)
パスワード再設定のテンプレート (HTML)
app/views/user_mailer/password_reset.html.erb
----------------------------
<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
<%= link_to "Reset password", edit_password_reset_url(@user.reset_token,
email: @user.email) %>
<p>This link will expire in two hours.</p>
<p>
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
</p>
----------------------------
テストコードを書く
パスワード再設定のプレビューメソッド (完成)
test/mailers/previews/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
user = User.first
user.reset_token = User.new_token
UserMailer.password_reset(user)
end
end
----------------------------
これで
http://ごにょごにょ/rails/mailers/user_mailer/password_reset
http://ごにょごにょ/rails/mailers/user_mailer/password_reset.txt
でプレビューできるようになった
有効なメールアドレスを送信した場合
サーバーログに表示されたパスワード再設定メールの例
----------------------------
Sent mail to michael@michaelhartl.com (66.8ms)
Date: Mon, 06 Jun 2016 22:00:41 +0000
From: noreply@example.com
To: michael@michaelhartl.com
Message-ID: <8722b257d04576a@mhartl-rails-tutorial-953753.mail>
Subject: Password reset
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_5407babbe3505_8722b257d045617";
charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_5407babbe3505_8722b257d045617
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
To reset your password click the link below:
https://rails-tutorial-mhartl.c9users.io/password_resets/3BdBrXe
QZSWqFIDRN8cxHA/edit?email=michael%40michaelhartl.com
This link will expire in two hours.
If you did not request your password to be reset, please ignore
this email and your password will stay as it is.
----==_mimepart_5407babbe3505_8722b257d045617
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<h1>Password reset</h1>
<p>To reset your password click the link below:</p>
<a href="https://rails-tutorial-mhartl.c9users.io/
password_resets/3BdBrXeQZSWqFIDRN8cxHA/
edit?email=michael%40michaelhartl.com">Reset password</a>
<p>This link will expire in two hours.</p>
<p>
If you did not request your password to be reset, please ignore
this email and your password will stay as it is.
</p>
----==_mimepart_5407babbe3505_8722b257d045617--
----------------------------
・12 2 2 送信メールのテスト
メイラーのテストを書いていく
パスワード再設定用メイラーメソッドのテストを追加する green
test/mailers/user_mailer_test.rb
----------------------------
require '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
test "password_reset" do
user = users(:michael)
user.reset_token = User.new_token
mail = UserMailer.password_reset(user)
assert_equal "Password reset", mail.subject
assert_equal [user.email], mail.to
assert_equal ["noreply@example.com"], mail.from
assert_match user.reset_token, mail.body.encoded
assert_match CGI.escape(user.email), mail.body.encoded
end
end
----------------------------
rails testでgreenになることを確認
ちなみにCGI.escape(user.email)ではなくuser.emailや[user.email]だとテストがredになるはず
user.emailの場合
ログ
----------------------------
ec2-user:~/environment/sample_app (password-reset) $ rails test
Running via Spring preloader in process 5325
Started with run options --seed 18925
FAIL["test_account_activation", UserMailerTest, 0.428123911000057]
test_account_activation#UserMailerTest (0.43s)
Expected /michael@example\.com/ to match # encoding: US-ASCII
"\r\n----==_mimepart_5e10a4e8bbd17_14cd4959a861732\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHi Michael Example,\r\n\r\nWelcome to the Sample App! Click on the link below to activate your account:\r\n\r\nhttp://example.com/account_activations/v-h6Bg9-z8w0J2T1-8pQ9g/edit?email=michael%40example.com\r\n\r\n----==_mimepart_5e10a4e8bbd17_14cd4959a861732\r\nContent-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n<!DOCTYPE html>\r\n<html>\r\n <head>\r\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\r\n <style>\r\n /* Email styles need to be inline */\r\n </style>\r\n </head>\r\n\r\n <body>\r\n <h1>Sample App</h1>\r\n\r\n<p>Hi Michael Example,</p>\r\n\r\n<p>\r\nWelcome to the Sample App! Click on the link below to activate your account:\r\n</p>\r\n\r\n<a href=\"http://example.com/account_activations/v-h6Bg9-z8w0J2T1-8pQ9g/edit?email=michael%40example.com\">Activate</a>\r\n </body>\r\n</html>\r\n\r\n----==_mimepart_5e10a4e8bbd17_14cd4959a861732--\r\n".
test/mailers/user_mailer_test.rb:14:in `block in <class:UserMailerTest>'
45/45: [==============] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.57483s
45 tests, 191 assertions, 1 failures, 0 errors, 0 skips
ec2-user:~/environment/sample_app (password-reset) $
----------------------------
[user.email]の場合
ログ
----------------------------
ec2-user:~/environment/sample_app (password-reset) $ rails test
Running via Spring preloader in process 5402
Started with run options --seed 7856
FAIL["test_account_activation", UserMailerTest, 1.4240181469999698]
test_account_activation#UserMailerTest (1.42s)
Expected /michael@example\.com/ to match # encoding: US-ASCII
"\r\n----==_mimepart_5e10a575345ed_151a4959a821455\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHi Michael Example,\r\n\r\nWelcome to the Sample App! Click on the link below to activate your account:\r\n\r\nhttp://example.com/account_activations/MHJUZoDUbASOfIGndM7_sg/edit?email=michael%40example.com\r\n\r\n----==_mimepart_5e10a575345ed_151a4959a821455\r\nContent-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n<!DOCTYPE html>\r\n<html>\r\n <head>\r\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\r\n <style>\r\n /* Email styles need to be inline */\r\n </style>\r\n </head>\r\n\r\n <body>\r\n <h1>Sample App</h1>\r\n\r\n<p>Hi Michael Example,</p>\r\n\r\n<p>\r\nWelcome to the Sample App! Click on the link below to activate your account:\r\n</p>\r\n\r\n<a href=\"http://example.com/account_activations/MHJUZoDUbASOfIGndM7_sg/edit?email=michael%40example.com\">Activate</a>\r\n </body>\r\n</html>\r\n\r\n----==_mimepart_5e10a575345ed_151a4959a821455--\r\n".
test/mailers/user_mailer_test.rb:14:in `block in <class:UserMailerTest>'
FAIL["test_password_reset", UserMailerTest, 1.4349113520001993]
test_password_reset#UserMailerTest (1.44s)
Expected ["michael@example.com"] to match # encoding: US-ASCII
"\r\n----==_mimepart_5e10a575373ad_151a4959a821520\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nTo reset your password click the link below:\r\n\r\nhttp://example.com/password_resets/AmOk-1T-YvHW7ssLhiPE6A/edit?email=michael%40example.com\r\n\r\nThis link will expire in two hours.\r\n\r\nIf you did not request your password to be reset, please ignore this email and\r\nyour password will stay as it is.\r\n\r\n----==_mimepart_5e10a575373ad_151a4959a821520\r\nContent-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n<!DOCTYPE html>\r\n<html>\r\n <head>\r\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\r\n <style>\r\n /* Email styles need to be inline */\r\n </style>\r\n </head>\r\n\r\n <body>\r\n <h1>Password reset</h1>\r\n\r\n<p>To reset your password click the link below:</p>\r\n\r\n<a href=\"http://example.com/password_resets/AmOk-1T-YvHW7ssLhiPE6A/edit?email=michael%40example.com\">Reset password</a>\r\n\r\n<p>This link will expire in two hours.</p>\r\n\r\n<p>\r\nIf you did not request your password to be reset, please ignore this email and\r\nyour password will stay as it is.\r\n</p>\r\n </body>\r\n</html>\r\n\r\n----==_mimepart_5e10a575373ad_151a4959a821520--\r\n".
test/mailers/user_mailer_test.rb:25:in `block in <class:UserMailerTest>'
45/45: [==============] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.67825s
45 tests, 191 assertions, 2 failures, 0 errors, 0 skips
ec2-user:~/environment/sample_app (password-reset) $
----------------------------
・12 3 パスワードを再設定する
editアクションの実装を進める。
統合テストも書いてく
・12 3 1 editアクションで再設定
パスワード再設定フォームを表示するビューが必要
このビューはユーザーの編集フォームと似ているが、今回はパスワード入力フィールドと確認用フィールドのみでOK
面倒な点があり、メールアドレスをキーとしてユーザーを検索するためには、editアクションとupdateアクションの両方でメールアドレスが必要
しかし、フォームを一度送信してしまうと、updateでこの情報が消える
それを解決するのが、ページ内に@userのemailをこっそり誰にも見えないように引っ張ってくる手法
パスワード再設定のフォーム
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_for(@user, url: password_reset_path(params[:id])) 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>
----------------------------
fを使っていない理由は、params[:email]に保存されない。つけるとparams[:user][:email]に保存される、という違いがある。
なぜそれがダメなのかはイマイチわかってない・・・。
editとupdateでユーザー情報を取得して、正しいユーザーでなければルートページに飛ばす
パスワード再設定のeditアクション
app/controllers/password_resets_controller.rb
----------------------------
class PasswordResetsController < ApplicationController
before_action :get_user, only: [:edit, :update]
before_action :valid_user, only: [:edit, :update]
.
.
.
def edit
end
private
def get_user
@user = User.find_by(email: params[:email])
end
# 正しいユーザーかどうか確認する
def valid_user
unless (@user && @user.activated? &&
@user.authenticated?(:reset, params[:id]))
redirect_to root_url
end
end
end
----------------------------
・12 3 2 パスワードを更新する
アカウント有効化にするときはeditで有効化していたが、
今回はeditでフォームから新しいパスワードを送信するようになっている
なので、フォームからの送信に対応するupdateアクションが必要になる
updateアクションでは以下の4つを考慮する
①パスワード再設定の有効期限が切れていないか
before_actionで対応
(password_reset_expired?はあとで解説)
②無効なパスワードであれば失敗させる
before_actionで対応
③新しいパスワードが空文字列になっていないか
特殊処理
④新しいパスワードが正しければ更新する
before_actionで対応
パスワード再設定のupdateアクション
app/controllers/password_resets_controller.rb
----------------------------
class PasswordResetsController < ApplicationController
before_action :get_user, only: [:edit, :update]
before_action :valid_user, only: [:edit, :update]
before_action :check_expiration, only: [:edit, :update] # (1) への対応
def new
end
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
@user.send_password_reset_email
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
def update
if params[:user][:password].empty? # (3) への対応
@user.errors.add(:password, :blank)
render 'edit'
elsif @user.update_attributes(user_params) # (4) への対応
log_in @user
flash[:success] = "Password has been reset."
redirect_to @user
else
render 'edit' # (2) への対応
end
end
private
def user_params
params.require(:user).permit(:password, :password_confirmation)
end
# beforeフィルタ
def get_user
@user = User.find_by(email: params[:email])
end
# 有効なユーザーかどうか確認する
def valid_user
unless (@user && @user.activated? &&
@user.authenticated?(:reset, params[:id]))
redirect_to root_url
end
end
# トークンが期限切れかどうか確認する
def check_expiration
if @user.password_reset_expired?
flash[:danger] = "Password reset has expired."
redirect_to new_password_reset_url
end
end
end
----------------------------
reset_sent_at < 2.hours.agoは、
reset_sent_atより2時間早い時刻という意味
・・・?
混乱しやすいから暗記してしまえばいいか
「reset_sent_at < 2.hours.ago」は、2時間以上経っているという認識に変えよう!
Userモデルにパスワード再設定用メソッドを追加する
app/models/user.rb
----------------------------
class User < ApplicationRecord
.
.
.
# パスワード再設定の期限が切れている場合はtrueを返す
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
private
.
.
.
end
----------------------------
これで、パスワードの再設定が失敗したら失敗した理由が表示される
(:dangerは赤色の文字)
パスワードの再設定が成功すると、Successメッセージが出てプロフィールページに飛ぶ(:successは緑色の文字)
・12 3 3 パスワードの再設定をテストする
送信に成功した場合と失敗した場合の統合テストを作成する
rails generate integration_test password_resets
ログ
----------------------------
ec2-user:~/environment/sample_app (password-reset) $ rails generate integration_test password_resets
Running via Spring preloader in process 5906
invoke test_unit
create test/integration/password_resets_test.rb
----------------------------
12.18: パスワード再設定の統合テスト
test/integration/password_resets_test.rb
----------------------------
require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
@user = users(:michael)
end
test "password resets" do
get new_password_reset_path
assert_template 'password_resets/new'
# メールアドレスが無効
post password_resets_path, params: { password_reset: { email: "" } }
assert_not flash.empty?
assert_template 'password_resets/new'
# メールアドレスが有効
post password_resets_path,
params: { password_reset: { email: @user.email } }
assert_not_equal @user.reset_digest, @user.reload.reset_digest
assert_equal 1, ActionMailer::Base.deliveries.size
assert_not flash.empty?
assert_redirected_to root_url
# パスワード再設定フォームのテスト
user = assigns(:user)
# メールアドレスが無効
get edit_password_reset_path(user.reset_token, email: "")
assert_redirected_to root_url
# 無効なユーザー
user.toggle!(:activated)
get edit_password_reset_path(user.reset_token, email: user.email)
assert_redirected_to root_url
user.toggle!(:activated)
# メールアドレスが有効で、トークンが無効
get edit_password_reset_path('wrong token', email: user.email)
assert_redirected_to root_url
# メールアドレスもトークンも有効
get edit_password_reset_path(user.reset_token, email: user.email)
assert_template 'password_resets/edit'
assert_select "input[name=email][type=hidden][value=?]", user.email
# 無効なパスワードとパスワード確認
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "foobaz",
password_confirmation: "barquux" } }
assert_select 'div#error_explanation'
# パスワードが空
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "",
password_confirmation: "" } }
assert_select 'div#error_explanation'
# 有効なパスワードとパスワード確認
patch password_reset_path(user.reset_token),
params: { email: user.email,
user: { password: "foobaz",
password_confirmation: "foobaz" } }
assert is_logged_in?
assert_not flash.empty?
assert_redirected_to user
end
end
----------------------------
inputというのが出てくる
これは、inputタグに正しい値が入っているかどうかを確認するためのもの
ここでrails testがgreenになるはず!
演習1 冗長をなくそう
update_columnsを使用するテンプレート
app/models/user.rb
----------------------------
class User < ApplicationRecord
attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
.
.
.
# アカウントを有効にする
def activate
update_columns(activated: true, activated_at: Time.zone.now)
end
# 有効化用のメールを送信する
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
# パスワード再設定の属性を設定する
def create_reset_digest
self.reset_token = User.new_token
update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)
end
# パスワード再設定用メールを送信する
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
private
# メールアドレスをすべて小文字にする
def downcase_email
self.email = email.downcase
end
# 有効化トークンとダイジェストを作成および代入する
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
----------------------------
演習2 パスワードの期限切れテストをしよう
パスワード再設定の期限切れのテストgreen
test/integration/password_resets_test.rb
----------------------------
require 'test_helper'
class PasswordResetsTest < ActionDispatch::IntegrationTest
def setup
ActionMailer::Base.deliveries.clear
@user = users(:michael)
end
.
.
.
test "expired token" do
get new_password_reset_path
post password_resets_path,
params: { password_reset: { email: @user.email } }
@user = assigns(:user)
@user.update_attribute(:reset_sent_at, 3.hours.ago)
patch password_reset_path(@user.reset_token),
params: { email: @user.email,
user: { password: "foobar",
password_confirmation: "foobar" } }
assert_response :redirect
follow_redirect!
assert_match "expired", response.body
end
end
----------------------------
演習3 パスワードが更新されたら更新できないようにしよう
パスワード再設定が成功したらダイジェストをnilにする
app/controllers/password_resets_controller.rb
----------------------------
class PasswordResetsController < ApplicationController
.
.
.
def update
if params[:user][:password].empty?
@user.errors.add(:password, :blank)
render 'edit'
elsif @user.update_attributes(user_params)
log_in @user
@user.update_attribute(:reset_digest, nil)
flash[:success] = "Password has been reset."
redirect_to @user
else
render 'edit'
end
end
.
.
.
end
----------------------------
演習4 演習3用のテストを作成しよう
test/integration/password_resets_test.rbの
password resetsテストの末尾に
assert_nil user.reload['reset_digest']を追加
・12 4 本番環境でのメール送信 (再掲)
source <(curl -sL https://cdn.learnenough.com/heroku_install)
heroku login --interactive
メールアドレス入力
パスワード入力
heroku create
git remote set-url heroku ごにょごにょ.gitのurlかく
heroku rename jun-killer-12
https://jun-killer-12.herokuapp.com/
ができた!
以下のhostに、アプリ名を入れる!
Railsのproduction環境でSendGridを使う設定
config/environments/production.rb
----------------------------
Rails.application.configure do
.
.
.
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :smtp
host = '<ここ注意ね!自分の作ったURL!!>.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_starttls_auto => true
}
.
.
.
end
----------------------------
新規ファイルを追加しているのでcommit -amは使えない
git add -A
git commit -m "Add password reset"
git checkout master
git merge password-reset
rails test
git push origin master
heroku maintenance:on
本番環境ではSendGridというHerokuアドオンを利用してアカウントを検証する必要がある
本チュートリアルではstarter tierを使う
heroku addons:create sendgrid:starter
失敗するようなら
heroku addons:add sendgrid:starter
を試してみる
今回は上のコマンドでいけた!
そして下記のコマンドで色々表示できた!
heroku config:get SENDGRID_USERNAME
heroku config:get SENDGRID_PASSWORD
git push heroku master
heroku pg:reset DATABASE
jun-killer-12
heroku run rails db:migrate
heroku run rails db:seed
heroku restart
heroku maintenance:off
https://jun-killer-12.herokuapp.com
いろいろ確認してみる
①ログインからforgot passwordに飛んでみる
↓
データベースにないe-mailを設定してみる
↓
Email address not found
↓
期待通りの動作
②アカウントを作成
↓
アカウント作成用アドレスが飛んでくる
↓
メイラーからアカウントを有効化
↓
ログアウト
↓
ログイン画面
↓
forgot password
↓
さっき登録したメールアドレスを入力
↓
Email sent with password reset instructions
↓
メイラーからパスワードリセットのリンクをたどる
↓
空のパスワードでやろうとしてみる
↓
The form contains 1 error.
Password can't be blank
↓
パスワード1文字でやろうとしてみる
↓
The form contains 1 error.
Password is too short (minimum is 6 characters)
↓
パスワード1文字の差異ありでやってみる
↓
The form contains 2 errors.
Password confirmation doesn't match Password
Password is too short (minimum is 6 characters)
↓
パスワード8文字の差異ありでやってみようとする
↓
The form contains 1 error.
Password confirmation doesn't match Password
↓
このあとでメイラーからまたリンクを辿ってみる
↓
それは普通に飛べる
↓
ちゃんと再設定してみる
↓
Password has been reset.
と表示され、プロフィール画面へ飛ぶ
↓
再度メイラーからまたリンクで辿ってみる
↓
トップページが表示される
↓
ログアウト
↓
ログイン
↓
前のパスワードでログインしてみようとする
↓
Invalid email/password combination
↓
新パスワードでログインしてみようとする
↓
普通にアカウントページに飛ぶ
↓
期待通りの動作
イイネ!!!
成果物
https://jun-killer-12.herokuapp.com
・12 5 最後に
パスワード再設定の実装が終わったことにより、サンプルアプリケーションのユーザー登録・ログイン・ログアウトの仕組みは、本物のアプリケーションと近いレベルに仕上がった。
残りの章では、Twitterのようなマイクロポスト機能(13章)と、フォロー中のユーザーの投稿を表示するステータスフィード機能(14章)を実装する
画像アップロード、カスタマイズしたデータベースへの問い合わせ、has_manyやhas_many :throughを使った高度なデータベースモデリングを扱う
・12 5 1 本章のまとめ
パスワードの再設定は、ActiveRecordオブジェクトではないが、セッションやアカウント有効化の場合と同様に、リソースでモデル化できる
Railsは、メール送信で扱うAction Mailerのアクションとビューを生成することができる
ActionMailerではデキストメールとHTMLメールの両方を利用できる
メイラーアックションで定義したインスタンス変数は、他のアクションやビューと同様、メイラーのビューから参照できる
メイラーアクションで定義したインスタンス変数は、他のアクションやビューと同様、メイラーのビューから参照できる
パスワードを再設定させるために、生成したトークンを使って一意のURLを作る
より安全なパスワード再設定のために、ハッシュ化したトークン(ダイジェスト)を使う
メイラーのテストと統合テストは、どちらもUserメイラーの振る舞いを確認するのに有用
SendGridを使うとproduction環境からメールを送信できる
・12 6 証明: 期限切れの比較
expiredは期限切れの意味
この記事が気に入ったらサポートをしてみませんか?