Merge branch 'develop' into fix/cw-6921-server-hmac-verification
This commit is contained in:
@@ -2,6 +2,8 @@ class Api::V1::Accounts::AssignableAgentsController < Api::V1::Accounts::BaseCon
|
||||
before_action :fetch_inboxes
|
||||
|
||||
def index
|
||||
# TODO: Remove this opt-in once mobile clients support AgentBot assignees in this payload.
|
||||
@include_agent_bots = params[:include_agent_bots].present?
|
||||
agent_ids = @inboxes.map do |inbox|
|
||||
authorize inbox, :show?
|
||||
member_ids = inbox.members.pluck(:user_id)
|
||||
@@ -10,6 +12,7 @@ class Api::V1::Accounts::AssignableAgentsController < Api::V1::Accounts::BaseCon
|
||||
agent_ids = agent_ids.inject(:&)
|
||||
agents = Current.account.users.where(id: agent_ids)
|
||||
@assignable_agents = (agents + Current.account.administrators).uniq
|
||||
@agent_bots = @include_agent_bots ? AgentBot.accessible_to(Current.account) : []
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -6,6 +6,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
page_access_token = params[:page_access_token]
|
||||
page_id = params[:page_id]
|
||||
inbox_name = params[:inbox_name]
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
facebook_channel = Current.account.facebook_pages.create!(
|
||||
page_id: page_id, user_access_token: user_access_token,
|
||||
@@ -15,6 +16,8 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
set_instagram_id(page_access_token, facebook_channel)
|
||||
set_avatar(@facebook_inbox, page_id)
|
||||
end
|
||||
rescue CustomExceptions::Inbox::LimitExceeded => e
|
||||
render_error_response(e)
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
Rails.logger.error "Error in register_facebook_page: #{e.message}"
|
||||
|
||||
@@ -66,6 +66,7 @@ class Api::V1::Accounts::Captain::PreferencesController < Api::V1::Accounts::Bas
|
||||
config = Llm::Models.feature_config(feature_key)
|
||||
route = Llm::FeatureRouter.resolve(feature: feature_key, account: Current.account)
|
||||
config.merge(
|
||||
default: default_model_for(feature_key),
|
||||
enabled: account_features[feature_key] == true,
|
||||
model: route[:model],
|
||||
selected: route[:model],
|
||||
@@ -74,4 +75,10 @@ class Api::V1::Accounts::Captain::PreferencesController < Api::V1::Accounts::Bas
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def default_model_for(feature_key)
|
||||
return Llm::FeatureRouter::CAPTAIN_V2_ASSISTANT_MODEL if feature_key == 'assistant' && Current.account.feature_enabled?('captain_integration_v2')
|
||||
|
||||
Llm::Models.default_model_for(feature_key)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,8 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
|
||||
def create
|
||||
process_create
|
||||
rescue CustomExceptions::Inbox::LimitExceeded => e
|
||||
render_error_response(e)
|
||||
rescue StandardError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
class Api::V1::Accounts::Conversations::ParticipantsController < Api::V1::Accounts::Conversations::BaseController
|
||||
include Events::Types
|
||||
|
||||
def show
|
||||
@participants = @conversation.conversation_participants
|
||||
end
|
||||
|
||||
def create
|
||||
participant_ids_to_add = participants_to_be_added_ids
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
@participants = participants_to_be_added_ids.map { |user_id| @conversation.conversation_participants.find_or_create_by(user_id: user_id) }
|
||||
@participants = participant_ids_to_add.map { |user_id| @conversation.conversation_participants.find_or_create_by(user_id: user_id) }
|
||||
end
|
||||
notify_unread_count_change if participant_ids_to_add.any?
|
||||
end
|
||||
|
||||
def update
|
||||
participant_ids_to_add = participants_to_be_added_ids
|
||||
participant_ids_to_remove = participants_to_be_removed_ids
|
||||
changed_participant_ids = participant_ids_to_add + participant_ids_to_remove
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
participants_to_be_added_ids.each { |user_id| @conversation.conversation_participants.find_or_create_by(user_id: user_id) }
|
||||
participants_to_be_removed_ids.each { |user_id| @conversation.conversation_participants.find_by(user_id: user_id)&.destroy }
|
||||
participant_ids_to_add.each { |user_id| @conversation.conversation_participants.find_or_create_by(user_id: user_id) }
|
||||
participant_ids_to_remove.each { |user_id| @conversation.conversation_participants.find_by(user_id: user_id)&.destroy }
|
||||
end
|
||||
notify_unread_count_change if changed_participant_ids.any?
|
||||
@participants = @conversation.conversation_participants
|
||||
render action: 'show'
|
||||
end
|
||||
|
||||
def destroy
|
||||
participant_ids_to_remove = current_participant_ids & params[:user_ids]
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
params[:user_ids].map { |user_id| @conversation.conversation_participants.find_by(user_id: user_id)&.destroy }
|
||||
end
|
||||
notify_unread_count_change if participant_ids_to_remove.any?
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -38,4 +51,11 @@ class Api::V1::Accounts::Conversations::ParticipantsController < Api::V1::Accoun
|
||||
def current_participant_ids
|
||||
@current_participant_ids ||= @conversation.conversation_participants.pluck(:user_id)
|
||||
end
|
||||
|
||||
def notify_unread_count_change
|
||||
return unless Current.account.feature_enabled?('conversation_unread_counts')
|
||||
return unless Current.account.feature_enabled?('unread_count_for_filters')
|
||||
|
||||
Rails.configuration.dispatcher.dispatch(CONVERSATION_UNREAD_COUNT_CHANGED, Time.zone.now, conversation: @conversation)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,12 +2,28 @@ class Api::V1::Accounts::Conversations::UnreadCountsController < Api::V1::Accoun
|
||||
before_action :ensure_unread_counts_enabled
|
||||
|
||||
def index
|
||||
counts = ::Conversations::UnreadCounts::Counter.new(account: Current.account, user: Current.user).perform
|
||||
counts = if filtered_unread_counts_enabled?
|
||||
instrumentation.summarize_request(account_id: Current.account.id) { unread_counts }
|
||||
else
|
||||
unread_counts
|
||||
end
|
||||
render json: { payload: counts }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unread_counts
|
||||
::Conversations::UnreadCounts::Counter.new(account: Current.account, user: Current.user).perform
|
||||
end
|
||||
|
||||
def filtered_unread_counts_enabled?
|
||||
Current.account.feature_enabled?(::Conversations::UnreadCounts::FilteredCounter::FEATURE_FLAG)
|
||||
end
|
||||
|
||||
def instrumentation
|
||||
::Conversations::UnreadCounts::FilteredCountInstrumentation
|
||||
end
|
||||
|
||||
def ensure_unread_counts_enabled
|
||||
return if Current.account.feature_enabled?('conversation_unread_counts')
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
|
||||
::Conversations::UnreadCounts::Notifier.new(@conversation).perform
|
||||
::Conversations::UnreadCounts::FilteredCountInvalidator.new(Current.account).conversation_changed!
|
||||
end
|
||||
|
||||
def should_update_last_seen?
|
||||
|
||||
@@ -2,7 +2,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
include Api::V1::InboxesHelper
|
||||
before_action :fetch_inbox, except: [:index, :create]
|
||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||
before_action :validate_limit, only: [:create]
|
||||
# we are already handling the authorization in fetch inbox
|
||||
before_action :check_authorization, except: [:show]
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts:
|
||||
validate_embedded_signup_params!
|
||||
channel = process_embedded_signup
|
||||
render_success_response(channel.inbox)
|
||||
rescue StandardError => e
|
||||
rescue CustomExceptions::Inbox::LimitExceeded => e
|
||||
render_error_response(e)
|
||||
rescue StandardError => e
|
||||
render_embedded_signup_error(e)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -55,7 +57,7 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts:
|
||||
render json: response
|
||||
end
|
||||
|
||||
def render_error_response(error)
|
||||
def render_embedded_signup_error(error)
|
||||
Rails.logger.error "[WHATSAPP AUTHORIZATION] Embedded signup error: #{error.message}"
|
||||
Rails.logger.error error.backtrace.join("\n")
|
||||
render json: {
|
||||
|
||||
@@ -2,6 +2,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
include WidgetHelper
|
||||
|
||||
before_action :validate_hmac, only: [:set_user]
|
||||
before_action :validate_hmac_for_identified_update, only: [:update]
|
||||
|
||||
def show; end
|
||||
|
||||
@@ -46,6 +47,16 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
@contact.identifier.present? && @contact.identifier != permitted_params[:identifier]
|
||||
end
|
||||
|
||||
# The plain update endpoint is also used for anonymous prechat updates
|
||||
# (name/email/phone/custom_attributes with no identifier), which must keep
|
||||
# working on hmac_mandatory inboxes. Only the identity-binding path, where an
|
||||
# identifier is supplied and the contact can be rebound, requires HMAC.
|
||||
def validate_hmac_for_identified_update
|
||||
return if params[:identifier].blank?
|
||||
|
||||
validate_hmac
|
||||
end
|
||||
|
||||
def validate_hmac
|
||||
return unless should_verify_hmac?
|
||||
|
||||
@@ -62,11 +73,15 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def valid_hmac?
|
||||
params[:identifier_hash] == OpenSSL::HMAC.hexdigest(
|
||||
expected_hash = OpenSSL::HMAC.hexdigest(
|
||||
'sha256',
|
||||
@web_widget.hmac_token,
|
||||
params[:identifier].to_s
|
||||
)
|
||||
identifier_hash = params[:identifier_hash].to_s
|
||||
return false unless identifier_hash.bytesize == expected_hash.bytesize
|
||||
|
||||
ActiveSupport::SecurityUtils.secure_compare(identifier_hash, expected_hash)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
module RequestExceptionHandler
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
QUERY_CANCELED_ERROR_MESSAGE_PATTERNS = [
|
||||
'ActiveRecord::QueryCanceled',
|
||||
'PG::QueryCanceled',
|
||||
'canceling statement due to statement timeout'
|
||||
].freeze
|
||||
|
||||
included do
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
||||
rescue_from CustomExceptions::Inbox::LimitExceeded, with: :render_error_response
|
||||
end
|
||||
|
||||
private
|
||||
@@ -18,6 +25,9 @@ module RequestExceptionHandler
|
||||
rescue ActionController::ParameterMissing => e
|
||||
log_handled_error(e)
|
||||
render_could_not_create_error(e.message)
|
||||
rescue ActiveRecord::QueryCanceled => e
|
||||
log_handled_error(e)
|
||||
render_could_not_create_error(database_query_canceled_message)
|
||||
ensure
|
||||
# to address the thread variable leak issues in Puma/Thin webserver
|
||||
Current.reset
|
||||
@@ -31,8 +41,8 @@ module RequestExceptionHandler
|
||||
render json: { error: message }, status: :not_found
|
||||
end
|
||||
|
||||
def render_could_not_create_error(message)
|
||||
render json: { error: message }, status: :unprocessable_entity
|
||||
def render_could_not_create_error(error)
|
||||
render json: { error: sanitized_error_message(error) }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def render_payment_required(message)
|
||||
@@ -59,4 +69,19 @@ module RequestExceptionHandler
|
||||
def log_handled_error(exception)
|
||||
logger.info("Handled error: #{exception.inspect}")
|
||||
end
|
||||
|
||||
def sanitized_error_message(message)
|
||||
return database_query_canceled_message if database_query_canceled_message?(message)
|
||||
|
||||
message
|
||||
end
|
||||
|
||||
def database_query_canceled_message?(message)
|
||||
error_message = message.to_s
|
||||
QUERY_CANCELED_ERROR_MESSAGE_PATTERNS.any? { |pattern| error_message.include?(pattern) }
|
||||
end
|
||||
|
||||
def database_query_canceled_message
|
||||
I18n.t('errors.database.query_canceled')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,6 +11,8 @@ class Instagram::CallbacksController < ApplicationController
|
||||
end
|
||||
|
||||
process_successful_authorization
|
||||
rescue CustomExceptions::Inbox::LimitExceeded => e
|
||||
handle_limit_error(e)
|
||||
rescue StandardError => e
|
||||
handle_error(e)
|
||||
end
|
||||
@@ -47,6 +49,14 @@ class Instagram::CallbacksController < ApplicationController
|
||||
redirect_to_error_page(error_info)
|
||||
end
|
||||
|
||||
def handle_limit_error(error)
|
||||
redirect_to_error_page(
|
||||
'error_type' => error.class.name,
|
||||
'code' => Rack::Utils.status_code(error.http_status),
|
||||
'error_message' => error.message
|
||||
)
|
||||
end
|
||||
|
||||
# Extract error details from the exception
|
||||
def extract_error_info(error)
|
||||
if error.is_a?(OAuth2::Error)
|
||||
|
||||
@@ -35,11 +35,15 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||
end
|
||||
|
||||
def valid_hmac?
|
||||
params[:identifier_hash] == OpenSSL::HMAC.hexdigest(
|
||||
expected_hash = OpenSSL::HMAC.hexdigest(
|
||||
'sha256',
|
||||
@inbox_channel.hmac_token,
|
||||
params[:identifier].to_s
|
||||
)
|
||||
identifier_hash = params[:identifier_hash].to_s
|
||||
return false unless identifier_hash.bytesize == expected_hash.bytesize
|
||||
|
||||
ActiveSupport::SecurityUtils.secure_compare(identifier_hash, expected_hash)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
||||
@@ -6,6 +6,8 @@ class Tiktok::CallbacksController < ApplicationController
|
||||
return handle_ungranted_scopes_error unless all_scopes_granted?
|
||||
|
||||
process_successful_authorization
|
||||
rescue CustomExceptions::Inbox::LimitExceeded => e
|
||||
handle_limit_error(e)
|
||||
rescue StandardError => e
|
||||
handle_error(e)
|
||||
end
|
||||
@@ -36,6 +38,14 @@ class Tiktok::CallbacksController < ApplicationController
|
||||
redirect_to_error_page(error_type: error.class.name, code: 500, error_message: error.message)
|
||||
end
|
||||
|
||||
def handle_limit_error(error)
|
||||
redirect_to_error_page(
|
||||
error_type: error.class.name,
|
||||
code: Rack::Utils.status_code(error.http_status),
|
||||
error_message: error.message
|
||||
)
|
||||
end
|
||||
|
||||
# Handles the case when a user denies permissions or cancels the authorization flow
|
||||
def handle_authorization_error
|
||||
redirect_to_error_page(
|
||||
|
||||
Reference in New Issue
Block a user