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
endHere's the #call method from that FindMemberByToken interactor called by ResetPassword::New#call:
def call(params)
@valid_token = params[:token]
@member = find_member
endSo 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 }
endThis 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.
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.