feat: new re-authorization flow for Microsoft (#9510)
This PR adds a cleaner re-authorization flow to Microsoft. This PR has the following changes 1. Use `reauthorization_required` value for Microsoft Channel 2. Refactor `InboxReconnectionRequired` to reuse the `banner` component 3. Refactor `microsoft/Reauthorize.vue` to reuse `InboxReconnectionRequired` component 4. Update `reauthorizable.rb` to update cache keys if the model has an inbox 5. Update `microsoft/callbacks_controller.rb` to handle the reauthorization case with a redirect to the inbox settings page if the inbox already exists at the time of authorization. ## How Has This Been Tested? - [x] Local Instance - [ ] Staging Instance - [x] Unit tests ## Pending Tasks - [ ] ~Success Toast~ will do this in a follow-up PR with the screen ## Demo The following video shows the whole process of creation and re-authorization of the Microsoft channel https://www.loom.com/share/e5cd9bd4439c4741b0dcfe66d67f88b3?sid=100f3642-43e4-46b3-8123-88a5dd9d8509 --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -7,9 +7,14 @@ class Microsoft::CallbacksController < ApplicationController
|
|||||||
redirect_uri: "#{base_url}/microsoft/callback"
|
redirect_uri: "#{base_url}/microsoft/callback"
|
||||||
)
|
)
|
||||||
|
|
||||||
inbox = find_or_create_inbox
|
inbox, already_exists = find_or_create_inbox
|
||||||
::Redis::Alfred.delete(users_data['email'].downcase)
|
::Redis::Alfred.delete(users_data['email'].downcase)
|
||||||
|
|
||||||
|
if already_exists
|
||||||
|
redirect_to app_microsoft_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
|
||||||
|
else
|
||||||
redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||||
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
ChatwootExceptionTracker.new(e).capture_exception
|
ChatwootExceptionTracker.new(e).capture_exception
|
||||||
redirect_to '/'
|
redirect_to '/'
|
||||||
@@ -40,9 +45,17 @@ class Microsoft::CallbacksController < ApplicationController
|
|||||||
|
|
||||||
def find_or_create_inbox
|
def find_or_create_inbox
|
||||||
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
|
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
|
||||||
|
# we need this value to know where to redirect on sucessful processing of the callback
|
||||||
|
channel_exists = channel_email.present?
|
||||||
|
|
||||||
channel_email ||= create_microsoft_channel_with_inbox
|
channel_email ||= create_microsoft_channel_with_inbox
|
||||||
update_microsoft_channel(channel_email)
|
update_microsoft_channel(channel_email)
|
||||||
channel_email.inbox
|
|
||||||
|
# reauthorize channel, this code path only triggers when microsoft auth is successful
|
||||||
|
# reauthorized will also update cache keys for the associated inbox
|
||||||
|
channel_email.reauthorized!
|
||||||
|
|
||||||
|
[channel_email.inbox, channel_exists]
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fallback name, for when name field is missing from users_data
|
# Fallback name, for when name field is missing from users_data
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"INBOX_MGMT": {
|
"INBOX_MGMT": {
|
||||||
"HEADER": "Inboxes",
|
"HEADER": "Inboxes",
|
||||||
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
|
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
|
||||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected, you will not receive any new messages. <a class=\"underline\" href=\"%{actionUrl}\">Click here</a> to reconnect.",
|
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||||
|
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "There are no inboxes attached to this account."
|
"404": "There are no inboxes attached to this account."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,10 +21,7 @@
|
|||||||
</woot-tabs>
|
</woot-tabs>
|
||||||
</setting-intro-banner>
|
</setting-intro-banner>
|
||||||
|
|
||||||
<inbox-reconnection-required
|
<microsoft-reauthorize v-if="microsoftUnauthorized" :inbox="inbox" />
|
||||||
v-if="isReconnectionRequired"
|
|
||||||
class="mx-8 mt-5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-if="selectedTabKey === 'inbox_settings'" class="mx-8">
|
<div v-if="selectedTabKey === 'inbox_settings'" class="mx-8">
|
||||||
<settings-section
|
<settings-section
|
||||||
@@ -440,7 +437,7 @@ import WeeklyAvailability from './components/WeeklyAvailability.vue';
|
|||||||
import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
|
import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
|
||||||
import ConfigurationPage from './settingsPage/ConfigurationPage.vue';
|
import ConfigurationPage from './settingsPage/ConfigurationPage.vue';
|
||||||
import CollaboratorsPage from './settingsPage/CollaboratorsPage.vue';
|
import CollaboratorsPage from './settingsPage/CollaboratorsPage.vue';
|
||||||
import InboxReconnectionRequired from './components/InboxReconnectionRequired';
|
import MicrosoftReauthorize from './channels/microsoft/Reauthorize.vue';
|
||||||
import WidgetBuilder from './WidgetBuilder.vue';
|
import WidgetBuilder from './WidgetBuilder.vue';
|
||||||
import BotConfiguration from './components/BotConfiguration.vue';
|
import BotConfiguration from './components/BotConfiguration.vue';
|
||||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||||
@@ -459,7 +456,7 @@ export default {
|
|||||||
WeeklyAvailability,
|
WeeklyAvailability,
|
||||||
WidgetBuilder,
|
WidgetBuilder,
|
||||||
SenderNameExamplePreview,
|
SenderNameExamplePreview,
|
||||||
InboxReconnectionRequired,
|
MicrosoftReauthorize,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, configMixin, inboxMixin],
|
mixins: [alertMixin, configMixin, inboxMixin],
|
||||||
data() {
|
data() {
|
||||||
@@ -621,8 +618,8 @@ export default {
|
|||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
isReconnectionRequired() {
|
microsoftUnauthorized() {
|
||||||
return false;
|
return this.inbox.microsoft_reauthorization;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
@@ -1,63 +1,44 @@
|
|||||||
<template>
|
<script setup>
|
||||||
<div class="mx-8">
|
import { ref } from 'vue';
|
||||||
<settings-section
|
import InboxReconnectionRequired from '../../components/InboxReconnectionRequired';
|
||||||
:title="$t('INBOX_MGMT.MICROSOFT.TITLE')"
|
import microsoftClient from 'dashboard/api/channel/microsoftClient';
|
||||||
:sub-title="$t('INBOX_MGMT.MICROSOFT.SUBTITLE')"
|
|
||||||
>
|
|
||||||
<div class="mb-6">
|
|
||||||
<form @submit.prevent="requestAuthorization">
|
|
||||||
<woot-submit-button
|
|
||||||
icon="brand-twitter"
|
|
||||||
button-text="Sign in with Microsoft"
|
|
||||||
type="submit"
|
|
||||||
:loading="isRequestingAuthorization"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</settings-section>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
import microsoftClient from '../../../../../../api/channel/microsoftClient';
|
|
||||||
import SettingsSection from '../../../../../../components/SettingsSection.vue';
|
|
||||||
|
|
||||||
export default {
|
import { useI18n } from 'dashboard/composables/useI18n';
|
||||||
components: {
|
import { useAlert } from 'dashboard/composables';
|
||||||
SettingsSection,
|
|
||||||
},
|
const { t } = useI18n();
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
const props = defineProps({
|
||||||
inbox: {
|
inbox: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
data() {
|
|
||||||
return { isRequestingAuthorization: false };
|
const isRequestingAuthorization = ref(false);
|
||||||
},
|
|
||||||
methods: {
|
async function requestAuthorization() {
|
||||||
async requestAuthorization() {
|
|
||||||
try {
|
try {
|
||||||
this.isRequestingAuthorization = true;
|
isRequestingAuthorization.value = true;
|
||||||
const response = await microsoftClient.generateAuthorization({
|
const response = await microsoftClient.generateAuthorization({
|
||||||
email: this.inbox.email,
|
email: props.inbox.email,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { url },
|
data: { url },
|
||||||
} = response;
|
} = response;
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(this.$t('INBOX_MGMT.ADD.MICROSOFT.ERROR_MESSAGE'));
|
useAlert(t('INBOX_MGMT.ADD.MICROSOFT.ERROR_MESSAGE'));
|
||||||
} finally {
|
} finally {
|
||||||
this.isRequestingAuthorization = false;
|
isRequestingAuthorization.value = false;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.smtp-details-wrap {
|
|
||||||
margin-bottom: var(--space-medium);
|
|
||||||
}
|
}
|
||||||
</style>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<inbox-reconnection-required
|
||||||
|
class="mx-8 mt-5"
|
||||||
|
@reauthorize="requestAuthorization"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||||
actionUrl: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<banner
|
||||||
class="flex items-center gap-2 px-4 py-3 text-sm text-white bg-red-500 rounded-md dark:bg-red-800/30 dark:text-red-50 min-h-10"
|
color-scheme="alert"
|
||||||
>
|
class="justify-start rounded-md"
|
||||||
<fluent-icon icon="error-circle" class="text-white dark:text-red-50" />
|
:banner-message="$t('INBOX_MGMT.RECONNECTION_REQUIRED')"
|
||||||
<slot>
|
:action-button-label="$t('INBOX_MGMT.CLICK_TO_RECONNECT')"
|
||||||
<span v-html="$t('INBOX_MGMT.RECONNECTION_REQUIRED', { actionUrl })" />
|
has-action-button
|
||||||
</slot>
|
@click="$emit('reauthorize')"
|
||||||
</div>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -104,10 +104,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<imap-settings :inbox="inbox" />
|
<imap-settings :inbox="inbox" />
|
||||||
<smtp-settings v-if="inbox.imap_enabled" :inbox="inbox" />
|
<smtp-settings v-if="inbox.imap_enabled" :inbox="inbox" />
|
||||||
<microsoft-reauthorize
|
|
||||||
v-if="inbox.microsoft_reauthorization"
|
|
||||||
:inbox="inbox"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="isAWhatsAppChannel && !isATwilioChannel">
|
<div v-else-if="isAWhatsAppChannel && !isATwilioChannel">
|
||||||
<div v-if="inbox.provider_config" class="mx-8">
|
<div v-if="inbox.provider_config" class="mx-8">
|
||||||
@@ -130,7 +126,7 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="whatsapp-settings--content items-center flex flex-1 justify-between mt-2"
|
class="flex items-center justify-between flex-1 mt-2 whatsapp-settings--content"
|
||||||
>
|
>
|
||||||
<woot-input
|
<woot-input
|
||||||
v-model.trim="whatsAppInboxAPIKey"
|
v-model.trim="whatsAppInboxAPIKey"
|
||||||
@@ -160,7 +156,6 @@ import inboxMixin from 'shared/mixins/inboxMixin';
|
|||||||
import SettingsSection from '../../../../../components/SettingsSection.vue';
|
import SettingsSection from '../../../../../components/SettingsSection.vue';
|
||||||
import ImapSettings from '../ImapSettings.vue';
|
import ImapSettings from '../ImapSettings.vue';
|
||||||
import SmtpSettings from '../SmtpSettings.vue';
|
import SmtpSettings from '../SmtpSettings.vue';
|
||||||
import MicrosoftReauthorize from '../channels/microsoft/Reauthorize.vue';
|
|
||||||
import { required } from 'vuelidate/lib/validators';
|
import { required } from 'vuelidate/lib/validators';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -168,7 +163,6 @@ export default {
|
|||||||
SettingsSection,
|
SettingsSection,
|
||||||
ImapSettings,
|
ImapSettings,
|
||||||
SmtpSettings,
|
SmtpSettings,
|
||||||
MicrosoftReauthorize,
|
|
||||||
},
|
},
|
||||||
mixins: [inboxMixin, alertMixin],
|
mixins: [inboxMixin, alertMixin],
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ module Reauthorizable
|
|||||||
update!(active: false)
|
update!(active: false)
|
||||||
mailer.automation_rule_disabled(self).deliver_later
|
mailer.automation_rule_disabled(self).deliver_later
|
||||||
end
|
end
|
||||||
|
|
||||||
|
invalidate_inbox_cache unless instance_of?(::AutomationRule)
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_integration_hook_reauthorization_emails(mailer)
|
def process_integration_hook_reauthorization_emails(mailer)
|
||||||
@@ -68,10 +70,16 @@ module Reauthorizable
|
|||||||
def reauthorized!
|
def reauthorized!
|
||||||
::Redis::Alfred.delete(authorization_error_count_key)
|
::Redis::Alfred.delete(authorization_error_count_key)
|
||||||
::Redis::Alfred.delete(reauthorization_required_key)
|
::Redis::Alfred.delete(reauthorization_required_key)
|
||||||
|
|
||||||
|
invalidate_inbox_cache unless instance_of?(::AutomationRule)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def invalidate_inbox_cache
|
||||||
|
inbox.update_account_cache if inbox.present?
|
||||||
|
end
|
||||||
|
|
||||||
def authorization_error_count_key
|
def authorization_error_count_key
|
||||||
format(::Redis::Alfred::AUTHORIZATION_ERROR_COUNT, obj_type: self.class.table_name.singularize, obj_id: id)
|
format(::Redis::Alfred::AUTHORIZATION_ERROR_COUNT, obj_type: self.class.table_name.singularize, obj_id: id)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -71,8 +71,11 @@ if resource.email?
|
|||||||
json.imap_address resource.channel.try(:imap_address)
|
json.imap_address resource.channel.try(:imap_address)
|
||||||
json.imap_port resource.channel.try(:imap_port)
|
json.imap_port resource.channel.try(:imap_port)
|
||||||
json.imap_enabled resource.channel.try(:imap_enabled)
|
json.imap_enabled resource.channel.try(:imap_enabled)
|
||||||
json.microsoft_reauthorization resource.channel.try(:microsoft?) && resource.channel.try(:provider_config).empty?
|
|
||||||
json.imap_enable_ssl resource.channel.try(:imap_enable_ssl)
|
json.imap_enable_ssl resource.channel.try(:imap_enable_ssl)
|
||||||
|
|
||||||
|
if resource.channel.try(:microsoft?)
|
||||||
|
json.microsoft_reauthorization resource.channel.try(:provider_config).empty? || resource.channel.try(:reauthorization_required?)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
## SMTP
|
## SMTP
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Rails.application.routes.draw do
|
|||||||
get '/app/accounts/:account_id/settings/inboxes/new/microsoft', to: 'dashboard#index', as: 'app_new_microsoft_inbox'
|
get '/app/accounts/:account_id/settings/inboxes/new/microsoft', to: 'dashboard#index', as: 'app_new_microsoft_inbox'
|
||||||
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_twitter_inbox_agents'
|
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_twitter_inbox_agents'
|
||||||
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_microsoft_inbox_agents'
|
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_microsoft_inbox_agents'
|
||||||
|
get '/app/accounts/:account_id/settings/inboxes/:inbox_id', to: 'dashboard#index', as: 'app_microsoft_inbox_settings'
|
||||||
|
|
||||||
resource :widget, only: [:show]
|
resource :widget, only: [:show]
|
||||||
namespace :survey do
|
namespace :survey do
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
|||||||
|
|
||||||
get microsoft_callback_url, params: { code: code }
|
get microsoft_callback_url, params: { code: code }
|
||||||
|
|
||||||
expect(response).to redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
expect(response).to redirect_to app_microsoft_inbox_settings_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||||
expect(account.inboxes.count).to be 1
|
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.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.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||||
|
|||||||
Reference in New Issue
Block a user