Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f3da3f814 | ||
|
|
675bb3b744 | ||
|
|
4f7d1a67cf | ||
|
|
1336b0c750 | ||
|
|
84445e0d1e | ||
|
|
d9cd8b8261 | ||
|
|
59766ebc95 | ||
|
|
d1d958601d | ||
|
|
46beab047d | ||
|
|
b832fdc329 | ||
|
|
5c957054fe | ||
|
|
df26e940da | ||
|
|
05c18de547 | ||
|
|
e54d426577 | ||
|
|
e6185536a8 | ||
|
|
9d4ce3964b | ||
|
|
5792542ee2 | ||
|
|
68d20a854e | ||
|
|
23e1d614bf | ||
|
|
a8ac78cd8e | ||
|
|
808a27c793 | ||
|
|
f57d0f9da9 | ||
|
|
db13956f81 | ||
|
|
47074e8a82 | ||
|
|
b8c237ca58 | ||
|
|
b6f73221b9 | ||
|
|
d974be86ee | ||
|
|
4582794ad4 | ||
|
|
f906b9a775 | ||
|
|
df373bec62 | ||
|
|
d7b564ea9f | ||
|
|
8bdb1a2ce0 | ||
|
|
a8e1fba1bd | ||
|
|
ea86184675 | ||
|
|
6ec5bbc2c7 | ||
|
|
32f25e3bd7 | ||
|
|
d51e3ad5bf | ||
|
|
2dd2cfeb87 | ||
|
|
bd7e1fba3f | ||
|
|
bece33792e | ||
|
|
ffb687d12f | ||
|
|
980f4da669 |
@@ -0,0 +1,32 @@
|
||||
class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::BaseController
|
||||
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/',
|
||||
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'
|
||||
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
|
||||
@@ -12,8 +12,8 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
|
||||
}
|
||||
)
|
||||
if redirect_url
|
||||
email = email.downcase
|
||||
::Redis::Alfred.setex(email, Current.account.id, 5.minutes)
|
||||
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
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
module GoogleConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def google_client
|
||||
app_id = GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
|
||||
app_secret = GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_SECRET', nil)
|
||||
|
||||
::OAuth2::Client.new(app_id, app_secret, {
|
||||
site: 'https://oauth2.googleapis.com',
|
||||
authorize_url: 'https://accounts.google.com/o/oauth2/auth',
|
||||
token_url: 'https://accounts.google.com/o/oauth2/token'
|
||||
})
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||
end
|
||||
end
|
||||
@@ -15,10 +15,6 @@ module MicrosoftConcern
|
||||
|
||||
private
|
||||
|
||||
def parsed_body
|
||||
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
|
||||
end
|
||||
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||
end
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
class Google::CallbacksController < OauthCallbackController
|
||||
include GoogleConcern
|
||||
|
||||
private
|
||||
|
||||
def provider_name
|
||||
'google'
|
||||
end
|
||||
|
||||
def imap_address
|
||||
'imap.gmail.com'
|
||||
end
|
||||
|
||||
def oauth_client
|
||||
# from GoogleConcern
|
||||
google_client
|
||||
end
|
||||
end
|
||||
@@ -1,90 +1,17 @@
|
||||
class Microsoft::CallbacksController < ApplicationController
|
||||
class Microsoft::CallbacksController < OauthCallbackController
|
||||
include MicrosoftConcern
|
||||
|
||||
def show
|
||||
@response = microsoft_client.auth_code.get_token(
|
||||
oauth_code,
|
||||
redirect_uri: "#{base_url}/microsoft/callback"
|
||||
)
|
||||
|
||||
inbox, already_exists = find_or_create_inbox
|
||||
::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)
|
||||
end
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
redirect_to '/'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def oauth_code
|
||||
params[:code]
|
||||
def oauth_client
|
||||
microsoft_client
|
||||
end
|
||||
|
||||
def users_data
|
||||
decoded_token = JWT.decode parsed_body[:id_token], nil, false
|
||||
decoded_token[0]
|
||||
def provider_name
|
||||
'microsoft'
|
||||
end
|
||||
|
||||
def parsed_body
|
||||
@parsed_body ||= @response.response.parsed
|
||||
end
|
||||
|
||||
def account_id
|
||||
::Redis::Alfred.get(users_data['email'].downcase)
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= Account.find(account_id)
|
||||
end
|
||||
|
||||
def find_or_create_inbox
|
||||
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
|
||||
update_microsoft_channel(channel_email)
|
||||
|
||||
# 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
|
||||
|
||||
# Fallback name, for when name field is missing from users_data
|
||||
def fallback_name
|
||||
users_data['email'].split('@').first.parameterize.titleize
|
||||
end
|
||||
|
||||
def create_microsoft_channel_with_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
|
||||
account.inboxes.create!(
|
||||
account: account,
|
||||
channel: channel_email,
|
||||
name: users_data['name'] || fallback_name
|
||||
)
|
||||
channel_email
|
||||
end
|
||||
end
|
||||
|
||||
def update_microsoft_channel(channel_email)
|
||||
channel_email.update!({
|
||||
imap_login: users_data['email'], imap_address: 'outlook.office365.com',
|
||||
imap_port: '993', imap_enabled: true,
|
||||
provider: 'microsoft',
|
||||
provider_config: {
|
||||
access_token: parsed_body['access_token'],
|
||||
refresh_token: parsed_body['refresh_token'],
|
||||
expires_on: (Time.current.utc + 1.hour).to_s
|
||||
}
|
||||
})
|
||||
def imap_address
|
||||
'outlook.office365.com'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
class OauthCallbackController < ApplicationController
|
||||
def show
|
||||
@response = oauth_client.auth_code.get_token(
|
||||
oauth_code,
|
||||
redirect_uri: "#{base_url}/#{provider_name}/callback"
|
||||
)
|
||||
|
||||
handle_response
|
||||
::Redis::Alfred.delete(cache_key)
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
redirect_to '/'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_response
|
||||
inbox, already_exists = find_or_create_inbox
|
||||
|
||||
if already_exists
|
||||
redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
|
||||
else
|
||||
redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_create_inbox
|
||||
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_channel_with_inbox
|
||||
update_channel(channel_email)
|
||||
|
||||
# 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
|
||||
|
||||
def update_channel(channel_email)
|
||||
channel_email.update!({
|
||||
imap_login: users_data['email'], imap_address: imap_address,
|
||||
imap_port: '993', imap_enabled: true,
|
||||
provider: provider_name,
|
||||
provider_config: {
|
||||
access_token: parsed_body['access_token'],
|
||||
refresh_token: parsed_body['refresh_token'],
|
||||
expires_on: (Time.current.utc + 1.hour).to_s
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
def provider_name
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def oauth_client
|
||||
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)
|
||||
account.inboxes.create!(
|
||||
account: account,
|
||||
channel: channel_email,
|
||||
name: users_data['name'] || fallback_name
|
||||
)
|
||||
channel_email
|
||||
end
|
||||
end
|
||||
|
||||
def users_data
|
||||
decoded_token = JWT.decode parsed_body[:id_token], nil, false
|
||||
decoded_token[0]
|
||||
end
|
||||
|
||||
def account_id
|
||||
::Redis::Alfred.get(cache_key)
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= Account.find(account_id)
|
||||
end
|
||||
|
||||
# Fallback name, for when name field is missing from users_data
|
||||
def fallback_name
|
||||
users_data['email'].split('@').first.parameterize.titleize
|
||||
end
|
||||
|
||||
def oauth_code
|
||||
params[:code]
|
||||
end
|
||||
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||
end
|
||||
|
||||
def parsed_body
|
||||
@parsed_body ||= @response.response.parsed
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class MicrosoftClient extends ApiClient {
|
||||
constructor() {
|
||||
super('google', { accountScoped: true });
|
||||
}
|
||||
|
||||
generateAuthorization(payload) {
|
||||
return axios.post(`${this.url}/authorization`, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default new MicrosoftClient();
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<button
|
||||
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
|
||||
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col justify-end transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<img :src="src" :alt="title" class="w-1/2 my-4 mx-auto" />
|
||||
|
||||
@@ -369,6 +369,14 @@
|
||||
"SIGN_IN": "Sign in with Microsoft",
|
||||
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
},
|
||||
"GOOGLE": {
|
||||
"TITLE": "Google Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"SIGN_IN": "Sign in with Google",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"HELP": "To add your Google account as a channel, you need to authenticate your Google account by clicking on 'Sign in with Google' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
@@ -736,6 +744,7 @@
|
||||
},
|
||||
"EMAIL_PROVIDERS": {
|
||||
"MICROSOFT": "Microsoft",
|
||||
"GOOGLE": "Google",
|
||||
"OTHER_PROVIDERS": "Other Providers"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,7 +541,8 @@ export default {
|
||||
this.isALineChannel ||
|
||||
this.isAPIInbox ||
|
||||
(this.isAnEmailChannel && !this.inbox.provider) ||
|
||||
(this.isAnEmailChannel && this.inbox.provider === 'microsoft') ||
|
||||
this.isAMicrosoftInbox ||
|
||||
this.isAGoogleInbox ||
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isAWebWidgetInbox
|
||||
) {
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<microsoft v-else-if="provider === 'microsoft'" />
|
||||
<google v-else-if="provider === 'google'" />
|
||||
<forward-to-option v-else-if="provider === 'other_provider'" />
|
||||
</template>
|
||||
<script>
|
||||
import ForwardToOption from './emailChannels/ForwardToOption.vue';
|
||||
import Microsoft from './emailChannels/Microsoft.vue';
|
||||
import Google from './emailChannels/Google.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import ChannelSelector from 'dashboard/components/ChannelSelector.vue';
|
||||
import PageHeader from '../../SettingsSubPageHeader.vue';
|
||||
@@ -34,6 +36,7 @@ export default {
|
||||
ChannelSelector,
|
||||
ForwardToOption,
|
||||
Microsoft,
|
||||
Google,
|
||||
PageHeader,
|
||||
},
|
||||
data() {
|
||||
@@ -52,6 +55,12 @@ export default {
|
||||
key: 'microsoft',
|
||||
src: '/assets/images/dashboard/channels/microsoft.png',
|
||||
},
|
||||
{
|
||||
title: this.$t('INBOX_MGMT.EMAIL_PROVIDERS.GOOGLE'),
|
||||
isEnabled: !!window.chatwootConfig.googleOAuthClientId,
|
||||
key: 'google',
|
||||
src: '/assets/images/dashboard/channels/google.png',
|
||||
},
|
||||
{
|
||||
title: this.$t('INBOX_MGMT.EMAIL_PROVIDERS.OTHER_PROVIDERS'),
|
||||
isEnabled: true,
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
|
||||
import googleClient from 'dashboard/api/channel/googleClient';
|
||||
import SettingsSubPageHeader from '../../../SettingsSubPageHeader.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const isRequestingAuthorization = ref(false);
|
||||
const email = ref('');
|
||||
|
||||
async function requestAuthorization() {
|
||||
try {
|
||||
isRequestingAuthorization.value = true;
|
||||
const response = await googleClient.generateAuthorization({
|
||||
email: email.value,
|
||||
});
|
||||
const {
|
||||
data: { url },
|
||||
} = response;
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
useAlert(t('INBOX_MGMT.ADD.GOOGLE.ERROR_MESSAGE'));
|
||||
} finally {
|
||||
isRequestingAuthorization.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
>
|
||||
<settings-sub-page-header
|
||||
:header-title="$t('INBOX_MGMT.ADD.GOOGLE.TITLE')"
|
||||
:header-content="$t('INBOX_MGMT.ADD.GOOGLE.DESCRIPTION')"
|
||||
/>
|
||||
<form class="mt-6" @submit.prevent="requestAuthorization">
|
||||
<woot-input
|
||||
v-model="email"
|
||||
type="email"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.GOOGLE.EMAIL_PLACEHOLDER')"
|
||||
/>
|
||||
<woot-submit-button
|
||||
icon="brand-twitter"
|
||||
:button-text="$t('INBOX_MGMT.ADD.GOOGLE.SIGN_IN')"
|
||||
type="submit"
|
||||
:loading="isRequestingAuthorization"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
const isValidPath = path => {
|
||||
if (!path) return false;
|
||||
if (path === null && path === '') return false;
|
||||
// if path is just a sequence of slashes
|
||||
if (/^\/+$/.test(path)) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Join multiple paths together with a base URL
|
||||
* NOTE: This function is not designed to handle query strings or fragments
|
||||
*
|
||||
* @param {string} baseUrl - The base URL to join the paths to
|
||||
* @param {...string} paths - The paths to join
|
||||
* @returns {string} - The full URL
|
||||
*/
|
||||
export function joinUrl(baseUrl, ...paths) {
|
||||
// remove empty undefined and null path items
|
||||
// also handle if the path is just a slash or just multiple slashes only
|
||||
const sanitizedPaths = paths.filter(isValidPath);
|
||||
|
||||
const fullUrl = sanitizedPaths.reduce(
|
||||
(acc, path) => {
|
||||
const sanitizedPath = path.replace(/^\/+|\/+$/g, ''); // Remove leading and trailing slashes from each path segment
|
||||
return `${acc}/${sanitizedPath}`; // Concatenate with a single slash
|
||||
},
|
||||
baseUrl.replace(/\/+$/, '')
|
||||
);
|
||||
|
||||
return fullUrl;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { joinUrl } from '../joinUrl';
|
||||
|
||||
describe('joinUrl', () => {
|
||||
it('should correctly join base URL and multiple paths', () => {
|
||||
expect(joinUrl('http://example.com', 'path1', 'path2', 'path3')).toBe(
|
||||
'http://example.com/path1/path2/path3'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle trailing slashes on the base URL', () => {
|
||||
expect(joinUrl('http://example.com/', 'path1', 'path2')).toBe(
|
||||
'http://example.com/path1/path2'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle leading and trailing slashes on path segments', () => {
|
||||
expect(joinUrl('http://example.com', '/path1/', '/path2/')).toBe(
|
||||
'http://example.com/path1/path2'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle a mix of slashes in base URL and paths', () => {
|
||||
expect(joinUrl('http://example.com/', '/path1/', '/path2/')).toBe(
|
||||
'http://example.com/path1/path2'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the base URL if no paths are provided', () => {
|
||||
expect(joinUrl('http://example.com')).toBe('http://example.com');
|
||||
});
|
||||
|
||||
it('should handle empty path segments', () => {
|
||||
expect(joinUrl('http://example.com', '', 'path1', '', 'path2')).toBe(
|
||||
'http://example.com/path1/path2'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle paths that are just slashes', () => {
|
||||
expect(joinUrl('http://example.com', '/', '//', '/path1/', '/')).toBe(
|
||||
'http://example.com/path1'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle null and undefined paths', () => {
|
||||
expect(
|
||||
joinUrl('http://example.com', null, 'path1', undefined, 'path2')
|
||||
).toBe('http://example.com/path1/path2');
|
||||
});
|
||||
|
||||
it('should handle paths with multiple consecutive slashes', () => {
|
||||
expect(joinUrl('http://example.com', '///path1///', 'path2///')).toBe(
|
||||
'http://example.com/path1/path2'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle only slashes in base URL and paths', () => {
|
||||
expect(joinUrl('http://example.com/', '/', '/')).toBe('http://example.com');
|
||||
});
|
||||
});
|
||||
@@ -47,6 +47,9 @@ export default {
|
||||
isAMicrosoftInbox() {
|
||||
return this.isAnEmailChannel && this.inbox.provider === 'microsoft';
|
||||
},
|
||||
isAGoogleInbox() {
|
||||
return this.isAnEmailChannel && this.inbox.provider === 'google';
|
||||
},
|
||||
isAPIInbox() {
|
||||
return this.channelType === INBOX_TYPES.API;
|
||||
},
|
||||
|
||||
@@ -57,6 +57,10 @@ class Channel::Email < ApplicationRecord
|
||||
provider == 'microsoft'
|
||||
end
|
||||
|
||||
def google?
|
||||
provider == 'google'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_forward_to_email
|
||||
|
||||
+7
-2
@@ -19,8 +19,8 @@ Rails.application.routes.draw do
|
||||
get '/app/accounts/:account_id/settings/inboxes/new/twitter', to: 'dashboard#index', as: 'app_new_twitter_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_microsoft_inbox_agents'
|
||||
get '/app/accounts/:account_id/settings/inboxes/:inbox_id', to: 'dashboard#index', as: 'app_microsoft_inbox_settings'
|
||||
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_email_inbox_agents'
|
||||
get '/app/accounts/:account_id/settings/inboxes/:inbox_id', to: 'dashboard#index', as: 'app_email_inbox_settings'
|
||||
|
||||
resource :widget, only: [:show]
|
||||
namespace :survey do
|
||||
@@ -209,6 +209,10 @@ Rails.application.routes.draw do
|
||||
resource :authorization, only: [:create]
|
||||
end
|
||||
|
||||
namespace :google do
|
||||
resource :authorization, only: [:create]
|
||||
end
|
||||
|
||||
resources :webhooks, only: [:index, :create, :update, :destroy]
|
||||
namespace :integrations do
|
||||
resources :apps, only: [:index, :show]
|
||||
@@ -445,6 +449,7 @@ Rails.application.routes.draw do
|
||||
end
|
||||
|
||||
get 'microsoft/callback', to: 'microsoft/callbacks#show'
|
||||
get 'google/callback', to: 'google/callbacks#show'
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Routes for external service verifications
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,51 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Google Authorization API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/google/authorization' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/google/authorization"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:administrator) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
it 'returns unathorized for agent' do
|
||||
post "/api/v1/accounts/#{account.id}/google/authorization",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { email: administrator.email },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'creates a new authorization and returns the redirect url' do
|
||||
post "/api/v1/accounts/#{account.id}/google/authorization",
|
||||
headers: administrator.create_new_auth_token,
|
||||
params: { email: administrator.email },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
google_service = Class.new { extend GoogleConcern }
|
||||
response_url = google_service.google_client.auth_code.authorize_url(
|
||||
{
|
||||
redirect_uri: "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback",
|
||||
scope: 'email profile https://mail.google.com/',
|
||||
response_type: 'code',
|
||||
prompt: 'consent',
|
||||
access_type: 'offline',
|
||||
client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
|
||||
}
|
||||
)
|
||||
expect(response.parsed_body['url']).to eq response_url
|
||||
expect(Redis::Alfred.get("google::#{administrator.email}")).to eq(account.id.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -41,7 +41,7 @@ RSpec.describe 'Microsoft Authorization API', type: :request do
|
||||
}
|
||||
)
|
||||
expect(response.parsed_body['url']).to eq response_url
|
||||
expect(Redis::Alfred.get(administrator.email)).to eq(account.id.to_s)
|
||||
expect(Redis::Alfred.get("microsoft::#{administrator.email}")).to eq(account.id.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Google::CallbacksController', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:code) { SecureRandom.hex(10) }
|
||||
let(:email) { Faker::Internet.email }
|
||||
let(:cache_key) { "google::#{email.downcase}" }
|
||||
|
||||
before do
|
||||
Redis::Alfred.set(cache_key, account.id)
|
||||
end
|
||||
|
||||
describe 'GET /google/callback' do
|
||||
let(:response_body_success) do
|
||||
{ id_token: JWT.encode({ email: email, name: 'test' }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
refresh_token: SecureRandom.hex(10) }
|
||||
end
|
||||
|
||||
let(:response_body_success_without_name) do
|
||||
{ id_token: JWT.encode({ email: email }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
|
||||
refresh_token: SecureRandom.hex(10) }
|
||||
end
|
||||
|
||||
it 'creates inboxes if authentication is successful' do
|
||||
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'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 }
|
||||
|
||||
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
|
||||
inbox = account.inboxes.last
|
||||
expect(inbox.name).to eq 'test'
|
||||
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 updates inbox channel config if inbox exists and authentication is successful' do
|
||||
inbox = create(:channel_email, account: account, email: email)&.inbox
|
||||
expect(inbox.channel.provider_config).to eq({})
|
||||
|
||||
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'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 }
|
||||
|
||||
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 '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
|
||||
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'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 }
|
||||
|
||||
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
|
||||
inbox = account.inboxes.last
|
||||
expect(inbox.name).to eq email.split('@').first.parameterize.titleize
|
||||
end
|
||||
|
||||
it 'redirects to google app in case of error' do
|
||||
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
|
||||
.with(body: { 'code' => code, 'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
|
||||
.to_return(status: 401)
|
||||
|
||||
get google_callback_url, params: { code: code }
|
||||
|
||||
expect(response).to redirect_to '/'
|
||||
expect(Redis::Alfred.get(cache_key).to_i).to eq account.id
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,9 +4,10 @@ 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(email, account.id)
|
||||
Redis::Alfred.set(cache_key, account.id)
|
||||
end
|
||||
|
||||
describe 'GET /microsoft/callback' do
|
||||
@@ -28,14 +29,14 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
|
||||
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_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
inbox = account.inboxes.last
|
||||
expect(inbox.name).to eq 'test'
|
||||
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(email)).to be_nil
|
||||
expect(Redis::Alfred.get(cache_key)).to be_nil
|
||||
end
|
||||
|
||||
it 'creates updates inbox channel config if inbox exists and authentication is successful' do
|
||||
@@ -49,12 +50,12 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
|
||||
get microsoft_callback_url, params: { code: code }
|
||||
|
||||
expect(response).to redirect_to app_microsoft_inbox_settings_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
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(email)).to be_nil
|
||||
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 +66,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
|
||||
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_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
inbox = account.inboxes.last
|
||||
expect(inbox.name).to eq email.split('@').first.parameterize.titleize
|
||||
@@ -80,7 +81,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
get microsoft_callback_url, params: { code: code }
|
||||
|
||||
expect(response).to redirect_to '/'
|
||||
expect(Redis::Alfred.get(email).to_i).to eq account.id
|
||||
expect(Redis::Alfred.get(cache_key).to_i).to eq account.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user