147
votes

I'm using Devise in a Rails 3 app, but in this case, a user must be created by an existing user, who determines what permissions he/she will have.

Because of this, I want:

  • To remove the route for users to sign up.
  • To still allow users to edit their profiles (change email address and password) after they have signed up

How can I do this?

Currently, I'm effectively removing this route by placing the following before devise_for :users:

match 'users/sign_up' => redirect('/404.html')

That works, but I imagine there's a better way, right?

Update

As Benoit Garret said, the best solution in my case is to skip creating the registrations routes en masse and just create the ones I actually want.

To do that, I first ran rake routes, then used the output to re-create the ones I wanted. The end result was this:

devise_for :users, :skip => [:registrations] 
as :user do
  get 'users/edit' => 'devise/registrations#edit', :as => 'edit_user_registration'
  put 'users' => 'devise/registrations#update', :as => 'user_registration'
end

Note that:

  • I still have :registerable in my User model
  • devise/registrations handles updating email and password
  • Updating other user attributes - permissions, etc - is handled by a different controller

Actual answer:

Remove the route for the default Devise paths; i.e.:

devise_for :users, path_names: {
  sign_up: ''
}
15
I actually think your original solution was much more simple, and clear. Is there any real problem with it security wise?counterbeing
For some reason your updated solution kept throwing an error saying I needed the ID. After an hour of hair pulling and many many server restarts, it somehow fixed itself. I have no idea... but if someone else experiences that, keep trying!Erik Trautman
@counterbeing - no problem that I know of, I just didn't like having unused routes or relying on ordering.Nathan Long
"Actual Answer" doesn't complete kill the route if it is redirected to from within devise controller. Default behavior will still route you to sign-up path if you hit GET route like https://example.com/users/. See my answer below.lacostenycoder
Security Flaw! The "Actual answer" that is shown only get's rid of the sign-up form, it does NOT get rid of the POST route that actually creates the user.Eric Terry

15 Answers

54
votes

I tried to do this as well, but a thread on the devise google group dissuaded me from searching for a really clean solution.

I'll quote José Valim (the Devise maintainer) :

There isn't a straight-forward option. You can either provide a patch or use :skip => :registerable and add only the routes you want.

The original question was :

Is there any good way to remove a specific route (the delete route) from Rails?

90
votes

you can do this in your model

# typical devise setup in User.rb
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable

change it to:

devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable

notice that the symbol :registerable was removed

That's it, nothing else is required. All routes and links to registration page are magically removed too.

30
votes

I had similar issue tried to remove devise_invitable paths for create and new :

before:

 devise_for :users

rake routes

accept_user_invitation GET    /users/invitation/accept(.:format)           devise/invitations#edit
       user_invitation POST   /users/invitation(.:format)                  devise/invitations#create
   new_user_invitation GET    /users/invitation/new(.:format)              devise/invitations#new
                       PUT    /users/invitation(.:format)                  devise/invitations#update

after

devise_for :users , :skip => 'invitation'
devise_scope :user do
  get "/users/invitation/accept", :to => "devise/invitations#edit",   :as => 'accept_user_invitation'
  put "/users/invitation",        :to => "devise/invitations#update", :as => nil
end

rake routes

accept_user_invitation GET    /users/invitation/accept(.:format)                 devise/invitations#edit
                       PUT    /users/invitation(.:format)                        devise/invitations#update

note 1 devise scope https://github.com/plataformatec/devise#configuring-routes

note 2 I'm applying it on devise_invitable but it will work with any devise *able feature

Important note: see that devise_scope is on user not users ? that's correct, watch out for this ! It can cause lot of pain giving you this problem:

Started GET "/users/invitation/accept?invitation_token=xxxxxxx" for 127.0.0.1 
Processing by Devise::InvitationsController#edit as HTML
  Parameters: {"invitation_token"=>"6Fy5CgFHtjWfjsCyr3hG"}
 [Devise] Could not find devise mapping for path "/users/invitation/accept?  invitation_token=6Fy5CgFHtjWfjsCyr3hG".
This may happen for two reasons:

1) You forgot to wrap your route inside the scope block. For example:

  devise_scope :user do
     match "/some/route" => "some_devise_controller"
  end

 2) You are testing a Devise controller bypassing the router.
   If so, you can explicitly tell Devise which mapping to use:

    @request.env["devise.mapping"] = Devise.mappings[:user]
21
votes

I found another post similar to this one and wanted to share an answer @chrisnicola gave. In the post they were attempting to only block user signup's during production.

You could also modify the registrations controller. You can use something like this:

In "app/controllers/registrations_controller.rb"

class RegistrationsController < Devise::RegistrationsController
  def new
    flash[:info] = 'Registrations are not open.'
    redirect_to root_path
  end

  def create
    flash[:info] = 'Registrations are not open.'
    redirect_to root_path
  end
end

This will override devise's controller and use the above methods instead. They added flash messages incase that someone somehow made it to the sign_up page. You should also be able to change the redirect to any path you like.

Also in "config/routes.rb" you can add this:

devise_for :users, :controllers => { :registrations => "registrations" }

Leaving it like this will allow you to use the standard devise edit your profile. If you wish you can still override the edit profile option by including

  def update
  end

in the "app/controllers/registrations_controller.rb"

13
votes

You can override the "devise_scope" by placing it before the "devise_for".

devise_scope :user do
  get "/users/sign_up",  :to => "sites#index"
end

devise_for :users

Not sure if this is the best way but its my solution currently, as it just redirects back to the sign in page.

13
votes

This is an old question - but I recently had solve the same issue and came up with a solution which is far more elegant than:

devise_for :users, :skip => [:registrations] 
as :user do
  get 'users/edit' => 'devise/registrations#edit', :as => 'edit_user_registration'
  put 'users' => 'devise/registrations#update', :as => 'user_registration'
end

And it gives the default names for the named routes (like cancel_user_registration) without being excessively verbose.

devise_for :users, skip: [:registrations]

# Recreates the Devise registrations routes
# They act on a singular user (the signed in user)
# Add the actions you want in 'only:'
resource :users,
    only: [:edit, :update, :destroy],
    controller: 'devise/registrations',
    as: :user_registration do
  get 'cancel'
end

rake routes output with the default devise modules:

                  Prefix Verb   URI Pattern                    Controller#Action
        new_user_session GET    /users/sign_in(.:format)       devise/sessions#new
            user_session POST   /users/sign_in(.:format)       devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format)      devise/sessions#destroy
           user_password POST   /users/password(.:format)      devise/passwords#create
       new_user_password GET    /users/password/new(.:format)  devise/passwords#new
      edit_user_password GET    /users/password/edit(.:format) devise/passwords#edit
                         PATCH  /users/password(.:format)      devise/passwords#update
                         PUT    /users/password(.:format)      devise/passwords#update
cancel_user_registration GET    /users/cancel(.:format)        devise/registrations#cancel
  edit_user_registration GET    /users/edit(.:format)          devise/registrations#edit
       user_registration PATCH  /users(.:format)               devise/registrations#update
                         PUT    /users(.:format)               devise/registrations#update
                         DELETE /users(.:format)               devise/registrations#destroy
5
votes

I liked @max's answer, but when trying to use it I ran into an error due to devise_mapping being nil.

I modified his solution slightly to one that seems to address the issue. It required wrapping the call to resource inside devise_scope.

devise_for :users, skip: [:registrations]

devise_scope :user do
  resource :users,
           only: [:edit, :update, :destroy],
           controller: 'devise/registrations',
           as: :user_registration do
    get 'cancel'
  end
end

Note that devise_scope expects the singular :user whereas resource expects the plural :users.

4
votes

Do This in routes.rb

devise_for :users, :controllers => {:registrations => "registrations"}, :skip => [:registrations]
  as :user do
    get 'users/edit' => 'devise/registrations#edit', :as => 'edit_user_registration'
    put 'users' => 'devise/registrations#update', :as => 'user_registration'
end

  devise_scope :user do
    get "/sign_in",  :to => "devise/sessions#new"
    get "/sign_up",  :to => "devise/registrations#new"
  end

you will get an error now while you come to sign in page, to fix it. Do this change in: app/views/devise/shared/_links.erb

<% if  request.path != "/sign_in" %>
    <%- if devise_mapping.registerable? && controller_name != 'registrations' %>
        <%= link_to "Sign up", new_registration_path(resource_name) %><br />
    <% end -%>
<% end %>
3
votes

I've found this to work well without messing with routes or adding application controller methods. My approach is to override the devise method. Add this to app/controllers/devise/registrations_controller.rb I've omitted the other methods for brevity.

class Devise::RegistrationsController < DeviseController
  ...
  # GET /resource/sign_up
  def new
    redirect_to root_path
  end
  ....
end

Also to remove illusion that this path is still reachable from other views you might also want to remove this code from app/views/devise/shared/_links.erb

<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
  <%= link_to "Sign up", new_registration_path(resource_name) %><br />
<% end -%>
2
votes

For others in my case.
With devise (3.5.2).
I successfully removed the routes to signup, but kept the ones to edit the profile, with the following code.

#routes.rb
devise_for :users, skip: [:registrations]
as :user do
  get 'users/edit' => 'devise/registrations#edit', :as => 'edit_user_registration'
  put '/users(.:format)' => 'devise/registrations#update', as: 'user_registration'
  patch '/users(.:format)' => 'devise/registrations#update'
end
1
votes

Here's the slightly different route I went. It makes it so you don't have to override the devise/shared/_links.html.erb view.

In app/models/user.rb:

devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable

In config/routes.rb:

devise_for :users
devise_scope :user do
  put 'users' => 'devise/registrations#update', as: 'user_registration'
  get 'users/edit' => 'devise/registrations#edit', as: 'edit_user_registration'
  delete 'users' => 'devise/registrations#destroy', as: 'registration'
end

Before:

$ rake routes | grep devise
           new_user_session GET    /users/sign_in(.:format)                    devise/sessions#new
               user_session POST   /users/sign_in(.:format)                    devise/sessions#create
       destroy_user_session DELETE /users/sign_out(.:format)                   devise/sessions#destroy
              user_password POST   /users/password(.:format)                   devise/passwords#create
          new_user_password GET    /users/password/new(.:format)               devise/passwords#new
         edit_user_password GET    /users/password/edit(.:format)              devise/passwords#edit
                            PATCH  /users/password(.:format)                   devise/passwords#update
                            PUT    /users/password(.:format)                   devise/passwords#update
   cancel_user_registration GET    /users/cancel(.:format)                     devise/registrations#cancel
          user_registration POST   /users(.:format)                            devise/registrations#create
      new_user_registration GET    /users/sign_up(.:format)                    devise/registrations#new
     edit_user_registration GET    /users/edit(.:format)                       devise/registrations#edit
                            PATCH  /users(.:format)                            devise/registrations#update
                            PUT    /users(.:format)                            devise/registrations#update
                            DELETE /users(.:format)                            devise/registrations#destroy

After:

$ rake routes | grep devise
           new_user_session GET    /users/sign_in(.:format)                    devise/sessions#new
               user_session POST   /users/sign_in(.:format)                    devise/sessions#create
       destroy_user_session DELETE /users/sign_out(.:format)                   devise/sessions#destroy
              user_password POST   /users/password(.:format)                   devise/passwords#create
          new_user_password GET    /users/password/new(.:format)               devise/passwords#new
         edit_user_password GET    /users/password/edit(.:format)              devise/passwords#edit
                            PATCH  /users/password(.:format)                   devise/passwords#update
                            PUT    /users/password(.:format)                   devise/passwords#update
          user_registration PUT    /users(.:format)                            devise/registrations#update
     edit_user_registration GET    /users/edit(.:format)                       devise/registrations#edit
               registration DELETE /users(.:format)                            devise/registrations#destroy
1
votes

Instead of searching for a hard solution. I used the below approaches.

  1. Delete the sign_up form from page (path devise/registrations/new.html.erb) and replace it with custom info.

  2. Redirect the incoming traffic to some other page. Like below in routes.rb

    get "/users/sign_up", to: redirect('/')

    post "/users/sign_up", to: redirect('/')

Make sure to write it before devise_for :users

0
votes

I had the same issue and I found it a bit bad practise to redirect users from the registration page. So my solution is basically is not using :registrable at all.

What I did was to create a similar page like edit user details which looked like:

<%= form_tag(update_user_update_path, method: :post) do %>  
    <br>
    <%= label_tag(:currPassword, 'Current password:') %> <%= password_field_tag(:currPassword) %> <br>
    <%= label_tag(:newPassword, 'New password:') %> <%= password_field_tag(:newPassword) %> <br>
    <%= label_tag(:newPasswordConfirm, 'Confirm new password:') %> <%= password_field_tag(:newPasswordConfirm) %> <br>
    <%= submit_tag('Update') %>
<% end %>

So this form submits into a new post end point that updates the password, which looks like:

  def update
    currPass = params['currPassword']
    newPass1 = params['newPassword']
    newPass2 = params['newPasswordConfirm']
    currentUserParams = Hash.new()
    currentUserParams[:current_password] = currPass
    currentUserParams[:password] = newPass1
    currentUserParams[:password_confirmation] = newPass2
    @result = current_user.update_with_password(currentUserParams)
  end

Later on you can use the @result in your view to tell the user whether the password is updated or not.

0
votes

By changing the routes there are a whole bunch of other problems that come with that. The easiest method I have found is to do the following.

ApplicationController < ActionController::Base
  before_action :dont_allow_user_self_registration

  private

  def dont_allow_user_self_registration
    if ['devise/registrations','devise_invitable/registrations'].include?(params[:controller]) && ['new','create'].include?(params[:action])
      redirect_to root_path
    end
  end
end
-7
votes

You could modify the devise gem itself. First, run this command to find the installed location of using:

gem which devise

Let's suppose the path is: /usr/local/lib/ruby/gems/1.9.1/gems/devise-1.4.2/lib/devise

Then go to

/usr/local/lib/ruby/gems/1.9.1/gems/devise-1.4.2/lib/devise/lib/devise/rails and edit routes.rb in that directory. There is a method called def devise_registration(mapping, controllers) which you can modify to get rid of the new action. You can also completely remove the mappings for devise_registration