0

I am trying to build an api only application using ruby on rails, for the authentication i am using the has_secure_password for my User model. So in my application there are two types of users one is admin one is guest, an admin is able to deactivate a guest user, the problem is when i call a put request to deactivate a user.

I get this error Password can't be blank. My problem is very similar to this Question rather it is more of follow up for this.

I believe the issue is that the has_secure_password is expecting me to provide as password in the params/body but for deactivating the guest user there's no need for the admin to know the password for that user right? an id is enough in that case

My url for the put query is this "/api/v1/users/:id/deactivate(.:format)" where in the :id params i just give the id for the guest which i want to deactivate

Here is the user model

class User < ApplicationRecord
  has_secure_password

  before_create :generate_token

  has_many :registrations
  has_many :organized_events, class_name: 'Event', foreign_key: 'organizer_id'
  has_many :attended_events, through: :registrations, source: :event

  validates :username, presence: true, uniqueness: true
  validates :email, presence: true, uniqueness: true,
    format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
  validates_presence_of :first_name, :password

  enum role: { admin: 0, guest: 1 }

  scope :active, -> { where active: true }

  private def generate_token
    self.authentication_token ||= SecureRandom.hex(6)
  end
end

And here is the code block where I'm trying to make the request to deactivate request

def deactivate(id)
    user = User.find_by(id: id)
    if user.blank?
      self.errors = "User not found"
    else
      self.errors = user.errors.full_messages unless user.update(active: false)
    end
  end

I'm very much stuck not sure how to proceed from here on

  • The user you are updating does not have a password set! This could possibly be legacy data for that user object, Check the id being passed in then check in the console that the user with that id has a password and password set then write a test to reproduce the problem – jamesc Aug 30 '23 at 19:05
  • @jamesc no that's not possible well firstly because there no legacy data and as you can see in the user model the password is required when creating a user as a failsafe , secondly the id I'm trying to deactivate clearly has a password when either checking with rails console to directly through the sql query, so the problem is not related to the password not being set – Ishmeet Singh Aug 30 '23 at 19:19
  • Since you are literally just deactivating a user (which does not seem like it needs to check the validity of the model) just use [`update_attribute`](https://apidock.com/rails/v5.2.3/ActiveRecord/Persistence/update_attribute) instead (From the docs: *"Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records."*) e.g. `user.update_attribute(:active, false)` – engineersmnky Aug 30 '23 at 19:36
  • @engineersmnky even don't need initialize User instance (base on the method body), just enough to send `UPDATE` query with `update_all` method (1 query vs 2 queries) – mechnicov Aug 30 '23 at 19:45
  • @mechnicov I assumed callbacks might be useful when deactivating a user but if not then sure (although the performance improvement for a single record is going to be negligible at best) – engineersmnky Aug 30 '23 at 20:39

2 Answers2

2

The hashed password is stored in a column named password_digest. When reloading a user from the database, then there is still the password_digest set, but not the plain text password.

Therefore, you must remove the :password column from this validation

validates_presence_of :first_name, :password

and change it to

validates_presence_of :first_name

And actually there is no need to add an extra present validation for a password, because has_secure_password already add this automatically.

spickermann
  • 100,941
  • 9
  • 101
  • 131
1

I believe that when you are admin and want to deactivate some user, you don't need to validate it

You just need update the field

I suggest to use update_all for this. It performs single UPDATE query, doesn't call validations and returns the number of updated rows. If this number is 0, it means there aren't such records in the database

def deactivate(id)
  if User.where(id: id).update_all(active: false).zero?
    self.errors = "User not found"
  end
end

Since it doesn't update updated_at field, you can add updated_at: Time.current if needed

mechnicov
  • 12,025
  • 4
  • 33
  • 56
  • It did solve the issue now able to deactivate the user without any error but the problem is under the test environment i'm trying to create a test guest user and deactivate it , it failed for some reason when checking with debugger i found out that its not able to update the object `(rdbg) guest # (ruby) user = User.where(id: guest.id) [#] (ruby) user.update_all(active: false) 1 (rdbg) guest # – Ishmeet Singh Aug 30 '23 at 20:05
  • 1
    It is just instance in memory. `guest.reload` should fetch updated data from the database – mechnicov Aug 30 '23 at 20:07
  • as you said i would now need to write guest.reload, is there any way that i would not have run reload rather everything should be completed within the "it" block – Ishmeet Singh Aug 30 '23 at 20:07
  • You can use User instance in the `deactivate` method, but it will be 2 queries (and in production too), in this case don't need to reload in the test. I think this option is worse than reload in the test. And BTW you can use `change` in RSpec to check `count` – mechnicov Aug 30 '23 at 20:11