Merge branch 'develop' into feat/email-forwarding
This commit is contained in:
@@ -151,13 +151,36 @@ class AutomationRules::ConditionsFilterService < FilterService
|
||||
" #{table_name}.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
|
||||
when 'standard'
|
||||
if attribute_key == 'labels'
|
||||
" tags.id #{filter_operator_value} #{query_operator} "
|
||||
build_label_query_string(query_hash, current_index, query_operator)
|
||||
else
|
||||
" #{table_name}.#{attribute_key} #{filter_operator_value} #{query_operator} "
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_label_query_string(query_hash, current_index, query_operator)
|
||||
case query_hash['filter_operator']
|
||||
when 'equal_to'
|
||||
return " 1=0 #{query_operator} " if query_hash['values'].blank?
|
||||
|
||||
value_placeholder = "value_#{current_index}"
|
||||
@filter_values[value_placeholder] = query_hash['values'].first
|
||||
" tags.name = :#{value_placeholder} #{query_operator} "
|
||||
when 'not_equal_to'
|
||||
return " 1=0 #{query_operator} " if query_hash['values'].blank?
|
||||
|
||||
value_placeholder = "value_#{current_index}"
|
||||
@filter_values[value_placeholder] = query_hash['values'].first
|
||||
" tags.name != :#{value_placeholder} #{query_operator} "
|
||||
when 'is_present'
|
||||
" tags.id IS NOT NULL #{query_operator} "
|
||||
when 'is_not_present'
|
||||
" tags.id IS NULL #{query_operator} "
|
||||
else
|
||||
" tags.id #{filter_operation(query_hash, current_index)} #{query_operator} "
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_relation
|
||||
@@ -166,7 +189,21 @@ class AutomationRules::ConditionsFilterService < FilterService
|
||||
).joins(
|
||||
'LEFT OUTER JOIN messages on messages.conversation_id = conversations.id'
|
||||
)
|
||||
|
||||
# Only add label joins when label conditions exist
|
||||
if label_conditions?
|
||||
records = records.joins(
|
||||
'LEFT OUTER JOIN taggings ON taggings.taggable_id = conversations.id AND taggings.taggable_type = \'Conversation\''
|
||||
).joins(
|
||||
'LEFT OUTER JOIN tags ON taggings.tag_id = tags.id'
|
||||
)
|
||||
end
|
||||
|
||||
records = records.where(messages: { id: @options[:message].id }) if @options[:message].present?
|
||||
records
|
||||
end
|
||||
|
||||
def label_conditions?
|
||||
@rule.conditions.any? { |condition| condition['attribute_key'] == 'labels' }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
class BaseTokenService
|
||||
pattr_initialize [:payload, :token]
|
||||
|
||||
def generate_token
|
||||
JWT.encode(token_payload, secret_key, algorithm)
|
||||
end
|
||||
|
||||
def decode_token
|
||||
JWT.decode(token, secret_key, true, algorithm: algorithm).first.symbolize_keys
|
||||
rescue JWT::ExpiredSignature, JWT::DecodeError
|
||||
{}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def token_payload
|
||||
payload || {}
|
||||
end
|
||||
|
||||
def secret_key
|
||||
Rails.application.secret_key_base
|
||||
end
|
||||
|
||||
def algorithm
|
||||
'HS256'
|
||||
end
|
||||
end
|
||||
@@ -76,7 +76,8 @@ class Line::IncomingMessageService
|
||||
|
||||
response = inbox.channel.client.get_message_content(message['id'])
|
||||
|
||||
file_name = "media-#{message['id']}.#{response.content_type.split('/')[1]}"
|
||||
extension = get_file_extension(response)
|
||||
file_name = message['fileName'] || "media-#{message['id']}.#{extension}"
|
||||
temp_file = Tempfile.new(file_name)
|
||||
temp_file.binmode
|
||||
temp_file << response.body
|
||||
@@ -93,12 +94,25 @@ class Line::IncomingMessageService
|
||||
)
|
||||
end
|
||||
|
||||
def get_file_extension(response)
|
||||
if response.content_type&.include?('/')
|
||||
response.content_type.split('/')[1]
|
||||
else
|
||||
'bin'
|
||||
end
|
||||
end
|
||||
|
||||
def event_type_message?(event)
|
||||
event['type'] == 'message' || event['type'] == 'sticker'
|
||||
end
|
||||
|
||||
def message_type_non_text?(type)
|
||||
[Line::Bot::Event::MessageType::Video, Line::Bot::Event::MessageType::Audio, Line::Bot::Event::MessageType::Image].include?(type)
|
||||
[
|
||||
Line::Bot::Event::MessageType::Video,
|
||||
Line::Bot::Event::MessageType::Audio,
|
||||
Line::Bot::Event::MessageType::Image,
|
||||
Line::Bot::Event::MessageType::File
|
||||
].include?(type)
|
||||
end
|
||||
|
||||
def account
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
class Mfa::AuthenticationService
|
||||
pattr_initialize [:user!, :otp_code, :backup_code]
|
||||
|
||||
def authenticate
|
||||
return false unless user
|
||||
|
||||
return authenticate_with_otp if otp_code.present?
|
||||
return authenticate_with_backup_code if backup_code.present?
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authenticate_with_otp
|
||||
user.validate_and_consume_otp!(otp_code)
|
||||
end
|
||||
|
||||
def authenticate_with_backup_code
|
||||
mfa_service = Mfa::ManagementService.new(user: user)
|
||||
mfa_service.validate_backup_code!(backup_code)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
class Mfa::ManagementService
|
||||
pattr_initialize [:user!]
|
||||
|
||||
def enable_two_factor!
|
||||
user.otp_secret = User.generate_otp_secret
|
||||
user.save!
|
||||
end
|
||||
|
||||
def disable_two_factor!
|
||||
user.otp_secret = nil
|
||||
user.otp_required_for_login = false
|
||||
user.otp_backup_codes = nil
|
||||
user.save!
|
||||
end
|
||||
|
||||
def verify_and_activate!
|
||||
ActiveRecord::Base.transaction do
|
||||
user.update!(otp_required_for_login: true)
|
||||
backup_codes_generated? ? nil : generate_backup_codes!
|
||||
end
|
||||
end
|
||||
|
||||
def two_factor_provisioning_uri
|
||||
return nil if user.otp_secret.blank?
|
||||
|
||||
issuer = 'Chatwoot'
|
||||
label = user.email
|
||||
user.otp_provisioning_uri(label, issuer: issuer)
|
||||
end
|
||||
|
||||
def generate_backup_codes!
|
||||
codes = Array.new(10) { SecureRandom.hex(4).upcase }
|
||||
user.otp_backup_codes = codes
|
||||
user.save!
|
||||
codes
|
||||
end
|
||||
|
||||
def validate_backup_code!(code)
|
||||
return false unless valid_backup_code_input?(code)
|
||||
|
||||
codes = user.otp_backup_codes
|
||||
found_index = find_matching_code_index(codes, code)
|
||||
|
||||
return false if found_index.nil?
|
||||
|
||||
mark_code_as_used(codes, found_index)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_backup_code_input?(code)
|
||||
user.otp_backup_codes.present? && code.present?
|
||||
end
|
||||
|
||||
def find_matching_code_index(codes, code)
|
||||
found_index = nil
|
||||
|
||||
# Constant-time comparison to prevent timing attacks
|
||||
codes.each_with_index do |stored_code, idx|
|
||||
is_match = ActiveSupport::SecurityUtils.secure_compare(stored_code, code)
|
||||
is_unused = stored_code != 'XXXXXXXX'
|
||||
found_index = idx if is_match && is_unused
|
||||
end
|
||||
|
||||
found_index
|
||||
end
|
||||
|
||||
def mark_code_as_used(codes, index)
|
||||
codes[index] = 'XXXXXXXX'
|
||||
user.otp_backup_codes = codes
|
||||
user.save!
|
||||
true
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
def backup_codes_generated?
|
||||
user.otp_backup_codes.present?
|
||||
end
|
||||
|
||||
def mfa_enabled?
|
||||
user.otp_required_for_login?
|
||||
end
|
||||
|
||||
def two_factor_setup_pending?
|
||||
user.otp_secret.present? && !user.otp_required_for_login?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
class Mfa::TokenService < BaseTokenService
|
||||
pattr_initialize [:user, :token]
|
||||
|
||||
MFA_TOKEN_EXPIRY = 5.minutes
|
||||
|
||||
def generate_token
|
||||
@payload = build_payload
|
||||
super
|
||||
end
|
||||
|
||||
def verify_token
|
||||
decoded = decode_token
|
||||
return nil if decoded.blank?
|
||||
|
||||
User.find(decoded[:user_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_payload
|
||||
{
|
||||
user_id: user.id,
|
||||
exp: MFA_TOKEN_EXPIRY.from_now.to_i
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -30,12 +30,12 @@ class Whatsapp::PopulateTemplateParametersService
|
||||
end
|
||||
end
|
||||
|
||||
def build_media_parameter(url, media_type)
|
||||
def build_media_parameter(url, media_type, media_name = nil)
|
||||
return nil if url.blank?
|
||||
|
||||
sanitized_url = sanitize_parameter(url)
|
||||
validate_url(sanitized_url)
|
||||
build_media_type_parameter(sanitized_url, media_type.downcase)
|
||||
build_media_type_parameter(sanitized_url, media_type.downcase, media_name)
|
||||
end
|
||||
|
||||
def build_named_parameter(parameter_name, value)
|
||||
@@ -89,14 +89,14 @@ class Whatsapp::PopulateTemplateParametersService
|
||||
}
|
||||
end
|
||||
|
||||
def build_media_type_parameter(sanitized_url, media_type)
|
||||
def build_media_type_parameter(sanitized_url, media_type, media_name = nil)
|
||||
case media_type
|
||||
when 'image'
|
||||
build_image_parameter(sanitized_url)
|
||||
when 'video'
|
||||
build_video_parameter(sanitized_url)
|
||||
when 'document'
|
||||
build_document_parameter(sanitized_url)
|
||||
build_document_parameter(sanitized_url, media_name)
|
||||
else
|
||||
raise ArgumentError, "Unsupported media type: #{media_type}"
|
||||
end
|
||||
@@ -110,8 +110,11 @@ class Whatsapp::PopulateTemplateParametersService
|
||||
{ type: 'video', video: { link: url } }
|
||||
end
|
||||
|
||||
def build_document_parameter(url)
|
||||
{ type: 'document', document: { link: url } }
|
||||
def build_document_parameter(url, media_name = nil)
|
||||
document_params = { link: url }
|
||||
document_params[:filename] = media_name if media_name.present?
|
||||
|
||||
{ type: 'document', document: document_params }
|
||||
end
|
||||
|
||||
def rich_formatting?(text)
|
||||
|
||||
@@ -141,7 +141,11 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||
# {
|
||||
# processed_params: {
|
||||
# body: { '1': 'John', '2': '123 Main St' },
|
||||
# header: { media_url: 'https://...', media_type: 'image' },
|
||||
# header: {
|
||||
# media_url: 'https://...',
|
||||
# media_type: 'image',
|
||||
# media_name: 'filename.pdf' # Optional, for document templates only
|
||||
# },
|
||||
# buttons: [{ type: 'url', parameter: 'otp123456' }]
|
||||
# }
|
||||
# }
|
||||
|
||||
@@ -60,9 +60,10 @@ class Whatsapp::TemplateProcessorService
|
||||
next if value.blank?
|
||||
|
||||
if media_url_with_type?(key, header_data)
|
||||
media_param = parameter_builder.build_media_parameter(value, header_data['media_type'])
|
||||
media_name = header_data['media_name']
|
||||
media_param = parameter_builder.build_media_parameter(value, header_data['media_type'], media_name)
|
||||
header_params << media_param if media_param
|
||||
elsif key != 'media_type'
|
||||
elsif key != 'media_type' && key != 'media_name'
|
||||
header_params << parameter_builder.build_parameter(value)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
class Widget::TokenService
|
||||
class Widget::TokenService < BaseTokenService
|
||||
DEFAULT_EXPIRY_DAYS = 180
|
||||
|
||||
pattr_initialize [:payload, :token]
|
||||
|
||||
def generate_token
|
||||
JWT.encode payload_with_expiry, secret_key, 'HS256'
|
||||
end
|
||||
|
||||
def decode_token
|
||||
JWT.decode(
|
||||
token, secret_key, true, algorithm: 'HS256'
|
||||
).first.symbolize_keys
|
||||
rescue StandardError
|
||||
{}
|
||||
JWT.encode(token_payload, secret_key, algorithm)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def payload_with_expiry
|
||||
payload.merge(exp: exp, iat: iat)
|
||||
def token_payload
|
||||
(payload || {}).merge(exp: exp, iat: iat)
|
||||
end
|
||||
|
||||
def iat
|
||||
@@ -34,8 +24,4 @@ class Widget::TokenService
|
||||
token_expiry_value = InstallationConfig.find_by(name: 'WIDGET_TOKEN_EXPIRY')&.value
|
||||
(token_expiry_value.presence || DEFAULT_EXPIRY_DAYS).to_i
|
||||
end
|
||||
|
||||
def secret_key
|
||||
Rails.application.secret_key_base
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user