Devise Forgot Password for logged in user

2019-02-06 11:17发布

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: 1. a user logs in with facebook, a fake password is generated for them 2. 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 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?

3条回答
你好瞎i
2楼-- · 2019-02-06 11:57

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.

查看更多
时光不老,我们不散
3楼-- · 2019-02-06 11:59

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
查看更多
倾城 Initia
4楼-- · 2019-02-06 12:00

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'}
查看更多
登录 后发表回答