tl;dr: Once again, being in a hurry will always slow you down. After forty years in this craft, the next counterexample I see will be the very first.
(See the bottom for an update.)
The reason that our token wasn't preserved from the ResetPassword::New
action to the ResetPassword::Create
action appears to be tied to the inconsistency with which that token is accessed throughout its journey.
Here's a URL fragment showing how the reset-password link might appear in an email sent to a Member:
https://conversagence.com/reset_password/new?token=YWZrbnRIbHBOeWVybU14ZVR6djBSN0F4
Here's the controller code implementing the endpoint specified by that URL:
# frozen_string_literal: true
module Web
module Controllers
module ResetPassword
class New
include Web::Action
include Web::RequireGuest
include Hanami::Action::Session
params do
required(:token) { format?(/^[[:alnum:]]{32}$/) }
end
expose :member
def call(params)
result = FindMemberByToken.new.call(params)
if result.success?
@member = result.member
else
message = result.errors.join('<br/>')
flash[:error] = message
redirect_to routes.root_path
end
end
end # class Web::Controllers::ResetPassword::New
end
end
end
Here's the #call
method from that FindMemberByToken
interactor called by ResetPassword::New#call
:
def call(params)
@valid_token = params[:token]
@member = find_member
end
So far, so good; :token
is accessible directly within the params
Hash-like object.
Now, here's the form rendered by the view associated with the ResetPassword::New
CAC, and you'll notice a difference:
.ui.segment
= form_for(:data, routes.reset_password_path) do
.input
= label :password
= password_field :password
.input
= label :password_confirmation
= password_field :password_confirmation
= hidden_field :token, hidden_field_value
.controls
= submit 'Set New Password'
= button 'Cancel', onclick: "window.location='/'"
Note that #hidden_field_value
is a method on our View object that reads
def hidden_field_value
{ value: params[:token].to_s }
end
"Hang on", you should be saying, "that params[:token]
doesn't look right. Looking at the form definition, shouldn't it be params[:data][:token]
?" Congratulations; you get a gold star. (And thank you, @kaikuchn
from the Gitter hanami/chat
channel for noticing.) It "worked" for the New
action because the token
parameter was coming in direct from the URL, not embedded within a form field. So params[:token]
in the New
controller becomes params[:data][:token]
in the Create
controller. The #hidden_field_value
code was copied from the New
View class (where it worked just fine) to the Create
View class without changing the params
reference.
Hilarity ensued.
Two solutions present themselves, One, proposed by Kai, is to change the link URL from
https://conversagence.com/reset_password/new?token=YWZrbnRIbHBOeWVybU14ZVR6djBSN0F4
to
https://conversagence.com/reset_password/new?data[token]=YWZrbnRIbHBOeWVybU14ZVR6djBSN0F4
along with the requisite changes to the New
controller action class. The other is to simply change the implementation of #hidden_field_value
in the Create
View only to
def hidden_field_value
{ value: params[:token].to_s }
end
This would require only that single change, if I understand correctly. I'm therefore going to try that first, and ncome back to the first solution if half an hour's wakeful effort doesn't yield victory.
Again many thanks to Kai and the gang in the Gitter hanami/chat
channel for their repeated help.
Update: 8 April 2019 23:55 SGT (GMT+8)
The hypothetical fix, where Views::ResetPassword::Create#hidden_field_value
was changed to reference params[:data][:token].to_s
rather than params[:token].to_s
, works as expected. Thanks again to all.