About overriding Devise or Clearance controllers - ruby-on-rails

Since authentication gems such as Devise or Clearance uses their own built in controllers, I have a few questions when overriding them. Everytime I've tried to override it, something seems to go wrong and I don't know what it is exactly that caused the error.
For example, to create a new user controller with Devise I understand I have to create a controller like this:
# app/controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
All good. Now let's say I want to add certain things to the def new parts of the controller.
1.) To leave the def create part of the controller alone, I have to put in
def create
super
end
Is that right? Or do I even need to reference it in the new controller at all?
2.) If I type
def new
#my custom code here
end
Does that replace the def new part of the original Devise controller, or does it just add to it? Meaning to say, do I also have to put in
resource = build_resource({})
respond_with_navigational(resource){ render_with_scope :new }
which is the default behavior for the def new part of the Devise registrations_controller.rb?
3.) There's a filter in Devise that prevents you from signing up if you're logged in, but I need to override this. How do I do this? I'm guessing it has something to do with the prepend_before_filter :require_no_authentication, :only => [ :new, :create, :cancel ] part of registrations_controller.rb, but I'm not too sure.
The same questions apply to Clearance, although with slightly different routes and files.. (I'm asking for Clearance too because I haven't decided which authentication gem to use yet -- Clearance appeals to me because of the lightweight code, but Devise has additional features that I would need too).

1) That's correct.
2) If you want to call the parent's logic, you can call super at the appropriate point in your sub-class logic.
3) If you override the RegistrationsController, you can call skip_before_filter :require_no_authentication. This should skip it entirely, so if you need the before filter in certain conditions, you would have to add another before_filter.

Related

How to implement multiple Devise signin pathways in a single Rails app

I'd appreciate some advise on best practice when implementing multiple sign in pathways within a single Rails app.
I'm using Devise, and the usual controllers/ routes are working fine to handle creating/destroying sessions when users visit the app in a browser.
I'm now need to add JS/ iframe plugins to embed limited app functions in external sites. These plugins need to be able to handle sign in, and sign in needs to behave slightly differently than usual - namely using different views for the signin form, and different after_sign_in_path and signed_in_root_path.
Is there a conventional way to handle this behavior?
I considered adding a custom controller that inherits from Devise and sets devise_mapping
#config/routes.rb
namespace :plugins do
resources :sessions, only: [:new, :create, :destroy]
end
#controllers/plugins/sessions_controller.rb
class Plugins::SessionsController < Devise::SessionsController
def new
#stuff for my form
end
def devise_mapping
Devise.mappings[:user]
end
def after_signin_path
my_path
end
end
This works OK but seemed like overkill and code duplication. I also could not figure out how to make signed_in_root_path aware of whether the app is being used via an embedded plugin or directly.
I could just embed a signin form in a template, pointing to the usual Devise session controller, and pass a plugin param on form submit.
#application_controller.rb
def after_sign_in_path_for(resource_or_scope, params)
if params,has_key?(:plugin)
#plugin_after_sign_in_path
else
#usual_after_sign_in_path
end
end
But this does not seem very clean, requires modifying devise sessions#create to pass the params to after_sign_in_path_for, and still does not solve the signed_in_root issue.
This situation also has implications elsewhere in the app. For example
rescue_from CanCan::AccessDenied do |exception|
unless user_signed_in?
if #we_are_in_the_plugin
redirect_to new_plugins_session_url, alert: "You must log in to continue."
else
redirect_to new_user_session_url, alert: "You must log in to continue."
end
end
end
This has the potential to become almost like maintaining two apps in one, and I can see things becoming complicated with the wrong approach.
There appear to be several ways to achieve multiple signin pathways, and this seems like it should be a reasonably common scenario. So my question is which is the "Rails" way to achieve this - i.e., will be clear to other developers, easily maintainable, and cause less long-term headaches?
The easiest way to do this would be to separate the flows and use a custom controller like you have started to do. You could do modify the default controller but it would so get messy when you want to have different sign up flows.
Having it namespaced under plugins/ makes it super easy to understand why the extra controller is there and will make it a lot easier to test. You shouldn't really have much duplicate code either as you'll be inheriting from the devise controller anyway and just adding a private method after_sign_in_path etc. It will also make it easier to render errors differently for the two situations.
EDIT: (additional code)
# app/controllers/plugins/sessions_controller.rb
module Plugins
class SessionsController < Devise::SessionsController
def create
begin
super
rescue CanCan::AccessDenied => e
**handle e here**
end
end
private
def after_sign_in_path_for(resource)
a_different_signed_in_path
end
end
end
# config/routes.rb
namespace :plugins do
resources :sessions, only: [:new, :create, :destroy]
end

Devise's current_user nil in ApplicationController but not in a different controller (using Simple Token Authentication)

I have a Rails 3.2.22 app running in production for +1 year which uses Devise to authenticate users.
I'm trying to implement token authentication, so I can send transactional e-mails with URL params that can log in the user automatically, using a Gem named Simple Token Authentication https://github.com/gonzalo-bulnes/simple_token_authentication
After following all the instructions, I replaced before_filter :authenticate_user! in my controllers with acts_as_token_authentication_handler_for User.
The gem has integration with and a default fallback to Devise, so devise doesn't need to be called in the controllers anymore; if the token is missing from the params (or wrong), Devise will take over.
In my tests, if I add this line to ApplicationController, everything works fine and I can log in users using the authentication_token= secret the gem generates.
But I don't need auth for ApplicationController, I need it for other controllers (like DashboardController), url being /dashboard
If I put acts_as_token_authentication_handler_for User in that controller (replacing Devise's call), I get the most bizarre of situations.
Using binding.pry, I can confirm that current_user is correctly set during the loading of the template.
But there comes a point in the template where it uses #last_emails, which is defined inside a method in ApplicationController.
Using binding.pry, I can confirm current_user is nil there.
This is the code:
class DashboardController < ApplicationController
layout 'material'
acts_as_token_authentication_handler_for User
And in ApplicationController:
class ApplicationController < ActionController::Base
layout 'omega'
before_filter :populate_last_contacts_for_menu
private
def populate_last_contacts_for_menu
if current_user
#last_contacts = Contact.where("user_id" => current_user.id).where('blocked != ? or blocked is null', true).last(10).reverse
end
end
Funny thing is: using binding.pry, like I said, I can check that current_user is defined in the template (which means sign_in was a success). It even is defined in the better errors console. But, if I go to homepage, I see that user is not logged in ...
I've looked all over the web for this: read all the issues inside the Gem's github and all posts in SO about current_user being nil, but no light at all.
My devise_for :users is not inside any scope in routes.rb and, as I said, I have many calls to current_user all over the app and this is the first time I have issues with Devise.
When you call the acts_as_token_authentication_handler_for directive in the DashboardController it declares some before_filters for the controller to authenticate a user.
But the problem is that when you inherit rails controllers, at first, filters of a parent controller are executed, then filters of a child controller.
The parent controller is ApplicationController. At the moment when it's populate_last_contacts_for_menu filter is called, the user is not authentacated, because the authenticating filters given by the acts_as_token_authentication_handler_for directive have not called yet, they are declared in the child controller.
Possible solutions:
1) Try to append the populate_last_contacts_for_menu filter:
append_before_filter :populate_last_contacts_for_menu
I am not sure it will work in your case, but you can try and find it out.
2) Call the acts_as_token_authentication_handler_for directive in the ApplicationControoler and somehow skip it for the controllers that don't need it. (I don't like this way, but it may help if the first one will not work. )
3) Move the populate_last_contacts_for_menu filter logic into helpers. I think it is the best solution. This logic doesn't belong to a controller. When requests are not 'get', this filter executes for nothing, because you don't need to render views in that case.
module ApplicationHelper
def last_contacts
#last_contacts ||= if signed_in?
Contact.where("user_id" => current_user.id).where('blocked != ? or blocked is null', true).last(10).reverse
else
[]
end
end
...
end
# View:
<% if last_contacts.present? %>
....
<% end %>

Issues with overriding Devise controllers

I'm trying to override a Devise controller to have some minor changes, for example adding a flash message when requesting a confirmation email for an unregistered email address.
I tried to override Devise::ConfirmationsController1 this way:
# app/controllers/confirmations_controller.rb
class ConfirmationsController < Devise::ConfirmationsController
include Devise::Controllers::InternalHelpers # tried to add this, no success
def create
self.resource = resource_class.send_confirmation_instructions(params[resource_name])
if successfully_sent?(resource)
respond_with({}, :location => after_resending_confirmation_instructions_path_for(resource_name))
else
respond_with(resource)
end
end
end
I think I added the route correctly:
devise_for :users, :controllers => { :confirmations => "confirmations" }
My controller method gets called, however it raises this exception:
NoMethodError in ConfirmationsController#create
undefined method `successfully_sent?' for #<ConfirmationsController:0x007fa49e229030>
In my overridden controller, I just copied the code of Devise:: ConfirmationsController#create, which itself calls successfully_sent?(resource)
The successfully_sent? method is defined in InternalHelpers 2, this is why I tried to add include Devise::Controllers::InternalHelpers
This is not the first time I try to override a Devise controller, and this is not the first time I fail. I always managed to get a workaround, but I'd like to understand what I'm missing... Thanks in advance for your help!
[EDIT]
Devise is in version 1.4.9
Rails is 3.0.10
Well, thanks to the help of Kyle in my question's comments, I will write the correct answer to this beginner's mistake.
Instead of looking at my own version of Devise to override the controller, I was simply looking at Devise's Github repository. Since the controller I was trying to override had changes between my version and the last committed one, the helper method I was trying to use was simply not defined in my version...
As indicated by Kyle, you can use bundle open devise to look at the code of the gem you're actually using, or you can look at its version number with gem list devise and find the code for this release on Github (for Devise they set the tags for each release so that you can browse the code for release 1.4.9 by selecting the corresponding tag).
Doing this, I would have overrode my controller's create method with the following code instead:
def create
self.resource = resource_class.send_confirmation_instructions(params[resource_name])
if successful_and_sane?(resource)
set_flash_message(:notice, :send_instructions) if is_navigational_format?
respond_with({}, :location => after_resending_confirmation_instructions_path_for(resource_name))
else
respond_with_navigational(resource){ render_with_scope :new }
end
end
which uses successful_and_sane? and not successfully_sent? ...
To conclude this answer, there may be a better way of adding a flash message to this method than overriding it. jarrad is advising to use around_filter, but I can't get it to work yet and I'm unsure I can still change the rendered view after I yielded it from the filter method... Comments welcomed!
This may not help you understand why overriding the Devise controller is failing, but it will keep your code DRY in that you do not need to copy the code from Devise::ConfirmationsController#crete
So, if you just want to set a flash message, look at Filters for ActionControllers
Specifically, look at the Around Filter:
class ConfirmationsController < Devise::ConfirmationsController
around_filter :my_custom_stuff, :only => :create
private
def my_custom_stuff
# do your thing here...
end
end

Namespaced ApplicationController in Rails

I have my regular ApplicationController class & I have a Admin::ApplicationController class. The problem is that Admin::ApplicationController doesn't seem to be getting loaded or executed or anything. Am I not allowed to have a namespaced application controller? The reasoning for wanting to have it is so that I can check if a user is an admin w/ CanCan & redirect them out if they're not.
Call this controller Admin::BaseController, as it is more to act as the base of the namespace than to do anything for the appilcation. For it to do what you want to do, you will need to make all admin namespaced controllers inherit from this controller.
The only times I've seen namespacing like that is when the controller is nested in a subfolder. So Admin::ApplicationController would expect to be in controllers/admin/application_controller.rb
One possible solution:
If you want everything except your home page to kick them out, simply set a before_filter on your application controller with an exception for home/index like this:
ApplicationController.rb
before_filter :authorize_admin
def authorize_admin
//dostuff
end
HomeController.rb
skip_before_filter :authorize_admin, :only => ['index']
Where index is your action that you want to skip. Leave off the only to skip the filter for the whole controller.

Custom Devise controller

I would like to customize my registrations controller for Devise in Rails. I understand that you must create a controller like this:
class AccountsController < Devise::SessionsController
def create
super
end
end
Well, that's all very good. But then let's say I want to fully control what happens in my #create action. How do I do that? How do I manually create a model and pass it all the params? Would Account.create(params[:account]) handle it smoothly? Is there some internal stuff going on I should know about or is my only option to call #super inside the action?
As long as you fulfil your required fields you can call Account.create in your example, I'm pretty sure the default Devise required fields are login, password and password_confirmation
We do this in a CRUD screen for creating devise users,
#admin = Admin.new(params[:admin])
if #admin.save
redirect_to admin_admins_path, :notice => 'New Administrator has been added'
else
render :action => "new"
end
and you don't want to extend the Devise session controller, a normal controller extending ApplicationController is fine or you can extend Devise::RegistrationsController and overwrite the methods you want to tweak in a registrations_controller.rb file
You can also have a look at the source on Github, if you want to be sure you're overriding things properly, and be sure you're not missing any processing...
https://github.com/plataformatec/devise/tree/master/app/controllers

Resources