feat(ott-2680): auth with google

keep-around/2fd168b18eeebc91eac94ae86d817c12edfe1576
Ruslan Khayrullin 3 years ago
parent 51c8dad31e
commit 76cd6ee767
  1. 2
      Makefile
  2. 3
      public/images/oauth/appleID.svg
  3. 3
      public/images/oauth/facebook.svg
  4. 3
      public/images/oauth/google.svg
  5. 14
      src/components/VisuallyHidden/index.tsx
  6. 2
      src/config/env.tsx
  7. 1
      src/config/index.tsx
  8. 5
      src/features/AuthServiceApp/components/App/index.tsx
  9. 33
      src/features/AuthServiceApp/components/Login/hooks.tsx
  10. 26
      src/features/AuthServiceApp/components/Login/index.tsx
  11. 89
      src/features/AuthServiceApp/components/Login/styled.tsx
  12. 69
      src/features/AuthServiceApp/components/Oauth/index.tsx
  13. 5
      src/features/AuthServiceApp/config/authProviders.tsx
  14. 1
      src/features/AuthServiceApp/config/lexics.tsx
  15. 1
      src/features/AuthServiceApp/config/pages.tsx
  16. 34
      src/features/AuthServiceApp/helpers/getAuthUrl/index.tsx
  17. 4
      src/features/AuthServiceApp/hooks/useParamsUrl.tsx
  18. 2
      src/features/AuthServiceApp/styled.tsx
  19. 2
      src/features/AuthStore/helpers.tsx
  20. 1
      src/helpers/index.tsx
  21. 3
      src/helpers/redirectToUrl/index.tsx

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

@ -0,0 +1,3 @@
<svg width="26" height="32" viewBox="0 0 26 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.22302 32C7.2094 31.9426 6.25557 31.481 5.55842 30.7104C4.72438 29.8723 3.9801 28.942 3.33873 27.9358C2.33685 26.4374 1.54491 24.7973 0.986859 23.0652C0.355292 21.2017 0.0230439 19.2419 0.00355306 17.2652C-0.0455116 15.3256 0.414381 13.409 1.33343 11.7228C2.00655 10.5074 2.96667 9.49251 4.12215 8.77487C5.26486 8.0603 6.56883 7.67566 7.90062 7.66034C8.90459 7.72237 9.89118 7.96204 10.8183 8.36913C11.5582 8.70921 12.3352 8.94828 13.1331 9.0796C14.0133 8.88703 14.8733 8.60509 15.701 8.23781C16.6805 7.83835 17.7176 7.61465 18.7686 7.57616C18.9088 7.57616 19.0474 7.57616 19.1812 7.59299C21.4864 7.66202 23.6303 8.8439 24.9843 10.7918C23.9276 11.3821 23.0482 12.267 22.4441 13.3479C21.8399 14.4289 21.5346 15.6637 21.5621 16.915C21.5516 17.8719 21.7353 18.8202 22.1013 19.6975C22.4672 20.5747 23.007 21.361 23.6851 22.0045C24.3024 22.6186 25.0211 23.1109 25.8065 23.4575C25.6453 23.9625 25.4599 24.4508 25.2632 24.9424C24.8167 26.0287 24.2658 27.065 23.619 28.0351C23.0074 29.007 22.2955 29.9058 21.496 30.7154C20.7668 31.4712 19.7948 31.917 18.7686 31.9663C17.8987 31.9261 17.0441 31.7129 16.2507 31.3384C15.395 30.9491 14.4755 30.7358 13.5425 30.7104C12.5832 30.7301 11.6365 30.9422 10.7538 31.335C9.99143 31.6942 9.1769 31.9179 8.34392 31.9966L8.22302 32ZM13.3007 7.57616C13.1798 7.57616 13.0589 7.57616 12.938 7.561C12.9126 7.3606 12.8992 7.15874 12.8977 6.95659C12.9505 5.27491 13.5797 3.66894 14.6709 2.4311C15.2813 1.71971 16.021 1.14225 16.8471 0.732362C17.6169 0.318459 18.4582 0.0694591 19.3215 0C19.3457 0.22055 19.3457 0.43605 19.3457 0.641448C19.323 2.2967 18.7275 3.88745 17.6692 5.1198C17.1568 5.83738 16.5002 6.42912 15.7455 6.85347C14.9909 7.27781 14.1564 7.52448 13.3007 7.57616Z" fill="#999999"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.84301 0C2.60624 0 0 2.60624 0 5.84301V26.157C0 29.3938 2.60624 32 5.84301 32H16.853V19.49H13.545V14.986H16.853V11.138C16.853 8.11481 18.8076 5.33901 23.31 5.33901C25.133 5.33901 26.481 5.51402 26.481 5.51402L26.375 9.72003C26.375 9.72003 25.0002 9.70704 23.5 9.70704C21.8764 9.70704 21.616 10.4551 21.616 11.6971V14.986H26.504L26.291 19.49H21.616V32.0001H26.157C29.3938 32.0001 32 29.3938 32 26.1571V5.84304C32 2.60627 29.3938 3.2e-05 26.157 3.2e-05H5.84298L5.84301 0Z" fill="#999999"/>
</svg>

After

Width:  |  Height:  |  Size: 602 B

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.1 13H16.1V19H24.9C24.1 23.2 20.3 26 16.1 25.8C10.9001 25.6 6.90005 21.6 6.70005 16.4C6.50005 11 10.7001 6.4 16.1 6.2C18.3001 6.2 20.5 7 22.3 8.6L26.9 4.2C24.1 1.4 20.1 0 16.3 0C7.50005 0 0.300049 7.2 0.300049 16C0.300049 24.8 7.50005 32 16.3 32C25.5 32 31.7 25.6 31.7 16.4C31.5 15.2 31.5 14.2 31.1 13Z" fill="#999999"/>
</svg>

After

Width:  |  Height:  |  Size: 476 B

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

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

@ -8,3 +8,4 @@ export * from './history'
export * from './devices'
export * from './currencies'
export * from './dashes'
export * from './env'

@ -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 = () => {
<Login />
</Route>
<Route path={PAGES.oauth}>
<Oauth />
</Route>
<Route path={PAGES.registration}>
<Registration />
</Route>

@ -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<Partial<Settings>>({
defaultValue: {},
key: 'urlParams',
validator: isObject,
})
const isSubmitDisabled = (
!email
|| !password
@ -79,6 +96,17 @@ export const useLoginForm = () => {
}
}
const handleAuthButtonClick = (authProvider: AuthProviders) => (
e: MouseEvent<HTMLButtonElement>,
) => {
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,

@ -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 = () => {
<RegisterButton to={`${PAGES.registration}${window.location.search}`}>
<T9n t='register' />
</RegisterButton>
<ContinueWith t='or_continue_with' />
<AuthButtonsContainer>
<AuthButton onClick={handleAuthButtonClick(AuthProviders.Google)}>
<AuthButtonImage authProvider={AuthProviders.Google} />
<AuthButtonText>Google</AuthButtonText>
</AuthButton>
{/* <AuthButton onClick={handleAuthButtonClick(AuthProviders.Facebook)}>
<AuthButtonImage authProvider={AuthProviders.Facebook} />
<AuthButtonText>Facebook</AuthButtonText>
</AuthButton> */}
{/* <AuthButton onClick={handleAuthButtonClick(AuthProviders.AppleID)}>
<AuthButtonImage authProvider={AuthProviders.AppleID} />
<AuthButtonText>Apple ID</AuthButtonText>
</AuthButton> */}
</AuthButtonsContainer>
</ButtonsBlock>
<Container>
<ForgotPass onClick={handleModalOpen}>

@ -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<TAuthButtonImage>`
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 ''
}
}}
`

@ -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<Partial<Settings>>({
clearOnUnmount: true,
defaultValue: {},
key: 'urlParams',
validator: isObject,
})
const formRef = useRef<HTMLFormElement>(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 <Redirect to={PAGES.login} />
return (
<form
hidden
method='POST'
ref={formRef}
action={url}
>
<input name='client_id' defaultValue={client_id} />
<input name='id_token' defaultValue={idToken} />
<input name='lang' defaultValue={lang} />
<input name='redirect_uri' defaultValue={redirect_uri} />
<input name='response_type' defaultValue={response_type} />
<input name='response_mode' defaultValue={response_mode} />
<input name='scope' defaultValue={scope} />
<input name='state' defaultValue={state} />
<input name='nonce' defaultValue={nonce} />
</form>
)
}
export default Oauth

@ -0,0 +1,5 @@
export enum AuthProviders {
Google,
Facebook,
AppleID,
}

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

@ -1,5 +1,6 @@
export const PAGES = {
change_password: '/change_password',
login: '/authorize',
oauth: '/oauth',
registration: '/registration',
}

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

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

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

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

@ -5,3 +5,4 @@ export * from './getProfileFallbackLogo'
export * from './getSportLexic'
export * from './msToMinutesAndSeconds'
export * from './secondsToHms'
export * from './redirectToUrl'

@ -0,0 +1,3 @@
export const redirectToUrl = (url: string) => {
window.location.href = url
}
Loading…
Cancel
Save