feat(ott-2872): auth with facebook

keep-around/61a2661a1c7b0f958a65795b5393822fb1d16524
Ruslan Khayrullin 3 years ago
parent 07b050b45c
commit 10881598de
  1. 2
      Makefile
  2. 17
      public/images/done.svg
  3. 3
      src/config/env.tsx
  4. 4
      src/features/AuthServiceApp/components/Login/index.tsx
  5. 7
      src/features/AuthServiceApp/components/Login/styled.tsx
  6. 79
      src/features/AuthServiceApp/components/Oauth/hooks.tsx
  7. 182
      src/features/AuthServiceApp/components/Oauth/index.tsx
  8. 137
      src/features/AuthServiceApp/components/Oauth/styled.tsx
  9. 21
      src/features/AuthServiceApp/helpers/getAuthUrl/index.tsx
  10. 11
      src/helpers/getRandomString/index.tsx
  11. 1
      src/helpers/index.tsx

@ -91,6 +91,7 @@ auth-build:
REACT_APP_TYPE=auth-service \
REACT_APP_ENV=staging \
REACT_APP_GOOGLE_CLIENT_ID=1022673777479-43mqb9lsqkpcr47umuvsoo0eemfpljf5.apps.googleusercontent.com \
REACT_APP_FACEBOOK_CLIENT_ID=798254931203361 \
BUILD_PATH=build_auth \
GENERATE_SOURCEMAP=false \
npx react-scripts build
@ -103,6 +104,7 @@ auth-production-build:
REACT_APP_TYPE=auth-service \
REACT_APP_ENV=production \
REACT_APP_GOOGLE_CLIENT_ID=1022673777479-43mqb9lsqkpcr47umuvsoo0eemfpljf5.apps.googleusercontent.com \
REACT_APP_FACEBOOK_CLIENT_ID=798254931203361 \
BUILD_PATH=build_auth \
GENERATE_SOURCEMAP=false \
npx react-scripts build

@ -0,0 +1,17 @@
<svg width="90" height="90" viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_36753_302675)">
<rect x="1" width="88" height="88" rx="44" fill="white"/>
<path d="M39.8569 51.8614L30.7834 42.7878C30.1139 42.1183 29.0285 42.1183 28.359 42.7878C27.6896 43.4573 27.6896 44.5427 28.359 45.2122L38.6448 55.4979C39.3142 56.1674 40.3996 56.1674 41.0691 55.4979L61.6405 34.9265C62.31 34.257 62.31 33.1716 61.6405 32.5021C60.9711 31.8326 59.8856 31.8326 59.2162 32.5021L39.8569 51.8614Z" fill="black"/>
</g>
<defs>
<filter id="filter0_d_36753_302675" x="0" y="0" width="90" height="90" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="0.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_36753_302675"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_36753_302675" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -16,4 +16,5 @@ 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 || '1022673777479-43mqb9lsqkpcr47umuvsoo0eemfpljf5.apps.googleusercontent.com'
export const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID || '1043133237396-kebgih109kro71b5c7c8qphtgjbd2gdk.apps.googleusercontent.com'
export const FACEBOOK_CLIENT_ID = process.env.REACT_APP_FACEBOOK_CLIENT_ID || '798254931203361'

@ -127,10 +127,10 @@ const Login = () => {
<AuthButtonImage authProvider={AuthProviders.Google} />
<AuthButtonText>Google</AuthButtonText>
</AuthButton>
{/* <AuthButton onClick={handleAuthButtonClick(AuthProviders.Facebook)}>
<AuthButton onClick={handleAuthButtonClick(AuthProviders.Facebook)}>
<AuthButtonImage authProvider={AuthProviders.Facebook} />
<AuthButtonText>Facebook</AuthButtonText>
</AuthButton> */}
</AuthButton>
{/* <AuthButton onClick={handleAuthButtonClick(AuthProviders.AppleID)}>
<AuthButtonImage authProvider={AuthProviders.AppleID} />
<AuthButtonText>Apple ID</AuthButtonText>

@ -90,12 +90,15 @@ export const AuthButtonImage = styled.div<TAuthButtonImage>`
return css`
width: 32px;
height: 32px;
background-image: url(/images/oauth/google.svg);
`
background-image: url(/images/oauth/google.svg); `
case AuthProviders.Facebook:
return css`
width: 32px;
height: 32px;
background-image: url(/images/oauth/facebook.svg);
${isMobileDevice ? css`width: 57px;` : ''};
`
case AuthProviders.AppleID:

@ -0,0 +1,79 @@
import {
useEffect,
useRef,
useState,
useCallback,
FormEvent,
FocusEvent,
ChangeEvent,
} from 'react'
import { isValidEmail } from 'features/AuthServiceApp/helpers/isValidEmail'
import { API_ROOT } from '../../config/routes'
export const useOauth = () => {
const [email, setEmail] = useState('')
const [error, setError] = useState('')
const [showEmailField, showShowEmailField] = useState(false)
const formRef = useRef<HTMLFormElement>(null)
const authorize = useCallback(async () => {
if (!formRef.current) return
const url = `${API_ROOT}/oauth`
const res = await fetch(url, {
body: new FormData(formRef.current),
method: 'POST',
})
if (res.ok && res.redirected) {
formRef.current.submit()
} else {
const data = await res.json()
if (data.error === 'Token missing email field') {
showShowEmailField(true)
}
}
}, [])
const handleEmailChange = ({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
setEmail(value)
}
const handleEmailFocus = () => {
setError('')
}
const handleEmailBlur = ({ target: { value } }: FocusEvent<HTMLInputElement>) => {
if (!isValidEmail(value) && value) {
setError('error_invalid_email_format')
}
}
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (error) return
authorize()
}
useEffect(() => {
authorize()
}, [authorize])
return {
email,
error,
formRef,
handleEmailBlur,
handleEmailChange,
handleEmailFocus,
handleSubmit,
showEmailField,
}
}

@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react'
import { Fragment } from 'react'
import { Redirect } from 'react-router-dom'
import { parse } from 'querystring'
@ -8,9 +8,25 @@ import { useLocalStore } from 'hooks'
import type { Settings } from 'features/AuthStore/helpers'
import { getClientSettings } from 'features/AuthStore/helpers'
import { T9n } from 'features/T9n'
import { API_ROOT } from '../../config/routes'
import { PAGES } from '../../config/pages'
import { useOauth } from './hooks'
import {
Header,
HeaderLogo,
Content,
DoneIcon,
Title,
Subtitle,
Form,
Input,
SubmitButton,
Text,
StyledLink,
Error,
} from './styled'
const url = `${API_ROOT}/oauth`
@ -28,9 +44,7 @@ const Oauth = () => {
validator: isObject,
})
const formRef = useRef<HTMLFormElement>(null)
const idToken = parse(window.location.hash).id_token as string | undefined
const idToken = parse(window.location.hash.replace('#', '')).id_token as string | undefined
const {
client_id,
@ -40,28 +54,162 @@ const Oauth = () => {
state,
} = urlParams
useEffect(() => {
formRef.current?.submit()
}, [])
const {
email,
error,
formRef,
handleEmailBlur,
handleEmailChange,
handleEmailFocus,
handleSubmit,
showEmailField,
} = useOauth()
if (!idToken || !client_id || !redirect_uri) return <Redirect to={PAGES.login} />
if (showEmailField) {
return (
<Fragment>
<Header>
<HeaderLogo />
</Header>
<Content>
<DoneIcon />
<Title>You are one step away from creating inSports account!</Title>
<Subtitle>Complete the registration form by entering your email:</Subtitle>
<Form
ref={formRef}
onSubmit={handleSubmit}
method='POST'
action={url}
>
<input
type='hidden'
name='client_id'
defaultValue={client_id}
/>
<input
type='hidden'
name='id_token'
defaultValue={idToken}
/>
<input
type='hidden'
name='lang'
defaultValue={lang}
/>
<input
type='hidden'
name='redirect_uri'
defaultValue={redirect_uri}
/>
<input
type='hidden'
name='response_type'
defaultValue={response_type}
/>
<input
type='hidden'
name='response_mode'
defaultValue={response_mode}
/>
<input
type='hidden'
name='scope'
defaultValue={scope}
/>
<input
type='hidden'
name='state'
defaultValue={state}
/>
<input
type='hidden'
name='nonce'
defaultValue={nonce}
/>
<Input
autoFocus
type='email'
name='email'
autoComplete='email'
placeholderLexic='form_email'
value={email}
onChange={handleEmailChange}
onFocus={handleEmailFocus}
onBlur={handleEmailBlur}
/>
<SubmitButton
type='submit'
disabled={!email}
>OK
</SubmitButton>
{error && (
<Error>
<T9n t='error_invalid_email_format' />
</Error>
)}
</Form>
<Text>
Also you can use any{' '}
<StyledLink to={PAGES.login}>other method of authorization</StyledLink>
</Text>
</Content>
</Fragment>
)
}
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} />
<input
type='hidden'
name='client_id'
defaultValue={client_id}
/>
<input
type='hidden'
name='id_token'
defaultValue={idToken}
/>
<input
type='hidden'
name='lang'
defaultValue={lang}
/>
<input
type='hidden'
name='redirect_uri'
defaultValue={redirect_uri}
/>
<input
type='hidden'
name='response_type'
defaultValue={response_type}
/>
<input
type='hidden'
name='response_mode'
defaultValue={response_mode}
/>
<input
type='hidden'
name='scope'
defaultValue={scope}
/>
<input
type='hidden'
name='state'
defaultValue={state}
/>
<input
type='hidden'
name='nonce'
defaultValue={nonce}
/>
</form>
)
}

@ -0,0 +1,137 @@
import { Link } from 'react-router-dom'
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { ButtonSolid } from 'features/Common'
import { Error as ErrorBase } from 'features/AuthServiceApp/styled'
import { HeaderLogo as HeaderLogoBase } from 'features/ProfileHeader/styled'
import { Input as InputBase } from 'components/Input'
export const Header = styled.div`
position: absolute;
padding: 30px 25px 0;
`
export const HeaderLogo = styled(HeaderLogoBase)`
${isMobileDevice
? css`
top: 20px;
`
: ''};
`
export const Content = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
padding: 20px 25px;
text-align: center;
`
export const DoneIcon = styled.div`
width: 88px;
height: 88px;
margin-bottom: 20px;
background-image: url(/images/done.svg);
background-size: cover;
${isMobileDevice
? css`
width: 65px;
height: 65px;
`
: ''};
`
export const Title = styled.div`
margin-bottom: 40px;
font-size: 24px;
font-weight: bold;
color: ${({ theme }) => theme.colors.white};
${isMobileDevice
? css`
margin-bottom: 15px;
line-height: 20px;
font-size: 16px;
`
: ''};
`
export const Subtitle = styled.div`
margin-bottom: 40px;
font-size: 18px;
line-height: 18px;
color: ${({ theme }) => theme.colors.white};
${isMobileDevice
? css`
margin-bottom: 15px;
font-size: 12px;
`
: ''};
`
export const Form = styled.form`
position: relative;
display: flex;
gap: 20px;
margin-bottom: 30px;
${isMobileDevice
? css`
gap: 10px;
width: 100%;
flex-direction: column;
`
: ''};
`
export const Input = styled(InputBase)`
width: 406px;
height: 50px;
${isMobileDevice
? css`
width: 100%;
height: 32px;
`
: ''};
`
export const SubmitButton = styled(ButtonSolid)`
width: 117.65px;
height: 50px;
font-size: 20px;
font-weight: 600;
${isMobileDevice
? css`
width: 100%;
height: 32px;
font-size: 12px;
`
: ''};
`
export const Text = styled.div`
font-size: 12px;
white-space: nowrap;
color: rgba(255, 255, 255, 0.7);
`
export const StyledLink = styled(Link)`
color: rgba(255, 255, 255, 0.7);
text-decoration: underline;
`
export const Error = styled(ErrorBase)`
position: absolute;
left: 0;
bottom: -25px;
`

@ -1,5 +1,8 @@
import { GOOGLE_CLIENT_ID, FACEBOOK_CLIENT_ID } from 'config'
import { getRandomString } from 'helpers'
import type { Settings } from 'features/AuthStore/helpers'
import { GOOGLE_CLIENT_ID } from 'config'
import { AuthProviders } from '../../config/authProviders'
import { PAGES } from '../../config/pages'
@ -16,18 +19,32 @@ const getQueryString = (authProvider: AuthProviders, urlParams: Settings) => {
state: urlParams.state || '',
}).toString()
case AuthProviders.Facebook:
return new URLSearchParams({
client_id: FACEBOOK_CLIENT_ID,
code_challenge: getRandomString(43),
nonce: urlParams.nonce || '0394852-3190485-2490358',
redirect_uri: `${window.location.origin}${PAGES.oauth}`,
response_type: 'id_token',
scope: 'openid email public_profile',
state: urlParams.state || '',
}).toString()
default:
return ''
}
}
export const getAuthUrl = (authProvider: AuthProviders, urlParams: Settings) => {
const queryString = getQueryString(AuthProviders.Google, urlParams)
const queryString = getQueryString(authProvider, urlParams)
switch (authProvider) {
case AuthProviders.Google:
return `https://accounts.google.com/o/oauth2/v2/auth?${queryString}`
case AuthProviders.Facebook:
return `https://www.facebook.com/v11.0/dialog/oauth?${queryString}`
default:
return ''
}

@ -0,0 +1,11 @@
export const getRandomString = (length: number) => {
let result = ''
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength))
}
return result
}

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

Loading…
Cancel
Save