Merge pull request #3761 from omnivore-app/feat/signup-captcha
Add recaptcha on the web sign up flow
This commit is contained in:
@ -47,6 +47,7 @@ import {
|
||||
} from './google_auth'
|
||||
import { createWebAuthToken } from './jwt_helpers'
|
||||
import { createMobileAccountCreationResponse } from './mobile/account_creation'
|
||||
import { verifyChallengeRecaptcha } from '../../utils/recaptcha'
|
||||
|
||||
export interface SignupRequest {
|
||||
email: string
|
||||
@ -55,6 +56,7 @@ export interface SignupRequest {
|
||||
username: string
|
||||
bio?: string
|
||||
pictureUrl?: string
|
||||
recaptchaToken?: string
|
||||
}
|
||||
|
||||
const signToken = promisify(jwt.sign)
|
||||
@ -64,20 +66,28 @@ const cookieParams = {
|
||||
maxAge: 365 * 24 * 60 * 60 * 1000,
|
||||
}
|
||||
|
||||
const isURLPresent = (input: string): boolean => {
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g
|
||||
return urlRegex.test(input)
|
||||
}
|
||||
|
||||
export const isValidSignupRequest = (obj: any): obj is SignupRequest => {
|
||||
return (
|
||||
'email' in obj &&
|
||||
obj.email.trim().length > 0 &&
|
||||
obj.email.trim().length < 512 && // email must not be empty
|
||||
!isURLPresent(obj.email) &&
|
||||
'password' in obj &&
|
||||
obj.password.length >= 8 &&
|
||||
obj.password.trim().length < 512 && // password must be at least 8 characters
|
||||
'name' in obj &&
|
||||
obj.name.trim().length > 0 &&
|
||||
obj.name.trim().length < 512 && // name must not be empty
|
||||
!isURLPresent(obj.name) &&
|
||||
'username' in obj &&
|
||||
obj.username.trim().length > 0 &&
|
||||
obj.username.trim().length < 512 // username must not be empty
|
||||
obj.username.trim().length < 512 && // username must not be empty
|
||||
!isURLPresent(obj.username)
|
||||
)
|
||||
}
|
||||
|
||||
@ -499,16 +509,27 @@ export function authRouter() {
|
||||
`${env.client.url}/auth/email-signup?errorCodes=INVALID_CREDENTIALS`
|
||||
)
|
||||
}
|
||||
const { email, password, name, username, bio, pictureUrl } = req.body
|
||||
const {
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
username,
|
||||
bio,
|
||||
pictureUrl,
|
||||
recaptchaToken,
|
||||
} = req.body
|
||||
|
||||
function isURLPresent(input: string): boolean {
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g
|
||||
return urlRegex.test(input)
|
||||
}
|
||||
if (recaptchaToken && process.env.RECAPTCHA_CHALLENGE_SECRET_KEY) {
|
||||
const verified = await verifyChallengeRecaptcha(recaptchaToken)
|
||||
console.log('recaptcha result: ', recaptchaToken, verified)
|
||||
// temporarily do not fail here so we can deploy this in stages
|
||||
// just log the verification
|
||||
|
||||
if (isURLPresent(email) || isURLPresent(name) || isURLPresent(username)) {
|
||||
res.redirect(`${env.client.url}/auth/email-signup?errorCodes=UNKNOWN`)
|
||||
return
|
||||
// if (!verified) {
|
||||
// return res.redirect(
|
||||
// `${env.client.url}/auth/email-signup?errorCodes=UNKNOWN`
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
// trim whitespace in email address
|
||||
|
||||
39
packages/api/src/utils/recaptcha.ts
Normal file
39
packages/api/src/utils/recaptcha.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import axios from 'axios'
|
||||
|
||||
type RecaptchaResponse = {
|
||||
success: boolean
|
||||
hostname: string
|
||||
score?: number
|
||||
action?: string
|
||||
}
|
||||
|
||||
export const isRecaptchaResponse = (data: any): data is RecaptchaResponse => {
|
||||
return 'success' in data && 'hostname' in data
|
||||
}
|
||||
|
||||
export const verifyChallengeRecaptcha = async (
|
||||
token: string
|
||||
): Promise<boolean> => {
|
||||
if (!process.env.RECAPTCHA_CHALLENGE_SECRET_KEY) {
|
||||
return false
|
||||
}
|
||||
|
||||
const url = `https://www.google.com/recaptcha/api/siteverify`
|
||||
const params = new URLSearchParams({
|
||||
secret: process.env.RECAPTCHA_CHALLENGE_SECRET_KEY,
|
||||
response: token,
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await axios.post(url, params)
|
||||
console.log('recaptcha response: ', response.data)
|
||||
|
||||
if (isRecaptchaResponse(response.data)) {
|
||||
return response.data.success
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('Error verifying reCAPTCHA:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@ import {
|
||||
import * as util from '../../src/utils/sendEmail'
|
||||
import { createTestUser } from '../db'
|
||||
import { generateFakeUuid, request } from '../util'
|
||||
import { isValidSignupRequest } from '../../src/routers/auth/auth_router'
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
@ -631,13 +632,60 @@ describe('auth router', () => {
|
||||
'ios'
|
||||
).expect(200)
|
||||
const user = await userRepository.findOneByOrFail({ name })
|
||||
const { count } = await searchLibraryItems(
|
||||
{ query: 'in:all' },
|
||||
user.id
|
||||
)
|
||||
const { count } = await searchLibraryItems({ query: 'in:all' }, user.id)
|
||||
|
||||
expect(count).to.eql(4)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidSignupRequest', () => {
|
||||
it('returns true for normal looking requests', async () => {
|
||||
const result = isValidSignupRequest({
|
||||
email: 'email@omnivore.app',
|
||||
password: 'superDuperPassword',
|
||||
name: "The User's Name",
|
||||
username: 'foouser',
|
||||
})
|
||||
expect(result).to.be.true
|
||||
})
|
||||
it('returns false for requests w/missing info', async () => {
|
||||
let result = isValidSignupRequest({
|
||||
password: 'superDuperPassword',
|
||||
name: "The User's Name",
|
||||
username: 'foouser',
|
||||
})
|
||||
expect(result).to.be.false
|
||||
|
||||
result = isValidSignupRequest({
|
||||
email: 'email@omnivore.app',
|
||||
name: "The User's Name",
|
||||
username: 'foouser',
|
||||
})
|
||||
expect(result).to.be.false
|
||||
|
||||
result = isValidSignupRequest({
|
||||
email: 'email@omnivore.app',
|
||||
password: 'superDuperPassword',
|
||||
username: 'foouser',
|
||||
})
|
||||
expect(result).to.be.false
|
||||
|
||||
result = isValidSignupRequest({
|
||||
email: 'email@omnivore.app',
|
||||
password: 'superDuperPassword',
|
||||
name: "The User's Name",
|
||||
})
|
||||
expect(result).to.be.false
|
||||
})
|
||||
|
||||
it('returns false for requests w/malicious info', async () => {
|
||||
let result = isValidSignupRequest({
|
||||
password: 'superDuperPassword',
|
||||
name: "You've won a cake sign up here: https://foo.bar",
|
||||
username: 'foouser',
|
||||
})
|
||||
expect(result).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText, StyledTextSpan } from '../../elements/StyledText'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { BorderedFormInput, FormLabel } from '../../elements/FormElements'
|
||||
import { TermAndConditionsFooter } from '../LoginForm'
|
||||
import { fetchEndpoint } from '../../../lib/appConfig'
|
||||
@ -10,26 +10,19 @@ import { logoutMutation } from '../../../lib/networking/mutations/logoutMutation
|
||||
import { useRouter } from 'next/router'
|
||||
import { formatMessage } from '../../../locales/en/messages'
|
||||
import { parseErrorCodes } from '../../../lib/queryParamParser'
|
||||
import {
|
||||
GoogleReCaptchaProvider,
|
||||
GoogleReCaptchaCheckbox,
|
||||
} from '@google-recaptcha/react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export function EmailSignup(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const SignUpForm = (): JSX.Element => {
|
||||
const [email, setEmail] = useState<string | undefined>()
|
||||
const [password, setPassword] = useState<string | undefined>()
|
||||
const [fullname, setFullname] = useState<string | undefined>()
|
||||
const [username, setUsername] = useState<string | undefined>()
|
||||
const [debouncedUsername, setDebouncedUsername] =
|
||||
useState<string | undefined>()
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
const errorCode = parseErrorCodes(router.query)
|
||||
const errorMsg = errorCode
|
||||
? formatMessage({ id: `error.${errorCode}` })
|
||||
: undefined
|
||||
setErrorMessage(errorMsg)
|
||||
}, [router.isReady, router.query])
|
||||
|
||||
const { isUsernameValid, usernameErrorMessage } = useValidateUsernameQuery({
|
||||
username: debouncedUsername ?? '',
|
||||
@ -46,174 +39,236 @@ export function EmailSignup(): JSX.Element {
|
||||
)
|
||||
|
||||
return (
|
||||
<form action={`${fetchEndpoint}/auth/email-signup`} method="POST">
|
||||
<VStack
|
||||
alignment="center"
|
||||
css={{
|
||||
padding: '16px',
|
||||
background: 'white',
|
||||
minWidth: '340px',
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
}}
|
||||
>
|
||||
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
|
||||
Sign Up
|
||||
</StyledText>
|
||||
<VStack
|
||||
css={{ width: '100%', minWidth: '320px', gap: '16px', pb: '16px' }}
|
||||
>
|
||||
<SpanBox css={{ width: '100%' }}>
|
||||
<FormLabel className="required">Email</FormLabel>
|
||||
<BorderedFormInput
|
||||
autoFocus={true}
|
||||
key="email"
|
||||
type="email"
|
||||
name="email"
|
||||
defaultValue={email}
|
||||
placeholder="Email"
|
||||
css={{ backgroundColor: 'white', color: 'black' }}
|
||||
onChange={(e) => {
|
||||
e.preventDefault()
|
||||
setEmail(e.target.value)
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</SpanBox>
|
||||
|
||||
<SpanBox css={{ width: '100%' }}>
|
||||
<FormLabel className="required">Password</FormLabel>
|
||||
<BorderedFormInput
|
||||
key="password"
|
||||
type="password"
|
||||
name="password"
|
||||
defaultValue={password}
|
||||
placeholder="Password"
|
||||
css={{ bg: 'white', color: 'black' }}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</SpanBox>
|
||||
|
||||
<SpanBox css={{ width: '100%' }}>
|
||||
<FormLabel className="required">Full Name</FormLabel>
|
||||
<BorderedFormInput
|
||||
key="fullname"
|
||||
type="text"
|
||||
name="name"
|
||||
defaultValue={fullname}
|
||||
placeholder="Full Name"
|
||||
css={{ bg: 'white', color: 'black' }}
|
||||
onChange={(e) => setFullname(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</SpanBox>
|
||||
|
||||
<SpanBox css={{ width: '100%' }}>
|
||||
<FormLabel className="required">Username</FormLabel>
|
||||
<BorderedFormInput
|
||||
key="username"
|
||||
type="text"
|
||||
name="username"
|
||||
defaultValue={username}
|
||||
placeholder="Username"
|
||||
css={{ bg: 'white', color: 'black' }}
|
||||
onChange={handleUsernameChange}
|
||||
required
|
||||
/>
|
||||
</SpanBox>
|
||||
{username && username.length > 0 && usernameErrorMessage && (
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{
|
||||
m: 0,
|
||||
pl: '$2',
|
||||
color: '$error',
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
{usernameErrorMessage}
|
||||
</StyledText>
|
||||
)}
|
||||
{isUsernameValid && (
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{
|
||||
m: 0,
|
||||
pl: '$2',
|
||||
alignSelf: 'flex-start',
|
||||
color: '$omnivoreGray',
|
||||
}}
|
||||
>
|
||||
Username is available.
|
||||
</StyledText>
|
||||
)}
|
||||
</VStack>
|
||||
|
||||
{errorMessage && <StyledText style="error">{errorMessage}</StyledText>}
|
||||
|
||||
<VStack css={{ width: '100%', minWidth: '320px', gap: '16px', pb: '16px' }}>
|
||||
<SpanBox css={{ width: '100%' }}>
|
||||
<FormLabel className="required">Email</FormLabel>
|
||||
<BorderedFormInput
|
||||
autoFocus={true}
|
||||
key="email"
|
||||
type="email"
|
||||
name="email"
|
||||
defaultValue={email}
|
||||
placeholder="Email"
|
||||
css={{ backgroundColor: 'white', color: 'black' }}
|
||||
onChange={(e) => {
|
||||
e.preventDefault()
|
||||
setEmail(e.target.value)
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</SpanBox>
|
||||
<SpanBox css={{ width: '100%' }}>
|
||||
<FormLabel className="required">Password</FormLabel>
|
||||
<BorderedFormInput
|
||||
key="password"
|
||||
type="password"
|
||||
name="password"
|
||||
defaultValue={password}
|
||||
placeholder="Password"
|
||||
css={{ bg: 'white', color: 'black' }}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</SpanBox>
|
||||
<SpanBox css={{ width: '100%' }}>
|
||||
<FormLabel className="required">Full Name</FormLabel>
|
||||
<BorderedFormInput
|
||||
key="fullname"
|
||||
type="text"
|
||||
name="name"
|
||||
defaultValue={fullname}
|
||||
placeholder="Full Name"
|
||||
css={{ bg: 'white', color: 'black' }}
|
||||
onChange={(e) => setFullname(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</SpanBox>
|
||||
<SpanBox css={{ width: '100%' }}>
|
||||
<FormLabel className="required">Username</FormLabel>
|
||||
<BorderedFormInput
|
||||
key="username"
|
||||
type="text"
|
||||
name="username"
|
||||
defaultValue={username}
|
||||
placeholder="Username"
|
||||
css={{ bg: 'white', color: 'black' }}
|
||||
onChange={handleUsernameChange}
|
||||
required
|
||||
/>
|
||||
</SpanBox>
|
||||
{username && username.length > 0 && usernameErrorMessage && (
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{
|
||||
p: '0px',
|
||||
color: '$omnivoreLightGray',
|
||||
m: 0,
|
||||
pl: '$2',
|
||||
color: '$error',
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
Omnivore will send you daily tips for your first week as a new user.
|
||||
If you don't like them you can unsubscribe.
|
||||
{usernameErrorMessage}
|
||||
</StyledText>
|
||||
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="end"
|
||||
css={{
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
height: '80px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={'ctaOutlineYellow'}
|
||||
css={{ color: '$omnivoreGray', borderColor: 'rgba(0, 0, 0, 0.06)' }}
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
window.localStorage.removeItem('authVerified')
|
||||
window.localStorage.removeItem('authToken')
|
||||
try {
|
||||
await logoutMutation()
|
||||
} catch (e) {
|
||||
console.log('error logging out', e)
|
||||
}
|
||||
window.location.href = '/'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" style={'ctaDarkYellow'}>
|
||||
Sign Up
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
)}
|
||||
{isUsernameValid && (
|
||||
<StyledText
|
||||
style="action"
|
||||
style="caption"
|
||||
css={{
|
||||
pt: '16px',
|
||||
color: '$omnivoreLightGray',
|
||||
textAlign: 'center',
|
||||
m: 0,
|
||||
pl: '$2',
|
||||
alignSelf: 'flex-start',
|
||||
color: '$omnivoreGray',
|
||||
}}
|
||||
>
|
||||
Already have an account?{' '}
|
||||
<Link href="/auth/email-login" passHref legacyBehavior>
|
||||
<StyledTextSpan style="actionLink" css={{ color: '$omnivoreGray' }}>
|
||||
Login instead
|
||||
</StyledTextSpan>
|
||||
</Link>
|
||||
Username is available.
|
||||
</StyledText>
|
||||
<TermAndConditionsFooter />
|
||||
</VStack>
|
||||
</form>
|
||||
)}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type RecaptchaProps = {
|
||||
setRecaptchaToken: (token: string) => void
|
||||
}
|
||||
|
||||
const Recaptcha = (props: RecaptchaProps): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<GoogleReCaptchaCheckbox
|
||||
key="recaptcha"
|
||||
onChange={(token) => {
|
||||
console.log('recaptcha: ', token)
|
||||
props.setRecaptchaToken(token)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function EmailSignup(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const recaptchaTokenRef = useRef<HTMLInputElement>(null)
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
const errorCode = parseErrorCodes(router.query)
|
||||
const errorMsg = errorCode
|
||||
? formatMessage({ id: `error.${errorCode}` })
|
||||
: undefined
|
||||
setErrorMessage(errorMsg)
|
||||
}, [router.isReady, router.query])
|
||||
|
||||
return (
|
||||
<>
|
||||
<form action={`${fetchEndpoint}/auth/email-signup`} method="POST">
|
||||
<VStack
|
||||
alignment="center"
|
||||
css={{
|
||||
padding: '16px',
|
||||
background: 'white',
|
||||
minWidth: '340px',
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
}}
|
||||
>
|
||||
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
|
||||
Sign Up
|
||||
</StyledText>
|
||||
|
||||
<SignUpForm />
|
||||
|
||||
{process.env.NEXT_PUBLIC_RECAPTCHA_CHALLENGE_SECRET_KEY && (
|
||||
<>
|
||||
<Recaptcha
|
||||
setRecaptchaToken={(token) => {
|
||||
if (recaptchaTokenRef.current) {
|
||||
recaptchaTokenRef.current.value = token
|
||||
} else {
|
||||
console.log('error updating recaptcha token')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={recaptchaTokenRef}
|
||||
type="hidden"
|
||||
name="recaptchaToken"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<StyledText style="error">{errorMessage}</StyledText>
|
||||
)}
|
||||
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{
|
||||
p: '0px',
|
||||
color: '$omnivoreLightGray',
|
||||
}}
|
||||
>
|
||||
Omnivore will send you daily tips for your first week as a new user.
|
||||
If you don't like them you can unsubscribe.
|
||||
</StyledText>
|
||||
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="end"
|
||||
css={{
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
height: '80px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={'ctaOutlineYellow'}
|
||||
css={{
|
||||
color: '$omnivoreGray',
|
||||
borderColor: 'rgba(0, 0, 0, 0.06)',
|
||||
}}
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
window.localStorage.removeItem('authVerified')
|
||||
window.localStorage.removeItem('authToken')
|
||||
try {
|
||||
await logoutMutation()
|
||||
} catch (e) {
|
||||
console.log('error logging out', e)
|
||||
}
|
||||
window.location.href = '/'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" style={'ctaDarkYellow'}>
|
||||
Sign Up
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
<StyledText
|
||||
style="action"
|
||||
css={{
|
||||
pt: '16px',
|
||||
color: '$omnivoreLightGray',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Already have an account?{' '}
|
||||
<Link href="/auth/email-login" passHref legacyBehavior>
|
||||
<StyledTextSpan
|
||||
style="actionLink"
|
||||
css={{ color: '$omnivoreGray' }}
|
||||
>
|
||||
Login instead
|
||||
</StyledTextSpan>
|
||||
</Link>
|
||||
</StyledText>
|
||||
<TermAndConditionsFooter />
|
||||
</VStack>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
const ContentSecurityPolicy = `
|
||||
default-src 'self';
|
||||
base-uri 'self';
|
||||
connect-src 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://proxy-prod.omnivore-image-cache.app https://accounts.google.com https://proxy-demo.omnivore-image-cache.app https://storage.googleapis.com https://api.segment.io https://cdn.segment.com https://widget.intercom.io https://api-iam.intercom.io https://static.intercomassets.com https://downloads.intercomcdn.com https://platform.twitter.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io wss://nexus-europe-websocket.intercom.io wss://nexus-australia-websocket.intercom.io https://uploads.intercomcdn.com https://tools.applemediaservices.com;
|
||||
connect-src 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://proxy-prod.omnivore-image-cache.app https://accounts.google.com https://proxy-demo.omnivore-image-cache.app https://storage.googleapis.com https://widget.intercom.io https://api-iam.intercom.io https://static.intercomassets.com https://downloads.intercomcdn.com https://platform.twitter.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io wss://nexus-europe-websocket.intercom.io wss://nexus-australia-websocket.intercom.io https://uploads.intercomcdn.com https://tools.applemediaservices.com;
|
||||
font-src 'self' data: https://cdn.jsdelivr.net https://js.intercomcdn.com https://fonts.intercomcdn.com;
|
||||
form-action 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://getpocket.com/auth/authorize https://intercom.help https://api-iam.intercom.io https://api-iam.eu.intercom.io https://api-iam.au.intercom.io https://www.notion.so https://api.notion.com;
|
||||
frame-ancestors 'none';
|
||||
frame-src 'self' https://accounts.google.com https://platform.twitter.com https://www.youtube.com https://www.youtube-nocookie.com;
|
||||
frame-src 'self' https://accounts.google.com https://platform.twitter.com https://www.youtube.com https://www.youtube-nocookie.com https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/;
|
||||
manifest-src 'self';
|
||||
script-src 'self' 'unsafe-inline' 'unsafe-eval' accounts.google.com https://widget.intercom.io https://js.intercomcdn.com https://platform.twitter.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://cdn.segment.com;
|
||||
script-src 'self' 'unsafe-inline' 'unsafe-eval' accounts.google.com https://widget.intercom.io https://js.intercomcdn.com https://platform.twitter.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/;
|
||||
style-src 'self' 'unsafe-inline' https://accounts.google.com https://cdnjs.cloudflare.com;
|
||||
img-src 'self' blob: data: https:;
|
||||
worker-src 'self' blob:;
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.9",
|
||||
"@google-recaptcha/react": "^1.0.3",
|
||||
"@radix-ui/react-avatar": "^0.1.1",
|
||||
"@radix-ui/react-checkbox": "^0.1.5",
|
||||
"@radix-ui/react-dialog": "1.0.5",
|
||||
@ -106,4 +107,4 @@
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,24 @@
|
||||
import { PageMetaData } from '../../components/patterns/PageMetaData'
|
||||
import { ProfileLayout } from '../../components/templates/ProfileLayout'
|
||||
import { EmailSignup } from '../../components/templates/auth/EmailSignup'
|
||||
import { GoogleReCaptchaProvider } from '@google-recaptcha/react'
|
||||
|
||||
export default function EmailRegistrationPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Sign up with Email - Omnivore" path="/auth-signup" />
|
||||
<ProfileLayout>
|
||||
<EmailSignup />
|
||||
</ProfileLayout>
|
||||
<div data-testid="auth-signup-page-tag" />
|
||||
<GoogleReCaptchaProvider
|
||||
type="v2-checkbox"
|
||||
isEnterprise={true}
|
||||
siteKey={process.env.NEXT_PUBLIC_RECAPTCHA_CHALLENGE_SECRET_KEY ?? ''}
|
||||
>
|
||||
<PageMetaData
|
||||
title="Sign up with Email - Omnivore"
|
||||
path="/auth-signup"
|
||||
/>
|
||||
<ProfileLayout>
|
||||
<EmailSignup />
|
||||
</ProfileLayout>
|
||||
</GoogleReCaptchaProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user