0

Handle Password and Email Changes in Your Rails API

Đây là phần 2 về vấn đề xác thực bằng cách sử dụng JWT. Bạn có thể xem phần 1 ở đây. Trong bài viết trước chúng ta đã thấy được tổng quát về JWT, cơ chế xác thực khác nhau, và các xác thực cơ bản API, giống như đăng ký, xác nhận và đăng nhập. Trong phần này, chúng ta sẽ xem các phần tiếp theo của API như pasword (reset và thay đổi) và cập nhật email.

Trong phần này sẽ là nhiều hơn một bài hướng dẫn JWT. Mục tiêu chính của nó là để xem làm thế nào để xây dựng giải quyết xác thực tùy theo cách riêng của bạn từ đầu và JWT chỉ là phương thức chúng ta chọn để sử dụng.

Chúng ta sẽ tiếp tục xây dựng trên ứng dụng ví dụ trong phần đầu mà chúng ta đã phát triển. Bạn có thể tìm thấy nó ở đây.

1. Password

API Chúng ta tìm hiểu ở đây lần lượt là quên password. Luồng sinh một password reset token cùng với một endpoint cho user để hợp lệ hóa token. Endpoint này được gọi khi user click vào link thiết lập lại password gửi cho họ qua email. Endpoint cuối là để cuối cùng đã thay đổi mật khẩu.

1.1. Forgot Password

Các endpoint quên password sinh một token thiết lập lại password, lưu nó trong database, và gửi một email đến user. Điều này cũng tương tự module hướng dẫn confirm như trong phần 1 chúng ta đã tìm hiểu. Hãy bắt đầu bằng cách thêm các column cần thiết cho chức năng reset password.

rails g migration AddPasswordResetColumnsToUser

Và trong file migration, thêm dòng sau vào:

add_column :users, :reset_password_token, :string
add_column :users, :reset_password_sent_at, :datetime

Hai cột này là đủ cho mục đích này. reset_password_token sẽ lưu trữ các token chúng tôi tạo ra và reset_password_sent_at lưu trữ0 thời gian token được gửi với mục đích hạn sử dụng. Hãy thêm các endpoint bây giờ. Bắt đầu bằng cách tạo controller password:

rails g controller passwords

Thêm routes vào file config/routes.rb:

post 'password/forgot', to: 'password#forgot'
post 'password/reset', to: 'password#reset'

Bây giờ hãy thêm action tương ứng cho route tương ứng vào file controllers/password_controller.rb:

...
  def forgot
    if params[:email].blank?
      return render json: {error: 'Email not present'}
    end

    user = User.find_by(email: email.downcase)

    if user.present? && user.confirmed_at?
      user.generate_password_token!
      # SEND EMAIL HERE
      render json: {status: 'ok'}, status: :ok
    else
      render json: {error: ['Email address not found. Please check and try again.']}, status: :not_found
    end
  end

  def reset
    token = params[:token].to_s

    if params[:email].blank?
      return render json: {error: 'Token not present'}
    end

    user = User.find_by(reset_password_token: token)

    if user.present? && user.password_token_valid?
      if user.reset_password!(params[:password])
        render json: {status: 'ok'}, status: :ok
      else
        render json: {error: user.errors.full_messages}, status: :unprocessable_entity
      end
    else
      render json: {error:  ['Link not valid or expired. Try generating a new link.']}, status: :not_found
    end
  end
 ...

Chúng ta sẽ xem qua đoạn code trên. Trong action forgot, lấy email trong request gửi lên và tìm user. Nếu tìm thấy user và đã được confirmed, gọi hàm generate_password_token trong model user và gửi email. Phần gửi email chúng ta ko tìm hiểu ở đây, nhưng phải chắc chắn bao gồm password_reset_token của user trong email. Trong action reset, lấy token được gửi trong request và xác nhận nó hợp lệ qua hàm password_token_valid? và thiết lập lại password thông qua reset_password. Giờ chúng ta sẽ thêm các method còn thiếu vào model user:

...
def generate_password_token!
  self.reset_password_token = generate_token
  self.reset_password_sent_at = Time.now.utc
  save!
end

def password_token_valid?
  (self.reset_password_sent_at + 4.hours) > Time.now.utc
end

def reset_password! password 
  self.reset_password_token = nil
  self.password = password
  save!
end

private

def generate_token
  SecureRandom.hex(10)
end
...

Trong phương thức generate_password_token! chúng ta sinh một token sử dụng phương thức generate_token và lưu trữ nó trong column reset_password_token, và cũng thiết lập reset_password_sent_at tại thời điểm hiện tại. Trong phương thức password_token_valid?, xác minh các token được gửi trong vòng 4 giờ tức là hết hạn reset password. Bạn được tự do thay đổi nó, tuy nhiên bạn sẽ thấy nó là phù hợp. Phương thức reset_password! cập nhật password mới của user và vô hiệu hóa các token reset.

Reset password đã thực hiện xong. Bạn có thể test nó bằng cách gửi request post đến /passwords/forgot với email trong body và /passwords/reset với một password mới và token trong body.

1.2. Update Password

Để thêm update password, hãy thêm route tương ứng vào file routes.rb:

put 'password/update', to: 'password#update'

Route ở trên tương ứng với action trong PasswordsController:

def update
  if !params[:password].present?
    render json: {error: 'Password not present'}, status: :unprocessable_entity
    return
  end

  if current_user.reset_password(params[:password])
    render json: {status: 'ok'}, status: :ok
  else
    render json: {errors: current_user.errors.full_messages}, status: :unprocessable_entity
  end
end

Action update password khá là đơn giản. Lấy password từ tham số gửi lên và lưu nó vào DB sử dụng phương thức reset_password mà chúng ta đã khai báo trước đó trong model User. Bạn có thể test URL update password bằng cách gửi một request PUT đến /password/update với password mới trong body. Tiếp theo chúng ta sẽ đi tìm hiểu chức năng tiếp theo, update email.

2. Email Update

Update Email cho phép user update email của chính họ trên tài khoản của mình. Khi có yêu cầu, chúng ta nên kiểm tra nếu email đã được sử dụng bởi bất cứ user nào khác. Nếu email là OK, lưu trữ nó và gửi một email xác nhận đến email mới. Sau khi xác nhận, chúng ta sẽ thay thế email chính với email mới và làm sạch trong các tokens.

Vì vậy, có hai API trong tổng số: Một để thực hiện một yêu cầu cập nhật email, một để thực sự cập nhật email.

Bắt đầu bằng cách tạo migration thêm một column cần thiết để hỗ trợ module này:

rails g migration AddUnconfirmedEmailTouser

Thêm nội dung và file vừa tạo và chạy rake db:migrate:

add_column :users, :unconfirmed_email, :string

2.1. Update

Bây giờ, hãy update route cho 2 endpoint. Và thêm chúng vào file config/routes.rb:

...
resources :users, only: [:create, :update] do
    collection do
        post 'email_update'
...

Thêm action tương ứng vào UsersController:

def update
    if current_user.update_new_email!(@new_email)
      # SEND EMAIL HERE
      render json: { status: 'Email Confirmation has been sent to your new Email.' }, status: :ok
    else
      render json: { errors: current_user.errors.values.flatten.compact }, status: :bad_request
    end
end

Cũng thêm một before_action để làm kiểm tra trên email mới, và thêm dòng này ở trên đầu class controller user với phương thức private:

class UsersController < ApplicationController
    before_action :validate_email_update, only: :update
    ...
    ...

    private
    def validate_email_update
      @new_email = params[:email].to_s.downcase

      if @new_email.blank?
        return render json: { status: 'Email cannot be blank' }, status: :bad_request
      end

      if  @new_email == current_user.email
        return render json: { status: 'Current Email and New email cannot be the same' }, status: :bad_request
      end

      if User.email_used?(@new_email)
        return render json: { error: 'Email is already in use.'] }, status: :unprocessable_entity
      end
    end
    ...

Ở đây chúng ta kiểm tra nếu email yêu cầu đã được sử dung và nếu email là giống với tài khoản nào đã có. Nếu mọi thứ đều ổn thì gọi **update_new_email!**và gửi email. Chú ý rằng, email phải gửi đến unconfirmed_email của user thay vì email chính của họ. Chúng ta đã sử dụng thêm 2 method model mới, giờ sẽ đi định nghĩa chúng trong file models/user.rb:

def update_new_email!(email)
 self.unconfirmed_email = email
 self.generate_confirmation_instructions
 save
end

def self.email_used?(email)
 existing_user = find_by("email = ?", email)

 if existing_user.present?
   return true
 else
   waiting_for_confirmation = find_by("unconfirmed_email = ?", email)
   return waiting_for_confirmation.present? && waiting_for_confirmation.confirmation_token_valid?
 end
end

Ở đây, trong email_used?, ngoài việc kiểm tra nếu email được sử dụng chính trong bất kỳ tài khoản nào, chúng ta cũng kiểm tra nếu nó được update và chờ đợi để confirm. Điều này có thể được loại bỏ tùy theo nhu cầu của bạn. Phương thức confirmation_token_valid? được thêm vào trong phần đầu của bài.

Bây giờ có thể kiểm tra route này bằng cách gửi một request POST đến /users/update với email trong body yêu cầu.

2.2. Email Update

Bây giờ hãy thêm action cho update email vào trong class UsersController:

def email_update
  token = params[:token].to_s
  user = User.find_by(confirmation_token: token)

  if !user || !user.confirmation_token_valid?
    render json: {error: 'The email link seems to be invalid / expired. Try requesting for a new one.'}, status: :not_found
  else
    user.update_new_email!
    render json: {status: 'Email updated successfully'}, status: :ok
  end
end

Action này khá là đơn giản. Lấy user bởi token và xem nếu token là hợp lệ. Nếu vậy, cập nhật email và trả lời. Hãy thêm phương thức update_new_email! vào model user:

def update_new_email!
  self.email = self.unconfirmed_email
  self.unconfirmed_email = nil
  self.mark_as_confirmed!
end

Ở đây chúng ta thay các email chính với email được update và thiết lập trường email đã update thành nil. Ngoài ra, gọi mark_as_confirmed! mà chúng ta thêm vào trong phần trước. Phương thức này làm vô hiệu các token xác nhận và thiết lập xác nhận theo giá trị. Hãy thử một yêu cầu POST đến /users/email_update với token email mà chúng ta đã tạo trong phần trước.

Tham khảo: https://www.sitepoint.com/handle-password-and-email-changes-in-your-rails-api/


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.