Search code examples
ruby-on-railsrubydevise

Rails devise reset :confirmed_at field when user email is changed


When a user changes their email address, the new email is not confirmed. However devise doesn't update/reset the confirmed_at field.

I have tried:

before_update :updateEmailConfirmed, if: :email_changed?

def updateEmailConfirmed
  # check if there is an unconfirmed_email
  user = User.find(id)
  if !user.unconfirmed_email.nil?
    # set confirmed_at to nil
    self.update!(confirmed_at:nil)
  end
end

I understand the :confirmed_at field is for any confirmation, so it is working as expected. However I am using this field to track to see if the email has been verified.

Currently I have added an extra field to my User model called :email_confirmed of type bool and I set that to true/false depending on whether the current :email field has been verfiied.

My question is, is there anything built into the Devise modules that will allow me to do this without introducing any new columns to my User table and modifying my User class.

Update1.)

Here are the tags set for my User model:

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
     :recoverable, :rememberable, :trackable, :validatable,
     :confirmable

  protected
  def confirmation_required?
    false
  end
end

This is what my User table looks like:

create_table "users", force: :cascade do |t|
  t.string "email", default: "", null: false
  t.string "encrypted_password", default: "", null: false
  t.string "reset_password_token"
  t.datetime "reset_password_sent_at"
  t.datetime "remember_created_at"
  t.integer "sign_in_count", default: 0, null: false
  t.datetime "current_sign_in_at"
  t.datetime "last_sign_in_at"
  t.string "current_sign_in_ip"
  t.string "last_sign_in_ip"
  t.string "confirmation_token"
  t.datetime "confirmed_at"
  t.datetime "confirmation_sent_at"
  t.string "unconfirmed_email"
  t.boolean "verified", default: false
  t.index ["confirmation_token"], name: "index_users_on_confirmation_token", 
    unique: true
  t.index ["email"], name: "index_users_on_email", unique: true
  t.index ["reset_password_token"], name: 
    "index_users_on_reset_password_token", unique: true
end

Solution

  • Best I could think of is Overriding the Devise::Mailer and the Confirmation::Controller provided by Devise.

    I realised I can pass in a type parameter inside the confirmation_url that gets sent to the users email.

    class DeviseMailer < Devise::Mailer
      def confirmation_instructions(record, token, opts={})
    
        # determine if this request is to change the email
        # or verify the existing email
        if !record.unconfirmed_email.nil?
          type = "change_email"
        else
          type = "verify_email"
        end
    
        # get the link for the confirmation link and append 
        # the type paramater to the end
        confirmation_link = Rails.application
          .routes.url_helpers
          .user_confirmation_url(confirmation_token: token).to_s + "&type="+type.to_s
        .
        .
        .
        # send email with new confirmation link
      end
    end
    

    Then on the other end, when someone clicks on the confirmation links, I can distinguish the type of request by looking at the parameters:

    class ConfirmationsController < Devise::ConfirmationsController
      def show
        # confirm user 
        self.resource = resource_class.confirm_by_token(params[:confirmation_token])
    
        if resource.errors.empty?
    
          # sign in user if they are not already signed in
          sign_in(resource_name, resource)
    
          # find out what kind of request this confirmation was for
          type = params[:type]
          if type == "verify_email"
            flash[:confirmed] = "Email successfully confirmed!"
    
            # update verified field which is used to keep track if the :email field has been verified
            current_user.update(verified:true)
    
          elsif type == "change_email"
            flash[:confirmed] = "Email successfully updated!"
    
            # update verified field which is used to keep track if the :email field has been verified
            current_user.update(verified:false)
    
            # send confirmation instructions for this new email so we can verify it
            current_user.send_confirmation_instructions
          end
    
          respond_with_navigational(resource){ redirect_to root_path }
        else
          p resource.errors
          flash[:unconfirmed] = "Something went wrong trying to confirm your email? Contact contact@email.com"
          respond_with_navigational(resource.errors, status: :unprocessable_entity){ redirect_to edit_user_registration_path }
        end
      end
    
      private
      def after_confirmation_path_for(resource_name, resource)
        sign_in(resource) # In case you want to sign in the user
        root_path
      end
    
    end