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:
@@ -88,7 +88,7 @@ export default {
|
||||
}
|
||||
|
||||
.signup-form--content {
|
||||
padding: var(--space-jumbo);
|
||||
padding: var(--space-larger);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,70 +1,79 @@
|
||||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="input-wrap">
|
||||
<auth-input
|
||||
v-model="credentials.fullName"
|
||||
:class="{ error: $v.credentials.fullName.$error }"
|
||||
:label="$t('REGISTER.FULL_NAME.LABEL')"
|
||||
icon-name="person"
|
||||
:placeholder="$t('REGISTER.FULL_NAME.PLACEHOLDER')"
|
||||
:error="
|
||||
$v.credentials.fullName.$error ? $t('REGISTER.FULL_NAME.ERROR') : ''
|
||||
"
|
||||
@blur="$v.credentials.fullName.$touch"
|
||||
<div>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="input-wrap">
|
||||
<div class="input-wrap__two-column">
|
||||
<auth-input
|
||||
v-model.trim="credentials.fullName"
|
||||
:class="{ error: $v.credentials.fullName.$error }"
|
||||
:label="$t('REGISTER.FULL_NAME.LABEL')"
|
||||
icon-name="person"
|
||||
:placeholder="$t('REGISTER.FULL_NAME.PLACEHOLDER')"
|
||||
:error="
|
||||
$v.credentials.fullName.$error
|
||||
? $t('REGISTER.FULL_NAME.ERROR')
|
||||
: ''
|
||||
"
|
||||
@blur="$v.credentials.fullName.$touch"
|
||||
/>
|
||||
<auth-input
|
||||
v-model.trim="credentials.accountName"
|
||||
:class="{ error: $v.credentials.accountName.$error }"
|
||||
icon-name="building-bank"
|
||||
:label="$t('REGISTER.COMPANY_NAME.LABEL')"
|
||||
:placeholder="$t('REGISTER.COMPANY_NAME.PLACEHOLDER')"
|
||||
:error="
|
||||
$v.credentials.accountName.$error
|
||||
? $t('REGISTER.COMPANY_NAME.ERROR')
|
||||
: ''
|
||||
"
|
||||
@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
|
||||
v-model.trim="credentials.password"
|
||||
type="password"
|
||||
:class="{ error: $v.credentials.password.$error }"
|
||||
icon-name="lock-closed"
|
||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
|
||||
:error="passwordErrorText"
|
||||
@blur="$v.credentials.password.$touch"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="globalConfig.hCaptchaSiteKey" class="h-captcha--box">
|
||||
<vue-hcaptcha
|
||||
ref="hCaptcha"
|
||||
:class="{ error: !hasAValidCaptcha && didCaptchaReset }"
|
||||
:sitekey="globalConfig.hCaptchaSiteKey"
|
||||
@verify="onRecaptchaVerified"
|
||||
/>
|
||||
<span v-if="!hasAValidCaptcha && didCaptchaReset" class="captcha-error">
|
||||
{{ $t('SET_NEW_PASSWORD.CAPTCHA.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
<auth-submit-button
|
||||
:label="$t('REGISTER.SUBMIT')"
|
||||
:is-disabled="isSignupInProgress || !hasAValidCaptcha"
|
||||
:is-loading="isSignupInProgress"
|
||||
icon="arrow-chevron-right"
|
||||
/>
|
||||
<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
|
||||
v-model="credentials.accountName"
|
||||
:class="{ error: $v.credentials.accountName.$error }"
|
||||
icon-name="building-bank"
|
||||
:label="$t('REGISTER.COMPANY_NAME.LABEL')"
|
||||
:placeholder="$t('REGISTER.COMPANY_NAME.PLACEHOLDER')"
|
||||
:error="
|
||||
$v.credentials.accountName.$error
|
||||
? $t('REGISTER.COMPANY_NAME.ERROR')
|
||||
: ''
|
||||
"
|
||||
@blur="$v.credentials.accountName.$touch"
|
||||
/>
|
||||
<auth-input
|
||||
v-model.trim="credentials.password"
|
||||
type="password"
|
||||
:class="{ error: $v.credentials.password.$error }"
|
||||
icon-name="lock-closed"
|
||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
|
||||
:error="passwordErrorText"
|
||||
@blur="$v.credentials.password.$touch"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="globalConfig.hCaptchaSiteKey" class="h-captcha--box">
|
||||
<vue-hcaptcha
|
||||
ref="hCaptcha"
|
||||
:class="{ error: !hasAValidCaptcha && didCaptchaReset }"
|
||||
:sitekey="globalConfig.hCaptchaSiteKey"
|
||||
@verify="onRecaptchaVerified"
|
||||
/>
|
||||
<span v-if="!hasAValidCaptcha && didCaptchaReset" class="captcha-error">
|
||||
{{ $t('SET_NEW_PASSWORD.CAPTCHA.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
<auth-submit-button
|
||||
:label="$t('REGISTER.SUBMIT')"
|
||||
:is-disabled="isSignupInProgress || !hasAValidCaptcha"
|
||||
:is-loading="isSignupInProgress"
|
||||
icon="arrow-chevron-right"
|
||||
/>
|
||||
</form>
|
||||
<GoogleOAuthButton v-if="showGoogleOAuth()">
|
||||
{{ $t('REGISTER.OAUTH.GOOGLE_SIGNUP') }}
|
||||
</GoogleOAuthButton>
|
||||
<p v-dompurify-html="termsLink" class="accept--terms" />
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -78,6 +87,7 @@ import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
|
||||
import AuthInput from '../AuthInput.vue';
|
||||
import AuthSubmitButton from '../AuthSubmitButton.vue';
|
||||
import { isValidPassword } from 'shared/helpers/Validators';
|
||||
import GoogleOAuthButton from 'dashboard/components/ui/Auth/GoogleOAuthButton.vue';
|
||||
var CompanyEmailValidator = require('company-email-validator');
|
||||
|
||||
export default {
|
||||
@@ -85,6 +95,7 @@ export default {
|
||||
AuthInput,
|
||||
AuthSubmitButton,
|
||||
VueHcaptcha,
|
||||
GoogleOAuthButton,
|
||||
},
|
||||
mixins: [globalConfigMixin, alertMixin],
|
||||
data() {
|
||||
@@ -183,6 +194,9 @@ export default {
|
||||
this.credentials.hCaptchaClientResponse = token;
|
||||
this.didCaptchaReset = false;
|
||||
},
|
||||
showGoogleOAuth() {
|
||||
return Boolean(window.chatwootConfig.googleOAuthClientId);
|
||||
},
|
||||
resetCaptcha() {
|
||||
if (!this.globalConfig.hCaptchaSiteKey) {
|
||||
return;
|
||||
@@ -214,4 +228,28 @@ export default {
|
||||
.accept--terms {
|
||||
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>
|
||||
|
||||
@@ -84,6 +84,7 @@ export default {
|
||||
background: var(--w-400);
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ const authIgnoreRoutes = [
|
||||
'auth_confirmation',
|
||||
'pushBack',
|
||||
'auth_password_edit',
|
||||
'oauth-callback',
|
||||
];
|
||||
|
||||
const routeValidators = [
|
||||
@@ -117,6 +118,7 @@ export const validateRouteAccess = (to, from, next, { getters }) => {
|
||||
|
||||
export const initalizeRouter = () => {
|
||||
const userAuthentication = store.dispatch('setUser');
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
AnalyticsHelper.page(to.name || '', {
|
||||
path: to.path,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="medium-12 column login">
|
||||
<div class="text-center medium-12 login__hero align-self-top">
|
||||
<main class="medium-12 column login">
|
||||
<section class="text-center medium-12 login__hero align-self-top">
|
||||
<img
|
||||
:src="globalConfig.logo"
|
||||
:alt="globalConfig.installationName"
|
||||
@@ -11,11 +11,16 @@
|
||||
useInstallationName($t('LOGIN.TITLE'), globalConfig.installationName)
|
||||
}}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="row align-center">
|
||||
</section>
|
||||
<section class="row align-center">
|
||||
<div v-if="!email" class="small-12 medium-4 column">
|
||||
<form class="login-box column align-self-top" @submit.prevent="login()">
|
||||
<div class="column log-in-form">
|
||||
<div class="login-box column align-self-top">
|
||||
<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 }">
|
||||
{{ $t('LOGIN.EMAIL.LABEL') }}
|
||||
<input
|
||||
@@ -46,9 +51,9 @@
|
||||
:loading="loginApi.showLoading"
|
||||
button-class="large expanded"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="column text-center sigin__footer">
|
||||
</form>
|
||||
</div>
|
||||
<div class="text-center column sigin__footer">
|
||||
<p v-if="!globalConfig.disableUserProfileUpdate">
|
||||
<router-link to="auth/reset/password">
|
||||
{{ $t('LOGIN.FORGOT_PASSWORD') }}
|
||||
@@ -62,19 +67,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<woot-spinner v-else size="" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, email } from 'vuelidate/lib/validators';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import WootSubmitButton from '../../components/buttons/FormSubmitButton';
|
||||
import WootSubmitButton from 'components/buttons/FormSubmitButton';
|
||||
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 {
|
||||
components: {
|
||||
WootSubmitButton,
|
||||
GoogleOAuthButton,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
@@ -83,6 +96,7 @@ export default {
|
||||
ssoConversationId: { type: String, default: '' },
|
||||
config: { type: String, default: '' },
|
||||
email: { type: String, default: '' },
|
||||
authError: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -119,6 +133,16 @@ export default {
|
||||
if (this.ssoAuthToken) {
|
||||
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: {
|
||||
showAlert(message) {
|
||||
@@ -128,7 +152,10 @@ export default {
|
||||
bus.$emit('newToastMessage', this.loginApi.message);
|
||||
},
|
||||
showSignupLink() {
|
||||
return window.chatwootConfig.signupEnabled === 'true';
|
||||
return parseBoolean(window.chatwootConfig.signupEnabled);
|
||||
},
|
||||
showGoogleOAuth() {
|
||||
return Boolean(window.chatwootConfig.googleOAuthClientId);
|
||||
},
|
||||
login() {
|
||||
this.loginApi.showLoading = true;
|
||||
@@ -172,3 +199,10 @@ export default {
|
||||
},
|
||||
};
|
||||
</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,
|
||||
ssoAccountId: route.query.sso_account_id,
|
||||
ssoConversationId: route.query.sso_conversation_id,
|
||||
authError: route.query.error,
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user