##はじめに
railsチュートリアル第十二章を進めていく中で、理解しにくかったところなど復習用に要約しているものとなっています!
##パスワードの再設定
第十一章でアカウントの有効化が済み、十二章ではパスワードの再設定を実装していく。
また、実装していく流れは十一章の有効化と似通っている部分が多くある。
パスワード再設定の全体の流れは次の通り。
・ユーザーがパスワードの再設定をリクエストすると、ユーザーが送信したメールアドレスをキーにしてデータベースからユーザーを見つける
・該当のメールアドレスがデータベースにある場合は、再設定用トークンとそれに対応するリセットダイジェストを生成する
・再設定用ダイジェストはデータベースに保存しておき、再設定用トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
・ユーザーがメールのリンクをクリックしたら、メールアドレスをキーとしてユーザーを探し、データベース内に保存しておいた再設定用ダイジェストと比較する (トークンを認証する)
・認証に成功したら、パスワード変更用のフォームをユーザーに表示する
今回も有効化の時と同様に、新たなモデルは作らずに、代わりに必要なデータ (再設定用のダイジェストなど) を Userモデル
に追加していく形で進めていく。
###PasswordResetsコントローラ
最初のステップとして、パスワード再設定用のコントローラを作っていく。
今回はビューも扱うので有効化と違い、newアクション
と editアクション
も一緒に生成しておく。
$ rails generate controller PasswordResets new edit
次にルーティングを設定する。
新しいパスワードを再設定するためのフォームと、Userモデル内のパスワードを変更するためのフォームが必要になるので、new、create、edit、updateのルーティングを用意しておく。
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
上記のコードは以下のRESTfulのルーティングに従い、名前付きルートを使っている。
HTTPリクエスト | URL | Action | 名前付きルート |
---|---|---|---|
GET | /password_resets/new | new | new_password_reset_path |
POST | /password_resets | create | password_resets_path |
GET | /password_resets//edit | edit | edit_password_reset_url(token) |
PATCH | /password_resets/ | update | password_reset_url(token) |
パスワード再設定画面へのリンクを追加しておく。
.
.
.
<%= f.label :password %>
#追加
<%= link_to "(forgot password)", new_password_reset_path %>
<%= f.password_field :password, class: 'form-control' %>
.
.
.
###新しいパスワードの設定
アカウント有効化の場合のように、パスワードの再設定でも、トークン用の仮想的な属性とそれに対応するダイジェストを用意していく。
もしトークンをハッシュ化せずに (つまり平文で) データベースに保存してしまうとするとセキュリティ的にダメだから再設定でもダイジェストを使うようにする。
また、再設定用のリンクに期限を設けるため、メールの送信時刻を記録する属性も追加する必要がある。
$ rails generate migration add_reset_to_users reset_digest:string \
> reset_sent_at:datetime
$ rails db:migrate
↑のようにreset_digest:string
とreset_sent_at:datetime
をusersテーブルに追加した。
新しいパスワード再設定の画面はログインの画面を参考に以下のようなものにする。
<% 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アクションでパスワード再設定
フォームから送信を行なった後、メールアドレスをキーとしてユーザーをデータベースから見つけ、パスワード再設定用トークンと送信時のタイムスタンプでデータベースの属性を更新する必要がある。
それに続いてルートURLにリダイレクトし、フラッシュメッセージをユーザーに表示する。
また送信が無効だった場合の処理も書いておく。
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モデル
にパスワード再設定用メソッドを追加する。
class User < ApplicationRecord
attr_accessor :remember_token, :activation_token, :reset_token
#省略
# パスワード再設定の属性を設定する
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
この時点でのアプリケーションは、無効なメールアドレスを入力した場合に正常に動作し、エラーメッセージを表示する。
正しいメールアドレスを送信した場合にもアプリケーションが正常に動作するためには、パスワード再設定のメイラーメソッドを定義する必要がある。
##パスワード再設定のメール送信
PasswordResetsコントローラで、createアクションがほぼ動作するところまできたから、次にパスワード再設定に関するメールを送信する部分を進めていく。
ここでは第十一章で生成した**Userメイラー (app/mailers/user_mailer.rb)**にデフォルトの password_resetメソッド
もまとめて生成されているからこちらの方で処理をかく。
###パスワード再設定のメールとテンプレート
UserMailer.password_reset(self).deliver_now
上のコードを実装するためにUserメイラーに password_resetメソッド
を作成し、続いて、テキストメールのテンプレートとHTMLメールのテンプレートをそれぞれ定義していく。
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
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.
<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アクションで再設定
無事に送信メールを生成できたので、次はPasswordResetsコントローラの editアクション
の実装を進めていく。
送られてきたurlをクリックして表示される、パスワード再設定のフォーム次のようなものとする。
<% 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アクションの両方でメールアドレスが必要になる。
ここで隠しフィールドとしてメールアドレスをページ内に保存することで、他の情報と一緒にメールアドレスが送信されるようになる。
editアクション
とupdateアクション
のどちらの場合も正当な @user
が存在する必要があるので、いくつかのbeforeフィルタを使って @user
の検索とバリデーションを行う。
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
###パスワードを更新する
AccountActivationsコントローラの editアクション
では、ユーザーの有効化ステータスをfalseからtrueに変更したが、今回の場合はフォームから新しいパスワードを送信する事になるからフォームからの送信に対応する updateアクション
が必要になる。
updateアクション
では、以下の4つを考慮する必要がある。
1パスワード再設定の有効期限が切れていないか
2無効なパスワードであれば失敗させる (失敗した理由も表示する)
3新しいパスワードが空文字列になっていないか (ユーザー情報の編集ではOKだった)
4新しいパスワードが正しければ、更新する
これらを考慮して以下のようにを定義する。
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 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
.
.
.
# トークンが期限切れかどうか確認する
def check_expiration
if @user.password_reset_expired?
flash[:danger] = "Password reset has expired."
redirect_to new_password_reset_url
end
end
end
パスワード再設定の有効期限が切れていないか
に関しては、check_expirationメソッド
を定義して解決する。
check_expirationメソッドを実装するには
password_reset_expired?
を定義する必要がある。
このメソッドではパスワード再設定の期限を設定して、2時間以上パスワードが再設定されなかった場合は期限切れとする処理を行う。
それではUserモデルで password_reset_expired?メソッド
を定義する。
class User < ApplicationRecord
.
.
.
# パスワード再設定の期限が切れている場合はtrueを返す
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
private
.
.
.
end
reset_sent_at < 2.hours.ago
は、
「パスワード再設定メールの送信時刻が、現在時刻より2時間以上前 (早い) の場合」
という風に解釈できる。
これでパスワード再設定の実装も終わり、あとは前章のようにproduction環境でも動くようにする。
セットアップの手順はアカウント有効化と全く同じとなっている。
##さいごに
パスワード再設定はアカウント有効かと似通う部分が多い。
次↓
https://qiita.com/jonnyjonnyj1397/items/01a5dfeee3a4e9133266