Search code examples
ruby-on-rails-3devise

How to redirect page after confirmation in Devise


Say a user clicks a link to a page that is protected. They are then redirected to a sign in screen where they can log in. If they do, then are successfully redirected to that page. But if they don't have an account they have to sign up. This is where things get tricky because I'm doing an email confirmation.

By clicking a link it creates a new session can I can't automatically redirect the user to that protected page. I'm trying to change this by putting in a reference to the redirect inside the confirmation link. I would like to do:

<%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token, :redirect_to => stored_location_for(@resource)) %>

But I can't figure out how to get access to stored_location_for (or if that is even the right location to get). It is defined in Devise::Controllers::Helpers, but it is an instance method so I can't do Devise::Controllers::Helpers.stored_location_for(…).

How do I get access to stored_location_for or what is the better way of doing this?

My goal is to do that and then in my custom ConfirmationsController define:

def show
  if params[:redirect_to]
    session["user_return_to"] = params[:redirect_to]
  end
  super

end

That should work right?


Solution

  • I figured it out. I'm not sure if this changes with the update Devise did yesterday in making Devise::Mailer put most of its functionality into a module. (See the code and ticket for more information).

    Basically it boils down to not being able to access the session inside of a mailer view. Therefore you have to pass the redirect as a variable. Devise uses an after_create method on your resource (User in my case) which then sends the confirmation email. This meant I couldn't just pass the session variable directly to the mailer. Thus I feel like this is a pretty nasty work-around in order to get this functionality, but here is the code:

    To get the redirect_to variable into the mailer you have to add a variable to the user, thus:

    class User < ActiveRecord::Base
      …
      attr_accessor :return_to
      …
    end
    

    Then you have to set that variable when you create the user for the first time.

    I already had a custom controller setup for registration. (See Devise' Readme on how to set this up, or see @ramc's answer for direction). But it was relatively easy to do this part, I just added it to the parameters and let the rest take care of itself.

    class RegistrationsController < Devise::RegistrationsController
      def create
        params[:user][:return_to] = session[:user_return_to] if session[:user_return_to]
        …
        super
      end
    end
    

    Now the user has a variable return_to which is set. We just need to access that in the confirmation_instructions email. I've already rewritten part of confirmation_instructions.html.erb so inside there I just added:

    <% if @resource.return_to %>
      <%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token, :redirect_to => @resource.return_to) %>
    <% else %>
      <%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %>
    <% end %>
    

    (For those who are new to this, @resource is the variable Devise uses to define your user).

    Now once the user clicks on that link we need to redirect them. @ramc's before filter works well for this:

    class ConfirmationsController < Devise::ConfirmationsController
      before_filter :set_redirect_location, :only => :show
    
      def set_redirect_location
        session[:user_return_to] = params[:redirect_to] if params[:redirect_to]
      end
    end
    

    That will take care of the case where a new user goes to a protected page then signs up, clicks on the confirmation link and is properly redirected to the protected page.

    Now we just need to take care of the case where a user does the above, but instead of clicking on the link, they try to go back to the protected page. In this case they are asked to sign-up/sign-in. They sign-in and then are asked to confirm their email and are given the option of resending the confirmation email. They put in their email and now we need to put the redirect_to variable in that new confirmation email.

    To do this we need to modify the ConfirmationController, similarly to how we did the RegistrationController. This time we need to modify the create method. The way it works out of the box is to call a class method on the user called send_confirmation_instructions. We want to rewrite that method so we can pass the return_to variable into it.

    class ConfirmationsController < Devise::ConfirmationsController
      def create
        self.resource = resource_class.send_confirmation_instructions(params[resource_name],session[:user_return_to])
    
        if resource.errors.empty?
          set_flash_message(:notice, :send_instructions) if is_navigational_format?
          respond_with resource, :location => after_resending_confirmation_instructions_path_for(resource_name)
        else
          respond_with_navigational(resource){ render_with_scope :new }
        end
      end
    end
    

    The only thing different than what comes with Devise is that first line of create, we pass two variables in. Now we need to rewrite that method:

    class User < ActiveRecord::Base
      def self.send_confirmation_instructions(attributes={},redirect=nil)
        confirmable = find_or_initialize_with_errors(confirmation_keys, attributes, :not_found)
        confirmable.return_to = redirect if confirmable.persisted?
        confirmable.resend_confirmation_token if confirmable.persisted?
        confirmable
      end
    end
    

    confirmable becomes an instance of User (the current user based on email). So we just need to set return_to.

    That's it.