diff --git a/.github/workflows/run_mfa_spec.yml b/.github/workflows/run_mfa_spec.yml index 61b406f8a..69d019cc9 100644 --- a/.github/workflows/run_mfa_spec.yml +++ b/.github/workflows/run_mfa_spec.yml @@ -70,6 +70,7 @@ jobs: spec/services/mfa/authentication_service_spec.rb \ spec/requests/api/v1/profile/mfa_controller_spec.rb \ spec/controllers/devise_overrides/sessions_controller_spec.rb \ + spec/models/application_record_external_credentials_encryption_spec.rb \ --profile=10 \ --format documentation env: diff --git a/.rubocop.yml b/.rubocop.yml index e30a71ee9..ea688792b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,7 +23,7 @@ Metrics/MethodLength: - 'enterprise/lib/captain/agent.rb' RSpec/ExampleLength: - Max: 25 + Max: 50 Style/Documentation: Enabled: false @@ -336,4 +336,4 @@ FactoryBot/RedundantFactoryOption: Enabled: false FactoryBot/FactoryAssociationWithStrategy: - Enabled: false \ No newline at end of file + Enabled: false diff --git a/Gemfile.lock b/Gemfile.lock index 53632b607..005342452 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -647,7 +647,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.0) + rack (3.2.3) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-contrib (2.5.0) @@ -941,7 +941,7 @@ GEM unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) uniform_notifier (1.17.0) - uri (1.0.3) + uri (1.0.4) uri_template (0.7.0) valid_email2 (5.2.6) activemodel (>= 3.2) diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index 86bcee54e..12a74ed9c 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -178,7 +178,13 @@ class Messages::MessageBuilder email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {}) normalized_content = normalize_email_body(@message.content) - email_attributes[:html_content] = build_html_content(normalized_content) + # Use custom HTML content if provided, otherwise generate from message content + email_attributes[:html_content] = if custom_email_content_provided? + build_custom_html_content + else + build_html_content(normalized_content) + end + email_attributes[:text_content] = build_text_content(normalized_content) email_attributes end @@ -213,4 +219,17 @@ class Messages::MessageBuilder ChatwootMarkdownRenderer.new(content).render_message.to_s end + + def custom_email_content_provided? + @params[:email_html_content].present? + end + + def build_custom_html_content + html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {}) + + html_content[:full] = @params[:email_html_content] + html_content[:reply] = @params[:email_html_content] + + html_content + end end diff --git a/app/builders/v2/reports/base_summary_builder.rb b/app/builders/v2/reports/base_summary_builder.rb index 4de65926d..d4a9e7c0b 100644 --- a/app/builders/v2/reports/base_summary_builder.rb +++ b/app/builders/v2/reports/base_summary_builder.rb @@ -10,10 +10,28 @@ class V2::Reports::BaseSummaryBuilder def load_data @conversations_count = fetch_conversations_count - @resolved_count = fetch_resolved_count - @avg_resolution_time = fetch_average_time('conversation_resolved') - @avg_first_response_time = fetch_average_time('first_response') - @avg_reply_time = fetch_average_time('reply_time') + load_reporting_events_data + end + + def load_reporting_events_data + # Extract the column name for indexing (e.g., 'conversations.team_id' -> 'team_id') + index_key = group_by_key.to_s.split('.').last + + results = reporting_events + .select( + "#{group_by_key} as #{index_key}", + "COUNT(CASE WHEN name = 'conversation_resolved' THEN 1 END) as resolved_count", + "AVG(CASE WHEN name = 'conversation_resolved' THEN #{average_value_key} END) as avg_resolution_time", + "AVG(CASE WHEN name = 'first_response' THEN #{average_value_key} END) as avg_first_response_time", + "AVG(CASE WHEN name = 'reply_time' THEN #{average_value_key} END) as avg_reply_time" + ) + .group(group_by_key) + .index_by { |record| record.public_send(index_key) } + + @resolved_count = results.transform_values(&:resolved_count) + @avg_resolution_time = results.transform_values(&:avg_resolution_time) + @avg_first_response_time = results.transform_values(&:avg_first_response_time) + @avg_reply_time = results.transform_values(&:avg_reply_time) end def reporting_events @@ -24,14 +42,6 @@ class V2::Reports::BaseSummaryBuilder # Override this method end - def fetch_average_time(event_name) - get_grouped_average(reporting_events.where(name: event_name)) - end - - def fetch_resolved_count - reporting_events.where(name: 'conversation_resolved').group(group_by_key).count - end - def group_by_key # Override this method end @@ -40,10 +50,6 @@ class V2::Reports::BaseSummaryBuilder # Override this method end - def get_grouped_average(events) - events.group(group_by_key).average(average_value_key) - end - def average_value_key ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value end diff --git a/app/builders/v2/reports/inbox_summary_builder.rb b/app/builders/v2/reports/inbox_summary_builder.rb index e27385856..935afeb82 100644 --- a/app/builders/v2/reports/inbox_summary_builder.rb +++ b/app/builders/v2/reports/inbox_summary_builder.rb @@ -13,10 +13,7 @@ class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder def load_data @conversations_count = fetch_conversations_count - @resolved_count = fetch_resolved_count - @avg_resolution_time = fetch_average_time('conversation_resolved') - @avg_first_response_time = fetch_average_time('first_response') - @avg_reply_time = fetch_average_time('reply_time') + load_reporting_events_data end def fetch_conversations_count diff --git a/app/controllers/api/v1/accounts/conversations/base_controller.rb b/app/controllers/api/v1/accounts/conversations/base_controller.rb index 500c7772f..223530e27 100644 --- a/app/controllers/api/v1/accounts/conversations/base_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/base_controller.rb @@ -5,6 +5,6 @@ class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::Base def conversation @conversation ||= Current.account.conversations.find_by!(display_id: params[:conversation_id]) - authorize @conversation.inbox, :show? + authorize @conversation, :show? end end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index e27869d82..4301eaa4a 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -160,7 +160,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro def conversation @conversation ||= Current.account.conversations.find_by!(display_id: params[:id]) - authorize @conversation.inbox, :show? + authorize @conversation, :show? end def inbox diff --git a/app/controllers/api/v1/accounts/integrations/dyte_controller.rb b/app/controllers/api/v1/accounts/integrations/dyte_controller.rb index c5f795d34..845caab5e 100644 --- a/app/controllers/api/v1/accounts/integrations/dyte_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/dyte_controller.rb @@ -22,7 +22,7 @@ class Api::V1::Accounts::Integrations::DyteController < Api::V1::Accounts::BaseC private def authorize_request - authorize @conversation.inbox, :show? + authorize @conversation, :show? end def render_response(response) diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index 9b0f9021f..338b290da 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -14,6 +14,7 @@ module AccessTokenAuthHelper ensure_access_token render_unauthorized('Invalid Access Token') && return if @access_token.blank? + # NOTE: This ensures that current_user is set and available for the rest of the controller actions @resource = @access_token.owner Current.user = @resource if allowed_current_user_type?(@resource) end diff --git a/app/controllers/devise_overrides/omniauth_callbacks_controller.rb b/app/controllers/devise_overrides/omniauth_callbacks_controller.rb index fd3dba87c..900125670 100644 --- a/app/controllers/devise_overrides/omniauth_callbacks_controller.rb +++ b/app/controllers/devise_overrides/omniauth_callbacks_controller.rb @@ -19,6 +19,19 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa redirect_to login_page_url(email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token) end + def sign_in_user_on_mobile + @resource.skip_confirmation! if confirmable_enabled? + + # once the resource is found and verified + # we can just send them to the login page again with the SSO params + # that will log them in + encoded_email = ERB::Util.url_encode(@resource.email) + params = { email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token }.to_query + + mobile_deep_link_base = GlobalConfigService.load('MOBILE_DEEP_LINK_BASE', 'chatwootapp') + redirect_to "#{mobile_deep_link_base}://auth/saml?#{params}", allow_other_host: true + end + def sign_up_user return redirect_to login_page_url(error: 'no-account-found') unless account_signup_allowed? return redirect_to login_page_url(error: 'business-account-only') unless validate_signup_email_is_business_domain? diff --git a/app/javascript/dashboard/api/captain/customTools.js b/app/javascript/dashboard/api/captain/customTools.js new file mode 100644 index 000000000..d0818d941 --- /dev/null +++ b/app/javascript/dashboard/api/captain/customTools.js @@ -0,0 +1,36 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainCustomTools extends ApiClient { + constructor() { + super('captain/custom_tools', { accountScoped: true }); + } + + get({ page = 1, searchKey } = {}) { + return axios.get(this.url, { + params: { page, searchKey }, + }); + } + + show(id) { + return axios.get(`${this.url}/${id}`); + } + + create(data = {}) { + return axios.post(this.url, { + custom_tool: data, + }); + } + + update(id, data = {}) { + return axios.put(`${this.url}/${id}`, { + custom_tool: data, + }); + } + + delete(id) { + return axios.delete(`${this.url}/${id}`); + } +} + +export default new CaptainCustomTools(); diff --git a/app/javascript/dashboard/api/changelog.js b/app/javascript/dashboard/api/changelog.js new file mode 100644 index 000000000..8cf0cdea1 --- /dev/null +++ b/app/javascript/dashboard/api/changelog.js @@ -0,0 +1,16 @@ +import axios from 'axios'; +import ApiClient from './ApiClient'; +import { CHANGELOG_API_URL } from 'shared/constants/links'; + +class ChangelogApi extends ApiClient { + constructor() { + super('changelog', { apiVersion: 'v1' }); + } + + // eslint-disable-next-line class-methods-use-this + fetchFromHub() { + return axios.get(CHANGELOG_API_URL); + } +} + +export default new ChangelogApi(); diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue index 31e18394f..8d67344e1 100644 --- a/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue @@ -10,6 +10,10 @@ const props = defineProps({ type: String, required: true, }, + translationKey: { + type: String, + required: true, + }, entity: { type: Object, required: true, @@ -25,7 +29,9 @@ const emit = defineEmits(['deleteSuccess']); const { t } = useI18n(); const store = useStore(); const deleteDialogRef = ref(null); -const i18nKey = computed(() => props.type.toUpperCase()); +const i18nKey = computed(() => { + return props.translationKey || props.type.toUpperCase(); +}); const deleteEntity = async payload => { if (!payload) return; diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/AuthConfig.vue b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/AuthConfig.vue new file mode 100644 index 000000000..208a94dba --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/AuthConfig.vue @@ -0,0 +1,73 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CreateCustomToolDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CreateCustomToolDialog.vue new file mode 100644 index 000000000..0745c6546 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CreateCustomToolDialog.vue @@ -0,0 +1,87 @@ + + +