Day14です!
今日もがんばっていきましょー!
読んでいただいている方、いつもありがとうございます!
初心者なもので、たまに間違った内容もあるかと思います。
その際にはコメントやTwitterでご指摘いただけると幸いです!
[Day14]第十二章12.1〜12.6作業ログ
今日はパスワード再設定のメール機能の実装
登録しているサービスで、パスワードを忘れちゃうことってありますよね。
その時にパスワードを再設定するためのメールを受け取ったりします。
今日はこのパスワード再設定のメール機能の実装!
昨日やった、アカウント認証と似ている部分がけっこうあるみたいなので、サクサクすすみそう。
PasswordResetsコントローラとルーティングの設定
なにはともあれ、まずはコントローラーとルーティングですよ。
コントローラーの作成。
rails generate controller PasswordResets new edit --no-test-framework
最後のオプションは、テストファイルを作成したくないときに使います。
ルーティングの追加
resources :password_resets, only: [:new, :create, :edit, :update]
これで下の表のような名前付きリンクが使えるようになります。
ログイン画面にパスワード再設定のページへのリンクを設置
さっきの名前付きルートを使ってあげましょう。
<% 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 :password %>
<%= link_to "(forgot password)", new_password_reset_path %>
<%= f.password_field :password, class: 'form-control' %>
...
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
activation_digestにならって、reset_digestカラムやビューの追加
上の表のようなカラムにします。コマンドは次。
rails generate migration add_reset_to_users reset_digest:string \
> reset_sent_at:datetimer?
rails db:migrate
パスワード再設定のページのビューは以下。
<% 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>
createアクションでパスワードの再設定
class PasswordResetsController < ApplicationController
...
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
...
end
太字のメソッドをこれから定義して、メールの送信やパスワードダイジェストの作成を行います。
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
メーラーメソッドの設定がまだなので、メールはまだ送れません。
パスワード再設定用のメーラーメソッドはこれから定義していきます。
メーラーメソッドの設定
次にメーラーメソッドの設定をしてあげます。
メールの宛先、件名はきのうとほとんど同じ。
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
メールのテンプレートはこんな感じ。
リンクにメールアドレスとreset_tokenを含めてあげるところがポイント。
<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>
パスワード再設定をするeditページの作成
これまでの実装でパスワード再設定のために必要な、リンクの設定ができました。
次はそのリンクからemailとreset_tokenを読み取って、パスワードの再設定を実際に行えるようにしていきます。
ここで、新パスワードの設定のためのeditアクションとパスワード更新のためのupdateアクションの2つのアクションで、リンクから読み取ったemailの値を使います。
そのために、一時的にemailの値を隠しフィールドとして保持できる手法を使います。
下のhidden_field_tag
が隠しフィールドです。
<% 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>
また、edit、updateアクションに行く前に、有効なユーザーなのか検証できるようにしておきます。
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
updateアクションでパスワードを再設定できるように
updateアクションでは以下の4点に注意して、設計を行います。
- パスワード再設定の有効期限が切れていないか
- 無効なパスワードであれば失敗させる (失敗した理由も表示する)
- 新しいパスワードが空文字列になっていないか (ユーザー情報の編集ではOKだった)
- 新しいパスワードが正しければ、更新する
これらをすべて通過すればパスワードを更新します。
実際に完成したコントローラーはこちら。
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
password_reset_expired?
メソッドはUserモデルに書いてあげます。
class User < ApplicationRecord
...
# パスワード再設定の期限が切れている場合はtrueを返す
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
private
...
end
ってことで今日の実装は終了!
Herokuにデプロイして、動作確認もできました!
開発(develop)環境からメールが送信されずびっくり
ちょっと余談です。自分で勝手にパニックになってただけの話。
プレビューやテストではメールが送られたようになっているのに、実際にぼくのgmailにメールが届きませんでした。
そこでどこかで間違えたか、ソースコードを読み返しまくりました。
でも、よく考えてみれば実際に送られてこなくて当たり前だったんです。
昨日も開発環境からはメールは送信できなかったことを思い出しました。
メールが送信できたのは、本番(production)環境のHerokuからだけだった。
これはおそらく、開発環境ではメーラーのSMTPとかの設定をしていないからだと思う。
メーラーの設定してないからメールを送信できなくて当たりまえ!
[Day14]まとめ
- 学習範囲:12.1〜12.6
- 学習時間:3時間40分
- 総学習時間:59時間
- 反省点:
- 備考:今週中に一周目終わらせたい。あと2日間で2章。できるはず。
読んでいただいている方、いつもありがとうございます!
初心者なもので、たまに間違った内容もあるかと思います。
その際にはコメントやTwitterでご指摘いただけると幸いです!
コメントを残す