From f6dbbf0d90378dbf7aee97c5aa51e0b5eca7703c Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 18 Jun 2025 17:39:06 +0530 Subject: [PATCH 01/70] refactor: use state-based authentication (#11690) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Muhsin Keloth --- .../google/authorizations_controller.rb | 15 ++-------- .../instagram/authorizations_controller.rb | 9 +----- .../microsoft/authorizations_controller.rb | 15 ++-------- .../oauth_authorization_controller.rb | 23 ++++++++++++++ app/controllers/concerns/google_concern.rb | 4 +-- app/controllers/concerns/microsoft_concern.rb | 4 +-- app/controllers/oauth_callback_controller.rb | 16 +++++----- .../inbox/channels/emailChannels/Google.vue | 1 - .../channels/emailChannels/Microsoft.vue | 1 - .../channels/emailChannels/OAuthChannel.vue | 14 +-------- .../google/authorization_controller_spec.rb | 27 +++++++++-------- .../authorization_controller_spec.rb | 30 +++++++++++-------- .../google/callbacks_controller_spec.rb | 17 ++++------- .../microsoft/callbacks_controller_spec.rb | 17 ++++------- 14 files changed, 85 insertions(+), 108 deletions(-) create mode 100644 app/controllers/api/v1/accounts/oauth_authorization_controller.rb diff --git a/app/controllers/api/v1/accounts/google/authorizations_controller.rb b/app/controllers/api/v1/accounts/google/authorizations_controller.rb index 1140a214b..87a7cfa3f 100644 --- a/app/controllers/api/v1/accounts/google/authorizations_controller.rb +++ b/app/controllers/api/v1/accounts/google/authorizations_controller.rb @@ -1,32 +1,23 @@ -class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::BaseController +class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController include GoogleConcern - before_action :check_authorization def create - email = params[:authorization][:email] redirect_url = google_client.auth_code.authorize_url( { redirect_uri: "#{base_url}/google/callback", - scope: 'email profile https://mail.google.com/', + scope: scope, response_type: 'code', prompt: 'consent', # the oauth flow does not return a refresh token, this is supposed to fix it access_type: 'offline', # the default is 'online' + state: state, client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil) } ) if redirect_url - cache_key = "google::#{email.downcase}" - ::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes) render json: { success: true, url: redirect_url } else render json: { success: false }, status: :unprocessable_entity end end - - private - - def check_authorization - raise Pundit::NotAuthorizedError unless Current.account_user.administrator? - end end diff --git a/app/controllers/api/v1/accounts/instagram/authorizations_controller.rb b/app/controllers/api/v1/accounts/instagram/authorizations_controller.rb index eace4411a..053c29731 100644 --- a/app/controllers/api/v1/accounts/instagram/authorizations_controller.rb +++ b/app/controllers/api/v1/accounts/instagram/authorizations_controller.rb @@ -1,7 +1,6 @@ -class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts::BaseController +class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController include InstagramConcern include Instagram::IntegrationHelper - before_action :check_authorization def create # https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#step-1--get-authorization @@ -21,10 +20,4 @@ class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts render json: { success: false }, status: :unprocessable_entity end end - - private - - def check_authorization - raise Pundit::NotAuthorizedError unless Current.account_user.administrator? - end end diff --git a/app/controllers/api/v1/accounts/microsoft/authorizations_controller.rb b/app/controllers/api/v1/accounts/microsoft/authorizations_controller.rb index df563094a..a300b5f59 100644 --- a/app/controllers/api/v1/accounts/microsoft/authorizations_controller.rb +++ b/app/controllers/api/v1/accounts/microsoft/authorizations_controller.rb @@ -1,28 +1,19 @@ -class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::BaseController +class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController include MicrosoftConcern - before_action :check_authorization def create - email = params[:authorization][:email] redirect_url = microsoft_client.auth_code.authorize_url( { redirect_uri: "#{base_url}/microsoft/callback", - scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile', + scope: scope, + state: state, prompt: 'consent' } ) if redirect_url - cache_key = "microsoft::#{email.downcase}" - ::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes) render json: { success: true, url: redirect_url } else render json: { success: false }, status: :unprocessable_entity end end - - private - - def check_authorization - raise Pundit::NotAuthorizedError unless Current.account_user.administrator? - end end diff --git a/app/controllers/api/v1/accounts/oauth_authorization_controller.rb b/app/controllers/api/v1/accounts/oauth_authorization_controller.rb new file mode 100644 index 000000000..feb218b59 --- /dev/null +++ b/app/controllers/api/v1/accounts/oauth_authorization_controller.rb @@ -0,0 +1,23 @@ +class Api::V1::Accounts::OauthAuthorizationController < Api::V1::Accounts::BaseController + before_action :check_authorization + + protected + + def scope + '' + end + + def state + Current.account.to_sgid(expires_in: 15.minutes).to_s + end + + def base_url + ENV.fetch('FRONTEND_URL', 'http://localhost:3000') + end + + private + + def check_authorization + raise Pundit::NotAuthorizedError unless Current.account_user.administrator? + end +end diff --git a/app/controllers/concerns/google_concern.rb b/app/controllers/concerns/google_concern.rb index 474b14aec..13de7ced3 100644 --- a/app/controllers/concerns/google_concern.rb +++ b/app/controllers/concerns/google_concern.rb @@ -14,7 +14,7 @@ module GoogleConcern private - def base_url - ENV.fetch('FRONTEND_URL', 'http://localhost:3000') + def scope + 'email profile https://mail.google.com/' end end diff --git a/app/controllers/concerns/microsoft_concern.rb b/app/controllers/concerns/microsoft_concern.rb index 507b9f8a3..c3e0994bd 100644 --- a/app/controllers/concerns/microsoft_concern.rb +++ b/app/controllers/concerns/microsoft_concern.rb @@ -15,7 +15,7 @@ module MicrosoftConcern private - def base_url - ENV.fetch('FRONTEND_URL', 'http://localhost:3000') + def scope + 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile email' end end diff --git a/app/controllers/oauth_callback_controller.rb b/app/controllers/oauth_callback_controller.rb index 9aa73956a..be0fa5008 100644 --- a/app/controllers/oauth_callback_controller.rb +++ b/app/controllers/oauth_callback_controller.rb @@ -6,7 +6,6 @@ class OauthCallbackController < ApplicationController ) handle_response - ::Redis::Alfred.delete(cache_key) rescue StandardError => e ChatwootExceptionTracker.new(e).capture_exception redirect_to '/' @@ -64,10 +63,6 @@ class OauthCallbackController < ApplicationController raise NotImplementedError end - def cache_key - "#{provider_name}::#{users_data['email'].downcase}" - end - def create_channel_with_inbox ActiveRecord::Base.transaction do channel_email = Channel::Email.create!(email: users_data['email'], account: account) @@ -86,12 +81,17 @@ class OauthCallbackController < ApplicationController decoded_token[0] end - def account_id - ::Redis::Alfred.get(cache_key) + def account_from_signed_id + raise ActionController::BadRequest, 'Missing state variable' if params[:state].blank? + + account = GlobalID::Locator.locate_signed(params[:state]) + raise 'Invalid or expired state' if account.nil? + + account end def account - @account ||= Account.find(account_id) + @account ||= account_from_signed_id end # Fallback name, for when name field is missing from users_data diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Google.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Google.vue index 1628d3e62..a5c16fa57 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Google.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Google.vue @@ -12,7 +12,6 @@ defineOptions({ provider="google" :title="$t('INBOX_MGMT.ADD.GOOGLE.TITLE')" :description="$t('INBOX_MGMT.ADD.GOOGLE.DESCRIPTION')" - :input-placeholder="$t('INBOX_MGMT.ADD.GOOGLE.EMAIL_PLACEHOLDER')" :submit-button-text="$t('INBOX_MGMT.ADD.GOOGLE.SIGN_IN')" :error-message="$t('INBOX_MGMT.ADD.GOOGLE.ERROR_MESSAGE')" /> diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Microsoft.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Microsoft.vue index 1e3706297..53cf5ccc7 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Microsoft.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Microsoft.vue @@ -12,7 +12,6 @@ defineOptions({ provider="microsoft" :title="$t('INBOX_MGMT.ADD.MICROSOFT.TITLE')" :description="$t('INBOX_MGMT.ADD.MICROSOFT.DESCRIPTION')" - :input-placeholder="$t('INBOX_MGMT.ADD.MICROSOFT.EMAIL_PLACEHOLDER')" :submit-button-text="$t('INBOX_MGMT.ADD.MICROSOFT.SIGN_IN')" :error-message="$t('INBOX_MGMT.ADD.MICROSOFT.ERROR_MESSAGE')" /> diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/OAuthChannel.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/OAuthChannel.vue index eb109c6f2..f2bfb63f3 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/OAuthChannel.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/OAuthChannel.vue @@ -30,14 +30,9 @@ const props = defineProps({ type: String, required: true, }, - inputPlaceholder: { - type: String, - required: true, - }, }); const isRequestingAuthorization = ref(false); -const email = ref(''); const client = computed(() => { if (props.provider === 'microsoft') { @@ -50,9 +45,7 @@ const client = computed(() => { async function requestAuthorization() { try { isRequestingAuthorization.value = true; - const response = await client.value.generateAuthorization({ - email: email.value, - }); + const response = await client.value.generateAuthorization(); const { data: { url }, } = response; @@ -75,11 +68,6 @@ async function requestAuthorization() { :header-content="description" />
- "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" }) .to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' }) - get google_callback_url, params: { code: code } + get google_callback_url, params: { code: code, state: state } expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id) expect(account.inboxes.count).to be 1 @@ -36,7 +32,6 @@ RSpec.describe 'Google::CallbacksController', type: :request do expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on') expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token] expect(inbox.channel.imap_address).to eq 'imap.gmail.com' - expect(Redis::Alfred.get(cache_key)).to be_nil end it 'updates inbox channel config if inbox exists with imap_login and authentication is successful' do @@ -49,14 +44,13 @@ RSpec.describe 'Google::CallbacksController', type: :request do 'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" }) .to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' }) - get google_callback_url, params: { code: code } + get google_callback_url, params: { code: code, state: state } expect(response).to redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: inbox.id) expect(account.inboxes.count).to be 1 expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on') expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token] expect(inbox.channel.imap_address).to eq 'imap.gmail.com' - expect(Redis::Alfred.get(cache_key)).to be_nil end it 'creates inboxes with fallback_name when account name is not present in id_token' do @@ -65,7 +59,7 @@ RSpec.describe 'Google::CallbacksController', type: :request do 'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" }) .to_return(status: 200, body: response_body_success_without_name.to_json, headers: { 'Content-Type' => 'application/json' }) - get google_callback_url, params: { code: code } + get google_callback_url, params: { code: code, state: state } expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id) expect(account.inboxes.count).to be 1 @@ -79,10 +73,9 @@ RSpec.describe 'Google::CallbacksController', type: :request do 'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" }) .to_return(status: 401) - get google_callback_url, params: { code: code } + get google_callback_url, params: { code: code, state: state } expect(response).to redirect_to '/' - expect(Redis::Alfred.get(cache_key).to_i).to eq account.id end end end diff --git a/spec/controllers/microsoft/callbacks_controller_spec.rb b/spec/controllers/microsoft/callbacks_controller_spec.rb index 41d8dbd29..6bd9a0583 100644 --- a/spec/controllers/microsoft/callbacks_controller_spec.rb +++ b/spec/controllers/microsoft/callbacks_controller_spec.rb @@ -4,11 +4,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do let(:account) { create(:account) } let(:code) { SecureRandom.hex(10) } let(:email) { Faker::Internet.email } - let(:cache_key) { "microsoft::#{email.downcase}" } - - before do - Redis::Alfred.set(cache_key, account.id) - end + let(:state) { account.to_sgid(expires_in: 15.minutes).to_s } describe 'GET /microsoft/callback' do let(:response_body_success) do @@ -27,7 +23,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do 'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" }) .to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' }) - get microsoft_callback_url, params: { code: code } + get microsoft_callback_url, params: { code: code, state: state } expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id) expect(account.inboxes.count).to be 1 @@ -36,7 +32,6 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on') expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token] expect(inbox.channel.imap_address).to eq 'outlook.office365.com' - expect(Redis::Alfred.get(cache_key)).to be_nil end it 'creates updates inbox channel config if inbox exists and authentication is successful' do @@ -48,14 +43,13 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do 'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" }) .to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' }) - get microsoft_callback_url, params: { code: code } + get microsoft_callback_url, params: { code: code, state: state } expect(response).to redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: account.inboxes.last.id) expect(account.inboxes.count).to be 1 expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on') expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token] expect(inbox.channel.imap_address).to eq 'outlook.office365.com' - expect(Redis::Alfred.get(cache_key)).to be_nil end it 'creates inboxes with fallback_name when account name is not present in id_token' do @@ -64,7 +58,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do 'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" }) .to_return(status: 200, body: response_body_success_without_name.to_json, headers: { 'Content-Type' => 'application/json' }) - get microsoft_callback_url, params: { code: code } + get microsoft_callback_url, params: { code: code, state: state } expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id) expect(account.inboxes.count).to be 1 @@ -78,10 +72,9 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do 'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" }) .to_return(status: 401) - get microsoft_callback_url, params: { code: code } + get microsoft_callback_url, params: { code: code, state: state } expect(response).to redirect_to '/' - expect(Redis::Alfred.get(cache_key).to_i).to eq account.id end end end From d2f5311400350b9686541af3a7c4aa20de0883e0 Mon Sep 17 00:00:00 2001 From: Baptiste Fontaine Date: Wed, 18 Jun 2025 23:51:23 +0200 Subject: [PATCH 02/70] fix: Disable custom context menu on img tags (#11762) # Pull Request Template ## Description Fixes #11761. See the issue for the details. ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? I deployed a modified version of Chatwoot with this patch and tested. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation (not applicable) - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works (not sure how to do this) - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- app/javascript/dashboard/components-next/message/Message.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/components-next/message/Message.vue b/app/javascript/dashboard/components-next/message/Message.vue index 98323f79a..28776a8d9 100644 --- a/app/javascript/dashboard/components-next/message/Message.vue +++ b/app/javascript/dashboard/components-next/message/Message.vue @@ -379,7 +379,7 @@ const shouldRenderMessage = computed(() => { function openContextMenu(e) { const shouldSkipContextMenu = e.target?.classList.contains('skip-context-menu') || - e.target?.tagName.toLowerCase() === 'a'; + ['a', 'img'].includes(e.target?.tagName.toLowerCase()); if (shouldSkipContextMenu || getSelection().toString()) { return; } From 2cfca6008b3d83b1ef560b702af47866c1e8a389 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:05:18 +0530 Subject: [PATCH 03/70] fix: Incorrect conversation count shown for filters/folders after idle period (#11770) --- app/javascript/dashboard/components/ChatList.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 928eea7df..439e7c682 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -756,6 +756,7 @@ function toggleSelectAll(check) { } useEmitter('fetch_conversation_stats', () => { + if (hasAppliedFiltersOrActiveFolders.value) return; store.dispatch('conversationStats/get', conversationFilters.value); }); From b683973e79c0abd8285f689537919a9cb7316660 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 19 Jun 2025 21:28:12 +0530 Subject: [PATCH 04/70] fix: Resolve styling issues in multiselect (#11728) --- .../dashboard/assets/scss/plugins/_multiselect.scss | 6 +++--- .../dashboard/modules/contact/components/MergeContact.vue | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index ffcfca545..1280eb069 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -47,7 +47,7 @@ @apply max-w-full; .multiselect__option { - @apply text-sm font-normal; + @apply text-sm font-normal flex justify-between items-center; span { @apply inline-block overflow-hidden text-ellipsis whitespace-nowrap w-fit; @@ -58,7 +58,7 @@ } &::after { - @apply bottom-0 flex items-center justify-center text-center; + @apply bottom-0 flex items-center justify-center text-center relative px-1 leading-tight; } &.multiselect__option--highlight { @@ -74,7 +74,7 @@ } &.multiselect__option--highlight::after { - @apply bg-transparent; + @apply bg-transparent text-n-slate-12; } &.multiselect__option--selected { diff --git a/app/javascript/dashboard/modules/contact/components/MergeContact.vue b/app/javascript/dashboard/modules/contact/components/MergeContact.vue index ae0d47f7f..4055a22e4 100644 --- a/app/javascript/dashboard/modules/contact/components/MergeContact.vue +++ b/app/javascript/dashboard/modules/contact/components/MergeContact.vue @@ -130,15 +130,15 @@ export default {
-
+