fix: downcase email before finding (#8921)

* fix: downcase email when finding

* feat: add `from_email` class

* refactor: use `from_email`

* feat: add rule to disallow find_by email directly

* chore:  remove redundant test

Since the previous imlpmentation didn't do a case-insentive search, a new user would be created, and the error would be raised at the DB layer. With the new changes, this test case is redundant

* refactor: use from_email
This commit is contained in:
Shivam Mishra
2024-02-21 18:51:00 +05:30
committed by GitHub
parent ebae547a60
commit c031cb19d2
17 changed files with 46 additions and 18 deletions

View File

@@ -2,6 +2,7 @@ require:
- rubocop-performance - rubocop-performance
- rubocop-rails - rubocop-rails
- rubocop-rspec - rubocop-rspec
- ./lib/rubocop/user_find_by.rb
Layout/LineLength: Layout/LineLength:
Max: 150 Max: 150
@@ -140,6 +141,10 @@ RSpec/MultipleExpectations:
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 14 Max: 14
# custom rules
UseFromEmail:
Enabled: true
AllCops: AllCops:
NewCops: enable NewCops: enable
Exclude: Exclude:

View File

@@ -59,7 +59,7 @@ class ContactIdentifyAction
def existing_email_contact def existing_email_contact
return if params[:email].blank? return if params[:email].blank?
@existing_email_contact ||= account.contacts.find_by(email: params[:email]) @existing_email_contact ||= account.contacts.from_email(params[:email])
end end
def existing_phone_number_contact def existing_phone_number_contact

View File

@@ -27,7 +27,7 @@ class AgentBuilder
# Finds a user by email or creates a new one with a temporary password. # Finds a user by email or creates a new one with a temporary password.
# @return [User] the found or created user. # @return [User] the found or created user.
def find_or_create_user def find_or_create_user
user = User.find_by(email: email) user = User.from_email(email)
return user if user return user if user
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}" temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"

View File

@@ -75,7 +75,7 @@ class ContactInboxWithContactBuilder
def find_contact_by_email(email) def find_contact_by_email(email)
return if email.blank? return if email.blank?
account.contacts.find_by(email: email.downcase) account.contacts.from_email(email)
end end
def find_contact_by_phone_number(phone_number) def find_contact_by_phone_number(phone_number)

View File

@@ -5,7 +5,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
skip_before_action :authenticate_user!, raise: false skip_before_action :authenticate_user!, raise: false
def create def create
@user = User.find_by(email: params[:email]) @user = User.from_email(params[:email])
if @user if @user
@user.send_reset_password_instructions @user.send_reset_password_instructions
build_response(I18n.t('messages.reset_password_success'), 200) build_response(I18n.t('messages.reset_password_success'), 200)

View File

@@ -33,7 +33,7 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
def process_sso_auth_token def process_sso_auth_token
return if params[:email].blank? return if params[:email].blank?
user = User.find_by(email: params[:email]) user = User.from_email(params[:email])
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token]) @resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
end end
end end

View File

@@ -8,7 +8,7 @@ class Platform::Api::V1::UsersController < PlatformController
def show; end def show; end
def create def create
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params)) @resource = (User.from_email(user_params[:email]) || User.new(user_params))
@resource.skip_confirmation! @resource.skip_confirmation!
@resource.save! @resource.save!
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource) @platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)

View File

@@ -91,7 +91,7 @@ class Imap::ImapMailbox
end end
def find_or_create_contact def find_or_create_contact
@contact = @inbox.contacts.find_by(email: @processed_mail.original_sender) @contact = @inbox.contacts.from_email(@processed_mail.original_sender)
if @contact.present? if @contact.present?
@contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact) @contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact)
else else

View File

@@ -86,7 +86,7 @@ class SupportMailbox < ApplicationMailbox
end end
def find_or_create_contact def find_or_create_contact
@contact = @inbox.contacts.find_by(email: original_sender_email) @contact = @inbox.contacts.from_email(original_sender_email)
if @contact.present? if @contact.present?
@contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact) @contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact)
else else

View File

@@ -165,6 +165,12 @@ class Contact < ApplicationRecord
email_format email_format
end end
def self.from_email(email)
# rubocop:disable UseFromEmail,Migration/DepartmentName
find_by(email: email.downcase)
# rubocop:enable UseFromEmail,Migration/DepartmentName
end
private private
def ip_lookup def ip_lookup

View File

@@ -156,6 +156,12 @@ class User < ApplicationRecord
mutations_from_database.changed?('email') mutations_from_database.changed?('email')
end end
def self.from_email(email)
# rubocop:disable UseFromEmail,Migration/DepartmentName
find_by(email: email.downcase)
# rubocop:enable UseFromEmail,Migration/DepartmentName
end
private private
def remove_macros def remove_macros

View File

@@ -35,7 +35,7 @@ class DataImport::ContactManager
def find_contact_by_email(params) def find_contact_by_email(params)
return unless params[:email] return unless params[:email]
@account.contacts.find_by(email: params[:email]) @account.contacts.from_email(params[:email])
end end
def find_contact_by_phone_number(params) def find_contact_by_phone_number(params)

View File

@@ -69,7 +69,7 @@ module Integrations::Slack::SlackMessageHelper
def sender def sender
user_email = slack_client.users_info(user: params[:event][:user])[:user][:profile][:email] user_email = slack_client.users_info(user: params[:event][:user])[:user][:profile][:email]
conversation.account.users.find_by(email: user_email) conversation.account.users.from_email(user_email)
end end
def private_note? def private_note?

View File

@@ -0,0 +1,16 @@
require 'rubocop'
# Enforces use of from_email for email attribute lookups
class UseFromEmail < RuboCop::Cop::Cop
MSG = 'Use `from_email` for email lookups to ensure case insensitivity.'.freeze
def_node_matcher :find_by_email?, <<~PATTERN
(send _ :find_by (hash (pair (sym :email) _)))
PATTERN
def on_send(node)
return unless find_by_email?(node)
add_offense(node, message: MSG)
end
end

View File

@@ -88,7 +88,7 @@ class Seeders::AccountSeeder
end end
def create_conversation(contact_inbox:, conversation_data:) def create_conversation(contact_inbox:, conversation_data:)
assignee = User.find_by(email: conversation_data['assignee']) if conversation_data['assignee'].present? assignee = User.from_email(conversation_data['assignee']) if conversation_data['assignee'].present?
conversation = contact_inbox.conversations.create!(account: contact_inbox.inbox.account, contact: contact_inbox.contact, conversation = contact_inbox.conversations.create!(account: contact_inbox.inbox.account, contact: contact_inbox.contact,
inbox: contact_inbox.inbox, assignee: assignee) inbox: contact_inbox.inbox, assignee: assignee)
create_messages(conversation: conversation, messages: conversation_data['messages']) create_messages(conversation: conversation, messages: conversation_data['messages'])
@@ -111,7 +111,7 @@ class Seeders::AccountSeeder
if message_data['message_type'] == 'incoming' if message_data['message_type'] == 'incoming'
conversation.contact conversation.contact
elsif message_data['sender'].present? elsif message_data['sender'].present?
User.find_by(email: message_data['sender']) User.from_email(message_data['sender'])
end end
end end

View File

@@ -115,11 +115,6 @@ RSpec.describe 'Platform Users API', type: :request do
) )
) )
expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id'] expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id']
post '/platform/api/v1/users/', params: { name: 'test', email: 'TesT@test.com', password: 'Password1!' },
headers: { api_access_token: platform_app.access_token.token }, as: :json
data = response.parsed_body
expect(data['message']).to eq('Email has already been taken')
end end
it 'fetch existing user and creates permissible for the user' do it 'fetch existing user and creates permissible for the user' do

View File

@@ -111,7 +111,7 @@ RSpec.describe DataImportJob do
described_class.perform_now(existing_data_import) described_class.perform_now(existing_data_import)
expect(existing_data_import.account.contacts.count).to eq(csv_length) expect(existing_data_import.account.contacts.count).to eq(csv_length)
contact = Contact.find_by(email: csv_data[0]['email']) contact = Contact.from_email(csv_data[0]['email'])
expect(contact).to be_present expect(contact).to be_present
expect(contact.phone_number).to eq("+#{csv_data[0]['phone_number']}") expect(contact.phone_number).to eq("+#{csv_data[0]['phone_number']}")
expect(contact.name).to eq((csv_data[0]['name']).to_s) expect(contact.name).to eq((csv_data[0]['name']).to_s)