Custom actions using devise - ruby-on-rails

Background:
The Rails 4 application I am working on has differing logic for API and web registrations, and part of this logic makes their integration extremely difficult. To that end, I'm attempting to separate the routes to deal with issues arising from inheritance, new reCAPTCHA gem, and new logic. (both actions call registrations#create after their respective logic.) I've solved most of the issues arising from separating these two; however, getting the routes working has proven difficult as well.
I'd greatly appreciate any help!
Desired Result:
I'm trying to define a route to a custom action using Devise, and prevent it from creating the default route as well. I've gotten one of them working, but not the second. Here's the excerpt from my routes.rb:
Registry::Application.routes.draw do
devise_for :user,
controllers: {
passwords: 'users/passwords',
sessions: 'users/sessions',
registrations: 'users/registrations'
}
devise_scope :user do
post 'users', to: 'users/registrations#custom_one'
end
# ...
namespace :api do
namespace :v1 do
# ...
devise_scope :user do
post 'users', to: 'registrations#custom_two'
end
end
end
end
Issues:
The issue is that this code generates two nearly-identical routes. Excerpt from rake routes:
user_registration POST /users(.:format) users/registrations#create
users POST /users(.:format) users/registrations#custom_one
api_v1_users POST /api/v1/users(.:format) api/v1/registrations#custom_two
I also want the custom route to have the correct prefix/route name (user_registration), though I've been unable to do this.
I've found plenty of documentation on custom names for Devise routes, but not for custom actions. Especially not when using devise_for.
To summarize:
I need to disable the default users/registrations#create route
and specify a route to a custom action (users/registrations#custom_one)
with the correct prefix/name (user_registration)
hopefully as elegantly as possible, as I would rather avoid specifying each route independently.

You can achieve this by using the :skip option to devise_for:
devise_for :users, :skip => [:registrations] do
get "/admin" => "devise/registrations#new", :as => :new_user_registration
post "/admin" => "devise/registrations#create", :as => :user_registration
end

Related

add your own custom route to devise

i've seen a bunch of posts on how to rename already-declared routes in devise. I want to expand devise to have my own route check for and idle session. I am implementing a simple js check every 1 minute that I want to hit 'check_active' in the devise sessions controller. I tried this but no luck:
devise_scope :sessions do
get 'check_active'
end
Is there way to expand devise with a custom route (not rename an already-existing one) ?
UPDATE - almost there, i did this
# already had this in routes
devise_for :users, :controllers =>
{ registrations: 'registrations',
confirmations: 'confirmations',
sessions: 'sessions',
passwords: 'passwords',
omniauth_callbacks: "omniauth_callbacks"}
# added this
devise_scope :sessions do
get '/check_active' => 'sessions#check_active'
end
I have a js firing, i have it get '/check_active' as rake routes shows this:
check_active GET /check_active(.:format)
But when it fires, the controller 404s with
AbstractController::ActionNotFound (Could not find devise mapping for path "/check_active".
This may happen for two reasons:
1) You forgot to wrap your route inside the scope block. For example:
devise_scope :user do
get "/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]
):
If you are overwriting Devise's default controllers, it is not any different from any other controller to add your own route.
After you create your devise controllers to overwrite, do the following:
Under sessions_controller declare a method
# app/controllers/devise/sessions_controller.rb
def check_active
# do what you want to do
end
And in your router:
# config/routes.rb
devise_scope :sessions do
get 'check_active', to: "devise/sessions#check_active"
end
I was trying the same thing and realized that the scope should be for user and not sessions, also ensure that it has to be singular.
devise_scope :user do
get '/check_active' => 'sessions#check_active'
end
Edit: Adding link to help docs for better understanding

RoR Links Breaking in Production but not Dev

I have a few links that are breaking. One, my logout, which I am using the delete method with, returns this error:
[Devise] Could not find devise mapping for path "/users/sign_out". This may happen for two reasons: 1) You forgot to wrap your route inside the scope block. For example: devise_scope :user do get "/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]
I have this in my routes: get '/users/sign_out', to: 'devise/sessions#destroy'
And my devise routes look like this:
devise_for :users, controllers: { sessions: 'sessions',
registrations: 'registrations',
invitations: 'invitations' }
Why is this breaking?
I have this in my routes: get '/users/sign_out', to: 'devise/sessions#destroy'
if you want to allow the user to sign out via GET method all you have to do is go to app/config/initializers/devise.rb and uncomment the line config.sign_out_via = :get
OR Try this
devise_scope :user do
get '/users/sign_out', to: 'devise/sessions#destroy'
end

Logout routing using OAuth and Rails

I'm using OAuth 2 gem to authenticate via google and facebook.
I need to do logout from google and facebook when I logout from my application. In OA documentation said to:
devise_scope :user do
delete 'sign_out', to: 'devise/sessions#destroy', as: :destroy_user_session
end
Add this to routes.rb. I did it so, my routs rb now looks like:
Rails.application.routes.draw do
devise_for :users, controllers: { omniauth_callbacks: 'callbacks' }
devise_scope :user do
delete 'sign_out', to: 'devise/sessions#destroy', as: :destroy_user_session
end
When I add this line, i got an error when i try to rails s my application:
/Users/damirik/.rvm/gems/ruby-2.3.0/gems/actionpack-5.0.0.1/lib/action_dispatch/routing/route_set.rb:507:in add_route': Invalid route name, already in use: 'destroy_user_session' (ArgumentError)
You may have defined two routes with the same name using the:as` option, or you may be overriding a route already defined by a resource with the same naming.
I really dont understand how to fix it. Help please
Looking at the devise_for method documentation, I can see that it already adds the exact delete 'sign_out' route, which makes it redundant.
This should be enough to make your code work.
Rails.application.routes.draw do
devise_for :users, controllers: { omniauth_callbacks: 'callbacks' }
end

Rails routing to multiple route

I'm using devise for rails.
I have the following route for devise.
devise_for :user
Which routes to 'user/sign_in' and several other.
So I want to change this route to: get 'login'. Is this possible?
I tried doing
match 'login', to: 'user/sign_in', via: :get
Which did not work as well, what am I doing wrong, and what does the above code do?
To use /login for sign_in add the following to your config/routes.rb:
devise_scope :user do
get 'login', to: 'devise/sessions#new'
end
This'll work:
devise_for :user, :path => 'login'
You might need :users and not :user, FYI.

Devise routing: is there a way to remove a route from Rails.application.routes?

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.
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.
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.
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.
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.

Resources