Devise: When resending confirmation email, change flash message - ruby-on-rails

I'm setting up Devise such that users can log in and use the site without having confirmed their email address, similar to this question. But there are a couple of features on the site that users can't use unless they've confirmed.
OK, that's fine. I can check for current_user.confirmed?. If they're not confirmed, I can put a button on the page to have them request the confirmation be sent again.
The issue I'm having is that when they do this while logged in, the flash message they see on the result page is "You are already signed in." Which isn't ideal - I just want to put up the message that the confirmation was sent.
I'm starting down the path of trying to figure out which method of the Devise::ConfirmationController to override, and to what, but I'm hoping someone has done this already.

The reason the flash says "You are already signed in" is because the user is being redirected to new_session_path from the after_resending_confirmation_instructions_path_for method. I would override this method to check if they are logged in. If they are, then don't redirect to new_session_path, set your flash message and redirect to another page.
Override the confirmations controller by putting it in controllers/users/confirmations_controller.rb
class Users::ConfirmationsController < Devise::ConfirmationsController
protected
def after_resending_confirmation_instructions_path_for(resource_name)
if signed_in?
flash[:notice] = "New message here" #this is optional since devise already sets the flash message
root_path
else
new_session_path(resource_name)
end
end
end
Add your confirmationsController to routes->
devise_for :users, :controllers => {:confirmations => 'users/confirmations' }

I think it should look something like this:
module Devise
module ConfirmationsController
extend ActiveSupport::Concern
included do
alias_method_chain :show, :new_flash
end
def show_with_new_flash
# do some stuff
flash[:notice] = "New message goes here"
end
end
end

Could edit
config/locales/devise.en.yml to be more relavent at line:
failure:
already_authenticated: 'You are already signed in.'
or you could do this in your view where flash message has been added
<%=content_tag :div, msg, id: "flash_#{name}" unless msg.blank? or msg == "You are already signed in."%>

I'm using Devise 3.1.0, there's a different method for this scenario instead of after_resending_confirmation_instructions_path_for described in the top voted answer. I modified mine like so:
class Users::ConfirmationsController < Devise::ConfirmationsController
protected
def after_confirmation_path_for(resource_name, resource)
if signed_in?
set_flash_message(:notice, :confirmed)
root_path
elsif Devise.allow_insecure_sign_in_after_confirmation
after_sign_in_path_for(resource)
else
new_session_path(resource_name)
end
end
end

Related

BCrypt::Errors::InvalidHash in SessionsController#create when I try to sign in a user? (Ruby on Rails)

I get this error when I try to sign in a user, and can't figure out why. It's weird because when I run the following code I get the BCrypt Error, however when I change the find_by line (line 7) from can_email (candidate's email) to can_name (candidate's first name) I don't get the error at all, it just doesn't sign in the user presenting an "invalid password/email combination" error message on the webpage regardless if the combination is right or not. It's something to do with the password but I can't pin point it.
class SessionsController < ApplicationController
def new
end
def create
candidate = Candidate.find_by_can_email(params[:can_email])
if candidate && candidate.authenticate(params[:password]) **Error highlights this line**
session[:candidate_id] = candidate.id
redirect_to candidate
else
flash.now[:error]="Invalid email/password combination"
render 'new'
end
end
def destroy
if signed_in?
session[:candidate_id] = nil
else
flash[:notice] = "You need to log in first"
end
redirect_to login_path
end
end
Having the SessionController i am assuming you have a route as follows
# This is just a sample
post 'login' => "sessions#create" # gives login_path
Since there will be no session model i assume you have the form as follows
form_for(:session, url: login_path)
Now if you are collecting eg can_email and password you get
{session: {password: 'foo', can_email: 'foo#bar.com'}}
Accessing params[:session] returns the hash containing email and passowrd
So i think you should obtain them as follows
def create
candidate = Candidate.find_by(can_email: params[:session][:can_email])
if candidate && candidate.authenticate(params[:session][:password])
# login the user
else
# whatever
end
end
I got this error too, but in my case it was the result of myself having changed the encrypted_password value of my user in the database a while back and then forgetting about it.
This was easily fixed just by updating the password :)

Devise Custom Session Controller not Persisting Flash Messages

I decided to customize(override) Devise's session controller to implement my own sign in logic. I've left the sessions#new controller alone and simply call super in it.
def new
super
end
However, I have removed the call to super in my custom sessions#create method so that I can send an HTTP request to a server for authentication. Subsequently, I either sign in the user or return the user to the sessions#new path to try their login again.
def create
#member = Member.find_by_username(params[:username])
response = authenticate_with_api
if response[:authenticated]
if #member
#member.update(response.[:member])
sign_in #member
else
#member.create(response.[:member])
sign_in #member
end
else
flash[:alert] = "Incorrect username or password"
flash[:error] = "Incorrect username or password"
flash[:notice] = "Incorrect username or password" #setting all the messages out of frustration!
redirect_to new_member_session_path
end
The logic flow is working correctly, however the flash messages are not displaying. I have set a flash message just before calling super in the new method to ensure that the view was setup properly:
def new
flash[:notice] = "Test flash message"
super
end
This works. I can see the flash message appear in the template. The only explanation I can come up with is that somehow, there are multiple redirects occurring. However, when I look in the logs, I can only see the redirect I specified. I have also tried using keep to persist the flash message through multiple requests, but even that does not work:
flash.keep[:notice] = "Test flash message"
redirect_to new_member_session_path #does not work
I also tried using now as a last ditch effort:
flash.now[:notice] = "Test flash message"
No dice. Lastly, I have tried placing the flash message inline with the redirect:
redirect_to new_member_session_path, flash: { :notice => "Test flash message" } #also does not work
I am at a loss. Not sure why the flash messages are being lost. I am running Rails 4.1.2 and Devise 3.2.4
EDIT:
I have decided this is not the best way to customize authentication. Instead, I've created a custom Warden strategy and told devise about it:
#config/initializers/devise.rb
config.warden do |manager|
manager.intercept_401 = false
manager.default_strategies(:scope => :member).unshift :member_login
end
I put all my logic in that file:
Warden::Strategies.add(:member_login) do
def member_params
ActionController::Parameters.new(#response).permit(:first_name, :last_name, :role, :username, :parkplace_user_id)
end
def valid?
params[:member] && ( params[:member][:username] || params[:member][:password] )
end
def authenticate!
member = Member.find_by_username(params[:member][:username])
if authenticate_with_ppd_api
if member
member.update(member_params)
success!(member)
else
member = Member.create(member_params)
success!(member)
end
else
fail!("Incorrect username or password")
end
end
def authenticate_with_api
#response = HTTParty.post('https://path.to.server/admin/login', body: { username: params[:member][:username], password: params[:member][:password] } )
#response = JSON.parse(#response.body).first
#response["authenticated"]
end
end
Flash messages are appearing now, but I have other problems with sign in. I created a new question for that. Thanks!

Devise and handling the flash

I am using Devise 3.1.1 with rails 3 and I have this flash handling code in my layout:
<% flash.each do |name, msg| %>
<%= content_tag :section, msg, :id => "flash_#{name}", :class => "flash" %>
<% end %>
I sign into my app, flash says:
"Signed in successfully."
then sign out, then sign in incorrectly and flash says:
"Signed out successfully."
"Invalid email or password."
I think I understand why I am getting two messages, when signing in incorrectly there is no redirect, just a render.
Not sure how to fix it though.
I figured out the reason.
When you dig into Devise's source of SessionsController, you'll find #create method as follows:
# POST /resource/sign_in
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
respond_with resource, :location => after_sign_in_path_for(resource)
end
In above code, Devise sets flash message for success signed in here. That's what the message you saw as "Signed in successfully.". It uses the method set_flash_message which is just a wrapper of flash[key]= "something". The same is true for #destroy method which show you "Signed out successfully".
Note in above code, there is no code to set error message such as "Invalid password or email". So how comes this message you saw? It is set in Devise::FailureApp
def recall
env["PATH_INFO"] = attempted_path
flash.now[:alert] = i18n_message(:invalid)
self.response = recall_app(warden_options[:recall]).call(env)
end
Note here, the method is flash.now, not flash. The difference is flash.now will deliver flash message in current request, not next.
By default, adding values to the flash will make them available to the next request, but sometimes you may want to access those values in the same request. For example, if the create action fails to save a resource and you render the new template directly, that's not going to result in a new request, but you may still want to display a message using the flash. To do this, you can use flash.now in the same way you use the normal flash. http://guides.rubyonrails.org/action_controller_overview.html#the-flash
So the reason is revealed now.
You signed out. You hit SessionsController#destroy. Devise destroyed your session, brings you to /users/sign_in, render 'new template for your sign in again. The flash object contains the successful signed out message and you saw it.
Now you tried to sign in in same page. This time your form submit hit #create. If error, Devise will not redirect you to anywhere but render the same 'new' template again with the flash.now object which contains sign in error message.
In step 2, you last flash object is not removed because no new request rendered, but another new flash.now object is added. So you see two message.
Solution
Of course it's possible to override Devise to change this behaviour, but that's troublesome and unnecessary.
A more convenient and user-friendly solution is, do not land the user on sign in page after either signed in or signed out.
This is easily by setting store_location and override after_sign_in_path_for and after_signed_out_path_for in your application controller.
def store_location
disable_pattern = /\/users/
session[:previous_url] = request.fullpath unless request.fullpath =~ disable_pattern
end
def after_sign_in_path_for(resource)
session[:previous_url] || root_path
end
def after_sign_out_path_for(resource)
after_sign_in_path_for(resource)
end
By this setting the user will land on his previously browsed page either after signed in or signed out, and they will not see two flash messages in the question again.
The reason is, when the user signed out, he will be redirect to previous page and see the signed out message. When he want to sign in, he need to go to sign in page which is a new request, then the previous signed out flash will be removed.

NoMethodError in ConfirmationController

I have been trying to solve the following problem for a couple of days. Forgive me if this is a common problem as I am new to rails and probably couldn't query the right question/keyword in stackoverflow or google.
I am building a system where a user will get an invite via email, click on a unique link, be taken to a page where he/she can accept or decline the invitation. I am getting stuck at the part where the user accepts or declines the invitation.
I've built it around two controllers: an invitations controller and a confirmations controller.The invitations controller creates a record containing a name, an email, and a uniquely generated token. The controller then emails a link with the token to the defined email. The link points to the confirmations controller and passes the unique token from the invitation. However, when clicking on the link and accepting the invitation, I get the following error:
NoMethodError in ConfirmationController#confirm
undefined method `update_attribute' for nil:NilClass
Here is some of the code for solving this issue:
Confirmation_controller.rb
class ConfirmationController < ApplicationController
def new
#confirmation = Invitation.find_by_invite_token(params[:invite_token])
end
def confirm
if #confirmation.update_attribute(:accepted, true)
flash[:success] = "Invitation confirmed!"
redirect_to 'static_pages/home'
else
flash[:notice] = "Failed :("
redirect_to 'static_pages/home'
end
end
end
routes.rb
match '/confirmation/:invite_token', to: 'confirmation#new'
match '/confirmation/:invite_token/confirm', to: 'confirmation#confirm'
app/views/confirmation/new.html.erb
Click here to accept:
<%= link_to "Confirm", :controller => "confirmation", :action => "confirm" %>
You need to get your Invitation in the confirm method too.
If you want rails to raise an exception if no invitation was found
def confirm
#confirmation = Invitation.find_by_invite_token!(params[:invite_token])
#confirmation.update_...
end
No exception will be raise. You may want to check manually with a condition in the following case.
def confirm
#confirmation = Invitation.find_by_invite_token(params[:invite_token])
if #confirmation
#confirmation.update_...
else
# do something
end
end
You should find confirmation record before calling update_attribute on it, like you did it in new action:
#confirmation = Invitation.find_by_invite_token(params[:invite_token])
Or, to throw exception when the record is not found and to render 404 page to the user:
#ocnfirmation = Invitation.find_by_invite_token!(params[:invite_token])
The problem is that you never told the program what #confirmation is. What you should do is find it first then run the update. Note this is different from the different answers, just thought I would throw in some variety.
def confirm
# You're missing this line below. Basic search for the confirmation.
# Note too that you will have to pass in the parameter `invite_token` for it to work
# I'm also assuming invite_token is unique among each invitation
confirmation = Invitation.where(invite_token: params[:invite_token])
# Notice that I'm first checking to see if the confirmation record exists, then doing an update
if confirmation and confirmation.update_attribute(:accepted, true)
flash[:success] = "Invitation confirmed!"
redirect_to 'static_pages/home'
else
flash[:notice] = "Failed :("
redirect_to 'static_pages/home'
end
end

How to stop soft deleted user's login with Devise

I currently use Devise for user registration/authentication in a Rails project.
When a user wants to cancel their account, the user object is soft deleted in a way like the following.
How to "soft delete" user with Devise
My implmenetation has a small difference this way.
User model has an attribute 'deleted_flag'.
And, soft_delete method executes "update_attribtue(:deleted_flag, true)"
But, I have to implment sign_in action.
In my implmenetation is the following.
class SessionsController < Devise::SessionsController
def create
resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
if resource.deleted_flag
p "deleted account : " + resource.deleted_flag.to_s
sign_out(resource)
render :controller => :users, :action => :index
else
if is_navigational_format?
if resource.sign_in_count == 1
set_flash_message(:notice, :signed_in_first_time)
else
set_flash_message(:notice, :signed_in)
end
end
sign_in(resource_name, resource)
respond_with resource, :location => redirect_location(resource_name, resource)
end
end
end
I think this code has strange points.
If deleted user tries to sing in,
the system permit logging and make log out immediately.
And, the system cann't display flash[:alert] message...
I want to know two points.
How do I implement to prohibit deleted users to login?
How do I implement to display flash[:alert] when deleted user tries to login?
To stop a user that has been 'soft deleted', the best way is to overwrite the find_for_authentication class method on the user model. Such as:
Class User < ActiveRecord::Base
def self.find_for_authentication(conditions)
super(conditions.merge(:deleted_flag => false))
end
This will generate a invalid email or password flash message by devise (because it cannot find the user to authenticate)
As far as your second question though, you'll need some for of method in your controller to add a particular flash message. However, in my opinion you should treat users that are 'soft' deleted the same as if they didn't exist in the database at all. Thus if they tried to log in, they should just get an valid email or password message.
See my solution here: https://stackoverflow.com/a/24365051/556388
Basically you need to override the active_for_authentication? method on the devise model (User).
I haven't tried anything like that but it seems if you want to catch the user before authentication you'll either have to write a Devise authentication strategy or a before_filter to be run before authenticate_user!. Something like:
before_filter :no_deleted_users
def no_deleted_users
if User.find(params[:email]).deleted?
redirect_to root_path, :flash => { :error => "Your user was deleted. You cannot log in." }
end
end
Although it might be more complex to get the user than that. I haven't played with Devise pre-authentication.
The modern and correct answer is this:
class User < ApplicationRecord
def active_for_authentication?
super && !discarded? # or whatever...
end
end
See the documentation here.

Resources