I am trying to customise my errors messages in Devise for the password renewal when the user has forgotten their password. I did all translations and customisations right for registrations but for passwords I am a bit struggling.
In my passwords/new view I have the usual helper call :
<%= devise_error_messages! %>
But the translation that is fetched from the locale file is this one :
translation missing: fr.errors.messages.not_saved
This is a bit strange as this message is the same used for registrations and is set with "Your account has not been saved" at the moment in my app. But in this case this is not about saving an account but rather confirming a renewal email has been sent.
(of course, on top of this, I have an error message related to the email field which shows correctly)
Is it tweakable ?
EDIT EDIT
Ok it seems it is coming from this bit of code from Devise error helper :
sentence = I18n.t("errors.messages.not_saved",
:count => resource.errors.count,
:resource => resource.class.model_name.human.downcase)
https://github.com/plataformatec/devise/blob/master/app/helpers/devise_helper.rb
Any idea I could tweak this bit of code so that it is different for each of Devise subcontrollers inside the Devise namespace ?
Related
I have a Rails 4 application set up to use Devise, and I'm running a problem with password resets. I have the mailer set up, and the password reset email sends fine. The link provided has the correct reset_password_token assigned to it, which I checked with that database. However, when I submit the form with correctly formatted passwords, it gives an error saying that the reset token is invalid.
However, the exact same code works fine locally through rails s. The email sends, and I can actually reset the password. The code I use is just the standard Devise code, I haven't overridden any of it.
Perhaps it's something with Apache? I'm not too familiar with it. Does anyone have any ideas?
Check the code in app/views/devise/mailer/reset_password_instructions.html.erb
The link should be generated with:
edit_password_url(#resource, :reset_password_token => #token)
If your view still uses this code, that will be the cause of the issue:
edit_password_url(#resource, :reset_password_token => #resource.password_reset_token)
Devise started storing hashes of the token, so the email needs to create the link using the real token (#token) rather than the hashed value stored in the database.
This change occurred in Devise in 143794d701
In addition to doctororange's fix, if you're overwriting resource.find_first_by_auth_conditions, you need to account for the case where warden_conditions contains a reset_password_token instead of an email or username.
EDIT: To elaborate:
Devise adds functionality to your model when you say 'devise :registerable, :trackable, ...'.
In your User model (or Admin, etc), you can overwrite the Devise method named find_first_by_auth_conditions. This special method is used by the Devise logic to locate the record that is attempting to be logged in to. Devise passes in some info in a parameter called warden_conditions. This will contain an email, a user-name, or a reset_password_token, or anything else you add to your devise log-in form (such as an account-id).
For example, you might have something that looks like this:
(app/models/user.rb)
class User
...
def self.find_first_by_auth_conditions warden_conditions
conditions = warden_conditions.dup
if (email = conditions.delete(:email)).present?
where(email: email.downcase).first
end
end
end
However, The above code will break the password-reset functionality, because devise is using a token to locate the record. The user doesn't enter an email, they enter the token via a query-string in the URL, which gets passed to this method to try and find the record.
Therefore, when you overwrite this special method you need to make it more robust to account for the password-reset case:
(app/models/user.rb)
class User
...
def self.find_first_by_auth_conditions warden_conditions
conditions = warden_conditions.dup
if (email = conditions.delete(:email)).present?
where(email: email.downcase).first
elsif conditions.has_key?(:reset_password_token)
where(reset_password_token: conditions[:reset_password_token]).first
end
end
end
If you are taking the URL from a log, it can appear like this:
web_1 | <p><a href=3D"http://localhost:3000/admin/password/edit?reset_password_to=
web_1 | ken=3DJ5Z5g6QNVQb3ZXkiKjTx">Change password</a></p>
In this case, using 3DJ5Z5g6QNVQb3ZXkiKjTx as the token will not work because =3D is really an = character encoded.
In this case, you need to use J5Z5g6QNVQb3ZXkiKjTx (with 3D removed)
Although the accepted answer is correct, wanted to explain why this is happening so you can use it in some other cases as well.
If you take a look at the method which is generating the password reset token:
def set_reset_password_token
raw, enc = Devise.token_generator.generate(self.class, :reset_password_token)
self.reset_password_token = enc
self.reset_password_sent_at = Time.now.utc
self.save(validate: false)
raw
end
You will see that the raw is being returned, and the enc is being saved in the database. If you are using the value from the database - enc to put into a password_reset_token in a hidden field of your form, then it will always say Token invalid as that is encrypted token. The one which you should use is the raw token.
This was done because in case some admin (or a hacker) can access the database, the admin could easily reset anyone's password by just using encrypted token, which is tried to be avoided.
Some information about this and some other changes in Devise can be found in the devise's change-log blog post or in the devise's issue discussion
It may also be worth noting (in addition to #doctororange's post ablve) the following if you are using a custom confirmation mailer view.
The link in the view has also changed here. This is the NEW link code:
<p><%= link_to 'Confirm my account', confirmation_url(#resource, confirmation_token: #token) %></p>
This is the OLD link code:
<p><%= link_to 'Confirm my account', user_confirmation_url(#resource, :confirmation_token => #resource.confirmation_token) %></p>
I successfully installed devise in my rails app and the user registration works perfect. I've also set it up such that users can confirm their accounts by sending an email. This works fine when the user signs up for the first time (They get the confirmation message with a confirmation link).
However, when the user changes his/her email address from exampleuser#gmail.com to exampleuser#hotmail.com, the mail gets delivered to exampleuser#gmail.com and the confirmation link has no confirmation token it looks like
http://{HOST}/users/confirmation
Instead of the normal
http://{HOST}/users/confirmation?confirmation_token=TOKEN_HERE
When I resave the new email exampleuser#hotmail.com it now gets delivered to this address but the confirmation token is invalid as it does not match the one in the db.
I don't know what went wrong.
confirmation_instructions.html.erb
<p>Welcome <%= #resource.unconfirmed_email? ? #resource.unconfirmed_email : #resource.email %>!</p>
<p>You can confirm your account email through the link below:</p>
<p><%= link_to 'Confirm my account', confirmation_url(#resource, :confirmation_token => #resource.confirmation_token) %></p>
I also have config.reconfirmable = true in devise initializer
Am also using sideqik for delayed jobs. The emails are all processed by sideqik
Any help?
Thanks
I realise it is sometime since you posted, but I have run into this same issue and resolved it.
In my case I had upgraded devise from v2.0.4 to v2.2.6 - which appears to be the newest version that supports Rails 3.
I'd skimmed the change log which mentions this change in v2.2.0:
All mailer methods now expect a second argument with delivery options.
Unfortunately it doesn't say what that second argument is specifically used for; however it turns out is literally what you'd expect... The options hash that is passed to your Mailers mail method.
I'm guessing if you're like me, your previous Devise::Mailer had a line something like the following:
def confirmation_instructions(record, opts)
mail :to => record.email, :template_name => 'confirmation_instructions'
end
The problem is that email is the previous confirmed email address, not the new unconfirmed one. Hence you probably want to call mail with the options hash you were passed, which will now contain the unconfirmed_email in its :to key, e.g.
{:to => "unconfirmed#email.com"}
So simply change your mail calls to something more like:
def confirmation_instructions(record, opts)
mail opts.merge(:template_name => 'confirmation_instructions')
end
I am using rails with devise for signing up. Also I added an invite code, so not everybody can sign up.
The invite code gets transmitted via query-string like "/users/sign_up?invite_code=wajdpapojapsd" and gets added to a hidden field of the sign-up form with "f.hidden_field :invite_code, :value => params[:invite_code]".
This works pretty well. The only problem is that if the sign up doesn't get validated and rejected, devise redirects to "/users" and loses the query string with the invite_code in it.
As the email stays in the sign up form after the failed attempt, I believe that this should also work for the invite code. As a worst case solution redirecting :back after a failed sign up and losing the email, but keeping the invite code, would be better than the way it works now.
EDIT: By now I ve set up a registration controller for devise, but don't have any idea how to get the desired behavior.
Any help on how to keep the query string or just the invite code would be awesome.
I found a working solution by now.
I used jstr's answer to set up the controller for devise and added the "session" line.
class MyRegistrationsController < Devise::RegistrationsController
prepend_view_path "app/views/devise"
def create
super
session[:invite_code] = resource.invite_code
end
def update
super
end
end
Afterwards I added following to the devise/registrations/new.html.erb
<% if params[:invite_code]
#invite_code = params[:invite_code]
else
#invite_code = session[:invite_code]
end %>
And changed the hidden_field to
<%= f.hidden_field :invite_code, :value => #invite_code %>
You might need to make your own subclassed Devise controllers to get this to work.
This answer has a good description of how to to this.
The basics:
Install the Devise views if you haven't already with rails generate devise:views
Create a subclassed Devise::RegistrationsController
Update your Devise routes declaration to get Devise to use your subclassed controller
#James Lever 's solution has one disadvantage. invite_code remains in session. In some solutions it can cause problems. The alternative soultion would be:
def create
# set invite code to session:
session[:invite_code] = resource.invite_code
# here the form is rendered and invite code from session would be used:
super
# delete invite code from session:
session.delete('invite_code')
end
why don't use instance variable such
#invite_code = params[:invite_code]
Then when a sign up didn't pass validation, you can use that variable in the view which will be displayed for failed sign up.
I mean before, it's redirected, you should keep the invite code parameters with instance variable. sorry if i'm mistaken.
After adding this line to the devise/registrations/new.html.haml file (view):
%div
= f.label :account_type
%br/
= f.select(:account_type, [["Usertype1","usertype1"],["Usertype2","usertype2"]], {:selected => nil, :prompt => 'Pick One'})
I get the following error after clicking on the confirmation link in the confirmation e-mail:
ActionController::ActionControllerError in Devise::ConfirmationsController#show
Cannot redirect to nil!
It only happens if I select Usertype2 upon registration. I also made the account_type attr_accessible. The account_type seems to be getting assigned (I checked in the rails console) and the development logs don't have any further information.
I think this is the line in the devise confirmations controller where the error is occurring:
respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) }
Also, the account is being confirmed, but when trying to log in, I get the following:
undefined method `user_url' for #<Devise::SessionsController:0x9d1659c>
which is in the create action of the devise sessions controller.
Any help would be appreciated. Thanks!
John
The two errors you mentioned are one and the same, essentially when you are signing-in successfully Devise is unable to detect where to redirect you. This problem often occurs when you have multiple models or try to setup a custom redirect (post sign in) in the routes file.
Try to define the path in the ApplicationController.
Devise docs say that the after_sign_in_path_for method takes the actual model object (ie: the model being signed-in)
def after_sign_in_path_for(resource)
signed_in_path_for_user
end
Note: You can do the same for several Devise paths / variables (override them). Also for more information on doing this for multiple Devise models in the same app, you can look at this question and it's answer.
I'm trying to integrate Devise into my application. I need implement login form at top of the page (I've implemented this form into layout page) and I've implemented registration which contains registration form.
But it shows validation errors for both form when I tried submit incorrect registration data.
Without more information, it's hard to guess what the problem is. I've found the Wiki pages to be really helpful (and increasingly so), though you may have already looked them over:
Devise Wiki Pages
Two pages that might be relevant to your needs:
Display a custom sign_in form anywhere in your app
Create custom layouts
Hope this helps!
-- ff
The problem of seeing the validation errors for both forms stems from the 2 things. First, devise forms use a generic 'resource' helper. This creates a User object, and that same user objet gets used for both the sign up and the sign in form. Second, devise errors are typically displayed using the 'devise_error_messages!' helper which uses that same shared resource.
To have sign in and sign up on the same page you need to create different user objects for each form, and a new way of displaying the error messages.
First off, you'll need to create your own registration controller (in app/controllers/users/)
class Users::RegistrationsController < Devise::RegistrationsController
include DevisePermittedParameters
protected
def build_resource(hash=nil)
super
# Create an instance var to use just for the sign up form
#sign_up_user = self.resource
end
end
And update your routes file accordingly
devise_for :users, controllers: {
registrations: 'users/registrations'
}
Next you'll need your own error messages and resource helpers. Create a new helper like devise_single_page_helper.rb and add the following:
module DeviseSinglePageHelper
def devise_error_messages_for_same_page(given_resource)
return "" if given_resource.errors.empty?
messages = given_resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
sentence = I18n.t("errors.messages.not_saved",
count: given_resource.errors.count,
resource: given_resource.class.model_name.human.downcase)
html = <<-HTML
<div id="error_explanation">
<h2>#{sentence}</h2>
<ul>#{messages}</ul>
</div>
HTML
html.html_safe
end
def sign_up_user
#sign_up_user ||= User.new(username: 'su')
end
def sign_in_user
#sign_in_user ||= User.new(username: 'si')
end
end
Finally, in your views, update your forms like so:
-# The sign up form
= simple_form_for(sign_up_user, url: registration_path(resource_name)) do |f|
-#...
= devise_error_messages_for_same_page(sign_up_user)
-# The sign in form
= simple_form_for(sign_in_user, url: sessions_path(resource_name)) do |f|
#...
= devise_error_messages_for_same_page(sign_in_user)
All of this together gives you 2 different objects - 1 for sign up and 1 for sign in. This will prevent the error messages from one showing in the other. Please note that recommend putting both forms on your sign in page (and perhaps having the default sign up page redirect to the sign in page) because by default a failed sign in attempt will redirect to the sign in page.
You should have two forms on the page — one for signing up and one for registering. If you want a single form and multiple potential actions you are going to need a couple buttons that get handled client side and change the form's action & method to the appropriate route depending you want to create a user or a session.
If you think you did this already, the problem almost certainly lies in your code. If you were to share it with us we could perhaps point out something you may have missed.