20
votes

devise_for creates routes including a DELETE route, which we want to remove, and devise_for doesn't support an :except or :only option.

How can I remove a route from Rails.application.routes? Either in the draw block, or afterward?


Here are details of a bug, which was the reason we needed to remove the route.

  • we were issuing a DELETE request to a custom UJS controller action

  • in the controller action we were removing what we wanted to, then doing a 302 redirect. This was a bad idea, and we have since corrected it by returning some JSON instead.

  • some clients, upon receiving the 302 would issue a new DELETE request to the redirect, which routes to a Devise delete route! Thereby inadvertantly deleting the person! Yikes. We were assuming this would be a GET. Bad assumption.

This bug has been fixed, but i would like to remove the route nonetheless.


Here is what I did in the end, which was suggested by the bounty-winner in his quote from Jose´ Valim:

In config/routes.rb, I added this above the devise_for call, which sets up the rest of my 'people' routes:

delete '/person', :to => 'people#destroy'

Then in my existing people_controller.rb, I added a no-op method:

def destroy
  render :nothing => true
end

I'm still a little irked that there isn't a simple way to just remove the route from the RouteSet. Also, the delete route still exists for the devise controller, but it won't get called because rails looks for the first match in config/routes.rb and returns it.

4
Nice questions. And would be nice to know what that IE8 bug is!Zabba
I added an edit with the story of our bug. Not sure the bug is exclusive to IE8, see this answer on 302s: stackoverflow.com/questions/4764297/….ipd
For posterity I've appended another solution to my answer.dwhalen
instead of render you can use 'redirect_to root_path'shadowbq

4 Answers

13
votes

Here is what Jose Valim (the author of devise) has to say on the subject:

There is no way to remove routes individually. Or you use :skip to remove all and draw the ones you need manually or you overwrite this routes by defining a route to the same path first in your config/ routes.rb

So the short answer to your question is no, you can't delete that one route. You can of course try doing things like patching the devise_for method, but that would be a somewhat involved undertaking (a day or several worth of effort). I'd just use the :skip option, then implement the routes you do want for that controller and leave off the one that you don't.

10
votes

Yes, kinda. You can completely overwrite devise controllers used and write your own (inheriting Devise's if needed). This wiki page can serve as guideline.

Edit

Why I have said kinda :)

Overriding sessions using:

devise_for :users, :controllers => { :sessions => 'custom_devise/sessions'}, :skip => [:sessions] do
  get 'sign_in' => 'custom_devise/sessions#new', :as => :new_user_session
  post 'sign_in' => 'custom_devise/sessions#create', :as => :user_session
end

will give you only two routes [:get, :post], but not :destroy

new_user_session GET  /sign_in(.:format) {:controller=>"custom_devise/sessions", :action=>"new"}
user_session POST /sign_in(.:format) {:controller=>"custom_devise/sessions", :action=>"create"}

So, effectively, you skip destroy/delete route. Now in controller you can go:

class SessionsController < Devise::SessionsController

  def new
    super
  end

  def create
    super
  end

end

You can now repeat the process for registrations, passwords and unlocks.

Second Edit

Ah, yes there is another, simpler way. You can manually create routes (documentation) using devise_scope also known as "as" without overriding:

as :user do
  get  "sign_in", :to => "devise/sessions#new"
  post "sign_in", :to => "devise/sessions#create"
  ...
end

Gives:

sign_in GET  /sign_in(.:format) {:controller=>"devise/sessions", :action=>"new"}
        POST /sign_in(.:format) {:controller=>"devise/sessions", :action=>"create"}

Third Edit

Also, you could overwrite part of Devise in charge of creating these routes, (only to be used in applications that will have no devise "destroy" route whatsoever), by creating an initializer:

module ActionDispatch::Routing
  extend ActionDispatch::Routing
  class Mapper

    protected
      def devise_session(mapping, controllers) #:nodoc:
        resource :session, :only => [], :controller => controllers[:sessions], :path => "" do
          get   :new,     :path => mapping.path_names[:sign_in],  :as => "new"
          post  :create,  :path => mapping.path_names[:sign_in]
        end
      end

      def devise_registration(mapping, controllers) #:nodoc:
        path_names = {
          :new => mapping.path_names[:sign_up],
          :cancel => mapping.path_names[:cancel]
        }

        resource :registration, :only => [:new, :create, :edit, :update], :path => mapping.path_names[:registration],
                 :path_names => path_names, :controller => controllers[:registrations] do
          get :cancel
        end
      end
  end
end

Note that this fix removes all destroy routes used in Devise (there are only two in "sessions" and "registrations") and is a fix only for this specific case.

In addition

You could also add :except option to routes. In order to do it, you must add devise_for method (copy it from original and modify to suit your wishes) to Mapper class so it sends [:except] member of options to above-mentioned (in code) private methods.. Then you should modify those to add routes based on conditions.

Fastest, dirty way, would be to add @scope[:except] = options[:except] and then to modify private methods so that except hash (if you decide to have fine grained route control like: :except => {:sessions => [:destroy]}, thus making :skip obsolete) or array (if you want to remove this specific action from all routes, like: :except => [:destroy]) is checked before adding route.

Anyway, there are plenty ways to achieve what you need. It's up to you to pick the one you think is best suited.

7
votes

Actually devise_for does support :skip and :only, for example (docs):

devise_for :user, :skip => :registration

This will skip all the registration controller's routes, rather than one specifically. You could then implement the routes you need. This seems cleaner than removing the route after the fact.

UPDATE:

Another possible solution is to use Rails' advanced constraints feature to block the unwanted route completely:

# config/routes.rb
constraints lambda {|req| req.url =~ /users/ && req.delete? ? false : true} do
  devise_for :users
end

Here is a post on using lambdas for route constraints. The request object is explained here. This might be the simplest solution.

2
votes

I found a simple solution with Devise 4.2.0 and Rails 5.0.1. I think this will work with Rails 4, and I'm uncertain about older versions of Devise.

Create an initializer overriding the devise_* route helpers. Examples methods are devise_session, devise_password, devise_confirmation, devise_unlock, and devise_registration. Check out the source.

Ensure the initializer is loaded after the Devise initializer by giving the filename a larger alphanumeric value.

For example, Devise creates a :confirmation route with the :new, :create, and :show actions. I only want the :create action.

# config/initializers/devise_harden.rb
module ActionDispatch::Routing
  class Mapper

    # Override devise's confirmation route setup, as we want to limit it to :create
    def devise_confirmation(mapping, controllers)
      resource :confirmation, only: [:create],
               path: mapping.path_names[:confirmation], controller: controllers[:confirmations]
    end
  end
end

Now POST /auth/confirmation is the only route setup for confirmation.