2
votes

There's a ton of related questions:
- Devise: Update Account without password confirmation
- rails: devise update user without password
- Devise 3 (rails 4) can't update user without password
- etc...

But when I tried to work with their answers I never ended up getting what I wanted so I've decided to post this Q&A style.

Situation:
I was working on a Rails app using devise and I wanted a "Manage Profile form." I wanted one form for updating user information, including email and password, but also first_name, last_name, and other non-devise fields. I only wanted to require the password fields (password and password_confirmation) if the user was trying to change their password. I also wanted to build my form with the form_for helper, and whitelist the fields the user is able to update. And I also wanted all the handy error messages from devise (like, "Your password must be at least 8 characters." or whatever they are).

2

2 Answers

2
votes

TL;DR: Strip params to only be what you want, I call this user_params. In the action that processes the form check if user_params[:password] is empty, and if it is try to update your @user model with @user.update_without_password(user_params) if it isn't try to update with @user.update(user_params). If the applicable update call returns false @user.errors holds the explanation.

Here's exactly how I solved this problem:
I defined a resource in my config/routes file:

resource :profile  

I made a controller for my resource that extends devise's authenticated controller ProfilesController < AuthenticatedController for managing profiles. Profiles controller contains several methods including user_params which mainly filters params down to a whitelist:

def user_params
  accessible = [
    :first_name,
    :last_name,
    :email,
    :password,
    :password_confirmation
  ]
  params.require(:user).permit(accessible)
end

and update which does the business of processing the form:

def update
  # current_user holds the logged in user
  @user = current_user
  # IF they've left the password field blank, 
  # AND the devise update_without_password method returns true
  # OR IF a full update of user (including password and password_confirmation) returns true
  # THEN re-sign them in to flush their session, and redirect them back to their dashboard, and send a success message.
  # ELSE re-present the edit form they were just on (there's a handy catcher
  # in the edit view script to render the form errors, you can find them on
  # @user.errors)

  if (user_params[:password].blank? && @user.update_without_password(user_params)) || @user.update(user_params)
    sign_in(@user, bypass: true)
    redirect_to '/dashboard', notice: 'Your profile changes have been saved.'
  else
    render 'edit'
  end
end

There's also of course a view script (a haml one in my case - app/views/profiles/edit.html.haml) that uses form_for to render the form:

= form_for current_user, as: :user, url: profile_path, html: { class: '' } do |f|
# [f.label, f.text_field, f.password_field, etc...]

My user model also has all the devise-y goodness included:

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
  has_many :receipts
  validate :first_name, presence: true
  validate :last_name, presence: true
  validate :email, presence: true, email: true
end
0
votes

If you really want to update the password without the current_password then you need to use the user reset_password method

@user.reset_password(params[:user][:password], params[:user][:password_confirmation])

Here is a complete working solution to the problem that works in all scenarios:

  if params.dig(:user, :password).blank?
    updated = @user.update_without_password(params[:user].to_unsafe_hash)
  else
    if params.dig(:user, :current_password).nil?
      @user.reset_password(params[:user][:password], params[:user][:password_confirmation])

      updated = @user.update_without_password(params[:user].to_unsafe_hash)
    else
      updated = @user.update_with_password(params[:user].to_unsafe_hash)
    end

    bypass_sign_in(@user)
  end