From 76cd6ee767963e4589de7fd1391f39e9ed75f543 Mon Sep 17 00:00:00 2001 From: Ruslan Khayrullin Date: Fri, 2 Sep 2022 16:14:23 +0300 Subject: [PATCH] feat(ott-2680): auth with google --- Makefile | 2 + public/images/oauth/appleID.svg | 3 + public/images/oauth/facebook.svg | 3 + public/images/oauth/google.svg | 3 + src/components/VisuallyHidden/index.tsx | 14 +++ src/config/env.tsx | 2 + src/config/index.tsx | 1 + .../AuthServiceApp/components/App/index.tsx | 5 ++ .../AuthServiceApp/components/Login/hooks.tsx | 33 ++++++- .../AuthServiceApp/components/Login/index.tsx | 26 +++++- .../components/Login/styled.tsx | 89 +++++++++++++++++++ .../AuthServiceApp/components/Oauth/index.tsx | 69 ++++++++++++++ .../AuthServiceApp/config/authProviders.tsx | 5 ++ src/features/AuthServiceApp/config/lexics.tsx | 1 + src/features/AuthServiceApp/config/pages.tsx | 1 + .../helpers/getAuthUrl/index.tsx | 34 +++++++ .../AuthServiceApp/hooks/useParamsUrl.tsx | 4 + src/features/AuthServiceApp/styled.tsx | 2 +- src/features/AuthStore/helpers.tsx | 2 +- src/helpers/index.tsx | 1 + src/helpers/redirectToUrl/index.tsx | 3 + 21 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 public/images/oauth/appleID.svg create mode 100644 public/images/oauth/facebook.svg create mode 100644 public/images/oauth/google.svg create mode 100644 src/components/VisuallyHidden/index.tsx create mode 100644 src/features/AuthServiceApp/components/Oauth/index.tsx create mode 100644 src/features/AuthServiceApp/config/authProviders.tsx create mode 100644 src/features/AuthServiceApp/helpers/getAuthUrl/index.tsx create mode 100644 src/helpers/redirectToUrl/index.tsx diff --git a/Makefile b/Makefile index b9b69019..236f5dd7 100644 --- a/Makefile +++ b/Makefile @@ -90,6 +90,7 @@ auth-build: REACT_APP_TYPE=auth-service \ REACT_APP_ENV=staging \ + REACT_APP_GOOGLE_CLIENT_ID=1043133237396-kebgih109kro71b5c7c8qphtgjbd2gdk.apps.googleusercontent.com \ BUILD_PATH=build_auth \ GENERATE_SOURCEMAP=false \ npx react-scripts build @@ -101,6 +102,7 @@ auth-production-build: REACT_APP_TYPE=auth-service \ REACT_APP_ENV=production \ + REACT_APP_GOOGLE_CLIENT_ID=1043133237396-kebgih109kro71b5c7c8qphtgjbd2gdk.apps.googleusercontent.com \ BUILD_PATH=build_auth \ GENERATE_SOURCEMAP=false \ npx react-scripts build diff --git a/public/images/oauth/appleID.svg b/public/images/oauth/appleID.svg new file mode 100644 index 00000000..5398863e --- /dev/null +++ b/public/images/oauth/appleID.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/oauth/facebook.svg b/public/images/oauth/facebook.svg new file mode 100644 index 00000000..48b88641 --- /dev/null +++ b/public/images/oauth/facebook.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/oauth/google.svg b/public/images/oauth/google.svg new file mode 100644 index 00000000..27259e0c --- /dev/null +++ b/public/images/oauth/google.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/VisuallyHidden/index.tsx b/src/components/VisuallyHidden/index.tsx new file mode 100644 index 00000000..efcdc8b6 --- /dev/null +++ b/src/components/VisuallyHidden/index.tsx @@ -0,0 +1,14 @@ +import { css } from 'styled-components/macro' + +export const visuallyHidden = css` + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + border: 0; + white-space: nowrap; + clip-path: inset(100%); + clip: rect(0 0 0 0); + overflow: hidden; +` diff --git a/src/config/env.tsx b/src/config/env.tsx index 55241b83..95a5ec4b 100644 --- a/src/config/env.tsx +++ b/src/config/env.tsx @@ -15,3 +15,5 @@ export const isProduction = ENV === 'production' || ENV === 'preproduction' export const stageENV = process.env.REACT_APP_STAGE || 'staging' export const STRIPE_PUBLIC_KEY = process.env.REACT_APP_STRIPE_PK || 'pk_test_51J5TEYEDSxVnTgDWhKLstuDAhx9XmGJmj2awyZ1HghpWdU46MhXqbQt1PyW9XsRlES5JFyuQWbPRjoSsiW3wvXOH00KMirJEGZ' + +export const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID || '1043133237396-kebgih109kro71b5c7c8qphtgjbd2gdk.apps.googleusercontent.com' diff --git a/src/config/index.tsx b/src/config/index.tsx index d03692c7..4a2c8d17 100644 --- a/src/config/index.tsx +++ b/src/config/index.tsx @@ -8,3 +8,4 @@ export * from './history' export * from './devices' export * from './currencies' export * from './dashes' +export * from './env' diff --git a/src/features/AuthServiceApp/components/App/index.tsx b/src/features/AuthServiceApp/components/App/index.tsx index cf83ccbd..dc9002be 100644 --- a/src/features/AuthServiceApp/components/App/index.tsx +++ b/src/features/AuthServiceApp/components/App/index.tsx @@ -18,6 +18,7 @@ import { lexics } from '../../config/lexics' const Login = lazy(() => import('../Login')) const Registration = lazy(() => import('../Registration')) const ChangePassword = lazy(() => import('../ChangePassword')) +const Oauth = lazy(() => import('../Oauth')) const Main = styled.main` width: 100%; @@ -45,6 +46,10 @@ export const App = () => { + + + + diff --git a/src/features/AuthServiceApp/components/Login/hooks.tsx b/src/features/AuthServiceApp/components/Login/hooks.tsx index 636e6a18..04c3b4fb 100644 --- a/src/features/AuthServiceApp/components/Login/hooks.tsx +++ b/src/features/AuthServiceApp/components/Login/hooks.tsx @@ -1,19 +1,29 @@ -import type { FormEvent } from 'react' +import type { FormEvent, MouseEvent } from 'react' import { useRef, useState, useEffect, } from 'react' +import isObject from 'lodash/isObject' + +import { redirectToUrl } from 'helpers' + +import { useLocalStore } from 'hooks' + +import type { Settings } from 'features/AuthStore/helpers' import { loginCheck } from 'features/AuthServiceApp/requests/auth' import { getApiUrl } from 'features/AuthServiceApp/config/routes' import { useAuthFields } from 'features/AuthServiceApp/hooks/useAuthFields' +import { AuthProviders } from '../../config/authProviders' +import { getAuthUrl } from '../../helpers/getAuthUrl' import { useParamsUrl } from '../../hooks/useParamsUrl' const url = getApiUrl('/authorize') export const useLoginForm = () => { + const urlParams = useParamsUrl() const { client_id, lang, @@ -21,7 +31,8 @@ export const useLoginForm = () => { response_mode, response_type, scope, - } = useParamsUrl() + } = urlParams + const [authError, setAuthError] = useState('') const [isFetching, setIsFetching] = useState(false) const [isRecoveryPopupOpen, setIsRecoveryPopupOpen] = useState(false) @@ -38,6 +49,12 @@ export const useLoginForm = () => { password, } = useAuthFields('login') + const [, setUrlParams] = useLocalStore>({ + defaultValue: {}, + key: 'urlParams', + validator: isObject, + }) + const isSubmitDisabled = ( !email || !password @@ -79,6 +96,17 @@ export const useLoginForm = () => { } } + const handleAuthButtonClick = (authProvider: AuthProviders) => ( + e: MouseEvent, + ) => { + e.preventDefault() + + const authUrl = getAuthUrl(authProvider, urlParams) + + setUrlParams(urlParams) + redirectToUrl(authUrl) + } + useEffect(() => { setAuthError('') }, [email, password]) @@ -89,6 +117,7 @@ export const useLoginForm = () => { email, formError, formRef, + handleAuthButtonClick, handleModalOpen, handleSubmit, isFetching, diff --git a/src/features/AuthServiceApp/components/Login/index.tsx b/src/features/AuthServiceApp/components/Login/index.tsx index 867a686f..089db6f4 100644 --- a/src/features/AuthServiceApp/components/Login/index.tsx +++ b/src/features/AuthServiceApp/components/Login/index.tsx @@ -4,6 +4,7 @@ import { RecoveryPopup } from 'features/AuthServiceApp/components/RecoveryPopup' import { client } from 'features/AuthServiceApp/config/clients' import { PAGES } from '../../config/pages' +import { AuthProviders } from '../../config/authProviders' import { LanguageSelect } from '../LanguageSelect' import { PasswordInput } from '../PasswordInput' import { Input } from '../../../../components/Input' @@ -23,7 +24,14 @@ import { Wrapper, ScLoaderWrapper, } from '../../styled' -import { RegisterButton } from './styled' +import { + RegisterButton, + AuthButtonsContainer, + AuthButton, + AuthButtonText, + AuthButtonImage, + ContinueWith, +} from './styled' import { CompanyInfo } from '../../../CompanyInfo' const Login = () => { @@ -33,6 +41,7 @@ const Login = () => { email, formError, formRef, + handleAuthButtonClick, handleModalOpen, handleSubmit, isFetching, @@ -112,6 +121,21 @@ const Login = () => { + + + + + Google + + {/* + + Facebook + */} + {/* + + Apple ID + */} + diff --git a/src/features/AuthServiceApp/components/Login/styled.tsx b/src/features/AuthServiceApp/components/Login/styled.tsx index 07aa204e..560c25cd 100644 --- a/src/features/AuthServiceApp/components/Login/styled.tsx +++ b/src/features/AuthServiceApp/components/Login/styled.tsx @@ -5,6 +5,15 @@ import styled, { css } from 'styled-components/macro' import { isMobileDevice } from 'config/userAgent' import { outlineButtonStyles } from 'features/Common/Button' +import { T9n } from 'features/T9n' + +import { visuallyHidden } from 'components/VisuallyHidden' + +import { AuthProviders } from '../../config/authProviders' + +type TAuthButtonImage = { + authProvider: AuthProviders, +} export const RegisterButton = styled(Link)` ${outlineButtonStyles} @@ -29,3 +38,83 @@ export const RegisterButton = styled(Link)` ` : ''}; ` + +export const ContinueWith = styled(T9n)` + margin: 20px auto; + font-size: 16px; + color: ${({ theme }) => theme.colors.white}; +` + +export const AuthButtonsContainer = styled.div` + width: 100%; + display: flex; + gap: 10px; + + ${isMobileDevice + ? css` + flex-direction: column; + ` + : ''}; +` + +export const AuthButton = styled.button` + ${outlineButtonStyles} + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 154px; + height: 50px; + border-radius: 5px; + + ${isMobileDevice + ? css` + width: 100%; + justify-content: flex-start; + height: 44px; + border-radius: 10px; + ` + : ''}; +` + +export const AuthButtonText = styled.span` + opacity: 0.8; + + ${isMobileDevice ? '' : visuallyHidden}; +` + +export const AuthButtonImage = styled.div` + background-repeat: no-repeat; + background-position: center; + width: 100%; + height: 32px; + + ${isMobileDevice + ? css` + width: 45%; + margin-right: 20px; + background-position: right; + ` + : ''} + + ${({ authProvider }) => { + switch (authProvider) { + case AuthProviders.Google: + return css` + background-image: url(/images/oauth/google.svg); + ` + + case AuthProviders.Facebook: + return css` + background-image: url(/images/oauth/facebook.svg); + ` + + case AuthProviders.AppleID: + return css` + background-image: url(/images/oauth/appleID.svg); + ` + + default: return '' + } + }} +` diff --git a/src/features/AuthServiceApp/components/Oauth/index.tsx b/src/features/AuthServiceApp/components/Oauth/index.tsx new file mode 100644 index 00000000..dd072031 --- /dev/null +++ b/src/features/AuthServiceApp/components/Oauth/index.tsx @@ -0,0 +1,69 @@ +import { useEffect, useRef } from 'react' +import { Redirect } from 'react-router-dom' + +import { parse } from 'querystring' +import isObject from 'lodash/isObject' + +import { useLocalStore } from 'hooks' + +import type { Settings } from 'features/AuthStore/helpers' +import { getClientSettings } from 'features/AuthStore/helpers' + +import { API_ROOT } from '../../config/routes' +import { PAGES } from '../../config/pages' + +const url = `${API_ROOT}/oauth` + +const Oauth = () => { + const { + response_mode, + response_type, + scope, + } = getClientSettings() + + const [urlParams] = useLocalStore>({ + clearOnUnmount: true, + defaultValue: {}, + key: 'urlParams', + validator: isObject, + }) + + const formRef = useRef(null) + + const idToken = parse(window.location.hash).id_token as string | undefined + + const { + client_id, + lang, + nonce, + redirect_uri, + state, + } = urlParams + + useEffect(() => { + formRef.current?.submit() + }, []) + + if (!idToken || !client_id || !redirect_uri) return + + return ( + + ) +} + +export default Oauth diff --git a/src/features/AuthServiceApp/config/authProviders.tsx b/src/features/AuthServiceApp/config/authProviders.tsx new file mode 100644 index 00000000..77b61a95 --- /dev/null +++ b/src/features/AuthServiceApp/config/authProviders.tsx @@ -0,0 +1,5 @@ +export enum AuthProviders { + Google, + Facebook, + AppleID, +} diff --git a/src/features/AuthServiceApp/config/lexics.tsx b/src/features/AuthServiceApp/config/lexics.tsx index ea51c171..8ed595f3 100644 --- a/src/features/AuthServiceApp/config/lexics.tsx +++ b/src/features/AuthServiceApp/config/lexics.tsx @@ -31,6 +31,7 @@ export const lexics = { i_accept: 15737, i_agree: 15430, login: 13404, + or_continue_with: 15118, password_new: 15056, password_repeat: 15057, privacy_policy_and_statement: 15404, diff --git a/src/features/AuthServiceApp/config/pages.tsx b/src/features/AuthServiceApp/config/pages.tsx index 9639849c..a5bb7416 100644 --- a/src/features/AuthServiceApp/config/pages.tsx +++ b/src/features/AuthServiceApp/config/pages.tsx @@ -1,5 +1,6 @@ export const PAGES = { change_password: '/change_password', login: '/authorize', + oauth: '/oauth', registration: '/registration', } diff --git a/src/features/AuthServiceApp/helpers/getAuthUrl/index.tsx b/src/features/AuthServiceApp/helpers/getAuthUrl/index.tsx new file mode 100644 index 00000000..f33525fc --- /dev/null +++ b/src/features/AuthServiceApp/helpers/getAuthUrl/index.tsx @@ -0,0 +1,34 @@ +import type { Settings } from 'features/AuthStore/helpers' +import { GOOGLE_CLIENT_ID } from 'config' + +import { AuthProviders } from '../../config/authProviders' +import { PAGES } from '../../config/pages' + +const getQueryString = (authProvider: AuthProviders, urlParams: Settings) => { + switch (authProvider) { + case AuthProviders.Google: + return new URLSearchParams({ + client_id: GOOGLE_CLIENT_ID, + nonce: urlParams.nonce || '0394852-3190485-2490358', + redirect_uri: `${window.location.origin}${PAGES.oauth}`, + response_type: 'token id_token', + scope: 'openid email', + state: urlParams.state || '', + }).toString() + + default: + return '' + } +} + +export const getAuthUrl = (authProvider: AuthProviders, urlParams: Settings) => { + const queryString = getQueryString(AuthProviders.Google, urlParams) + + switch (authProvider) { + case AuthProviders.Google: + return `https://accounts.google.com/o/oauth2/v2/auth?${queryString}` + + default: + return '' + } +} diff --git a/src/features/AuthServiceApp/hooks/useParamsUrl.tsx b/src/features/AuthServiceApp/hooks/useParamsUrl.tsx index a3860677..3a8c2427 100644 --- a/src/features/AuthServiceApp/hooks/useParamsUrl.tsx +++ b/src/features/AuthServiceApp/hooks/useParamsUrl.tsx @@ -11,10 +11,12 @@ export const useParamsUrl = () => { const { client_id, + nonce, redirect_uri, response_mode, response_type, scope, + state, } = getClientSettings() const urlSearchParams = useMemo(() => new URLSearchParams(location.search), [location.search]) @@ -29,10 +31,12 @@ export const useParamsUrl = () => { return { client_id, + nonce, redirect_uri, response_mode, response_type, scope, + state, ...params, lang, } diff --git a/src/features/AuthServiceApp/styled.tsx b/src/features/AuthServiceApp/styled.tsx index 32fa799f..bcbb245d 100644 --- a/src/features/AuthServiceApp/styled.tsx +++ b/src/features/AuthServiceApp/styled.tsx @@ -137,7 +137,7 @@ export const ForgotPass = styled.div` export const Container = styled.div` min-width: 100%; - margin-top: 15px; + margin-top: 20px; display: flex; flex-direction: row; align-items: center; diff --git a/src/features/AuthStore/helpers.tsx b/src/features/AuthStore/helpers.tsx index f7ee6f07..922e1980 100644 --- a/src/features/AuthStore/helpers.tsx +++ b/src/features/AuthStore/helpers.tsx @@ -6,7 +6,7 @@ import { AUTH_SERVICE } from 'config/routes' import { ClientIds, ClientNames } from 'config/clients/types' import { ENV, stageENV } from 'config/env' -interface Settings extends UserManagerSettings { +export interface Settings extends UserManagerSettings { client_id: ClientIds, lang?: string, nonce?: string, diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx index 9b48538e..cb43b81d 100644 --- a/src/helpers/index.tsx +++ b/src/helpers/index.tsx @@ -5,3 +5,4 @@ export * from './getProfileFallbackLogo' export * from './getSportLexic' export * from './msToMinutesAndSeconds' export * from './secondsToHms' +export * from './redirectToUrl' diff --git a/src/helpers/redirectToUrl/index.tsx b/src/helpers/redirectToUrl/index.tsx new file mode 100644 index 00000000..01085e9a --- /dev/null +++ b/src/helpers/redirectToUrl/index.tsx @@ -0,0 +1,3 @@ +export const redirectToUrl = (url: string) => { + window.location.href = url +}