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:
Shivam Mishra
2024-05-23 16:03:40 +05:30
committed by GitHub
parent 616e3a8092
commit eafd3ae44d
10 changed files with 88 additions and 95 deletions

View File

@@ -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

View File

@@ -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."
}, },

View File

@@ -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: {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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]