app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def account_activation(user)
@user = user
mail to: user.email, subject: "アカウントを有効化"
end
def password_reset(user)
@user = user
# インスタンス変数に代入する
mail to: user.email, subject: "パスワードをリセット"
end
end
app/views/user_mailer/password_reset.text.erb
パスワードをリセットするには、以下のリンクをクリックしてください:
<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>
このリンクは2時間で期限切れになります
パスワードのリセットをリクエストしなかった場合は、このメールを無視して、
パスワードはそのままです。
### app/views/user_mailer/password_reset.html.erb
.erb
<h1>パスワードをリセット</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>このリンクは2時間が期限です。</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
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 "アカウントを有効化", 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 "パスワードをリセット", 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
app/views/password_resets/edit.html.erb
.erb
<% provide(:title, 'パスワードをリセット') %>
<h1>Reset password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(model: @user, url: password_reset_path(params[:id]),
local: true) do |f| %>
<%= render 'shared/error_messages' %>
<%= hidden_field_tag :email, @user.email %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Update password", class: "btn btn-primary" %>
<% end %>
</div>
</div>
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)への対応
# 編集、更新をする前にget_user,valid_userメソッドが作動
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] = "パスワード再発行のためにメールを送信します。"
redirect_to root_url
else
flash.now[:danger] = "Eメールアドレスが見つかりませんでした。"
render 'new'
end
end
def edit
end
def update
if params[:user][:password].empty? # (3)への対応
# ユーザーのパスワードが空ではないか?
@user.errors.add(:password, :blank)
# 例
# > user.errors.add(:name, '文字数オーバー')
# > user.errors.full_messages
# => ["Name 文字数オーバー"]
# 多分 :blankにメッセージが入っていると思う
render 'edit'
# 表示させるviewファイルを指定して表示
elsif @user.update(user_params) # (4)への対応
# インスタンス変数が更新できれば
log_in @user
# ログインする
flash[:success] = "Password has been reset."
# メッセージを表示
redirect_to @user
# view の表示には直接は関係なく、新たな HttpRequest が発行されます。※GET のみ
else
render 'edit' # (2)への対応
# 編集ページを表示
end
end
private
def user_params
params.require(:user).permit(:password, :password_confirmation)
# RailsでDBを更新する際に、不要なパラメータを取り除く(必要なパラメータだけに絞り込む)ためのメソッドです。
# ユーザーの:passwordと:password_confirmation属性だけ取り出す
end
def get_user
@user = User.find_by(email: params[:email])
# dbからメアドを基に検索 インスタンス変数に代入
# params[:email]のメールアドレスに対応するユーザーをこの変数に保存します。
end
# 正しいユーザーかどうか確認する
def valid_user
unless (@user && @user.activated? &&
@user.authenticated?(:reset, params[:id]))
# インスタンス変数が有効性且つ有効化且つトークンの認証
# def authenticated?(attribute, token)
# digest = send("#{attribute}_digest")
# シンボルだけでいい
# 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
app/models/user.rb
class User < ApplicationRecord
# 継承させる
attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
# コストパラメータはテストでは最小にする
BCrypt::Password.create(string, cost: cost)
end
# ランダムなトークンを返す
def User.new_token
SecureRandom.urlsafe_base64
end
# 永続セッションのためにユーザーをデータベースに記憶する
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
# トークンがダイジェストと一致したらtrueを返す
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
# ユーザーのログイン情報を破棄する
def forget
update_attribute(:remember_digest, nil)
end
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
# パスワード再設定の期限が切れている場合はtrueを返す
def password_reset_expired?
reset_sent_at < 2.hours.ago
# 2時間まで
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
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'
# 新しくメアド、パスワードを変更画面(ログイン画面)か確認
assert_select 'input[name=?]', 'password_reset[email]'
# inputの欄にemailに入力できるか?
post password_resets_path, params: { password_reset: { email: "" } }
# メールアドレスが無効
# わからない
assert_not flash.empty?
# flashに中身があるか確認
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
# reset_digestと再取得した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
end
最後のテストはわからないことばっかりだ。
頑張ろう。