Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
An Exercise in Tail-Chasing, Punctuated by A Lesson in Humility

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment