I'm wondering if there is a manner of calling the 'forgot password' procedure without forcing my user to log out
The case I'm running into is:
a user logs in with Facebook, a fake password is generated for them
the user then wants to change their email/name/password, or just use non-facebook login
since devise requires a password to change these fields, as it should, the user is unable to modify them
I had thought about just not forcing the password to be set but that doesn't make sense to security wise so instead I just display the fields as text and notify the user to follow the 'forgot password' procedure in order to set a password and then they can change the fields
The issue then is that I cannot simply link to this from the user profile since devise will tell the user that they can't do this while already logged in.
So is there a manner of overriding the forgot password or /users/password/edit method so that a logged-in user can perform this action as well?
The reason that you cannot reset password is because the devise tries to authenticate the user with the current session and when succeeded you are automatically redirected to whatever path it is supposed to go to. What you need is to override the edit and update action of passwords controller to make it skip this step.
Here's the code. In your passwords controller add the following codes (you can ask devise to generate the controllers for you, or you can just create the following controller). The override for update is necessary because otherwise a logged in user will be automatically signout after your reset password. (Or if you want it to be like that you can get rid of the #update override)
class PasswordsController < Devise::PasswordsController
# here we need to skip the automatic authentication based on current session for the following two actions
# edit: shows the reset password form. need to skip, otherwise it will go directly to root
# update: updates the password, need to skip otherwise it won't even reset if already logged in
skip_before_filter :require_no_authentication, :only => [:edit, :update]
# we need to override the update, too.
# After a password is reset, all outstanding sessions are gone.
# When already logged in, sign_in is a no op, so the session will expire, too.
# The solution is to logout and then re-login which will make the session right.
def update
super
if resource.errors.empty?
sign_out(resource_name)
sign_in(resource_name, resource)
end
end
end
The routes are like the following
# config/routes.rb
devise_for :users, :controllers => {:passwords => 'passwords'}
You can use the #user.send_reset_password_instructions to generate the password reset token and send the email. If you just call the mailer directly, a password reset token won't be generated to authenticate the reset.
My complete solution here, because I then also learned that the user would have to log out after clicking the link in the email, was to add an some additional UserController actions for actually editing the password as well as saving it. This is not an ideal solution and cold probably be done in a better manner but it works for me.
users controller; added methods to do the reset
before_filter :authenticate_user!, :except => [:do_reset_password, :reset_password_edit]
def reset_password
id = params[:id]
if id.nil?
id = current_user.id
end
if (!user_signed_in? || current_user.id.to_s != id.to_s)
flash[:alert] = "You don't have that right."
redirect_to '/home'
return
end
#user = User.find(id)
#user.send_reset_password_instructions
respond_to do |format|
format.html { redirect_to '/users/edit', notice: 'You will receive an email with instructions about how to reset your password in a few minutes.' }
end
end
def do_reset_password
id = params[:id]
if id.nil? && !current_user.nil?
id = current_user.id
end
if id.nil?
#user = User.where(:reset_password_token => params[:user][:reset_password_token]).first
else
#user = User.find(id)
end
if #user.nil? || #user.reset_password_token.to_s != params[:user][:reset_password_token]
flash[:alert] = "Url to reset was incorrect, please resend reset email."
redirect_to '/home'
return
end
# there may be a better way of doing this, devise should be able to give us these messages
if params[:user][:password] != params[:user][:password_confirmation]
flash[:alert] = "Passwords must match."
redirect_to :back
return
end
if #user.reset_password!(params[:user][:password],params[:user][:password_confirmation])
#user.hasSetPassword = true
#user.save
respond_to do |format|
format.html { redirect_to '/home', notice: 'Your password has been changed.' }
end
else
flash[:alert] = "Invalid password, must be at least 6 charactors."
redirect_to :back
end
end
def reset_password_edit
#user = User.where(:reset_password_token => params[:reset_password_token]).first
if #user.nil? || !#user.reset_password_period_valid?
flash[:alert] = "Password reset period expired, please resend reset email"
redirect_to "/home"
return
end
end
views/devise/registrations/edit; changed the view to not let the user edit fields that require a password
<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %>
<%= devise_error_messages! %>
<% if !resource.hasSetPassword %>
<%= f.label :name %><br />
<p style="line-height:24px;"><b><%= #user.name %></b></p>
<div><%= f.label :email %><br />
<p style="line-height:24px;"><b><%= #user.email %> </b></p>
<p style="position:relative; left:150px; width:420px;">
<i>you cannot change any settings because you have not set a password <br />yet, you can do so by following the </i>
<%= link_to "Forgot your password", "/users/reset_password" %> <i> procedure</i>
</p>
</div>
<% else %>
<p><%= f.label :name %><br />
<%= f.text_field :name %></p>
<div><%= f.label :email %><br />
<%= f.email_field :email %></div>
<div><%= f.label :password %> <br />
<%= f.password_field :password %><i>(leave blank if you don't want to change it)</i></div>
<div><%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation %></div>
<div><%= f.label :current_password %> <br />
<%= f.password_field :current_password %>
<i>(we need your current password to confirm your changes)</i>
</div>
<div><%= f.submit "Update" %></div>
<% end %>
<% end %>
views/devise/mailer/reset_password_instructions; had to change it to point to the right URL in our new case
<p>Hello <%= #resource.email %>!</p>
<p>Someone has requested a link to change your password, and you can do this through the link below.</p>
<% if !#resource.hasSetPassword %>
<p><%= link_to 'Change my password', 'http://streetsbehind.me/users/reset_password_edit?reset_password_token='+#resource.reset_password_token %></p>
<!-- todo: there's probably a better way of doing this than just hardcoding streetsbehind.me -->
<% else %>
<p><%= link_to 'Change my password', edit_password_url(#resource, :reset_password_token => #resource.reset_password_token) %></p>
<% end %>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>
views/users/reset_password_edit.erb
<%= form_for(#user, :url => url_for(:action => :do_reset_password) , :html => { :method => :post }) do |f| %>
<%= f.hidden_field :reset_password_token %>
<div><%= f.label :password, "New password" %><br />
<%= f.password_field :password %></div>
<div><%= f.label :password_confirmation, "Confirm new password" %><br />
<%= f.password_field :password_confirmation %></div>
<div><%= f.submit "Change my password" %></div>
<% end %>
config/routes.rb
get "users/reset_password"
get "users/reset_password_edit"
resource :users do
post 'do_reset_password'
end
I adapted #user3294438's answer to make it work perfectly for me.
class PasswordsController < Devise::PasswordsController
# here we need to skip the automatic authentication based on current session for the following four actions
# new : shows the "enter email to reset". need to skip, otherwise it will go directly to root
# create : launches the reset email
# edit: shows the reset password form. need to skip, otherwise it will go directly to root
# update: updates the password, need to skip otherwise it won't even reset if already logged in
skip_before_action :require_no_authentication, :only => [:new, :create, :edit, :update]
# we need to override the update, too.
# After a password is reset, all outstanding sessions are gone.
# When already logged in, sign_in is a no op, so the session will expire, too.
# The solution is to logout and then re-login which will make the session right.
def update
super
if resource.errors.empty?
sign_out(resource_name)
sign_in(resource_name, resource)
end
end
private
# Overriding this method allows to show a nice flash message to the signed-in user that just
# asked for a password reset by email. Otherwise he gets a "you are already signed in" falsh error
def after_sending_reset_password_instructions_path_for(resource_name)
if current_user
flash[:info] = I18n.t "devise.passwords.send_instructions"
return root_path
end
super
end
end
Related
When a user clicks on "Forgot My Password" on the login screen, they are redirected to a route '/password-reset'. Right now, I'm trying to understand how to right the form for entering your email to receive and SMS from Twilio with a code.
<div class="form-group", style="width:50%;">
<%= form_for #user, url: password_patch_path(current_user) do |f| %>
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, class: "form-control" %>
</div>
<%= f.submit "Get Confirmation Code", class: "btn btn-default" %>
<% end %>
</div>
The issue I'm running into is that #user is nil, and I'm uncertain if the url is correct at the beginning of the form for. It makes sense to me that #user is nil because no one is logged in, so I'm not sure what that should be.
My routes are
get '/password-reset', :to => 'passwords#edit', as: :password_reset
post '/password-reset', :to => 'passwords#reset', as: :password_edit
patch '/password-confirmation', :to => 'passwords#update', as: :password_patch
and my passwords controller looks like
class PasswordsController < ApplicationController
before_action :authenticated?, only: [:edit, :update]
def reset
ConfirmationSender.send_confirmation_to(current_user)
redirect_to new_confirmation_path
end
def edit
#user = current_user
end
def update
if passwords_not_empty? && passwords_equal?
current_user.update(password_params)
redirect_to users_dashboard_path(current_user.username), success: "Password Updated"
session[:authenticated] = false
else
redirect_to password_edit_path(current_user.username), warning: "Error, please try again."
end
end
private
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
def passwords_not_empty?
params[:user][:password].length > 0 && params[:user][:password_confirmation].length > 0
end
def passwords_equal?
params[:user][:password] == params[:user][:password_confirmation]
end
def authenticated?
render :file => "#{Rails.root}/public/404.html", :status => 404 unless session[:authenticated]
end
end
You are right that there will be no current_user if a user forgot his/her password. I would redesign as follows:
PasswordsContoller
class PasswordsController < ApplicationController
before_action :authenticated?, only: [:update]
def reset
#user = User.find_by(email: params[:user][:email])
if #user.present?
ConfirmationSender.send_confirmation_to(#user)
redirect_to new_confirmation_path
else
redirect_to password_reset_path, warning: "Email not found."
end
end
def edit
#user = User.new
end
...
end
Form
<div class="form-group", style="width:50%;">
<%= form_for #user, url: password_edit_path do |f| %>
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, class: "form-control" %>
</div>
<%= f.submit "Get Confirmation Code", class: "btn btn-default" %>
<% end %>
</div>
The new edit method seeds the form with a blank user. The new reset method looks up the user by email and sends the token if the user is found. If not, it displays an email not found flash message and redirects back to the forgotten password form.
This also makes the form use the correct path for requesting a password confirmation.
My app allows a user to log in from two different places, the header and the new session page. The new session page logs a user in and redirects them to the correct page, but the home pages just reloads the home page without redirecting the user or logging them in.
This is my sessionscontroller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
session[:user_id] = user.id
redirect_back_or feed_user_path(user)
else
session[:user_id] = nil
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
session[:user_id] = nil
redirect_to root_url
end
end
new.html.erb
<%= form_for(:session, url: sessions_path) do |f| %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.submit "Log in", class: "btn btn-large btn-info" %>
<% end %>
Code in my _header.html.erb
<%= form_for :session, :url => {:controller => "sessions", :action => "new"} do |f| %>
<div class="home-login form-group">
<%= f.label :email %>
<%= f.text_field :email, :placeholder => "Email" %>
</div>
<div class="home-login form-group">
<%= f.label :password %>
<%= f.password_field :password, :placeholder => "Password" %>
</div>
<div class="form-group home-login">
<%= f.submit "Log in", class: "btn-info" %>
</div>
<% end %>
This is what shows up in the terminal
--- !ruby/hash:ActionController::Parameters
utf8: ✓
authenticity_token: AegIbI8c1TIddIBPVWTt/B2CBoCAgbJxL+NWDe782Cc=
session: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
email: ** # **
password: ****
commit: Log in
controller: pages
action: home
EDIT****
This is my application controller
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
include SessionsHelper
end
Home is a static page in my pages controller
class PagesController < ApplicationController
def home
end
end
EDIT****
This is my SessionsHelper
module SessionsHelper
def signed_in?
!!session[:user_id]
end
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
def current_user=(user) # set current user
#current_user = user # session[:user_id] = user.id
end
def current_user?(user) # get current user
user == current_user
end
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in."
end
end
def redirect_back_or(default)
redirect_to(session[:return_to] || default)
session.delete(:return_to)
end
end
EDIT****
Routes
sessions POST /sessions(.:format) sessions#create
new_session GET /sessions/new(.:format) sessions#new
session DELETE /sessions/:id(.:format) sessions#destroy
try changing your form_for tag to direct it to the create action with a method of post. something like this:
<%= form_for :session, url: session_path, method: 'post', action: 'create' do |f| %>
There's no difference with where do you put your login form unless you refer to any controller-specific data there. I don't see that, so you should be good to go with just copying the exact same form_for call that actually produces working results. At least I don't see anything that prevents doing this.
And the problem is the action. new. Wrong. It's create. "New" is a display page for the form (and uses GET because of that), the actual form's action (data send path) is "create" (that uses POST). HTTP method is picked from the routes, execute rake routes to verify that they are correct.
A form is not bound to a page it's on. Instead, it has its own path, that should accept input data from the form.
I figured it out. The ERB form was wrapped in an HTML form. That was causing the problem.
My question refers to setting up the view and the controller for updating a user's "profile" by confirming the password from the user before updating the attributes. As you've all probably seen a million times before, the user would go to /users/:id/edit, enter a new email in the text field, enter the current password in the password field and click on the submit button to ultimately update the user's email. If the password entered is incorrect then the edit template is rendered again, otherwise the user record is updated with the new email and redirected to :show (or whatever is appropriate for the app). While in the update action I think it makes sense to stick with using the update_attributes method. However the current password value would end up throwing us off.
What I'm really asking though is if there's anything wrong with my approach. I ended up with including a call to password_field_tag for the :current_password field inside the form_for block in order to call update_attributes with params[:user] without making attr_accessible angry. But then I looked up a couple forms in websites that already do this (hulu and destroyallsoftware for example) and they seem to accept the :current_password value in the user hash (assuming they're built with rails). Looking up twitter's settings page it looks like they retrieve this in a separate hash in param (so params[:current_password] instead of params[:user][:current_password]).
Is it wrong to use password_field_tag within form_for? How are these other sites really doing this? The only thing I can think of is that they're either deleting :current_password from the params hash or assigning each attribute individually.
Here is what I basically ended up with:
# /app/models/user.rb
class User < Activerecord::Base
attr_accessible :email, # ...
# ...
end
# /app/views/users/edit.html.erb
<%= form_for #user do |f| %>
# this is stored in params[:user][:email]
<%= f.label :email, 'Your new email' %>
<%= f.text_field :email, type: :email %>
# this is stored in params[:current_password]
<%= label_tag :current_password, 'Re-enter your password to update your email' %>
<%= password_field_tag :current_password %>
<%= f.submit 'Save changes' %>
<% end %>
# /app/controllers/users_controller.rb
# ...
def update
#user = User.find(params[:id])
if #user.authenticate(params[:current_password])
if #user.update_attributes(params[:user])
sign_in #user
flash[:success] = 'Sweet!'
redirect_to #user
else
render :edit
end
else
flash.now[:error] = 'Incorrect password'
render :edit
end
Otherwise, this is the one other way I thought of:
# /app/views/users/edit.html.erb
<%= form_for #user do |f| %>
# this is stored in params[:user][:email]
<%= f.label :email, 'Your new email' %>
<%= f.text_field :email, type: :email %>
# this is stored in params[:user][:current_password]
<%= f.label :current_password, 'Re-enter your password to update your email' %>
<%= f.password_field :current_password %>
<%= f.submit 'Save changes' %>
<% end %>
# /app/controllers/users_controller.rb
# ...
def update
#user = User.find(params[:id])
if #user.authenticate(params[:user][:current_password])
params[:user].delete(:current_password) # <-- this makes me feel a bit uneasy
if #user.update_attributes(params[:user])
sign_in #user
flash[:success] = 'Sweet!'
redirect_to #user
else
render :edit
end
else
flash.now[:error] = 'Incorrect password'
render :edit
end
Or, should I just do this in the controller?:
def update
#user = User.find(params[:id])
if #user.authenticate(params[:user][:current_password])
#user.email = params[:user][:email]
if #user.save
# ...
Any advice is appreciated.
P.S. - Additionally how would you go about refactoring that update action? I tried out a before_filter to authenticate with :current_password and keep only the #update_attributes part in #update, but it got a bit messy. This post is getting long enough though so maybe I'll post this as a separate question if I can not figure it out by next week.
I've recently done something similar to this, except I used a virtual attribute to handle the current_password. You can then add the :current_password attribute to attr_accessible and keep it happy.
I've got two controllers: admin and customers, plus one more called sessions for handling login and authentication. I'm trying to use one login form so that when an admin logs in, they are redirected to their part of the site, and if a customer logs in, they are taken to their part.
Edit: changed the params to login by email, but now getting the error No route matches {:action=>"show", :controller=>"customers"} when I try to log in as a customer :S!!
Code:
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by_email(params[:email])
customer = Customer.find_by_email(params[:email])
if user and user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to admin_url
elsif customer and customer.authenticate(params[:password])
session[:customer_id] = customer.id
redirect to customer_url
else
redirect_to login_url, alert: "Invalid user/password combination"
end
end
def destroy
session[:user_id] = nil
session[:customer_id] = nil
redirect_to store_url, notice: "Logged out"
end
end
Code for the login page (stored in app/sessions/new.html.erb):
<div class="depot_form">
<% if flash[:alert] %>
<p id="notice"><%= flash[:alert] %></p>
<% end %>
<%= form_tag do %>
<fieldset>
<legend>Please Log In</legend>
<div>
<%= label_tag :email, 'Email:' %>
<%= text_field_tag :email, params[:email] %>
</div>
<div>
<%= label_tag :password, 'Password:' %>
<%= password_field_tag :password, params[:password] %>
</div>
<div>
<%= submit_tag "Login" %>
</div>
</fieldset>
<% end %>
</div>
Also, if relevant, I've got this in the config/routes file:
controller :sessions do
get 'login' => :new
post 'login' => :create
delete 'logout' => :destroy
end
Edit: changed the params to login by email, but now getting the error No route matches {:action=>"show", :controller=>"customers"} when I try to log in as a customer :S!!
The problem is that your params[:email] does not exist at all! Try using the params[:name], and hope that your customer knows that he has to write his email into the name field.
user = User.find_by_name(params[:name])
customer = Customer.find_by_email(params[:name])
If you only have one login form, then the input for the username/email will either end up in the params hash as either
params[:email]
or
params[:name]
But it looks like params[:email] doesn't exist since you're only using one form. Since you can successfully authenticate as an admin, I would guess that the input for the username is titled "name", so in your params hash, when you authenticate as a customer, your authenticating against
params[:name] #this is what I think the input on the form is named
instead of
params[:email] #which is what it looks like your code is looking for.
Can you post the view code?
This is my first time doing validation on a rails application. I saw many tutorials which made it seem easy. I don't know why I cant get it to work.
Below is my setup.
Controller Admin (action = login)
def login
session[:user_id] = nil
if request.post?
#user = User.authenticate(params[:userId], params[:password])
if true
session[:user_id] = #user.user_id
flash.now[:notice] = "Login Successful"
redirect_to(:controller => "pages", :action => "mainpage")
else
flash.now[:notice] = "Invalid user/password combination"
end
end
end
So first time user comes to admin/login they are just presented with a form below
login.erb.html
<% form_for :user do |f| %>
<p><label for="name">User ID:</label>
<%= f.text_field :userid %>
</p>
<p><label for="password">Password:</label>
<%= f.password_field :password%>
</p>
<p style="padding-left:100px">
<%= submit_tag 'Login' %>
</p>
<% end %>
My User model:
class User < ActiveRecord::Base
validates_presence_of :userid, :password
def self.authenticate(userid, password)
user = self.find_by_userid_and_password(userid, password)
user
end
end
Actual field names for userId and password in my DB: userid password
I am expecting behavior that when user does not enter anything in the fields and just clicks submit. it will tell them that userid and password are required fields. However, this is not happening
From the console I can see the messages:
>> #user = User.new(:userid => "", :password => "dsf")
=> #<User id: nil, userid: "", password: "dsf", created_at: nil, updated_at: nil>
>> #user.save
=> false
>> #user.errors.full_messages
=> ["Userid can't be blank"]
So error is somewhere in my form submit...
UPDATE: validations only happen when u SAVE the object....here I am not saving anything. So in this case I have to do javascript validations?
It's the if true line. Change it to
if #user = User.authenticate(params[:userId], params[:password])
or
#user = User.authenticate(params[:userId], params[:password])
if #user
...
end
I'd also add redirect_to login_path to the failure case.
You can also slim down your auth method:
def self.authenticate(userid, password)
find_by_userid_and_password(userid, password)
end
It turns out, there are several issues here, and I'll try to cover them all. Let's start with your model:
class User < ActiveRecord::Base
validates_presence_of :userid, :password
def self.authenticate(userid, password)
self.find_by_userid_and_password(userid, password)
end
end
The validation doesn't come into play for logging in, only for creating and updating user records. The authentication has been trimmed, because ruby automatically returns the last calculated value in a method.
Next, the login action of your controller:
def login
session[:user_id] = nil
if request.post?
if #user = User.authenticate(params[:userId], params[:password])
session[:user_id] = #user.user_id
flash[:notice] = "Login Successful"
redirect_to(:controller => "pages", :action => "mainpage")
else
flash.now[:error] = "Invalid user/password combination"
end
end
end
Notice we don't use flash.now on a redirect - flash.now is only if you're NOT redirecting, to keep rails from showing the message twice.
Finally, you shouldn't be using form_for, because this is not a restful resource form. You're not creating or editing a user, so use form_tag instead:
<% form_tag url_for(:controller => :users, :action => :login), :method => :post do %>
<%= content_tag(:p, flash[:error]) if flash[:error] %>
<p><label for="name">User ID:</label>
<%= text_field_tag :userid %>
</p>
<p><label for="password">Password:</label>
<%= password_field_tag :password%>
</p>
<p style="padding-left:100px">
<%= submit_tag 'Login' %>
</p>
<% end %>
This will do what you want. This is a great learning exercise, but if you're serious about user authentication in a production application, checkout rails plugins like restful_authentication or clearance that do this for you in a much more sophisticated (and RESTful) way.