mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
feat: Google OAuth for login & signup (#6346)
This PR adds Google OAuth for all existing users, allowing users to log in or sign up via their Google account. --------- Co-authored-by: Pranav Raj S <pranav@chatwoot.com> Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
@@ -131,6 +131,11 @@ TWITTER_ENVIRONMENT=
|
|||||||
SLACK_CLIENT_ID=
|
SLACK_CLIENT_ID=
|
||||||
SLACK_CLIENT_SECRET=
|
SLACK_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# Google OAuth
|
||||||
|
GOOGLE_OAUTH_CLIENT_ID=
|
||||||
|
GOOGLE_OAUTH_CLIENT_SECRET=
|
||||||
|
GOOGLE_OAUTH_CALLBACK_URL=
|
||||||
|
|
||||||
### Change this env variable only if you are using a custom build mobile app
|
### Change this env variable only if you are using a custom build mobile app
|
||||||
## Mobile app env variables
|
## Mobile app env variables
|
||||||
IOS_APP_ID=L7YLMN4634.com.chatwoot.app
|
IOS_APP_ID=L7YLMN4634.com.chatwoot.app
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -62,3 +62,7 @@ test/cypress/videos/*
|
|||||||
/config/*.enc
|
/config/*.enc
|
||||||
|
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
|
||||||
|
# yalc for local testing
|
||||||
|
.yalc
|
||||||
|
yalc.lock
|
||||||
@@ -68,7 +68,6 @@ Naming/AccessorMethodName:
|
|||||||
- 'app/controllers/api/v1/accounts_controller.rb'
|
- 'app/controllers/api/v1/accounts_controller.rb'
|
||||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||||
- 'app/controllers/api/v1/conversations_controller.rb'
|
- 'app/controllers/api/v1/conversations_controller.rb'
|
||||||
- 'app/controllers/passwords_controller.rb'
|
|
||||||
|
|
||||||
# Offense count: 9
|
# Offense count: 9
|
||||||
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
|
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
|
||||||
|
|||||||
6
Gemfile
6
Gemfile
@@ -199,5 +199,11 @@ group :development, :test do
|
|||||||
gem 'spring'
|
gem 'spring'
|
||||||
gem 'spring-watcher-listen'
|
gem 'spring-watcher-listen'
|
||||||
end
|
end
|
||||||
|
|
||||||
# worked with microsoft refresh token
|
# worked with microsoft refresh token
|
||||||
gem 'omniauth-oauth2'
|
gem 'omniauth-oauth2'
|
||||||
|
|
||||||
|
# need for google auth
|
||||||
|
gem 'omniauth'
|
||||||
|
gem 'omniauth-google-oauth2'
|
||||||
|
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
||||||
|
|||||||
11
Gemfile.lock
11
Gemfile.lock
@@ -472,9 +472,17 @@ GEM
|
|||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
rack-protection
|
rack-protection
|
||||||
|
omniauth-google-oauth2 (1.1.1)
|
||||||
|
jwt (>= 2.0)
|
||||||
|
oauth2 (~> 2.0.6)
|
||||||
|
omniauth (~> 2.0)
|
||||||
|
omniauth-oauth2 (~> 1.8.0)
|
||||||
omniauth-oauth2 (1.8.0)
|
omniauth-oauth2 (1.8.0)
|
||||||
oauth2 (>= 1.4, < 3)
|
oauth2 (>= 1.4, < 3)
|
||||||
omniauth (~> 2.0)
|
omniauth (~> 2.0)
|
||||||
|
omniauth-rails_csrf_protection (1.0.1)
|
||||||
|
actionpack (>= 4.2)
|
||||||
|
omniauth (~> 2.0)
|
||||||
openssl (3.1.0)
|
openssl (3.1.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
@@ -810,7 +818,10 @@ DEPENDENCIES
|
|||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
newrelic_rpm
|
newrelic_rpm
|
||||||
|
omniauth
|
||||||
|
omniauth-google-oauth2
|
||||||
omniauth-oauth2
|
omniauth-oauth2
|
||||||
|
omniauth-rails_csrf_protection (~> 1.0)
|
||||||
pg
|
pg
|
||||||
pg_search
|
pg_search
|
||||||
procore-sift
|
procore-sift
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController
|
||||||
|
include EmailHelper
|
||||||
|
|
||||||
|
def omniauth_success
|
||||||
|
get_resource_from_auth_hash
|
||||||
|
|
||||||
|
@resource.present? ? sign_in_user : sign_up_user
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sign_in_user
|
||||||
|
@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)
|
||||||
|
redirect_to login_page_url(email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token)
|
||||||
|
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_business_account?
|
||||||
|
|
||||||
|
create_account_for_user
|
||||||
|
token = @resource.send(:set_reset_password_token)
|
||||||
|
frontend_url = ENV.fetch('FRONTEND_URL', nil)
|
||||||
|
redirect_to "#{frontend_url}/app/auth/password/edit?config=default&reset_password_token=#{token}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def login_page_url(error: nil, email: nil, sso_auth_token: nil)
|
||||||
|
frontend_url = ENV.fetch('FRONTEND_URL', nil)
|
||||||
|
params = { email: email, sso_auth_token: sso_auth_token }.compact
|
||||||
|
params[:error] = error if error.present?
|
||||||
|
|
||||||
|
"#{frontend_url}/app/login?#{params.to_query}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_signup_allowed?
|
||||||
|
# set it to true by default, this is the behaviour across the app
|
||||||
|
GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') != 'false'
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_class(_mapping = nil)
|
||||||
|
User
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_resource_from_auth_hash # rubocop:disable Naming/AccessorMethodName
|
||||||
|
# find the user with their email instead of UID and token
|
||||||
|
@resource = resource_class.where(
|
||||||
|
email: auth_hash['info']['email']
|
||||||
|
).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_business_account?
|
||||||
|
# return true if the user is a business account, false if it is a gmail account
|
||||||
|
auth_hash['info']['email'].exclude?('@gmail.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_account_for_user
|
||||||
|
@resource, @account = AccountBuilder.new(
|
||||||
|
account_name: extract_domain_without_tld(auth_hash['info']['email']),
|
||||||
|
user_full_name: auth_hash['info']['name'],
|
||||||
|
email: auth_hash['info']['email'],
|
||||||
|
locale: I18n.locale,
|
||||||
|
confirmed: auth_hash['info']['email_verified']
|
||||||
|
).perform
|
||||||
|
Avatar::AvatarFromUrlJob.perform_later(@resource, auth_hash['info']['image'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_devise_mapping
|
||||||
|
'user'
|
||||||
|
end
|
||||||
|
end
|
||||||
6
app/helpers/email_helper.rb
Normal file
6
app/helpers/email_helper.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module EmailHelper
|
||||||
|
def extract_domain_without_tld(email)
|
||||||
|
domain = email.split('@').last
|
||||||
|
domain.split('.').first
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import GoogleOAuthButton from './GoogleOAuthButton.vue';
|
||||||
|
|
||||||
|
function getWrapper(showSeparator, buttonSize) {
|
||||||
|
return shallowMount(GoogleOAuthButton, {
|
||||||
|
propsData: { showSeparator: showSeparator, buttonSize: buttonSize },
|
||||||
|
methods: {
|
||||||
|
$t(text) {
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GoogleOAuthButton.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
window.chatwootConfig = {
|
||||||
|
googleOAuthClientId: 'clientId',
|
||||||
|
googleOAuthCallbackUrl: 'http://localhost:3000/test-callback',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.chatwootConfig = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the OR separator if showSeparator is true', () => {
|
||||||
|
const wrapper = getWrapper(true);
|
||||||
|
expect(wrapper.find('.separator').exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render the OR separator if showSeparator is false', () => {
|
||||||
|
const wrapper = getWrapper(false);
|
||||||
|
expect(wrapper.find('.separator').exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates the correct Google Auth URL', () => {
|
||||||
|
const wrapper = getWrapper();
|
||||||
|
const googleAuthUrl = new URL(wrapper.vm.getGoogleAuthUrl());
|
||||||
|
|
||||||
|
const params = googleAuthUrl.searchParams;
|
||||||
|
expect(googleAuthUrl.origin).toBe('https://accounts.google.com');
|
||||||
|
expect(googleAuthUrl.pathname).toBe('/o/oauth2/auth/oauthchooseaccount');
|
||||||
|
expect(params.get('client_id')).toBe('clientId');
|
||||||
|
expect(params.get('redirect_uri')).toBe(
|
||||||
|
'http://localhost:3000/test-callback'
|
||||||
|
);
|
||||||
|
expect(params.get('response_type')).toBe('code');
|
||||||
|
expect(params.get('scope')).toBe('email profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('responds to buttonSize prop properly', () => {
|
||||||
|
let wrapper = getWrapper(true, 'tiny');
|
||||||
|
expect(wrapper.find('.button.tiny').exists()).toBe(true);
|
||||||
|
|
||||||
|
wrapper = getWrapper(true, 'small');
|
||||||
|
expect(wrapper.find('.button.small').exists()).toBe(true);
|
||||||
|
|
||||||
|
wrapper = getWrapper(true, 'large');
|
||||||
|
expect(wrapper.find('.button.large').exists()).toBe(true);
|
||||||
|
|
||||||
|
// should not render either
|
||||||
|
wrapper = getWrapper(true, 'default');
|
||||||
|
expect(wrapper.find('.button.small').exists()).toBe(false);
|
||||||
|
expect(wrapper.find('.button.tiny').exists()).toBe(false);
|
||||||
|
expect(wrapper.find('.button.large').exists()).toBe(false);
|
||||||
|
expect(wrapper.find('.button').exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="showSeparator" class="separator">
|
||||||
|
OR
|
||||||
|
</div>
|
||||||
|
<a :href="getGoogleAuthUrl()">
|
||||||
|
<button
|
||||||
|
class="button expanded button__google_login"
|
||||||
|
:class="{
|
||||||
|
// Explicit checking to ensure no other value is used
|
||||||
|
large: buttonSize === 'large',
|
||||||
|
small: buttonSize === 'small',
|
||||||
|
tiny: buttonSize === 'tiny',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/assets/images/auth/google.svg"
|
||||||
|
alt="Google Logo"
|
||||||
|
class="icon"
|
||||||
|
/>
|
||||||
|
<slot>{{ $t('LOGIN.OAUTH.GOOGLE_LOGIN') }}</slot>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const validButtonSizes = ['small', 'tiny', 'large'];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
showSeparator: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
buttonSize: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
validator: value =>
|
||||||
|
validButtonSizes.includes(value) || value === undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getGoogleAuthUrl() {
|
||||||
|
// Ideally a request to /auth/google_oauth2 should be made
|
||||||
|
// Creating the URL manually because the devise-token-auth with
|
||||||
|
// omniauth has a standing issue on redirecting the post request
|
||||||
|
// https://github.com/lynndylanhurley/devise_token_auth/issues/1466
|
||||||
|
const baseUrl =
|
||||||
|
'https://accounts.google.com/o/oauth2/auth/oauthchooseaccount';
|
||||||
|
const clientId = window.chatwootConfig.googleOAuthClientId;
|
||||||
|
const redirectUri = window.chatwootConfig.googleOAuthCallbackUrl;
|
||||||
|
const responseType = 'code';
|
||||||
|
const scope = 'email profile';
|
||||||
|
|
||||||
|
// Build the query string
|
||||||
|
const queryString = new URLSearchParams({
|
||||||
|
client_id: clientId,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
response_type: responseType,
|
||||||
|
scope: scope,
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
// Construct the full URL
|
||||||
|
return `${baseUrl}?${queryString}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.separator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: var(--space-two) var(--space-zero);
|
||||||
|
gap: var(--space-one);
|
||||||
|
color: var(--s-300);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--s-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.button__google_login {
|
||||||
|
background: var(--white);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-one);
|
||||||
|
border: 1px solid var(--s-100);
|
||||||
|
color: var(--b-800);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -14,6 +14,11 @@
|
|||||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later",
|
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later",
|
||||||
"UNAUTH": "Username / Password Incorrect. Please try again"
|
"UNAUTH": "Username / Password Incorrect. Please try again"
|
||||||
},
|
},
|
||||||
|
"OAUTH": {
|
||||||
|
"GOOGLE_LOGIN": "Login with Google",
|
||||||
|
"BUSINESS_ACCOUNTS_ONLY": "Please use your company email address to login",
|
||||||
|
"NO_ACCOUNT_FOUND": "We couldn't find an account for your email address."
|
||||||
|
},
|
||||||
"FORGOT_PASSWORD": "Forgot your password?",
|
"FORGOT_PASSWORD": "Forgot your password?",
|
||||||
"CREATE_NEW_ACCOUNT": "Create new account",
|
"CREATE_NEW_ACCOUNT": "Create new account",
|
||||||
"SUBMIT": "Login"
|
"SUBMIT": "Login"
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
"TESTIMONIAL_HEADER": "All it takes is one step to move forward",
|
"TESTIMONIAL_HEADER": "All it takes is one step to move forward",
|
||||||
"TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.",
|
"TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.",
|
||||||
"TERMS_ACCEPT": "By creating an account, you agree to our <a href=\"https://www.chatwoot.com/terms\">T & C</a> and <a href=\"https://www.chatwoot.com/privacy-policy\">Privacy policy</a>",
|
"TERMS_ACCEPT": "By creating an account, you agree to our <a href=\"https://www.chatwoot.com/terms\">T & C</a> and <a href=\"https://www.chatwoot.com/privacy-policy\">Privacy policy</a>",
|
||||||
|
"OAUTH": {
|
||||||
|
"GOOGLE_SIGNUP": "Sign up with Google"
|
||||||
|
},
|
||||||
"COMPANY_NAME": {
|
"COMPANY_NAME": {
|
||||||
"LABEL": "Company name",
|
"LABEL": "Company name",
|
||||||
"PLACEHOLDER": "Enter your company name. eg: Wayne Enterprises",
|
"PLACEHOLDER": "Enter your company name. eg: Wayne Enterprises",
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.signup-form--content {
|
.signup-form--content {
|
||||||
padding: var(--space-jumbo);
|
padding: var(--space-larger);
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<form @submit.prevent="submit">
|
<form @submit.prevent="submit">
|
||||||
<div class="input-wrap">
|
<div class="input-wrap">
|
||||||
|
<div class="input-wrap__two-column">
|
||||||
<auth-input
|
<auth-input
|
||||||
v-model="credentials.fullName"
|
v-model.trim="credentials.fullName"
|
||||||
:class="{ error: $v.credentials.fullName.$error }"
|
:class="{ error: $v.credentials.fullName.$error }"
|
||||||
:label="$t('REGISTER.FULL_NAME.LABEL')"
|
:label="$t('REGISTER.FULL_NAME.LABEL')"
|
||||||
icon-name="person"
|
icon-name="person"
|
||||||
:placeholder="$t('REGISTER.FULL_NAME.PLACEHOLDER')"
|
:placeholder="$t('REGISTER.FULL_NAME.PLACEHOLDER')"
|
||||||
:error="
|
:error="
|
||||||
$v.credentials.fullName.$error ? $t('REGISTER.FULL_NAME.ERROR') : ''
|
$v.credentials.fullName.$error
|
||||||
|
? $t('REGISTER.FULL_NAME.ERROR')
|
||||||
|
: ''
|
||||||
"
|
"
|
||||||
@blur="$v.credentials.fullName.$touch"
|
@blur="$v.credentials.fullName.$touch"
|
||||||
/>
|
/>
|
||||||
<auth-input
|
<auth-input
|
||||||
v-model.trim="credentials.email"
|
v-model.trim="credentials.accountName"
|
||||||
type="email"
|
|
||||||
:class="{ error: $v.credentials.email.$error }"
|
|
||||||
icon-name="mail"
|
|
||||||
:label="$t('REGISTER.EMAIL.LABEL')"
|
|
||||||
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
|
|
||||||
:error="$v.credentials.email.$error ? $t('REGISTER.EMAIL.ERROR') : ''"
|
|
||||||
@blur="$v.credentials.email.$touch"
|
|
||||||
/>
|
|
||||||
<auth-input
|
|
||||||
v-model="credentials.accountName"
|
|
||||||
:class="{ error: $v.credentials.accountName.$error }"
|
:class="{ error: $v.credentials.accountName.$error }"
|
||||||
icon-name="building-bank"
|
icon-name="building-bank"
|
||||||
:label="$t('REGISTER.COMPANY_NAME.LABEL')"
|
:label="$t('REGISTER.COMPANY_NAME.LABEL')"
|
||||||
@@ -35,6 +29,17 @@
|
|||||||
"
|
"
|
||||||
@blur="$v.credentials.accountName.$touch"
|
@blur="$v.credentials.accountName.$touch"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<auth-input
|
||||||
|
v-model.trim="credentials.email"
|
||||||
|
type="email"
|
||||||
|
:class="{ error: $v.credentials.email.$error }"
|
||||||
|
icon-name="mail"
|
||||||
|
:label="$t('REGISTER.EMAIL.LABEL')"
|
||||||
|
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
|
||||||
|
:error="$v.credentials.email.$error ? $t('REGISTER.EMAIL.ERROR') : ''"
|
||||||
|
@blur="$v.credentials.email.$touch"
|
||||||
|
/>
|
||||||
<auth-input
|
<auth-input
|
||||||
v-model.trim="credentials.password"
|
v-model.trim="credentials.password"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -63,8 +68,12 @@
|
|||||||
:is-loading="isSignupInProgress"
|
:is-loading="isSignupInProgress"
|
||||||
icon="arrow-chevron-right"
|
icon="arrow-chevron-right"
|
||||||
/>
|
/>
|
||||||
<p v-dompurify-html="termsLink" class="accept--terms" />
|
|
||||||
</form>
|
</form>
|
||||||
|
<GoogleOAuthButton v-if="showGoogleOAuth()">
|
||||||
|
{{ $t('REGISTER.OAUTH.GOOGLE_SIGNUP') }}
|
||||||
|
</GoogleOAuthButton>
|
||||||
|
<p v-dompurify-html="termsLink" class="accept--terms" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -78,6 +87,7 @@ import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
|
|||||||
import AuthInput from '../AuthInput.vue';
|
import AuthInput from '../AuthInput.vue';
|
||||||
import AuthSubmitButton from '../AuthSubmitButton.vue';
|
import AuthSubmitButton from '../AuthSubmitButton.vue';
|
||||||
import { isValidPassword } from 'shared/helpers/Validators';
|
import { isValidPassword } from 'shared/helpers/Validators';
|
||||||
|
import GoogleOAuthButton from 'dashboard/components/ui/Auth/GoogleOAuthButton.vue';
|
||||||
var CompanyEmailValidator = require('company-email-validator');
|
var CompanyEmailValidator = require('company-email-validator');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -85,6 +95,7 @@ export default {
|
|||||||
AuthInput,
|
AuthInput,
|
||||||
AuthSubmitButton,
|
AuthSubmitButton,
|
||||||
VueHcaptcha,
|
VueHcaptcha,
|
||||||
|
GoogleOAuthButton,
|
||||||
},
|
},
|
||||||
mixins: [globalConfigMixin, alertMixin],
|
mixins: [globalConfigMixin, alertMixin],
|
||||||
data() {
|
data() {
|
||||||
@@ -183,6 +194,9 @@ export default {
|
|||||||
this.credentials.hCaptchaClientResponse = token;
|
this.credentials.hCaptchaClientResponse = token;
|
||||||
this.didCaptchaReset = false;
|
this.didCaptchaReset = false;
|
||||||
},
|
},
|
||||||
|
showGoogleOAuth() {
|
||||||
|
return Boolean(window.chatwootConfig.googleOAuthClientId);
|
||||||
|
},
|
||||||
resetCaptcha() {
|
resetCaptcha() {
|
||||||
if (!this.globalConfig.hCaptchaSiteKey) {
|
if (!this.globalConfig.hCaptchaSiteKey) {
|
||||||
return;
|
return;
|
||||||
@@ -214,4 +228,28 @@ export default {
|
|||||||
.accept--terms {
|
.accept--terms {
|
||||||
margin: var(--space-normal) 0 var(--space-smaller) 0;
|
margin: var(--space-normal) 0 var(--space-smaller) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-wrap {
|
||||||
|
.input-wrap__two-column {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.6rem;
|
||||||
|
grid-template-columns: repeat(2, minmax(100px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 2rem 0rem;
|
||||||
|
gap: var(--space-normal);
|
||||||
|
color: var(--s-300);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--s-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export default {
|
|||||||
background: var(--w-400);
|
background: var(--w-400);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1;
|
flex: 1 1;
|
||||||
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const authIgnoreRoutes = [
|
|||||||
'auth_confirmation',
|
'auth_confirmation',
|
||||||
'pushBack',
|
'pushBack',
|
||||||
'auth_password_edit',
|
'auth_password_edit',
|
||||||
|
'oauth-callback',
|
||||||
];
|
];
|
||||||
|
|
||||||
const routeValidators = [
|
const routeValidators = [
|
||||||
@@ -117,6 +118,7 @@ export const validateRouteAccess = (to, from, next, { getters }) => {
|
|||||||
|
|
||||||
export const initalizeRouter = () => {
|
export const initalizeRouter = () => {
|
||||||
const userAuthentication = store.dispatch('setUser');
|
const userAuthentication = store.dispatch('setUser');
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
AnalyticsHelper.page(to.name || '', {
|
AnalyticsHelper.page(to.name || '', {
|
||||||
path: to.path,
|
path: to.path,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="medium-12 column login">
|
<main class="medium-12 column login">
|
||||||
<div class="text-center medium-12 login__hero align-self-top">
|
<section class="text-center medium-12 login__hero align-self-top">
|
||||||
<img
|
<img
|
||||||
:src="globalConfig.logo"
|
:src="globalConfig.logo"
|
||||||
:alt="globalConfig.installationName"
|
:alt="globalConfig.installationName"
|
||||||
@@ -11,11 +11,16 @@
|
|||||||
useInstallationName($t('LOGIN.TITLE'), globalConfig.installationName)
|
useInstallationName($t('LOGIN.TITLE'), globalConfig.installationName)
|
||||||
}}
|
}}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</section>
|
||||||
<div class="row align-center">
|
<section class="row align-center">
|
||||||
<div v-if="!email" class="small-12 medium-4 column">
|
<div v-if="!email" class="small-12 medium-4 column">
|
||||||
<form class="login-box column align-self-top" @submit.prevent="login()">
|
<div class="login-box column align-self-top">
|
||||||
<div class="column log-in-form">
|
<GoogleOAuthButton
|
||||||
|
v-if="showGoogleOAuth()"
|
||||||
|
button-size="large"
|
||||||
|
class="oauth-reverse"
|
||||||
|
/>
|
||||||
|
<form class="column log-in-form" @submit.prevent="login()">
|
||||||
<label :class="{ error: $v.credentials.email.$error }">
|
<label :class="{ error: $v.credentials.email.$error }">
|
||||||
{{ $t('LOGIN.EMAIL.LABEL') }}
|
{{ $t('LOGIN.EMAIL.LABEL') }}
|
||||||
<input
|
<input
|
||||||
@@ -46,9 +51,9 @@
|
|||||||
:loading="loginApi.showLoading"
|
:loading="loginApi.showLoading"
|
||||||
button-class="large expanded"
|
button-class="large expanded"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
<div class="column text-center sigin__footer">
|
</div>
|
||||||
|
<div class="text-center column sigin__footer">
|
||||||
<p v-if="!globalConfig.disableUserProfileUpdate">
|
<p v-if="!globalConfig.disableUserProfileUpdate">
|
||||||
<router-link to="auth/reset/password">
|
<router-link to="auth/reset/password">
|
||||||
{{ $t('LOGIN.FORGOT_PASSWORD') }}
|
{{ $t('LOGIN.FORGOT_PASSWORD') }}
|
||||||
@@ -62,19 +67,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<woot-spinner v-else size="" />
|
<woot-spinner v-else size="" />
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { required, email } from 'vuelidate/lib/validators';
|
import { required, email } from 'vuelidate/lib/validators';
|
||||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||||
import WootSubmitButton from '../../components/buttons/FormSubmitButton';
|
import WootSubmitButton from 'components/buttons/FormSubmitButton';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { parseBoolean } from '@chatwoot/utils';
|
||||||
|
import GoogleOAuthButton from '../../components/ui/Auth/GoogleOAuthButton.vue';
|
||||||
|
|
||||||
|
const ERROR_MESSAGES = {
|
||||||
|
'no-account-found': 'LOGIN.OAUTH.NO_ACCOUNT_FOUND',
|
||||||
|
'business-account-only': 'LOGIN.OAUTH.BUSINESS_ACCOUNTS_ONLY',
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootSubmitButton,
|
WootSubmitButton,
|
||||||
|
GoogleOAuthButton,
|
||||||
},
|
},
|
||||||
mixins: [globalConfigMixin],
|
mixins: [globalConfigMixin],
|
||||||
props: {
|
props: {
|
||||||
@@ -83,6 +96,7 @@ export default {
|
|||||||
ssoConversationId: { type: String, default: '' },
|
ssoConversationId: { type: String, default: '' },
|
||||||
config: { type: String, default: '' },
|
config: { type: String, default: '' },
|
||||||
email: { type: String, default: '' },
|
email: { type: String, default: '' },
|
||||||
|
authError: { type: String, default: '' },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -119,6 +133,16 @@ export default {
|
|||||||
if (this.ssoAuthToken) {
|
if (this.ssoAuthToken) {
|
||||||
this.login();
|
this.login();
|
||||||
}
|
}
|
||||||
|
if (this.authError) {
|
||||||
|
const message = ERROR_MESSAGES[this.authError] ?? 'LOGIN.API.UNAUTH';
|
||||||
|
this.showAlert(this.$t(message));
|
||||||
|
// wait for idle state
|
||||||
|
window.requestIdleCallback(() => {
|
||||||
|
// Remove the error query param from the url
|
||||||
|
const { query } = this.$route;
|
||||||
|
this.$router.replace({ query: { ...query, error: undefined } });
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showAlert(message) {
|
showAlert(message) {
|
||||||
@@ -128,7 +152,10 @@ export default {
|
|||||||
bus.$emit('newToastMessage', this.loginApi.message);
|
bus.$emit('newToastMessage', this.loginApi.message);
|
||||||
},
|
},
|
||||||
showSignupLink() {
|
showSignupLink() {
|
||||||
return window.chatwootConfig.signupEnabled === 'true';
|
return parseBoolean(window.chatwootConfig.signupEnabled);
|
||||||
|
},
|
||||||
|
showGoogleOAuth() {
|
||||||
|
return Boolean(window.chatwootConfig.googleOAuthClientId);
|
||||||
},
|
},
|
||||||
login() {
|
login() {
|
||||||
this.loginApi.showLoading = true;
|
this.loginApi.showLoading = true;
|
||||||
@@ -172,3 +199,10 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.oauth-reverse {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default {
|
|||||||
ssoAuthToken: route.query.sso_auth_token,
|
ssoAuthToken: route.query.sso_auth_token,
|
||||||
ssoAccountId: route.query.sso_account_id,
|
ssoAccountId: route.query.sso_account_id,
|
||||||
ssoConversationId: route.query.sso_conversation_id,
|
ssoConversationId: route.query.sso_conversation_id,
|
||||||
|
authError: route.query.error,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ class User < ApplicationRecord
|
|||||||
:trackable,
|
:trackable,
|
||||||
:validatable,
|
:validatable,
|
||||||
:confirmable,
|
:confirmable,
|
||||||
:password_has_required_content
|
:password_has_required_content,
|
||||||
|
:omniauthable, omniauth_providers: [:google_oauth2]
|
||||||
|
|
||||||
# TODO: remove in a future version once online status is moved to account users
|
# TODO: remove in a future version once online status is moved to account users
|
||||||
# remove the column availability from users
|
# remove the column availability from users
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
hostURL: '<%= ENV.fetch('FRONTEND_URL', '') %>',
|
hostURL: '<%= ENV.fetch('FRONTEND_URL', '') %>',
|
||||||
helpCenterURL: '<%= ENV.fetch('HELPCENTER_URL', '') %>',
|
helpCenterURL: '<%= ENV.fetch('HELPCENTER_URL', '') %>',
|
||||||
fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>',
|
fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>',
|
||||||
|
googleOAuthClientId: '<%= ENV.fetch('GOOGLE_OAUTH_CLIENT_ID', nil) %>',
|
||||||
|
googleOAuthCallbackUrl: '<%= ENV.fetch('GOOGLE_OAUTH_CALLBACK_URL', nil) %>',
|
||||||
fbApiVersion: '<%= @global_config['FACEBOOK_API_VERSION'] %>',
|
fbApiVersion: '<%= @global_config['FACEBOOK_API_VERSION'] %>',
|
||||||
signupEnabled: '<%= @global_config['ENABLE_ACCOUNT_SIGNUP'] %>',
|
signupEnabled: '<%= @global_config['ENABLE_ACCOUNT_SIGNUP'] %>',
|
||||||
isEnterprise: '<%= @global_config['IS_ENTERPRISE'] %>',
|
isEnterprise: '<%= @global_config['IS_ENTERPRISE'] %>',
|
||||||
|
|||||||
5
config/initializers/omniauth.rb
Normal file
5
config/initializers/omniauth.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Rails.application.config.middleware.use OmniAuth::Builder do
|
||||||
|
provider :google_oauth2, ENV.fetch('GOOGLE_OAUTH_CLIENT_ID', nil), ENV.fetch('GOOGLE_OAUTH_CLIENT_SECRET', nil), {
|
||||||
|
provider_ignores_state: true
|
||||||
|
}
|
||||||
|
end
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
# AUTH STARTS
|
# AUTH STARTS
|
||||||
match 'auth/:provider/callback', to: 'home#callback', via: [:get, :post]
|
|
||||||
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
|
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
|
||||||
confirmations: 'devise_overrides/confirmations',
|
confirmations: 'devise_overrides/confirmations',
|
||||||
passwords: 'devise_overrides/passwords',
|
passwords: 'devise_overrides/passwords',
|
||||||
sessions: 'devise_overrides/sessions',
|
sessions: 'devise_overrides/sessions',
|
||||||
token_validations: 'devise_overrides/token_validations'
|
token_validations: 'devise_overrides/token_validations',
|
||||||
|
omniauth_callbacks: 'devise_overrides/omniauth_callbacks'
|
||||||
}, via: [:get, :post]
|
}, via: [:get, :post]
|
||||||
|
|
||||||
## renders the frontend paths only if its not an api only server
|
## renders the frontend paths only if its not an api only server
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const resolve = {
|
|||||||
survey: path.resolve('./app/javascript/survey'),
|
survey: path.resolve('./app/javascript/survey'),
|
||||||
assets: path.resolve('./app/javascript/dashboard/assets'),
|
assets: path.resolve('./app/javascript/dashboard/assets'),
|
||||||
components: path.resolve('./app/javascript/dashboard/components'),
|
components: path.resolve('./app/javascript/dashboard/components'),
|
||||||
|
helpers: path.resolve('./app/javascript/shared/helpers'),
|
||||||
'./iconfont.eot': 'vue-easytable/libs/font/iconfont.eot',
|
'./iconfont.eot': 'vue-easytable/libs/font/iconfont.eot',
|
||||||
'./iconfont.woff': 'vue-easytable/libs/font/iconfont.woff',
|
'./iconfont.woff': 'vue-easytable/libs/font/iconfont.woff',
|
||||||
'./iconfont.ttf': 'vue-easytable/libs/font/iconfont.ttf',
|
'./iconfont.ttf': 'vue-easytable/libs/font/iconfont.ttf',
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braid/vue-formulate": "^2.5.2",
|
"@braid/vue-formulate": "^2.5.2",
|
||||||
"@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git",
|
"@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git",
|
||||||
"@chatwoot/utils": "^0.0.10",
|
"@chatwoot/utils": "^0.0.11",
|
||||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||||
"@june-so/analytics-next": "^1.36.5",
|
"@june-so/analytics-next": "^1.36.5",
|
||||||
"@rails/actioncable": "6.1.3",
|
"@rails/actioncable": "6.1.3",
|
||||||
|
|||||||
1
public/assets/images/auth/google.svg
Normal file
1
public/assets/images/auth/google.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/><path d="M1 1h22v22H1z" fill="none"/></svg>
|
||||||
|
After Width: | Height: | Size: 742 B |
110
spec/controllers/devise/omniauth_callbacks_controller_spec.rb
Normal file
110
spec/controllers/devise/omniauth_callbacks_controller_spec.rb
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'DeviseOverrides::OmniauthCallbacksController', type: :request do
|
||||||
|
let(:account_builder) { double }
|
||||||
|
let(:user_double) { object_double(:user) }
|
||||||
|
|
||||||
|
def set_omniauth_config(for_email = 'test@example.com')
|
||||||
|
OmniAuth.config.test_mode = true
|
||||||
|
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
|
||||||
|
provider: 'google',
|
||||||
|
uid: '123545',
|
||||||
|
info: {
|
||||||
|
name: 'test',
|
||||||
|
email: for_email,
|
||||||
|
image: 'https://example.com/image.jpg'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#omniauth_sucess' do
|
||||||
|
it 'allows signup' do
|
||||||
|
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||||
|
set_omniauth_config('test_not_preset@example.com')
|
||||||
|
allow(AccountBuilder).to receive(:new).and_return(account_builder)
|
||||||
|
allow(account_builder).to receive(:perform).and_return(user_double)
|
||||||
|
allow(Avatar::AvatarFromUrlJob).to receive(:perform_later).and_return(true)
|
||||||
|
|
||||||
|
get '/omniauth/google_oauth2/callback'
|
||||||
|
|
||||||
|
# expect a 302 redirect to auth/google_oauth2/callback
|
||||||
|
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||||
|
follow_redirect!
|
||||||
|
|
||||||
|
expect(AccountBuilder).to have_received(:new).with({
|
||||||
|
account_name: 'example',
|
||||||
|
user_full_name: 'test',
|
||||||
|
email: 'test_not_preset@example.com',
|
||||||
|
locale: I18n.locale,
|
||||||
|
confirmed: nil
|
||||||
|
})
|
||||||
|
expect(account_builder).to have_received(:perform)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'blocks personal accounts signup' do
|
||||||
|
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||||
|
set_omniauth_config('personal@gmail.com')
|
||||||
|
get '/omniauth/google_oauth2/callback'
|
||||||
|
|
||||||
|
# expect a 302 redirect to auth/google_oauth2/callback
|
||||||
|
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||||
|
follow_redirect!
|
||||||
|
|
||||||
|
# expect a 302 redirect to app/login with error disallowing personal accounts
|
||||||
|
expect(response).to redirect_to(%r{/app/login\?error=business-account-only$})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This test does not affect line coverage, but it is important to ensure that the logic
|
||||||
|
# does not allow any signup if the ENV explicitly disables it
|
||||||
|
it 'blocks signup if ENV disabled' do
|
||||||
|
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'false' do
|
||||||
|
set_omniauth_config('does-not-exist-for-sure@example.com')
|
||||||
|
get '/omniauth/google_oauth2/callback'
|
||||||
|
|
||||||
|
# expect a 302 redirect to auth/google_oauth2/callback
|
||||||
|
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||||
|
follow_redirect!
|
||||||
|
|
||||||
|
# expect a 302 redirect to app/login with error disallowing signup
|
||||||
|
expect(response).to redirect_to(%r{/app/login\?error=no-account-found$})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows login' do
|
||||||
|
create(:user, email: 'test@example.com')
|
||||||
|
set_omniauth_config('test@example.com')
|
||||||
|
|
||||||
|
get '/omniauth/google_oauth2/callback'
|
||||||
|
# expect a 302 redirect to auth/google_oauth2/callback
|
||||||
|
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||||
|
|
||||||
|
follow_redirect!
|
||||||
|
expect(response).to redirect_to(%r{/app/login\?email=.+&sso_auth_token=.+$})
|
||||||
|
|
||||||
|
# expect app/login page to respond with 200 and render
|
||||||
|
follow_redirect!
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
# from a line coverage point of view this may seem redundant
|
||||||
|
# but to ensure that the logic allows for existing users even if they have a gmail account
|
||||||
|
# we need to test this explicitly
|
||||||
|
it 'allows personal account login' do
|
||||||
|
create(:user, email: 'personal-existing@gmail.com')
|
||||||
|
set_omniauth_config('personal-existing@gmail.com')
|
||||||
|
|
||||||
|
get '/omniauth/google_oauth2/callback'
|
||||||
|
# expect a 302 redirect to auth/google_oauth2/callback
|
||||||
|
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
|
||||||
|
|
||||||
|
follow_redirect!
|
||||||
|
expect(response).to redirect_to(%r{/app/login\?email=.+&sso_auth_token=.+$})
|
||||||
|
|
||||||
|
# expect app/login page to respond with 200 and render
|
||||||
|
follow_redirect!
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
18
yarn.lock
18
yarn.lock
@@ -1410,12 +1410,12 @@
|
|||||||
prosemirror-utils "^0.9.6"
|
prosemirror-utils "^0.9.6"
|
||||||
prosemirror-view "^1.17.2"
|
prosemirror-view "^1.17.2"
|
||||||
|
|
||||||
"@chatwoot/utils@^0.0.10":
|
"@chatwoot/utils@^0.0.11":
|
||||||
version "0.0.10"
|
version "0.0.11"
|
||||||
resolved "https://registry.npmjs.org/@chatwoot/utils/-/utils-0.0.10.tgz#59f68cc28d8718b261ebed8b9c94d2c493b6c67f"
|
resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.11.tgz#6922492e21c20bdb0ef733967a0b94829d8f620f"
|
||||||
integrity sha512-Zd+wQTblWKUV1mhcXoabcfoLygx/Ock5pP0JQdfqW64lubhjYaRR4gCutEgqUcQB4nuOUH7MZ7BTzdZm4RoM/g==
|
integrity sha512-uiLsuBYTlZGXJ/d7QfJ+hlO1u7U1750ON5iu0pus8t6GlJQdxvMQWuf6fHQtfsDNcvL1aXsQu3H6BUk/nVZLlw==
|
||||||
dependencies:
|
dependencies:
|
||||||
date-fns "^2.22.1"
|
date-fns "^2.29.1"
|
||||||
|
|
||||||
"@cnakazawa/watch@^1.0.3":
|
"@cnakazawa/watch@^1.0.3":
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
@@ -6902,10 +6902,10 @@ date-fns@^1.27.2:
|
|||||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
|
||||||
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
|
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
|
||||||
|
|
||||||
date-fns@^2.22.1:
|
date-fns@^2.29.1:
|
||||||
version "2.28.0"
|
version "2.29.3"
|
||||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
|
||||||
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
|
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
|
||||||
|
|
||||||
date-format-parse@^0.2.6:
|
date-format-parse@^0.2.6:
|
||||||
version "0.2.6"
|
version "0.2.6"
|
||||||
|
|||||||
Reference in New Issue
Block a user