createアクション
まずはcreateアクションのソースコードから読んでみます。
github.com
def create
self.resource = resource_class.send_reset_password_instructions(resource_params)
yield resource if block_given?
if successfully_sent?(resource)
respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name))
else
respond_with(resource)
end
end
resource_classは User のように、deviseを使って認証を行うモデルのクラスが返ります。
send_reset_password_instructionsはどんな処理でしょうか。
send_reset_password_instructions
def send_reset_password_instructions(attributes = {})
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
recoverable.send_reset_password_instructions if recoverable.persisted?
recoverable
end
コメントアウト部分で挙動について丁寧に説明されているので、内部で呼んでいるメソッドについては読まなくてもいいかなと思いつつ、
念のため目を通してみます。
find_or_initialize_with_errors
github.com
def find_or_initialize_with_errors(required_attributes, attributes, error = :invalid)
attributes.try(:permit!)
attributes = attributes.to_h.with_indifferent_access
.slice(*required_attributes)
.delete_if { |key, value| value.blank? }
if attributes.size == required_attributes.size
record = find_first_by_auth_conditions(attributes) and return record
end
new(devise_parameter_filter.filter(attributes)).tap do |record|
required_attributes.each do |key|
record.errors.add(key, attributes[key].blank? ? :blank : error)
end
end
end
pry-byebugのstepメソッドで処理の内部に入って、適宜処理を実行して挙動の把握を試みます。
$ attributes = attributes.to_h.with_indifferent_access.slice(*required_attributes).delete_if { |key, value| value.blank? }
=> {"email"=>"sample@example.com"}
$ record = find_first_by_auth_conditions(attributes) and return record
=> nil
$ record = new(devise_parameter_filter.filter(attributes)).tap do |record|
required_attributes.each do |key|
record.errors.add(key, attributes[key].blank? ? :blank : error)
end
end
$ record
=>
$ record.errors
=>
@base=
@details={:email=>[{:error=>:not_found}]},
@messages={:email=>["not found"]}>
attrを加工して必要なattrと数が一致していればレコードを引いてきて、適宜errorsオブジェクトにエラーを格納していました。
個人的には加工とエラー格納をprivateメソッドにしてメソッドの粒度を細かくしたいなと読んでて思いましたが余談ですね、、、
しかも12年前のコードが現役で動いていることに感動、、、またまた余談でした。
make User#send_reset_password_instructions to require all authenticat… · heartcombo/devise@850afec · GitHub
さて、recordを引いてくる部分のメソッドを読んでみます。
find_first_by_auth_conditions
github.com
def find_first_by_auth_conditions(tainted_conditions, opts = {})
to_adapter.find_first(devise_parameter_filter.filter(tainted_conditions).merge(opts))
end
こちらもstepメソッドで適宜実行してみます。
$ tainted_conditions
=> {"email"=>"sample@example.com"}
$ to_adapter
=>
@klass=
User(id: integer, email: string, encrypted_password: string, reset_password_token: string, reset_password_sent_at: datetime, remember_created_at: datetime, unique_session_id: string, created_at: datetime, updated_at: datetime)>
$ devise_parameter_filter
=>
$ devise_parameter_filter.filter(tainted_conditions)
=> {"email"=>"sample@example.com"}
$ to_adapter.find_first(devise_parameter_filter.filter(tainted_conditions).merge(opts))
=> nil
From: /usr/local/bundle/gems/orm_adapter-0.5.0/lib/orm_adapter/adapters/active_record.rb:22 OrmAdapter::ActiveRecord
21: def find_first(options = {})
=> 22: construct_relation(klass, options).first
23: end
$ klass
=> User(id: integer, email: string, encrypted_password: string, reset_password_token: string, reset_password_sent_at: datetime, remember_created_at: datetime, unique_session_id: string, created_at: datetime, updated_at: datetime)
$ options
=> {"email"=>"sample@example.com"}
$ construct_relation(klass, options)
User Load (5.0ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 [["email", "sample@example.com"]]
=> []
From: /usr/local/bundle/gems/orm_adapter-0.5.0/lib/orm_adapter/adapters/active_record.rb:42 OrmAdapter::ActiveRecord
41: def construct_relation(relation, options)
42: conditions, order, limit, offset = extract_conditions!(options)
43:
44: relation = relation.where(conditions_to_fields(conditions))
45: relation = relation.order(order_clause(order)) if order.any?
46: relation = relation.limit(limit) if limit
47: relation = relation.offset(offset) if offset
48:
=>49: relation
50: end
$ relation
User Load (5.0ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 [["email", "sample@example.com"]]
=> []
最終的にdeviseのコードではないところのコードになりましたが、、、
find_first_by_auth_conditionsメソッドは引数でユーザーの情報を検索するメソッドでした。
find_or_initialize_with_errorsメソッドの処理もこれで一通り把握できました。
send_reset_password_instructions
ようやくself.send_reset_password_instructionsメソッドの2行目に突入です、、、
github.com
def send_reset_password_instructions
token = set_reset_password_token
send_reset_password_instructions_notification(token)
token
end
set_reset_password_token
github.com
def set_reset_password_token
raw, enc = Devise.token_generator.generate(self.class, :reset_password_token)
self.reset_password_token = enc
self.reset_password_sent_at = Time.now.utc
save(validate: false)
raw
end
encryptedされたtokenをreset_password_tokenとし、パスワードリセットメール送信日時を設定し、バリデーションを実行せずにレコードをDBに保存しています。
返り値は先ほど訳したコメントアウトにもある通り、トークン(encryptedされてない生の)が返っています。
send_reset_password_instructions_notification
github.com
def send_reset_password_instructions_notification(token)
send_devise_notification(:reset_password_instructions, token, {})
end
ここからはstepメソッドで処理を追ってみます。
From: /usr/local/bundle/gems/devise-4.8.0/lib/devise/models/recoverable.rb:99 Devise::Models::Recoverable
98: def send_reset_password_instructions_notification(token)
=> 99: send_devise_notification(:reset_password_instructions, token, {})
100: end
$ step
From: /usr/local/bundle/gems/devise-4.8.0/lib/devise/models/authenticatable.rb:201 Devise::Models::Authenticatable
200: def send_devise_notification(notification, *args)
=> 201: message = devise_mailer.send(notification, self, *args)
202:
203: if message.respond_to?(:deliver_now)
204: message.deliver_now
205: else
206: message.deliver
207: end
208: end
$ message = devise_mailer.send(notification, self, *args)
=>
DeviseのMailerを呼ぶ処理を実行していました。以下のメソッドが実行されるようです。
def reset_password_instructions(record, token, opts = {})
@token = token
devise_mail(record, :reset_password_instructions, opts)
end
devise_mailの処理はこちら。
github.com
これでようやくsend_reset_password_instructionsメソッドも挙動の把握ができました、、、
successfully_sent?
controllerの処理に戻ってこれました、、、もう一度createアクションを記載します。
def create
self.resource = resource_class.send_reset_password_instructions(resource_params)
yield resource if block_given?
if successfully_sent?(resource)
respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name))
else
respond_with(resource)
end
end
successfully_sent?というメソッド名からどんな処理か想像はつきますが、見に行ってみましょう。
github.com
def successfully_sent?(resource)
notice = if Devise.paranoid
resource.errors.clear
:send_paranoid_instructions
elsif resource.errors.empty?
:send_instructions
end
if notice
set_flash_message! :notice, notice
true
end
end
Devise.paranoidをどのように使うかは以下の記事が詳しかったです。
techracho.bpsinc.jp
noticeの値に応じて表示するフラッシュメッセージを変えているようでした。